export interface FabricVariationGeometryLike {
  width?: unknown;
  height?: unknown;
  scaleX?: unknown;
  scaleY?: unknown;
}

export const FABRIC_VARIATION_FRAME_PROPERTY = '_asukaVariationFrame';

export interface FabricVariationFrame extends FabricVariationGeometryLike {
  width: number;
  height: number;
  scaleX: number;
  scaleY: number;
  left: number;
  top: number;
  originX: string;
  originY: string;
  angle: number;
  skewX: number;
  skewY: number;
  flipX: boolean;
  flipY: boolean;
  opacity: number;
}

export interface FabricVariationIntrinsicSize {
  width: number;
  height: number;
}

export interface StableFabricVariationGeometry extends FabricVariationIntrinsicSize {
  scaleX: number;
  scaleY: number;
}

const finiteNumber = (value: unknown, fallback: number): number => {
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : fallback;
};

const cloneFrame = (frame: FabricVariationFrame): FabricVariationFrame => ({ ...frame });

const normalizeFrame = (value: unknown): FabricVariationFrame | null => {
  if (!value || typeof value !== 'object') return null;
  const source = value as Record<string, unknown>;
  const width = finiteNumber(source.width, 0);
  const height = finiteNumber(source.height, 0);
  if (width <= 0 || height <= 0) return null;

  return {
    width,
    height,
    scaleX: finiteNumber(source.scaleX, 1) || 1,
    scaleY: finiteNumber(source.scaleY, 1) || 1,
    left: finiteNumber(source.left, 0),
    top: finiteNumber(source.top, 0),
    originX: String(source.originX ?? 'left'),
    originY: String(source.originY ?? 'top'),
    angle: finiteNumber(source.angle, 0),
    skewX: finiteNumber(source.skewX, 0),
    skewY: finiteNumber(source.skewY, 0),
    flipX: source.flipX === true,
    flipY: source.flipY === true,
    opacity: finiteNumber(source.opacity, 1),
  };
};

/** Whether an object already owns a valid canonical variation placement. */
export const hasFabricVariationFrame = (source: any): boolean => {
  if (!source || typeof source !== 'object') return false;
  return normalizeFrame(source[FABRIC_VARIATION_FRAME_PROPERTY]) !== null;
};

export const captureFabricVariationFrame = (source: any): FabricVariationFrame | null => {
  if (!source || typeof source !== 'object') return null;
  return normalizeFrame({
    width: source.width,
    height: source.height,
    scaleX: source.scaleX,
    scaleY: source.scaleY,
    left: source.left,
    top: source.top,
    originX: source.originX,
    originY: source.originY,
    angle: source.angle,
    skewX: source.skewX,
    skewY: source.skewY,
    flipX: source.flipX,
    flipY: source.flipY,
    opacity: source.opacity,
  });
};

/**
 * Returns the immutable placement frame used by every bitmap in a variation
 * family. Imported documents do not have this property yet, so their current
 * placement becomes the canonical frame on the first swap.
 */
export const ensureFabricVariationFrame = (source: any): FabricVariationFrame | null => {
  if (!source || typeof source !== 'object') return null;
  const stored = normalizeFrame(source[FABRIC_VARIATION_FRAME_PROPERTY]);
  if (stored) return cloneFrame(stored);

  const captured = captureFabricVariationFrame(source);
  if (!captured) return null;
  source[FABRIC_VARIATION_FRAME_PROPERTY] = cloneFrame(captured);
  return captured;
};

/** Update the canonical frame after an intentional move/resize by the editor. */
export const refreshFabricVariationFrame = (source: any): FabricVariationFrame | null => {
  if (!source || typeof source !== 'object') return null;
  const captured = captureFabricVariationFrame(source);
  if (!captured) return null;
  source[FABRIC_VARIATION_FRAME_PROPERTY] = cloneFrame(captured);
  return captured;
};

export const applyFabricVariationFrame = (
  target: any,
  frame: FabricVariationFrame,
  replacement: FabricVariationIntrinsicSize,
): StableFabricVariationGeometry | null => {
  if (!target || !frame) return null;
  const geometry = resolveStableFabricVariationGeometry(frame, replacement);
  if (!geometry) return null;

  const placement = {
    width: geometry.width,
    height: geometry.height,
    scaleX: geometry.scaleX,
    scaleY: geometry.scaleY,
    left: frame.left,
    top: frame.top,
    originX: frame.originX,
    originY: frame.originY,
    angle: frame.angle,
    skewX: frame.skewX,
    skewY: frame.skewY,
    flipX: frame.flipX,
    flipY: frame.flipY,
    opacity: frame.opacity,
  };
  try {
    if (typeof target.set === 'function') target.set(placement);
    else Object.assign(target, placement);
  } catch {
    Object.assign(target, placement);
  }
  target[FABRIC_VARIATION_FRAME_PROPERTY] = cloneFrame(frame);
  return geometry;
};

const signedScale = (previousScale: number, renderedSize: number, intrinsicSize: number): number => {
  const fallback = Math.max(0.0001, Math.abs(previousScale) || 1);
  const magnitude = renderedSize > 0 && intrinsicSize > 0
    ? Math.max(0.0001, renderedSize / intrinsicSize)
    : fallback;
  return (previousScale < 0 ? -1 : 1) * magnitude;
};

/**
 * Fits a replacement bitmap into the exact visual frame occupied by the
 * previous variation. Variation files may have different intrinsic pixel
 * sizes, but swapping them must not move or resize the configured part.
 */
export const resolveStableFabricVariationGeometry = (
  previous: FabricVariationGeometryLike,
  replacement: FabricVariationIntrinsicSize,
): StableFabricVariationGeometry | null => {
  const width = finiteNumber(replacement?.width, 0);
  const height = finiteNumber(replacement?.height, 0);
  if (width <= 0 || height <= 0) return null;

  const previousWidth = Math.max(0, finiteNumber(previous?.width, 0));
  const previousHeight = Math.max(0, finiteNumber(previous?.height, 0));
  const previousScaleX = finiteNumber(previous?.scaleX, 1) || 1;
  const previousScaleY = finiteNumber(previous?.scaleY, 1) || 1;
  const renderedWidth = previousWidth * Math.abs(previousScaleX);
  const renderedHeight = previousHeight * Math.abs(previousScaleY);

  return {
    width,
    height,
    scaleX: signedScale(previousScaleX, renderedWidth, width),
    scaleY: signedScale(previousScaleY, renderedHeight, height),
  };
};
