// Genuine browser-based image processing pipeline for SHiRE CoverCanvas.
// Non-destructive: original image + settings => regenerated processed canvas.
// All operations modify real pixel data. Nothing here is a mock.

export const DEFAULT_SETTINGS = {
  brightness: 1,            // 0..2
  contrast: 1,              // 0..2
  greyscale: false,
  thresholdEnabled: false,
  threshold: 0.5,           // 0..1
  invert: false,
  removeWhiteBackground: false,
  whiteTolerance: 0.85,     // 0..1 (luminance above this counts as white)
  trimPadding: false,
  noiseRemoval: 0,          // 0..1 strength
  removeIsolated: false,
  edgeSmoothing: 0,         // 0..1
  edgeSharpening: 0,        // 0..1
  minLineThickness: 0,      // px radius (dilation of ink)
  closeGaps: 0,             // px radius (morphological closing)
  removeSmallRegions: 0,    // minimum ink area in px
  preserveDetail: 0.5       // 0..1 (higher => less aggressive smoothing)
};

const clamp = (v) => (v < 0 ? 0 : v > 255 ? 255 : v);

export function loadImage(src) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.crossOrigin = 'anonymous';
    img.onload = () => resolve(img);
    img.onerror = () => reject(new Error('Image failed to load'));
    img.src = src;
  });
}

export function imageToCanvas(img, maxDim = 1024) {
  const scale = Math.min(1, maxDim / Math.max(img.naturalWidth || 1, img.naturalHeight || 1));
  const w = Math.max(1, Math.round((img.naturalWidth || 1) * scale));
  const h = Math.max(1, Math.round((img.naturalHeight || 1) * scale));
  const c = document.createElement('canvas');
  c.width = w; c.height = h;
  c.getContext('2d').drawImage(img, 0, 0, w, h);
  return c;
}

export function canvasFromImageData(imgData) {
  const c = document.createElement('canvas');
  c.width = imgData.width; c.height = imgData.height;
  c.getContext('2d').putImageData(imgData, 0, 0);
  return c;
}

// ---- Geometric transforms on the source/base image (undoable) ----

export function rotateCanvas(src, turns) {
  const t = ((turns % 4) + 4) % 4;
  const w = src.width, h = src.height;
  const c = document.createElement('canvas');
  if (t === 1 || t === 3) { c.width = h; c.height = w; } else { c.width = w; c.height = h; }
  const ctx = c.getContext('2d');
  ctx.translate(c.width / 2, c.height / 2);
  ctx.rotate((t * Math.PI) / 2);
  ctx.drawImage(src, -w / 2, -h / 2);
  return c;
}

export function mirrorCanvas(src, axis) {
  const c = document.createElement('canvas');
  c.width = src.width; c.height = src.height;
  const ctx = c.getContext('2d');
  ctx.translate(axis === 'v' ? 0 : c.width, axis === 'v' ? c.height : 0);
  ctx.scale(axis === 'v' ? 1 : -1, axis === 'v' ? -1 : 1);
  ctx.drawImage(src, 0, 0);
  return c;
}

export function cropCanvas(src, rect) {
  const x = Math.max(0, Math.round(rect.x));
  const y = Math.max(0, Math.round(rect.y));
  const w = Math.min(src.width - x, Math.round(rect.w));
  const h = Math.min(src.height - y, Math.round(rect.h));
  if (w <= 0 || h <= 0) return src;
  const c = document.createElement('canvas');
  c.width = w; c.height = h;
  c.getContext('2d').drawImage(src, x, y, w, h, 0, 0, w, h);
  return c;
}

export function trimTransparent(src) {
  const ctx = src.getContext('2d');
  const { width: w, height: h } = src;
  if (!w || !h) return src;
  const { data } = ctx.getImageData(0, 0, w, h);
  let minX = w, minY = h, maxX = -1, maxY = -1;
  for (let y = 0; y < h; y++) {
    for (let x = 0; x < w; x++) {
      if (data[(y * w + x) * 4 + 3] > 8) {
        if (x < minX) minX = x; if (x > maxX) maxX = x;
        if (y < minY) minY = y; if (y > maxY) maxY = y;
      }
    }
  }
  if (maxX < 0) return src;
  return cropCanvas(src, { x: minX, y: minY, w: maxX - minX + 1, h: maxY - minY + 1 });
}

// ---- Mask morphology helpers ----

function dilate(mask, w, h, r) {
  if (r <= 0) return mask;
  // separable: dilate horizontally then vertically
  let cur = mask;
  cur = dilateAxis(cur, w, h, r, true);
  cur = dilateAxis(cur, w, h, r, false);
  return cur;
}
function dilateAxis(mask, w, h, r, horizontal) {
  const out = new Uint8Array(w * h);
  const len = horizontal ? w : h;
  for (let i = 0; i < w * h; i++) {
    if (mask[i]) { out[i] = 1; continue; }
    const x = i % w, y = (i / w) | 0;
    const c = horizontal ? x : y;
    let hit = 0;
    for (let d = -r; d <= r && !hit; d++) {
      const p = c + d;
      if (p < 0 || p >= len) continue;
      const idx = horizontal ? y * w + p : p * w + x;
      if (mask[idx]) hit = 1;
    }
    out[i] = hit;
  }
  return out;
}
function erode(mask, w, h, r) {
  if (r <= 0) return mask;
  let cur = mask;
  cur = erodeAxis(cur, w, h, r, true);
  cur = erodeAxis(cur, w, h, r, false);
  return cur;
}
function erodeAxis(mask, w, h, r, horizontal) {
  const out = new Uint8Array(w * h);
  const len = horizontal ? w : h;
  for (let i = 0; i < w * h; i++) {
    const x = i % w, y = (i / w) | 0;
    const c = horizontal ? x : y;
    let keep = 1;
    for (let d = -r; d <= r && keep; d++) {
      const p = c + d;
      if (p < 0 || p >= len) { keep = 0; break; }
      const idx = horizontal ? y * w + p : p * w + x;
      if (!mask[idx]) keep = 0;
    }
    out[i] = keep;
  }
  return out;
}
function closeOp(mask, w, h, r) { return erode(dilate(mask, w, h, r), w, h, r); }

function removeIsolated(mask, w, h) {
  const out = new Uint8Array(w * h);
  for (let y = 0; y < h; y++) {
    for (let x = 0; x < w; x++) {
      const i = y * w + x;
      if (!mask[i]) continue;
      let nb = 0;
      for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
        if (!dx && !dy) continue;
        const nx = x + dx, ny = y + dy;
        if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
        if (mask[ny * w + nx]) nb++;
      }
      out[i] = nb >= 1 ? 1 : 0;
    }
  }
  return out;
}

function denoiseMask(mask, w, h, strength) {
  // majority filter 3x3, repeated based on strength
  const passes = Math.max(1, Math.round(strength * 2));
  let cur = mask;
  for (let p = 0; p < passes; p++) {
    const out = new Uint8Array(w * h);
    for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) {
      let count = 0, total = 0;
      for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
        const nx = x + dx, ny = y + dy;
        if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
        total++; if (cur[ny * w + nx]) count++;
      }
      out[y * w + x] = count > total / 2 ? 1 : 0;
    }
    cur = out;
  }
  return cur;
}

function removeSmallRegions(mask, w, h, minArea) {
  const labels = new Int32Array(w * h);
  let next = 1;
  const stack = [];
  const sizes = [0];
  for (let i = 0; i < w * h; i++) {
    if (!mask[i] || labels[i]) continue;
    labels[i] = next; stack.push(i); let size = 0;
    while (stack.length) {
      const j = stack.pop(); size++;
      const x = j % w, y = (j / w) | 0;
      for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
        if (!dx && !dy) continue;
        const nx = x + dx, ny = y + dy;
        if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
        const k = ny * w + nx;
        if (mask[k] && !labels[k]) { labels[k] = next; stack.push(k); }
      }
    }
    sizes[next] = size; next++;
  }
  const out = new Uint8Array(w * h);
  for (let i = 0; i < w * h; i++) {
    const lbl = labels[i];
    if (lbl && sizes[lbl] >= minArea) out[i] = 1;
  }
  return out;
}

function smoothEdges(rgba, w, h, amount) {
  // 3x3 box blur on alpha only, weighted by amount, to anti-alias mask edges
  if (amount <= 0) return;
  const a0 = new Uint8ClampedArray(w * h);
  for (let i = 0, p = 0; i < rgba.length; i += 4, p++) a0[p] = rgba[i + 3];
  const out = new Float32Array(w * h);
  for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) {
    let sum = 0, n = 0;
    for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
      const nx = x + dx, ny = y + dy;
      if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
      sum += a0[ny * w + nx]; n++;
    }
    out[y * w + x] = sum / n;
  }
  for (let p = 0; p < w * h; p++) {
    const blended = a0[p] * (1 - amount) + out[p] * amount;
    rgba[p * 4 + 3] = Math.round(blended);
  }
}

function sharpen(rgba, w, h, amount) {
  // unsharp mask on RGB of opaque pixels
  const k = amount;
  const orig = new Uint8ClampedArray(rgba);
  const kernel = [0, -1, 0, -1, 5, -1, 0, -1, 0];
  for (let y = 1; y < h - 1; y++) for (let x = 1; x < w - 1; x++) {
    const i = (y * w + x) * 4;
    if (rgba[i + 3] < 16) continue;
    for (let ch = 0; ch < 3; ch++) {
      let s = 0, ki = 0;
      for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
        const j = ((y + dy) * w + (x + dx)) * 4 + ch;
        s += orig[j] * kernel[ki++];
      }
      rgba[i + ch] = clamp(orig[i + ch] * (1 - k) + s * k);
    }
  }
}

function trimImageData(imgData, w, h) {
  const { data } = imgData;
  let minX = w, minY = h, maxX = -1, maxY = -1;
  for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) {
    if (data[(y * w + x) * 4 + 3] > 8) {
      if (x < minX) minX = x; if (x > maxX) maxX = x;
      if (y < minY) minY = y; if (y > maxY) maxY = y;
    }
  }
  if (maxX < 0) return imgData;
  const nw = maxX - minX + 1, nh = maxY - minY + 1;
  const out = new ImageData(nw, nh);
  for (let y = 0; y < nh; y++) for (let x = 0; x < nw; x++) {
    const si = (((y + minY) * w) + (x + minX)) * 4, di = (y * nw + x) * 4;
    out.data[di] = data[si]; out.data[di + 1] = data[si + 1];
    out.data[di + 2] = data[si + 2]; out.data[di + 3] = data[si + 3];
  }
  return out;
}

// Pure pixel pipeline: ImageData + settings => processed ImageData.
// DOM-free (no canvas context) so it can run inside a Web Worker.
export function processImageData(srcImageData, settings) {
  const s = { ...DEFAULT_SETTINGS, ...settings };
  const w = srcImageData.width, h = srcImageData.height;
  const sd = srcImageData.data;
  const out = new ImageData(w, h);
  const od = out.data;

  // 1. Tonal adjustments on RGB
  for (let i = 0; i < sd.length; i += 4) {
    let r = sd[i] * s.brightness;
    let g = sd[i + 1] * s.brightness;
    let b = sd[i + 2] * s.brightness;
    r = (r - 128) * s.contrast + 128;
    g = (g - 128) * s.contrast + 128;
    b = (b - 128) * s.contrast + 128;
    if (s.greyscale) { const l = 0.299 * r + 0.587 * g + 0.114 * b; r = g = b = l; }
    if (s.invert) { r = 255 - r; g = 255 - g; b = 255 - b; }
    od[i] = clamp(r); od[i + 1] = clamp(g); od[i + 2] = clamp(b); od[i + 3] = sd[i + 3];
  }

  // 2. Build binary mask (1 = ink/active)
  const mask = new Uint8Array(w * h);
  const wt = s.whiteTolerance * 255;
  for (let i = 0, p = 0; i < od.length; i += 4, p++) {
    const a = od[i + 3];
    if (a < 16) { mask[p] = 0; continue; }
    const r = od[i], g = od[i + 1], b = od[i + 2];
    const lum = 0.299 * r + 0.587 * g + 0.114 * b;
    let ink;
    if (s.removeWhiteBackground) {
      const mx = Math.max(r, g, b), mn = Math.min(r, g, b);
      const sat = mx === 0 ? 0 : (mx - mn) / mx;
      ink = !(lum > wt && sat < 0.18);
    } else if (s.thresholdEnabled) {
      ink = s.invert ? lum > s.threshold * 255 : lum < s.threshold * 255;
    } else {
      ink = true;
    }
    mask[p] = ink ? 1 : 0;
  }

  // 3. Morphology / cleanup on the mask
  let m = mask;
  if (s.removeIsolated) m = removeIsolated(m, w, h);
  if (s.minLineThickness > 0) m = dilate(m, w, h, Math.min(8, Math.round(s.minLineThickness)));
  if (s.closeGaps > 0) m = closeOp(m, w, h, Math.min(8, Math.round(s.closeGaps)));
  if (s.removeSmallRegions > 0) m = removeSmallRegions(m, w, h, Math.round(s.removeSmallRegions));
  if (s.noiseRemoval > 0) m = denoiseMask(m, w, h, s.noiseRemoval * (1 - s.preserveDetail * 0.5));

  // 4. Compose output RGBA from the final mask
  for (let i = 0, p = 0; i < od.length; i += 4, p++) {
    if (m[p]) {
      if (s.thresholdEnabled) {
        const v = s.invert ? 255 : 0;
        od[i] = v; od[i + 1] = v; od[i + 2] = v; od[i + 3] = 255;
      } else {
        od[i + 3] = 255; // keep tonal RGB, force opaque ink
      }
    } else {
      od[i + 3] = 0; // transparent background
    }
  }

  // 5. Edge smoothing (anti-alias) + sharpening
  if (s.edgeSmoothing > 0) smoothEdges(od, w, h, s.edgeSmoothing * (1 - s.preserveDetail));
  if (s.edgeSharpening > 0) sharpen(od, w, h, s.edgeSharpening);

  // 6. Optional trim of transparent padding
  let result = out;
  if (s.trimPadding) result = trimImageData(out, w, h);

  return result;
}

// ---- Main pipeline: srcCanvas + settings => processed canvas (main thread) ----
// The heavy work is exported as processImageData above so a Web Worker can run it
// off the main thread; this wrapper remains for synchronous fallbacks.
export function processImage(srcCanvas, settings) {
  const ctx = srcCanvas.getContext('2d');
  const src = ctx.getImageData(0, 0, srcCanvas.width, srcCanvas.height);
  return canvasFromImageData(processImageData(src, settings));
}

export function canvasToBlob(canvas) {
  return new Promise((resolve) => canvas.toBlob((b) => resolve(b), 'image/png'));
}