import { AssetEditAction, AssetEditActionItem } from 'src/dtos/editing.dto';
import { AssetOcrResponseDto } from 'src/dtos/ocr.dto';
import { ImageDimensions } from 'src/types';
import { applyToPoint, compose, flipX, flipY, identity, Matrix, rotate, scale, translate } from 'transformation-matrix';

export const getOutputDimensions = (
  edits: AssetEditActionItem[],
  startingDimensions: ImageDimensions,
): ImageDimensions => {
  let { width, height } = startingDimensions;

  const crop = edits.find((edit) => edit.action === AssetEditAction.Crop);
  if (crop) {
    width = crop.parameters.width;
    height = crop.parameters.height;
  }

  for (const edit of edits) {
    if (edit.action === AssetEditAction.Rotate) {
      const angleDegrees = edit.parameters.angle;
      if (angleDegrees === 90 || angleDegrees === 270) {
        [width, height] = [height, width];
      }
    }
  }

  return { width, height };
};

export const createAffineMatrix = (
  edits: AssetEditActionItem[],
  scalingParameters?: {
    pointSpace: ImageDimensions;
    targetSpace: ImageDimensions;
  },
): Matrix => {
  let scalingMatrix: Matrix = identity();

  if (scalingParameters) {
    const { pointSpace, targetSpace } = scalingParameters;
    const scaleX = targetSpace.width / pointSpace.width;
    scalingMatrix = scale(scaleX);
  }

  return compose(
    scalingMatrix,
    ...edits.map((edit) => {
      switch (edit.action) {
        case 'rotate': {
          const angleInRadians = (-edit.parameters.angle * Math.PI) / 180;
          return rotate(angleInRadians);
        }
        case 'mirror': {
          return edit.parameters.axis === 'horizontal' ? flipY() : flipX();
        }
        default: {
          return identity();
        }
      }
    }),
  );
};

export type Point = { x: number; y: number };

type TransformState = {
  points: Point[];
  currentWidth: number;
  currentHeight: number;
};

/**
 * Transforms an array of points through a series of edit operations (crop, rotate, mirror).
 * Points should be in absolute pixel coordinates relative to the starting dimensions.
 */
export const transformPoints = (
  points: Point[],
  edits: AssetEditActionItem[],
  startingDimensions: ImageDimensions,
  { inverse = false } = {},
): TransformState => {
  let currentWidth = startingDimensions.width;
  let currentHeight = startingDimensions.height;
  let transformedPoints = [...points];

  // Handle crop first if not inverting
  if (!inverse) {
    const crop = edits.find((edit) => edit.action === 'crop');
    if (crop) {
      const { x: cropX, y: cropY, width: cropWidth, height: cropHeight } = crop.parameters;
      transformedPoints = transformedPoints.map((p) => ({
        x: p.x - cropX,
        y: p.y - cropY,
      }));
      currentWidth = cropWidth;
      currentHeight = cropHeight;
    }
  }

  // Apply rotate and mirror transforms
  const editSequence = inverse ? edits.toReversed() : edits;
  for (const edit of editSequence) {
    let matrix: Matrix = identity();
    if (edit.action === 'rotate') {
      const angleDegrees = edit.parameters.angle;
      const angleRadians = (angleDegrees * Math.PI) / 180;
      const newWidth = angleDegrees === 90 || angleDegrees === 270 ? currentHeight : currentWidth;
      const newHeight = angleDegrees === 90 || angleDegrees === 270 ? currentWidth : currentHeight;

      matrix = compose(
        translate(newWidth / 2, newHeight / 2),
        rotate(inverse ? -angleRadians : angleRadians),
        translate(-currentWidth / 2, -currentHeight / 2),
      );

      currentWidth = newWidth;
      currentHeight = newHeight;
    } else if (edit.action === 'mirror') {
      matrix = compose(
        translate(currentWidth / 2, currentHeight / 2),
        edit.parameters.axis === 'horizontal' ? flipY() : flipX(),
        translate(-currentWidth / 2, -currentHeight / 2),
      );
    } else {
      // Skip non-affine transformations
      continue;
    }

    transformedPoints = transformedPoints.map((p) => applyToPoint(matrix, p));
  }

  // Handle crop last if inverting
  if (inverse) {
    const crop = edits.find((edit) => edit.action === 'crop');
    if (crop) {
      const { x: cropX, y: cropY } = crop.parameters;
      transformedPoints = transformedPoints.map((p) => ({
        x: p.x + cropX,
        y: p.y + cropY,
      }));
    }
  }

  return {
    points: transformedPoints,
    currentWidth,
    currentHeight,
  };
};

type FaceBoundingBox = {
  boundingBoxX1: number;
  boundingBoxX2: number;
  boundingBoxY1: number;
  boundingBoxY2: number;
  imageWidth: number;
  imageHeight: number;
};

export const transformFaceBoundingBox = (
  box: FaceBoundingBox,
  edits: AssetEditActionItem[],
  imageDimensions: ImageDimensions,
): FaceBoundingBox => {
  if (edits.length === 0) {
    return box;
  }

  const scaleX = imageDimensions.width / box.imageWidth;
  const scaleY = imageDimensions.height / box.imageHeight;

  const points: Point[] = [
    { x: box.boundingBoxX1 * scaleX, y: box.boundingBoxY1 * scaleY },
    { x: box.boundingBoxX2 * scaleX, y: box.boundingBoxY2 * scaleY },
  ];

  const { points: transformedPoints, currentWidth, currentHeight } = transformPoints(points, edits, imageDimensions);

  // Ensure x1,y1 is top-left and x2,y2 is bottom-right
  const [p1, p2] = transformedPoints;
  return {
    boundingBoxX1: Math.min(p1.x, p2.x),
    boundingBoxY1: Math.min(p1.y, p2.y),
    boundingBoxX2: Math.max(p1.x, p2.x),
    boundingBoxY2: Math.max(p1.y, p2.y),
    imageWidth: currentWidth,
    imageHeight: currentHeight,
  };
};

const reorderQuadPointsForRotation = (points: Point[], rotationDegrees: number): Point[] => {
  const [p1, p2, p3, p4] = points;
  switch (rotationDegrees) {
    case 90: {
      return [p4, p1, p2, p3];
    }
    case 180: {
      return [p3, p4, p1, p2];
    }
    case 270: {
      return [p2, p3, p4, p1];
    }
    default: {
      return points;
    }
  }
};

export const transformOcrBoundingBox = (
  box: AssetOcrResponseDto,
  edits: AssetEditActionItem[],
  imageDimensions: ImageDimensions,
): AssetOcrResponseDto => {
  if (edits.length === 0) {
    return box;
  }

  const points: Point[] = [
    { x: box.x1 * imageDimensions.width, y: box.y1 * imageDimensions.height },
    { x: box.x2 * imageDimensions.width, y: box.y2 * imageDimensions.height },
    { x: box.x3 * imageDimensions.width, y: box.y3 * imageDimensions.height },
    { x: box.x4 * imageDimensions.width, y: box.y4 * imageDimensions.height },
  ];

  const { points: transformedPoints, currentWidth, currentHeight } = transformPoints(points, edits, imageDimensions);

  // Reorder points to maintain semantic ordering (topLeft, topRight, bottomRight, bottomLeft)
  const netRotation = edits.find((e) => e.action == AssetEditAction.Rotate)?.parameters.angle ?? 0 % 360;
  const reorderedPoints = reorderQuadPointsForRotation(transformedPoints, netRotation);

  const [p1, p2, p3, p4] = reorderedPoints;
  return {
    ...box,
    x1: p1.x / currentWidth,
    y1: p1.y / currentHeight,
    x2: p2.x / currentWidth,
    y2: p2.y / currentHeight,
    x3: p3.x / currentWidth,
    y3: p3.y / currentHeight,
    x4: p4.x / currentWidth,
    y4: p4.y / currentHeight,
  };
};
