// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import Viewport from '../viewports/viewport';

import {Matrix4, clamp, vec2} from '@math.gl/core';
import {pixelsToWorld} from '@math.gl/web-mercator';

import type {Padding} from './viewport';

const viewMatrix = new Matrix4().lookAt({eye: [0, 0, 1]});

function getProjectionMatrix({
  width,
  height,
  near,
  far,
  padding
}: {
  width: number;
  height: number;
  near: number;
  far: number;
  padding: Padding | null;
}) {
  let left = -width / 2;
  let right = width / 2;
  let bottom = -height / 2;
  let top = height / 2;
  if (padding) {
    const {left: l = 0, right: r = 0, top: t = 0, bottom: b = 0} = padding;
    const offsetX = clamp((l + width - r) / 2, 0, width) - width / 2;
    const offsetY = clamp((t + height - b) / 2, 0, height) - height / 2;
    left -= offsetX;
    right -= offsetX;
    bottom += offsetY;
    top += offsetY;
  }

  return new Matrix4().ortho({
    left,
    right,
    bottom,
    top,
    near,
    far
  });
}

export type OrthographicViewportOptions = {
  /** Name of the viewport */
  id?: string;
  /** Left offset from the canvas edge, in pixels */
  x?: number;
  /** Top offset from the canvas edge, in pixels */
  y?: number;
  /** Viewport width in pixels */
  width?: number;
  /** Viewport height in pixels */
  height?: number;
  /** The world position at the center of the viewport. Default `[0, 0, 0]`. */
  target?: [number, number, number] | [number, number];
  /**  The zoom level of the viewport. `zoom: 0` maps one unit distance to one pixel on screen, and increasing `zoom` by `1` scales the same object to twice as large.
   *   To apply independent zoom levels to the X and Y axes, supply an array `[zoomX, zoomY]`. Default `0`. */
  zoom?: number | [number, number];
  /** Independent zoom along the X axis. Overrides `zoom`. */
  zoomX?: number;
  /** Independent zoom along the Y axis. Overrides `zoom`. */
  zoomY?: number;
  /** Padding around the viewport, in pixels. */
  padding?: Padding | null;
  /** Distance of near clipping plane. Default `0.1`. */
  near?: number;
  /** Distance of far clipping plane. Default `1000`. */
  far?: number;
  /** Whether to use top-left coordinates (`true`) or bottom-left coordinates (`false`). Default `true`. */
  flipY?: boolean;
};

export default class OrthographicViewport extends Viewport {
  static displayName = 'OrthographicViewport';

  target: [number, number, number] | [number, number];
  zoomX: number;
  zoomY: number;
  flipY: boolean;

  constructor(props: OrthographicViewportOptions) {
    const {
      width,
      height,
      near = 0.1,
      far = 1000,
      zoom = 0,
      target = [0, 0, 0],
      padding = null,
      flipY = true
    } = props;
    const zoomX = props.zoomX ?? (Array.isArray(zoom) ? zoom[0] : zoom);
    const zoomY = props.zoomY ?? (Array.isArray(zoom) ? zoom[1] : zoom);
    const zoom_ = Math.min(zoomX, zoomY);
    const scale = Math.pow(2, zoom_);

    let distanceScales;
    if (zoomX !== zoomY) {
      const scaleX = Math.pow(2, zoomX);
      const scaleY = Math.pow(2, zoomY);

      distanceScales = {
        unitsPerMeter: [scaleX / scale, scaleY / scale, 1],
        metersPerUnit: [scale / scaleX, scale / scaleY, 1]
      };
    }

    super({
      ...props,
      // in case viewState contains longitude/latitude values,
      // make sure that the base Viewport class does not treat this as a geospatial viewport
      longitude: undefined,
      position: target,
      viewMatrix: viewMatrix.clone().scale([scale, scale * (flipY ? -1 : 1), scale]),
      projectionMatrix: getProjectionMatrix({
        width: width || 1,
        height: height || 1,
        padding,
        near,
        far
      }),
      zoom: zoom_,
      distanceScales
    });

    this.target = target;
    this.zoomX = zoomX;
    this.zoomY = zoomY;
    this.flipY = flipY;
  }

  projectFlat([X, Y]: number[]): [number, number] {
    const {unitsPerMeter} = this.distanceScales;
    return [X * unitsPerMeter[0], Y * unitsPerMeter[1]];
  }

  unprojectFlat([x, y]: number[]): [number, number] {
    const {metersPerUnit} = this.distanceScales;
    return [x * metersPerUnit[0], y * metersPerUnit[1]];
  }

  /* Needed by LinearInterpolator */
  panByPosition(
    coords: number[],
    pixel: number[],
    startPixel?: number[]
  ): OrthographicViewportOptions {
    const fromLocation = pixelsToWorld(pixel, this.pixelUnprojectionMatrix);
    const toLocation = this.projectFlat(coords);

    const translate = vec2.add([], toLocation, vec2.negate([], fromLocation));
    const newCenter = vec2.add([], this.center, translate);

    return {target: this.unprojectFlat(newCenter)};
  }
}
