import log from '../utils/log';
import { createMat4, getCameraPosition, getFrustumPlanes } from '../utils/math-utils';
import { Matrix4, Vector3, equals, clamp, mat4 } from '@math.gl/core';
import { getDistanceScales, getMeterZoom, lngLatToWorld, worldToLngLat, worldToPixels, pixelsToWorld } from '@math.gl/web-mercator';
import { PROJECTION_MODE } from '../lib/constants';
const DEGREES_TO_RADIANS = Math.PI / 180;
const IDENTITY = createMat4();
const ZERO_VECTOR = [0, 0, 0];
const DEFAULT_DISTANCE_SCALES = {
  unitsPerMeter: [1, 1, 1],
  metersPerUnit: [1, 1, 1]
};

function createProjectionMatrix({
  width,
  height,
  orthographic,
  fovyRadians,
  focalDistance,
  padding,
  near,
  far
}) {
  const aspect = width / height;
  const matrix = orthographic ? new Matrix4().orthographic({
    fovy: fovyRadians,
    aspect,
    focalDistance,
    near,
    far
  }) : new Matrix4().perspective({
    fovy: fovyRadians,
    aspect,
    near,
    far
  });

  if (padding) {
    const {
      left = 0,
      right = 0,
      top = 0,
      bottom = 0
    } = padding;
    const offsetX = clamp((left + width - right) / 2, 0, width) - width / 2;
    const offsetY = clamp((top + height - bottom) / 2, 0, height) - height / 2;
    matrix[8] -= offsetX * 2 / width;
    matrix[9] += offsetY * 2 / height;
  }

  return matrix;
}

export default class Viewport {
  constructor(opts = {}) {
    this.id = void 0;
    this.x = void 0;
    this.y = void 0;
    this.width = void 0;
    this.height = void 0;
    this.padding = void 0;
    this.isGeospatial = void 0;
    this.zoom = void 0;
    this.focalDistance = void 0;
    this.position = void 0;
    this.modelMatrix = void 0;
    this.distanceScales = void 0;
    this.scale = void 0;
    this.center = void 0;
    this.cameraPosition = void 0;
    this.projectionMatrix = void 0;
    this.viewMatrix = void 0;
    this.viewMatrixUncentered = void 0;
    this.viewMatrixInverse = void 0;
    this.viewProjectionMatrix = void 0;
    this.pixelProjectionMatrix = void 0;
    this.pixelUnprojectionMatrix = void 0;
    this.resolution = void 0;
    this._frustumPlanes = {};
    this.id = opts.id || this.constructor.displayName || 'viewport';
    this.x = opts.x || 0;
    this.y = opts.y || 0;
    this.width = opts.width || 1;
    this.height = opts.height || 1;
    this.zoom = opts.zoom || 0;
    this.padding = opts.padding;
    this.distanceScales = opts.distanceScales || DEFAULT_DISTANCE_SCALES;
    this.focalDistance = opts.focalDistance || 1;
    this.position = opts.position || ZERO_VECTOR;
    this.modelMatrix = opts.modelMatrix || null;
    const {
      longitude,
      latitude
    } = opts;
    this.isGeospatial = Number.isFinite(latitude) && Number.isFinite(longitude);

    this._initProps(opts);

    this._initMatrices(opts);

    this.equals = this.equals.bind(this);
    this.project = this.project.bind(this);
    this.unproject = this.unproject.bind(this);
    this.projectPosition = this.projectPosition.bind(this);
    this.unprojectPosition = this.unprojectPosition.bind(this);
    this.projectFlat = this.projectFlat.bind(this);
    this.unprojectFlat = this.unprojectFlat.bind(this);
  }

  get subViewports() {
    return null;
  }

  get metersPerPixel() {
    return this.distanceScales.metersPerUnit[2] / this.scale;
  }

  get projectionMode() {
    if (this.isGeospatial) {
      return this.zoom < 12 ? PROJECTION_MODE.WEB_MERCATOR : PROJECTION_MODE.WEB_MERCATOR_AUTO_OFFSET;
    }

    return PROJECTION_MODE.IDENTITY;
  }

  equals(viewport) {
    if (!(viewport instanceof Viewport)) {
      return false;
    }

    if (this === viewport) {
      return true;
    }

    return viewport.width === this.width && viewport.height === this.height && viewport.scale === this.scale && equals(viewport.projectionMatrix, this.projectionMatrix) && equals(viewport.viewMatrix, this.viewMatrix);
  }

  project(xyz, {
    topLeft = true
  } = {}) {
    const worldPosition = this.projectPosition(xyz);
    const coord = worldToPixels(worldPosition, this.pixelProjectionMatrix);
    const [x, y] = coord;
    const y2 = topLeft ? y : this.height - y;
    return xyz.length === 2 ? [x, y2] : [x, y2, coord[2]];
  }

  unproject(xyz, {
    topLeft = true,
    targetZ
  } = {}) {
    const [x, y, z] = xyz;
    const y2 = topLeft ? y : this.height - y;
    const targetZWorld = targetZ && targetZ * this.distanceScales.unitsPerMeter[2];
    const coord = pixelsToWorld([x, y2, z], this.pixelUnprojectionMatrix, targetZWorld);
    const [X, Y, Z] = this.unprojectPosition(coord);

    if (Number.isFinite(z)) {
      return [X, Y, Z];
    }

    return Number.isFinite(targetZ) ? [X, Y, targetZ] : [X, Y];
  }

  projectPosition(xyz) {
    const [X, Y] = this.projectFlat(xyz);
    const Z = (xyz[2] || 0) * this.distanceScales.unitsPerMeter[2];
    return [X, Y, Z];
  }

  unprojectPosition(xyz) {
    const [X, Y] = this.unprojectFlat(xyz);
    const Z = (xyz[2] || 0) * this.distanceScales.metersPerUnit[2];
    return [X, Y, Z];
  }

  projectFlat(xyz) {
    if (this.isGeospatial) {
      const result = lngLatToWorld(xyz);
      result[1] = clamp(result[1], -318, 830);
      return result;
    }

    return xyz;
  }

  unprojectFlat(xyz) {
    if (this.isGeospatial) {
      return worldToLngLat(xyz);
    }

    return xyz;
  }

  getBounds(options = {}) {
    const unprojectOption = {
      targetZ: options.z || 0
    };
    const topLeft = this.unproject([0, 0], unprojectOption);
    const topRight = this.unproject([this.width, 0], unprojectOption);
    const bottomLeft = this.unproject([0, this.height], unprojectOption);
    const bottomRight = this.unproject([this.width, this.height], unprojectOption);
    return [Math.min(topLeft[0], topRight[0], bottomLeft[0], bottomRight[0]), Math.min(topLeft[1], topRight[1], bottomLeft[1], bottomRight[1]), Math.max(topLeft[0], topRight[0], bottomLeft[0], bottomRight[0]), Math.max(topLeft[1], topRight[1], bottomLeft[1], bottomRight[1])];
  }

  getDistanceScales(coordinateOrigin) {
    if (coordinateOrigin) {
      return getDistanceScales({
        longitude: coordinateOrigin[0],
        latitude: coordinateOrigin[1],
        highPrecision: true
      });
    }

    return this.distanceScales;
  }

  containsPixel({
    x,
    y,
    width = 1,
    height = 1
  }) {
    return x < this.x + this.width && this.x < x + width && y < this.y + this.height && this.y < y + height;
  }

  getFrustumPlanes() {
    if (this._frustumPlanes.near) {
      return this._frustumPlanes;
    }

    Object.assign(this._frustumPlanes, getFrustumPlanes(this.viewProjectionMatrix));
    return this._frustumPlanes;
  }

  panByPosition(coords, pixel) {
    return null;
  }

  _initProps(opts) {
    const longitude = opts.longitude;
    const latitude = opts.latitude;

    if (this.isGeospatial) {
      if (!Number.isFinite(opts.zoom)) {
        this.zoom = getMeterZoom({
          latitude
        }) + Math.log2(this.focalDistance);
      }

      this.distanceScales = opts.distanceScales || getDistanceScales({
        latitude,
        longitude
      });
    }

    const scale = Math.pow(2, this.zoom);
    this.scale = scale;
    const {
      position,
      modelMatrix
    } = opts;
    let meterOffset = ZERO_VECTOR;

    if (position) {
      meterOffset = modelMatrix ? new Matrix4(modelMatrix).transformAsVector(position, []) : position;
    }

    if (this.isGeospatial) {
      const center = this.projectPosition([longitude, latitude, 0]);
      this.center = new Vector3(meterOffset).scale(this.distanceScales.unitsPerMeter).add(center);
    } else {
      this.center = this.projectPosition(meterOffset);
    }
  }

  _initMatrices(opts) {
    const {
      viewMatrix = IDENTITY,
      projectionMatrix = null,
      orthographic = false,
      fovyRadians,
      fovy = 75,
      near = 0.1,
      far = 1000,
      padding = null,
      focalDistance = 1
    } = opts;
    this.viewMatrixUncentered = viewMatrix;
    this.viewMatrix = new Matrix4().multiplyRight(viewMatrix).translate(new Vector3(this.center).negate());
    this.projectionMatrix = projectionMatrix || createProjectionMatrix({
      width: this.width,
      height: this.height,
      orthographic,
      fovyRadians: fovyRadians || fovy * DEGREES_TO_RADIANS,
      focalDistance,
      padding,
      near,
      far
    });
    const vpm = createMat4();
    mat4.multiply(vpm, vpm, this.projectionMatrix);
    mat4.multiply(vpm, vpm, this.viewMatrix);
    this.viewProjectionMatrix = vpm;
    this.viewMatrixInverse = mat4.invert([], this.viewMatrix) || this.viewMatrix;
    this.cameraPosition = getCameraPosition(this.viewMatrixInverse);
    const viewportMatrix = createMat4();
    const pixelProjectionMatrix = createMat4();
    mat4.scale(viewportMatrix, viewportMatrix, [this.width / 2, -this.height / 2, 1]);
    mat4.translate(viewportMatrix, viewportMatrix, [1, -1, 0]);
    mat4.multiply(pixelProjectionMatrix, viewportMatrix, this.viewProjectionMatrix);
    this.pixelProjectionMatrix = pixelProjectionMatrix;
    this.pixelUnprojectionMatrix = mat4.invert(createMat4(), this.pixelProjectionMatrix);

    if (!this.pixelUnprojectionMatrix) {
      log.warn('Pixel project matrix not invertible')();
    }
  }

}
Viewport.displayName = 'Viewport';
//# sourceMappingURL=viewport.js.map