import type Point from "../../../geometry/Point.js";
import type Layer from "../../../layers/Layer.js";
import type MapView from "../../MapView.js";
import type ViewState from "../ViewState.js";
import type LayerView from "../../layers/LayerView.js";
import type { ScreenPoint } from "../../../core/types.js";
import type { ViewHit } from "../../types.js";
import type { LayerViewProperties } from "../../layers/LayerView.js";

export interface BaseLayerView2DProperties extends LayerViewProperties, Partial<Pick<BaseLayerView2D, "tiles">> {}

export interface RenderParameters {
  /** The [canvas 2D context](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) in which to draw content. */
  context: CanvasRenderingContext2D;
  /** The pixel ratio. */
  pixelRatio: number;
  /** The object that describes view state. */
  state: ViewState;
  /** The stationary state of the `MapView`. */
  stationary: boolean;
}

/** Represents a tile reference. */
export interface Tile {
  /** The tile string identifier in the format `level/row/col/world` */
  id: string;
  /** The level identifier of the [LOD](https://developers.arcgis.com/javascript/latest/references/core/layers/support/LOD/) to which the tile belongs */
  level: number;
  /** The row identifier. */
  row: number;
  /** The column identifier. */
  col: number;
  /** When the projection allows world wrapping (e.g. Web Mercator), identifies the instance of the world this tile's `level`/`row`/`col`. */
  world: number;
  /** The number of map units per pixel in the tile. */
  resolution: number;
  /** The map scale at the tile's level. */
  scale: number;
  /** The coordinates of the top-left corner of the tile as an array of two numbers. The coordinates are in un-normalized map units. */
  coords: [
      number,
      number
  ];
  /** The bounds of the tile as an array of four numbers that be readily converted to an [Extent](https://developers.arcgis.com/javascript/latest/references/core/geometry/Extent/) object. */
  bounds: [
      number,
      number,
      number,
      number
  ];
}

/**
 * Represents the [LayerView](https://developers.arcgis.com/javascript/latest/references/core/views/layers/LayerView/) of a [Layer](https://developers.arcgis.com/javascript/latest/references/core/layers/Layer/)
 * after it has been added to a [Map](https://developers.arcgis.com/javascript/latest/references/core/Map/) with a [MapView](https://developers.arcgis.com/javascript/latest/references/core/views/MapView/).
 *
 * This class may be extended to create a custom [LayerView](https://developers.arcgis.com/javascript/latest/references/core/views/layers/LayerView/) for a Layer.
 * A [LayerView](https://developers.arcgis.com/javascript/latest/references/core/views/layers/LayerView/) is created on demand by the [MapView](https://developers.arcgis.com/javascript/latest/references/core/views/MapView/) when a layer is
 * added the to list of layers of its map.
 *
 * The subclass can implement multiple functions of the LayerView lifecycle. First, the [attach()](https://developers.arcgis.com/javascript/latest/references/core/views/2d/layers/BaseLayerView2D/#attach) method
 * is called when the LayerView is about to start drawing the layer's content. Then during the life of the LayerView,
 * the [render()](https://developers.arcgis.com/javascript/latest/references/core/views/2d/layers/BaseLayerView2D/#render) method is called during the MapView rendering phase. The `render()` method has access to a canvas 2d context
 * in which it can render the content available for display. Finally the [detach()](https://developers.arcgis.com/javascript/latest/references/core/views/2d/layers/BaseLayerView2D/#detach) method is called after the layer
 * is removed from the map. It releases all allocated resources and stops on-going requests.
 *
 * @since 4.8
 * @example
 * let TileBorderLayerView2D = BaseLayerView2D.createSubclass({
 *    // Example of a render implementation that draws tile boundaries
 *    render(renderParameters) {
 *      let tileSize = this.layer.tileInfo.size[0];
 *      let state = renderParameters.state;
 *      let pixelRatio = state.pixelRatio;
 *      let width = state.size[0];
 *      let height = state.size[1];
 *      let context = renderParameters.context;
 *      let coords = [0, 0];
 *
 *      context.fillStyle = "rgba(0,0,0,0.25)";
 *      context.fillRect(0, 0, width * pixelRatio, height * pixelRatio);
 *
 *      // apply rotation for everything that will be applied to the canvas
 *      if (state.rotation !== 0) {
 *        context.translate(width * pixelRatio * 0.5, height * pixelRatio * 0.5);
 *        context.rotate((state.rotation * Math.PI) / 180);
 *        context.translate(- width * pixelRatio * 0.5, -height * pixelRatio * 0.5);
 *      }
 *
 *      // Set the style for all the text.
 *      context.font = "24px monospace";
 *      context.fillStyle = "black";
 *      context.shadowBlur = 1;
 *
 *      for (const tile of this.tiles) {
 *        let screenScale = tile.resolution / state.resolution * pixelRatio;
 *
 *        state.toScreenNoRotation(coords, tile.coords);
 *
 *        // Draw the tile boundaries
 *        context.strokeRect(coords[0], coords[1], tileSize * screenScale, tileSize * screenScale);
 *
 *        // Draw the tile information
 *        context.shadowColor = "white";
 *        context.fillText(
 *          tile.level + "/" + tile.row + "/" + tile.col + "/" + tile.world,
 *          coords[0] + 12,
 *          coords[1] + 24,
 *          tileSize * screenScale
 *        );
 *        context.shadowColor = "transparent";
 *      }
 *    }
 *  });
 *
 *  let CustomTileLayer = Layer.createSubclass({
 *    tileInfo: TileInfo.create({ spatialReference: { wkid: 3857 }}),
 *
 *    createLayerView(view) {
 *      if (view.type === "2d") {
 *        return new TileBorderLayerView2D({
 *          view: view,
 *          layer: this
 *        });
 *      }
 *    }
 *  });
 */
export default abstract class BaseLayerView2D extends LayerView {
  constructor(properties?: BaseLayerView2DProperties);
  /** References the layer this [LayerView](https://developers.arcgis.com/javascript/latest/references/core/views/layers/LayerView/) represents. */
  get layer(): Layer;
  /**
   * The array of [Tile](https://developers.arcgis.com/javascript/latest/references/core/views/2d/layers/BaseLayerView2D/#Tile) objects computed to cover the MapView's visible area.
   * This array is updated when the view is animating or the user is interacting with it. Then if tiles have been added or removed,
   * [tilesChanged()](https://developers.arcgis.com/javascript/latest/references/core/views/2d/layers/BaseLayerView2D/#tilesChanged) is called.
   *
   * @see [tilesChanged()](https://developers.arcgis.com/javascript/latest/references/core/views/2d/layers/BaseLayerView2D/#tilesChanged)
   */
  tiles: Tile[];
  /**
   * References the [MapView](https://developers.arcgis.com/javascript/latest/references/core/views/MapView/) this [LayerView](https://developers.arcgis.com/javascript/latest/references/core/views/layers/LayerView/) belongs to.
   *
   * @since 4.28
   * @example
   * // Check for the first time layerView.updating becomes false. Then query for
   * // features that are visible within the view associated with the layer view.
   * await reactiveUtils.whenOnce(() => !layerView.updating);
   * const query = layerView.createQuery();
   * query.geometry = layerView.view.extent;
   * const result = layerView.queryFeatures(query);
   */
  get view(): MapView;
  /**
   * Method called when after the [LayerView](https://developers.arcgis.com/javascript/latest/references/core/views/layers/LayerView/) is created and right before it's asked to draw the layer's content.
   * Typically this method is implemented to start watching property changes on the layer for example.
   *
   * @see [detach()](https://developers.arcgis.com/javascript/latest/references/core/views/2d/layers/BaseLayerView2D/#detach)
   * @example
   * attach() {
   *   this._propertyHandle = reactiveUtils.watch(
   *     () => this.layer.opacity,
   *     () => this.requestRender()
   *   );
   * }
   */
  attach(): void;
  /**
   * Method called after the layer is removed and the [LayerView](https://developers.arcgis.com/javascript/latest/references/core/views/layers/LayerView/) is about to be removed.
   * Typically, this method is implemented to free resources like watchers.
   *
   * @see [attach()](https://developers.arcgis.com/javascript/latest/references/core/views/2d/layers/BaseLayerView2D/#attach)
   * @example
   * // remove the watchers on the layer that are added in attach()
   * detach() {
   *   this._propertyHandle.remove();
   *   this._propertyHandle = null;
   * }
   */
  detach(): void;
  /**
   * Method to implement that is responsible for providing objects hit at the specified screen coordinates.
   * This method is called internally by the [MapView](https://developers.arcgis.com/javascript/latest/references/core/views/MapView/) each time
   * its [MapView.hitTest()](https://developers.arcgis.com/javascript/latest/references/core/views/MapView/#hitTest) method is called.
   *
   * @param mapPoint - The point in map units.
   * @param screenPoint - The point in screen coordinates.
   * @returns A Promise that resolves to an array of hits.
   */
  hitTest(mapPoint: Point, screenPoint: ScreenPoint): Promise<ViewHit[] | null | undefined>;
  /**
   * The method to implement that is responsible of drawing the content of the layer.
   * This method is called every time the MapView's state changes, or if [requestRender()](https://developers.arcgis.com/javascript/latest/references/core/views/2d/layers/BaseLayerView2D/#requestRender) has been called.
   *
   * @param renderParameters
   * @example
   * // Example of a render implementation that draws tile boundaries
   * render(renderParameters) {
   *   let tileSize = this.layer.tileInfo.size[0];
   *   let state = renderParameters.state;
   *   let pixelRatio = state.pixelRatio;
   *   let width = state.size[0];
   *   let height = state.size[1];
   *   let context = renderParameters.context;
   *   let coords = [0, 0];
   *
   *   context.fillStyle = "rgba(0,0,0,0.25)";
   *   context.fillRect(0, 0, width * pixelRatio, height * pixelRatio);
   *
   *   // apply rotation for everything that will be applied to the canvas
   *   if (state.rotation !== 0) {
   *     context.translate(width * pixelRatio * 0.5, height * pixelRatio * 0.5);
   *     context.rotate((state.rotation * Math.PI) / 180);
   *     context.translate(- width * pixelRatio * 0.5, -height * pixelRatio * 0.5);
   *   }
   *
   *   // Set the style for all the text.
   *   context.font = "24px monospace";
   *   context.fillStyle = "black";
   *   context.shadowBlur = 1;
   *
   *   for (const tile of this.tiles) {
   *     let screenScale = tile.resolution / state.resolution * pixelRatio;
   *
   *     state.toScreenNoRotation(coords, tile.coords);
   *
   *     // Draw the tile boundaries
   *     context.strokeRect(coords[0], coords[1], tileSize * screenScale, tileSize * screenScale);
   *
   *     // Draw the tile information
   *     context.shadowColor = "white";
   *     context.fillText(
   *       tile.level + "/" + tile.row + "/" + tile.col + "/" + tile.world,
   *       coords[0] + 12,
   *       coords[1] + 24,
   *       tileSize * screenScale
   *     );
   *     context.shadowColor = "transparent";
   *   }
   * }
   */
  abstract render(renderParameters: RenderParameters): void;
  /**
   * The [LayerView](https://developers.arcgis.com/javascript/latest/references/core/views/layers/LayerView/) can call this method to ask the MapView to schedule a new rendering frame.
   *
   * @example
   * // Call requestRender whenever the layer opacity has changed.
   * attach() {
   *   this._propertyHandle = reactiveUtils.watch(
   *     () => this.layer.opacity,
   *     () => this.requestRender()
   *   );
   * }
   */
  requestRender(): void;
  /**
   * Method to implement, which notifies of tiles being added or removed for the current view state.
   * This function can be implemented to start and stop fetching new data, or allocate and dispose resources.
   *
   * @param added - The tile objects added for the current view viewport.
   * @param removed - The tile objects removed from the view viewport.
   */
  tilesChanged(added: Tile[], removed: Tile[]): void;
}