import {assert} from './assert';
import {log2, clamp} from './math-utils';
import {MAX_LATITUDE, lngLatToWorld, worldToLngLat} from './web-mercator-utils';

/**
 * Options for fitBounds
 */
export type FitBoundsOptions = {
  /**  viewport width  */
  width: number;
  /**  viewport height  */
  height: number;
  /**  [[lon, lat], [lon, lat]]  */
  bounds: [[number, number], [number, number]];
  /**  The width/height of the bounded area will never be smaller than this. 0.01 would be about 1000 meters (degree is ~110KM)  */
  minExtent?: number;
  /**  The maximum zoom level to fit the bounds within. */
  maxZoom?: number; // ~x4,000,000 => About 10 meter extents
  /**
   * padding - The amount of padding in pixels to add to the given bounds.
   * Can also be an object with top, bottom, left and right properties defining the padding.
   */
  padding?: number | Padding;
  /** The center of the given bounds relative to the map's center,  */
  offset?: number[];
};

/**
 * An object describing the padding to add to the bounds.
 */
export type Padding = {
  /** Padding from top in pixels to add to the given bounds */
  top: number;
  /** Padding from bottom in pixels to add to the given bounds */
  bottom: number;
  /** Padding from left in pixels to add to the given bounds */
  left: number;
  /** Padding from right in pixels to add to the given bounds */
  right: number;
};

type ViewportProps = {
  longitude: number;
  latitude: number;
  zoom: number;
};

/**
 * Returns map settings {latitude, longitude, zoom}
 * that will contain the provided corners within the provided width.
 *
 * > _Note: Only supports non-perspective mode._
 *
 * @param options fit bounds parameters
 * @returns - latitude, longitude and zoom
 */
export function fitBounds(options: FitBoundsOptions): ViewportProps {
  const {
    width,
    height,
    bounds,
    minExtent = 0, // 0.01 would be about 1000 meters (degree is ~110KM)
    maxZoom = 24, // ~x4,000,000 => About 10 meter extents
    offset = [0, 0]
  } = options;

  const [[west, south], [east, north]] = bounds;
  const padding = getPaddingObject(options.padding);

  const nw = lngLatToWorld([west, clamp(north, -MAX_LATITUDE, MAX_LATITUDE)]);
  const se = lngLatToWorld([east, clamp(south, -MAX_LATITUDE, MAX_LATITUDE)]);

  // width/height on the Web Mercator plane
  const size = [
    Math.max(Math.abs(se[0] - nw[0]), minExtent),
    Math.max(Math.abs(se[1] - nw[1]), minExtent)
  ];

  const targetSize = [
    width - padding.left - padding.right - Math.abs(offset[0]) * 2,
    height - padding.top - padding.bottom - Math.abs(offset[1]) * 2
  ];

  assert(targetSize[0] > 0 && targetSize[1] > 0);

  // scale = screen pixels per unit on the Web Mercator plane
  const scaleX = targetSize[0] / size[0];
  const scaleY = targetSize[1] / size[1];

  // Find how much we need to shift the center
  const offsetX = (padding.right - padding.left) / 2 / scaleX;
  const offsetY = (padding.top - padding.bottom) / 2 / scaleY;

  const center = [(se[0] + nw[0]) / 2 + offsetX, (se[1] + nw[1]) / 2 + offsetY];

  const centerLngLat = worldToLngLat(center);
  const zoom = Math.min(maxZoom, log2(Math.abs(Math.min(scaleX, scaleY))));

  assert(Number.isFinite(zoom));

  return {
    longitude: centerLngLat[0],
    latitude: centerLngLat[1],
    zoom
  };
}

// Helpers
function getPaddingObject(padding: number | Padding = 0): Padding {
  if (typeof padding === 'number') {
    return {
      top: padding,
      bottom: padding,
      left: padding,
      right: padding
    };
  }

  // Make sure all the required properties are set
  assert(
    Number.isFinite(padding.top) &&
      Number.isFinite(padding.bottom) &&
      Number.isFinite(padding.left) &&
      Number.isFinite(padding.right)
  );

  return padding;
}
