import {lerp} from './math-utils';
import {scaleToZoom, zoomToScale, lngLatToWorld, worldToLngLat} from './web-mercator-utils';
import {vec2} from '@math.gl/core';

import type {ViewportProps} from './normalize-viewport-props';

const EPSILON = 0.01;
const VIEWPORT_TRANSITION_PROPS = ['longitude', 'latitude', 'zoom'] as const;
const DEFAULT_OPTS = {
  curve: 1.414,
  speed: 1.2
  // screenSpeed and maxDuration are used only if specified
};

export type FlytoTransitionOptions = {
  curve?: number;
  speed?: number;
  screenSpeed?: number;
  maxDuration?: number;
};

/**
 * mapbox-gl-js flyTo : https://www.mapbox.com/mapbox-gl-js/api/#map#flyto.
 * It implements “Smooth and efficient zooming and panning.” algorithm by
 * "Jarke J. van Wijk and Wim A.A. Nuij"
 */
export function flyToViewport(
  startProps: ViewportProps,
  endProps: ViewportProps,
  t: number,
  options?: FlytoTransitionOptions
): {
  longitude: number;
  latitude: number;
  zoom: number;
} {
  // Equations from above paper are referred where needed.

  const {startZoom, startCenterXY, uDelta, w0, u1, S, rho, rho2, r0} = getFlyToTransitionParams(
    startProps,
    endProps,
    options
  );

  // If change in center is too small, do linear interpolaiton.
  if (u1 < EPSILON) {
    const viewport = {};
    for (const key of VIEWPORT_TRANSITION_PROPS) {
      const startValue = startProps[key];
      const endValue = endProps[key];
      // @ts-ignore-error properties are populated dynamically
      viewport[key] = lerp(startValue, endValue, t);
    }
    // @ts-expect-error properties are populated dynamically
    return viewport;
  }

  const s = t * S;

  const w = Math.cosh(r0) / Math.cosh(r0 + rho * s);
  const u = (w0 * ((Math.cosh(r0) * Math.tanh(r0 + rho * s) - Math.sinh(r0)) / rho2)) / u1;

  const scaleIncrement = 1 / w; // Using w method for scaling.
  const newZoom = startZoom + scaleToZoom(scaleIncrement);

  const newCenterWorld = vec2.scale([], uDelta, u);
  vec2.add(newCenterWorld, newCenterWorld, startCenterXY);

  const newCenter = worldToLngLat(newCenterWorld);
  return {
    longitude: newCenter[0],
    latitude: newCenter[1],
    zoom: newZoom
  };
}

// returns transition duration in milliseconds
export function getFlyToDuration(
  startProps: ViewportProps,
  endProps: ViewportProps,
  options?: FlytoTransitionOptions
): number {
  const opts = {...DEFAULT_OPTS, ...options};
  const {screenSpeed, speed, maxDuration} = opts;
  const {S, rho} = getFlyToTransitionParams(startProps, endProps, opts);
  const length = 1000 * S;
  let duration: number;
  if (Number.isFinite(screenSpeed)) {
    duration = length / (screenSpeed / rho);
  } else {
    duration = length / speed;
  }

  return Number.isFinite(maxDuration) && duration > maxDuration ? 0 : duration;
}

// Private Methods

// Calculate all parameters that are static for given startProps and endProps
function getFlyToTransitionParams(
  startProps: ViewportProps,
  endProps: ViewportProps,
  opts: FlytoTransitionOptions
): {
  startZoom: number;
  startCenterXY: number[];
  uDelta: number[];
  w0: number;
  u1: number;
  S: number;
  rho: number;
  rho2: number;
  r0: number;
  r1: number;
} {
  opts = Object.assign({}, DEFAULT_OPTS, opts);
  const rho = opts.curve;
  const startZoom = startProps.zoom;
  const startCenter = [startProps.longitude, startProps.latitude];
  const startScale = zoomToScale(startZoom);
  const endZoom = endProps.zoom;
  const endCenter = [endProps.longitude, endProps.latitude];
  const scale = zoomToScale(endZoom - startZoom);

  const startCenterXY = lngLatToWorld(startCenter);
  const endCenterXY = lngLatToWorld(endCenter);
  const uDelta = vec2.sub([] as number[], endCenterXY, startCenterXY);

  const w0 = Math.max(startProps.width, startProps.height);
  const w1 = w0 / scale;
  const u1 = vec2.length(uDelta) * startScale;
  // u0 is treated as '0' in Eq (9).

  // If u1 is too small, will generate invalid number
  const _u1 = Math.max(u1, EPSILON);

  // Implement Equation (9) from above algorithm.
  const rho2 = rho * rho;
  const b0 = (w1 * w1 - w0 * w0 + rho2 * rho2 * _u1 * _u1) / (2 * w0 * rho2 * _u1);
  const b1 = (w1 * w1 - w0 * w0 - rho2 * rho2 * _u1 * _u1) / (2 * w1 * rho2 * _u1);
  const r0 = Math.log(Math.sqrt(b0 * b0 + 1) - b0);
  const r1 = Math.log(Math.sqrt(b1 * b1 + 1) - b1);
  const S = (r1 - r0) / rho;

  return {startZoom, startCenterXY, uDelta, w0, u1, S, rho, rho2, r0, r1};
}
