// ── Types ──────────────────────────────────────────────────────────────────
export type ResizeMode = 'crop-square' | 'fit' | 'stretch' | 'keep-aspect';
export type PreviewMode = 'single' | 'repeat-2' | 'repeat-3' | 'repeat-4';
export type DetailPreservation = 'low' | 'medium' | 'high';

export interface TilesetSettings {
  outputWidth: number;
  outputHeight: number;
  resizeMode: ResizeMode;
  seamWidth: number;
  blendStrength: number;
  detailPreservation: DetailPreservation;
  webpQuality: number;
  previewMode: PreviewMode;
}

export const defaultSettings: TilesetSettings = {
  outputWidth: 1024,
  outputHeight: 1024,
  resizeMode: 'crop-square',
  seamWidth: 32,
  blendStrength: 70,
  detailPreservation: 'medium',
  webpQuality: 90,
  previewMode: 'repeat-3',
};

// ── Helpers ─────────────────────────────────────────────────────────────────

/** Hermite / smoothstep interpolation — same as GLSL smoothstep */
function smoothstep(edge0: number, edge1: number, x: number): number {
  const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
  return t * t * (3 - 2 * t);
}

function makeCanvas(w: number, h: number): HTMLCanvasElement {
  const c = document.createElement('canvas');
  c.width = w;
  c.height = h;
  return c;
}

// ── Step 1+2: Load & Normalize ───────────────────────────────────────────────

export async function loadAndNormalizeImage(
  file: File,
  settings: TilesetSettings,
): Promise<HTMLCanvasElement> {
  const bitmap = await createImageBitmap(file);
  return normalizeImage(bitmap, settings);
}

function normalizeImage(
  bitmap: ImageBitmap,
  settings: TilesetSettings,
): HTMLCanvasElement {
  const { outputWidth: ow, outputHeight: oh, resizeMode } = settings;
  const sw = bitmap.width;
  const sh = bitmap.height;

  const canvas = makeCanvas(ow, oh);
  const ctx = canvas.getContext('2d')!;

  if (resizeMode === 'stretch') {
    ctx.drawImage(bitmap, 0, 0, ow, oh);
    return canvas;
  }

  if (resizeMode === 'crop-square') {
    // Determine side of the center-crop square
    const side = Math.min(sw, sh);
    const sx = (sw - side) / 2;
    const sy = (sh - side) / 2;
    ctx.drawImage(bitmap, sx, sy, side, side, 0, 0, ow, oh);
    return canvas;
  }

  const srcRatio = sw / sh;
  const dstRatio = ow / oh;

  if (resizeMode === 'fit') {
    // Letterbox / pillarbox — fill destination with scaled source, centered
    let dw: number, dh: number, dx: number, dy: number;
    if (srcRatio > dstRatio) {
      dw = ow; dh = ow / srcRatio; dx = 0; dy = (oh - dh) / 2;
    } else {
      dh = oh; dw = oh * srcRatio; dx = (ow - dw) / 2; dy = 0;
    }
    ctx.drawImage(bitmap, 0, 0, sw, sh, dx, dy, dw, dh);
    return canvas;
  }

  // keep-aspect — same as fit (preserves aspect within target bounds)
  {
    const scale = Math.min(ow / sw, oh / sh);
    const dw = sw * scale;
    const dh = sh * scale;
    const dx = (ow - dw) / 2;
    const dy = (oh - dh) / 2;
    ctx.drawImage(bitmap, 0, 0, sw, sh, dx, dy, dw, dh);
    return canvas;
  }
}

// ── Step 3: Wrapped Offset ───────────────────────────────────────────────────

export function applyWrappedOffset(
  source: HTMLCanvasElement,
  offsetX: number,
  offsetY: number,
): HTMLCanvasElement {
  const { width: w, height: h } = source;
  const out = makeCanvas(w, h);
  const ctx = out.getContext('2d')!;

  // Integer pixel offsets (wrap around)
  const ox = Math.round(offsetX) % w;
  const oy = Math.round(offsetY) % h;

  // Draw source in all four wrapped quadrants
  const positions = [
    [ox,     oy    ],
    [ox - w, oy    ],
    [ox,     oy - h],
    [ox - w, oy - h],
  ];

  for (const [dx, dy] of positions) {
    ctx.drawImage(source, dx, dy);
  }

  return out;
}

// ── Step 4: Seam Repair ──────────────────────────────────────────────────────

export function repairCenterSeams(
  source: HTMLCanvasElement,
  settings: Pick<TilesetSettings, 'seamWidth' | 'blendStrength' | 'detailPreservation'>,
): HTMLCanvasElement {
  const { width: w, height: h } = source;
  const { seamWidth, blendStrength, detailPreservation } = settings;

  const out = makeCanvas(w, h);
  const outCtx = out.getContext('2d')!;
  // Copy source first so unmodified pixels carry over
  outCtx.drawImage(source, 0, 0);

  const srcCtx = source.getContext('2d')!;
  const srcData = srcCtx.getImageData(0, 0, w, h);
  const outData = outCtx.getImageData(0, 0, w, h);

  const src = srcData.data;
  const dst = outData.data;

  // How much original pixel to keep (adds "detail preservation")
  const detailRetain =
    detailPreservation === 'high' ? 0.4 :
    detailPreservation === 'medium' ? 0.2 : 0;

  const blendFactor = blendStrength / 100;
  const halfSeam = seamWidth / 2;

  // Helper: get RGBA at (x, y) with wrapping
  const getPixel = (x: number, y: number, ch: number): number => {
    const px = ((x % w) + w) % w;
    const py = ((y % h) + h) % h;
    return src[(py * w + px) * 4 + ch];
  };

  // Helper: sample a soft average of neighbor pixels across the seam
  const sampleNeighbors = (
    x: number,
    y: number,
    direction: 'h' | 'v', // 'h' = horizontal seam (sample above/below), 'v' = vertical
    reach: number,
    ch: number,
  ): number => {
    let total = 0;
    let weight = 0;
    const steps = Math.min(reach, 8); // sample up to 8 pixels away
    for (let i = 1; i <= steps; i++) {
      const w1 = (steps - i + 1) / steps;
      const [ax, ay] = direction === 'v' ? [x + i, y] : [x, y + i];
      const [bx, by] = direction === 'v' ? [x - i, y] : [x, y - i];
      total += getPixel(ax, ay, ch) * w1 + getPixel(bx, by, ch) * w1;
      weight += 2 * w1;
    }
    return weight > 0 ? total / weight : getPixel(x, y, ch);
  };

  const cx = Math.floor(w / 2);
  const cy = Math.floor(h / 2);

  for (let y = 0; y < h; y++) {
    for (let x = 0; x < w; x++) {
      const idx = (y * w + x) * 4;

      const dv = Math.abs(x - cx); // distance from vertical seam
      const dh = Math.abs(y - cy); // distance from horizontal seam

      const inV = dv < halfSeam;
      const inH = dh < halfSeam;

      if (!inV && !inH) continue;

      for (let ch = 0; ch < 4; ch++) {
        const orig = src[idx + ch];
        let blended = orig;

        if (inV) {
          const tV = 1 - smoothstep(0, halfSeam, dv);
          const neighborV = sampleNeighbors(x, y, 'v', Math.ceil(halfSeam), ch);
          const mixV = orig * (1 - tV * blendFactor) + neighborV * (tV * blendFactor);
          blended = mixV;
        }

        if (inH) {
          const tH = 1 - smoothstep(0, halfSeam, dh);
          const neighborH = sampleNeighbors(x, y, 'h', Math.ceil(halfSeam), ch);
          const mixH = blended * (1 - tH * blendFactor) + neighborH * (tH * blendFactor);
          blended = mixH;
        }

        // Apply detail preservation — pull back toward original
        dst[idx + ch] = Math.round(blended * (1 - detailRetain) + orig * detailRetain);
      }
    }
  }

  outCtx.putImageData(outData, 0, 0);
  return out;
}

// ── Full Pipeline ────────────────────────────────────────────────────────────

export async function generateTileableTexture(
  file: File,
  settings: TilesetSettings,
  onProgress?: (step: string) => void,
): Promise<HTMLCanvasElement> {
  onProgress?.('Loading image…');
  const bitmap = await createImageBitmap(file);

  onProgress?.('Normalizing…');
  const normalized = normalizeImage(bitmap, settings);

  onProgress?.('Applying offset…');
  const offsetCanvas = applyWrappedOffset(
    normalized,
    settings.outputWidth / 2,
    settings.outputHeight / 2,
  );

  onProgress?.('Repairing seams…');
  // Run seam repair in next microtask so the UI can update
  await new Promise<void>((res) => setTimeout(res, 0));
  const repaired = repairCenterSeams(offsetCanvas, settings);

  onProgress?.('Done');
  return repaired;
}

// ── Export ───────────────────────────────────────────────────────────────────

export function exportAsWebP(
  canvas: HTMLCanvasElement,
  quality: number,
  width: number,
  height: number,
): Promise<void> {
  return new Promise((resolve, reject) => {
    // Resize to export dimensions if needed
    let exportCanvas = canvas;
    if (canvas.width !== width || canvas.height !== height) {
      exportCanvas = makeCanvas(width, height);
      exportCanvas.getContext('2d')!.drawImage(canvas, 0, 0, width, height);
    }

    exportCanvas.toBlob(
      (blob) => {
        if (!blob) { reject(new Error('Canvas export failed')); return; }
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `tileset-generator-${width}x${height}.webp`;
        a.click();
        setTimeout(() => URL.revokeObjectURL(url), 5000);
        resolve();
      },
      'image/webp',
      quality / 100,
    );
  });
}

// ── Validation ───────────────────────────────────────────────────────────────

export const SUPPORTED_TYPES = ['image/png', 'image/jpeg', 'image/webp'];
export const MAX_DIMENSION = 4096;
export const WARN_DIMENSION = 2048;

export interface ValidationResult {
  ok: boolean;
  error?: string;
  warning?: string;
}

export function validateFile(file: File): ValidationResult {
  if (!SUPPORTED_TYPES.includes(file.type)) {
    return {
      ok: false,
      error: 'Unsupported file type. Please upload a PNG, JPG, or WebP image.',
    };
  }
  return { ok: true };
}

export async function validateImageDimensions(file: File): Promise<ValidationResult> {
  const bitmap = await createImageBitmap(file).catch(() => null);
  if (!bitmap) return { ok: false, error: 'Image failed to load. It may be corrupted.' };
  if (bitmap.width > MAX_DIMENSION || bitmap.height > MAX_DIMENSION) {
    return {
      ok: false,
      error: `Image is too large (${bitmap.width}×${bitmap.height}). Maximum is ${MAX_DIMENSION}×${MAX_DIMENSION} px.`,
    };
  }
  if (bitmap.width > WARN_DIMENSION || bitmap.height > WARN_DIMENSION) {
    return {
      ok: true,
      warning: `This image is very large (${bitmap.width}×${bitmap.height}). The tool will resize it before processing.`,
    };
  }
  return { ok: true };
}
