import { Margins } from "../figurl-canvas";
import { matrix, Matrix } from "mathjs";
import { useMemo } from "react";

// TODO: Other canvas transform computation code (e.g. in TimeScrollView) should be combined in here
// TODO: Delete the equivalent stuff out of Geometry.ts, it was overbuilt

export const nullTransformMatrix = matrix([
  [0, 0, 0],
  [0, 0, 0],
]);

/**
 * @param pixelWidth Width of the drawing space, in pixels. This *includes* the margins (which will be subtracted internally).
 * @param pixelHeight Height of the drawing space, in pixels. Highest values will be at the visual bottom of the panel.
 * @param margins Pixel dimensions for offsets that establish the drawing space within the Canvas' width and height.
 * @param xmin Minimum observed x-value in the data.
 * @param xmax Maximum observed x-value in the data.
 * @param ymin Minimum observed y-value in the data.
 * @param ymax Maximum observed y-value in the data.
 * @param invertY Default true. If set, the returned matrix will flip the y-axis so that the origin is in the lower (not upper) left corner of the canvas.
 * You probably want this.
 * @param preserveDataAspect Default true. If set, the returned matrix will ensure that the effective drawing space matches the aspect ratio of the
 * input data. You probably want this, but might not.
 */
export type TwoDTransformProps = {
  pixelWidth: number;
  pixelHeight: number;
  margins: Margins;
  xmin: number;
  xmax: number;
  ymin: number;
  ymax: number;
  invertY?: boolean;
  preserveDataAspect?: boolean;
};

/**
 * @param preserveDataAspect (Optional) If false, this hook is a no-op.
 * @param pixelWidth Width of the available drawing canvas, in pixels.
 * @param pixelHeight Height of the available drawing canvas, in pixles.
 * @param margins Desired minimal margins for the drawing space, in pixels. Any additional margin needed
 * to preserve the aspect ratio of the underlying data will be added to these margins.
 * @param xrange Range of x-values in the native-space data (i.e. xmax - xmin)
 * @param yrange Range of y-values in the native-space data (i.e. ymax - ymin)
 */
export type AspectTrimProps = {
  preserveDataAspect?: boolean;
  pixelWidth: number;
  pixelHeight: number;
  margins?: Margins;
  xrange: number;
  yrange: number;
};

export const emptyMargins = { top: 0, left: 0, bottom: 0, right: 0 };

/**
 * Hook returns an updated set of margins which ensure that the actual drawing space will be a rectangle that preserves
 * the aspect ratio of the underlying data set. The margins also ensure the resulting rectangle is centered in the
 * available canvas space.
 *
 * @param props
 * @returns Final set of margins that correctly size and position the drawing space, centered in the canvas.
 */
export const useAspectTrimming = (props: AspectTrimProps): Margins => {
  const {
    preserveDataAspect,
    pixelHeight,
    pixelWidth,
    margins,
    xrange,
    yrange,
  } = props;
  const baseMargins = margins || emptyMargins;
  return useMemo(() => {
    if (
      pixelWidth === undefined ||
      pixelWidth === 0 ||
      pixelHeight === undefined ||
      pixelHeight === 0 ||
      xrange <= 0 ||
      yrange <= 0
    ) {
      // TODO: Warn about this? Or is it expected?
      return baseMargins;
    }
    if (preserveDataAspect === undefined || preserveDataAspect) {
      const dataAspect = xrange / yrange;
      const effectiveCanvasWidth =
        pixelWidth - baseMargins.left - baseMargins.right;
      const effectiveCanvasHeight =
        pixelHeight - baseMargins.top - baseMargins.bottom;
      const canvasAspect = effectiveCanvasWidth / effectiveCanvasHeight;
      if (dataAspect > canvasAspect) {
        // data width / data height > canvasx / canvas y --> trim canvas height
        const rightHeight = effectiveCanvasWidth / dataAspect;
        const trimAmt = Math.floor((effectiveCanvasHeight - rightHeight) / 2);
        return {
          ...baseMargins,
          top: baseMargins.top + trimAmt,
          bottom: baseMargins.bottom + trimAmt,
        };
      } else if (dataAspect < canvasAspect) {
        // datax / datay < canvasx / canvasy --> trim canvas width
        const rightWidth = effectiveCanvasHeight * dataAspect;
        const trimAmt = Math.floor((effectiveCanvasWidth - rightWidth) / 2);
        return {
          ...baseMargins,
          left: baseMargins.left + trimAmt,
          right: baseMargins.right + trimAmt,
        };
      }
    }
    return baseMargins;
  }, [
    baseMargins,
    pixelHeight,
    pixelWidth,
    preserveDataAspect,
    xrange,
    yrange,
  ]);
};

/**
 * Returns a (memoized) transformation matrix mapping from data units to the pixel units for a given-sized Canvas panel.
 * The panel is assumed to run from [0, panelWidth] in the x-direction and [panelHeight, 0] in the y-direction.
 *
 * @param props The properties of the data to be projected and the drawing space to project it into.
 *
 * @returns Transformation matrix that can left-multiply an [[xs], [ys], [1]] augmented data matrix to yield the corresponding pixel positions.
 */
export const use2DTransformationMatrix = (
  props: TwoDTransformProps,
): Matrix => {
  const {
    pixelWidth,
    pixelHeight,
    margins,
    xmin,
    xmax,
    ymin,
    ymax,
    invertY,
    preserveDataAspect,
  } = props;
  const _invertY = invertY === undefined ? true : invertY;
  const xrange = xmax - xmin;
  const yrange = ymax - ymin;
  const finalMargins = useAspectTrimming({
    preserveDataAspect,
    pixelWidth,
    pixelHeight,
    xrange,
    yrange,
    margins,
  });

  return useMemo(() => {
    if (
      pixelWidth === undefined ||
      pixelWidth === 0 ||
      pixelHeight === undefined ||
      pixelHeight === 0
    ) {
      console.warn(
        "Attempt to compute drawing matrix with degenerate canvas shape.",
      );
      return nullTransformMatrix;
    }
    if (xmax <= xmin || ymax <= ymin) {
      console.warn(
        "Error creating drawing matrix: data has a nonpositive range in one or both dimensions.",
      );
      return nullTransformMatrix;
    }

    const xScale =
      (pixelWidth - finalMargins.left - finalMargins.right) / xrange;
    const xOffset = xmin * -xScale + finalMargins.left;
    const yScale =
      ((_invertY ? -1 : 1) *
        (pixelHeight - finalMargins.top - finalMargins.bottom)) /
      yrange;
    const yOffset = -yScale * (_invertY ? ymax : ymin) + finalMargins.top;
    // TODO: Something about user scaling for zoom?

    return matrix([
      [xScale, 0, xOffset],
      [0, yScale, yOffset],
    ]);
  }, [
    pixelWidth,
    pixelHeight,
    xmax,
    xmin,
    ymax,
    ymin,
    finalMargins.left,
    finalMargins.right,
    finalMargins.top,
    finalMargins.bottom,
    xrange,
    _invertY,
    yrange,
  ]);
};
