import { Bound, type IBound, type IVec, Vec } from './model/index.js';

function getExpandedBound(a: IBound, b: IBound): IBound {
  const minX = Math.min(a.x, b.x);
  const minY = Math.min(a.y, b.y);
  const maxX = Math.max(a.x + a.w, b.x + b.w);
  const maxY = Math.max(a.y + a.h, b.y + b.h);
  const width = Math.abs(maxX - minX);
  const height = Math.abs(maxY - minY);

  return {
    x: minX,
    y: minY,
    w: width,
    h: height,
  };
}

export function getPointsFromBoundWithRotation(
  bounds: IBound,
  getPoints: (bounds: IBound) => IVec[] = ({ x, y, w, h }: IBound) => [
    // left-top
    [x, y],
    // right-top
    [x + w, y],
    // right-bottom
    [x + w, y + h],
    // left-bottom
    [x, y + h],
  ],
  resPadding: [number, number] = [0, 0]
): IVec[] {
  const { rotate } = bounds;
  let points = getPoints({
    x: bounds.x - resPadding[1],
    y: bounds.y - resPadding[0],
    w: bounds.w + resPadding[1] * 2,
    h: bounds.h + resPadding[0] * 2,
  });

  if (rotate) {
    const { x, y, w, h } = bounds;
    const cx = x + w / 2;
    const cy = y + h / 2;

    const m = new DOMMatrix()
      .translateSelf(cx, cy)
      .rotateSelf(rotate)
      .translateSelf(-cx, -cy);

    points = points.map(point => {
      const { x, y } = new DOMPoint(...point).matrixTransform(m);
      return [x, y];
    });
  }

  return points;
}

export function getQuadBoundWithRotation(bounds: IBound): DOMRect {
  const { x, y, w, h, rotate } = bounds;
  const rect = new DOMRect(x, y, w, h);

  if (!rotate) return rect;

  return new DOMQuad(
    ...getPointsFromBoundWithRotation(bounds).map(
      point => new DOMPoint(...point)
    )
  ).getBounds();
}

export function getBoundWithRotation(bound: IBound): IBound {
  const { x, y, width: w, height: h } = getQuadBoundWithRotation(bound);
  return { x, y, w, h };
}

/**
 * Returns the common bound of the given bounds.
 * The rotation of the bounds is not considered.
 * @param bounds
 * @returns
 */
export function getCommonBound(bounds: IBound[]): Bound | null {
  if (!bounds.length) {
    return null;
  }

  if (bounds.length === 1) {
    const { x, y, w, h } = bounds[0];
    return new Bound(x, y, w, h);
  }

  let result = bounds[0];

  for (let i = 1; i < bounds.length; i++) {
    result = getExpandedBound(result, bounds[i]);
  }

  return new Bound(result.x, result.y, result.w, result.h);
}

/**
 * Like `getCommonBound`, but considers the rotation of the bounds.
 * @returns
 */
export function getCommonBoundWithRotation(bounds: IBound[]): Bound {
  if (bounds.length === 0) {
    return new Bound(0, 0, 0, 0);
  }

  return bounds.reduce(
    (pre, bound) => {
      return pre.unite(
        bound instanceof Bound ? bound : Bound.from(getBoundWithRotation(bound))
      );
    },
    Bound.from(getBoundWithRotation(bounds[0]))
  );
}

export function getIBoundFromPoints(
  points: IVec[],
  rotation = 0
): IBound & {
  maxX: number;
  maxY: number;
  minX: number;
  minY: number;
} {
  let minX = Infinity;
  let minY = Infinity;
  let maxX = -Infinity;
  let maxY = -Infinity;

  if (points.length < 1) {
    minX = 0;
    minY = 0;
    maxX = 1;
    maxY = 1;
  } else {
    for (const [x, y] of points) {
      minX = Math.min(x, minX);
      minY = Math.min(y, minY);
      maxX = Math.max(x, maxX);
      maxY = Math.max(y, maxY);
    }
  }

  if (rotation !== 0) {
    return getIBoundFromPoints(
      points.map(pt =>
        Vec.rotWith(pt, [(minX + maxX) / 2, (minY + maxY) / 2], rotation)
      )
    );
  }

  return {
    minX,
    minY,
    maxX,
    maxY,
    x: minX,
    y: minY,
    w: maxX - minX,
    h: maxY - minY,
  };
}

export function getBoundFromPoints(points: IVec[]) {
  return Bound.from(getIBoundFromPoints(points));
}

export function inflateBound(bound: IBound, delta: number) {
  const half = delta / 2;

  const newBound = new Bound(
    bound.x - half,
    bound.y - half,
    bound.w + delta,
    bound.h + delta
  );

  if (newBound.w <= 0 || newBound.h <= 0) {
    throw new Error('Invalid delta range or bound size.');
  }

  return newBound;
}

export function transformPointsToNewBound<T extends { x: number; y: number }>(
  points: T[],
  oldBound: IBound,
  oldMargin: number,
  newBound: IBound,
  newMargin: number
) {
  const wholeOldMargin = oldMargin * 2;
  const wholeNewMargin = newMargin * 2;
  const oldW = Math.max(oldBound.w - wholeOldMargin, 1);
  const oldH = Math.max(oldBound.h - wholeOldMargin, 1);
  const newW = Math.max(newBound.w - wholeNewMargin, 1);
  const newH = Math.max(newBound.h - wholeNewMargin, 1);

  const transformedPoints = points.map(p => {
    return {
      ...p,
      x: newW * ((p.x - oldMargin) / oldW) + newMargin,
      y: newH * ((p.y - oldMargin) / oldH) + newMargin,
    } as T;
  });

  return {
    points: transformedPoints,
    bound: new Bound(
      newBound.x,
      newBound.y,
      newW + wholeNewMargin,
      newH + wholeNewMargin
    ),
  };
}
