// PiBead — color & pattern utilities
//
// Pure helpers for the photo-to-bead workflow:
//   - hexToRgb(hex)
//   - rgbDistance(a, b)
//   - findNearestBeadColor(rgb, palette)
//   - generatePatternFromImage(file | imageElement, options) -> Promise<GeneratedPattern>
//   - countBeads(grid)
//   - calculateEstimatedKitPrice(beadCounts, palette)
//
// All functions are framework-agnostic. The React component layer in
// pages/create.jsx is the only piece that ties this to UI state.

(function () {

  function hexToRgb(hex) {
    let h = hex.replace('#', '');
    if (h.length === 3) h = h.split('').map(c => c + c).join('');
    const n = parseInt(h, 16);
    return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
  }
  window.hexToRgb = hexToRgb;

  function rgbDistance(a, b) {
    const dr = a.r - b.r, dg = a.g - b.g, db = a.b - b.b;
    return Math.sqrt(dr * dr + dg * dg + db * db);
  }
  window.rgbDistance = rgbDistance;

  // Find the nearest bead color in a palette to a target RGB.
  // Optional `limitIds` restricts the search to a subset (used by color quantization).
  function findNearestBeadColor(rgb, palette, limitIds) {
    let best = palette[0];
    let bestDist = Infinity;
    for (const b of palette) {
      if (limitIds && !limitIds.has(b.id)) continue;
      const d = rgbDistance(rgb, b.rgb);
      if (d < bestDist) { bestDist = d; best = b; }
    }
    return best;
  }
  window.findNearestBeadColor = findNearestBeadColor;

  // Read a File into an Image. Returns the image and the object URL (caller can
  // revoke when finished).
  function loadImageFromFile(file) {
    return new Promise((resolve, reject) => {
      const url = URL.createObjectURL(file);
      const img = new Image();
      img.onload = () => resolve({ img, url });
      img.onerror = (e) => { URL.revokeObjectURL(url); reject(new Error('Could not load image')); };
      img.src = url;
    });
  }
  window.loadImageFromFile = loadImageFromFile;

  // Downsample an image to `size`×`size` on an offscreen canvas and read raw
  // pixels back. Returns the imageData plus the canvas as a data URL (for
  // showing in cart / order summary).
  function sampleImageToGrid(img, size, opts = {}) {
    const canvas = document.createElement('canvas');
    canvas.width = size;
    canvas.height = size;
    const ctx = canvas.getContext('2d', { willReadFrequently: true });
    ctx.imageSmoothingEnabled = true;
    ctx.imageSmoothingQuality = 'high';

    // Cover fit: crop to square, scale down.
    const srcSize = Math.min(img.width, img.height);
    const sx = (img.width - srcSize) / 2;
    const sy = (img.height - srcSize) / 2;
    ctx.drawImage(img, sx, sy, srcSize, srcSize, 0, 0, size, size);

    const data = ctx.getImageData(0, 0, size, size);
    return { imageData: data, canvas };
  }
  window.sampleImageToGrid = sampleImageToGrid;

  // Color quantization stub: count usage of each palette color, sort descending,
  // and keep only the top `colorLimit`. Pixels mapped to a removed color get
  // re-snapped to the nearest survivor.
  function applyColorLimit(grid, palette, colorLimit) {
    if (!colorLimit || colorLimit >= palette.length) return grid;
    const counts = {};
    for (const row of grid) for (const id of row) counts[id] = (counts[id] || 0) + 1;
    const sortedIds = Object.keys(counts).sort((a, b) => counts[b] - counts[a]);
    const survivors = new Set(sortedIds.slice(0, colorLimit));
    const survivorPal = palette.filter(p => survivors.has(p.id));
    const out = grid.map(row => row.slice());
    for (let y = 0; y < out.length; y++) {
      for (let x = 0; x < out[0].length; x++) {
        if (!survivors.has(out[y][x])) {
          const old = window.BEAD_BY_ID[out[y][x]];
          if (old) out[y][x] = findNearestBeadColor(old.rgb, survivorPal).id;
        }
      }
    }
    return out;
  }
  window.applyColorLimit = applyColorLimit;

  // Heuristic background removal: snap edge-cluster pixels to a single
  // "background" color and replace them with the most common edge color
  // (then keep, since users may want a backdrop). When `removeToTransparent`
  // is set, we instead snap them to the first cream-ish color in the palette.
  function removeBackgroundLite(grid, palette) {
    const h = grid.length, w = grid[0].length;
    const edgeIds = {};
    for (let x = 0; x < w; x++) {
      [grid[0][x], grid[h-1][x]].forEach(id => edgeIds[id] = (edgeIds[id] || 0) + 1);
    }
    for (let y = 0; y < h; y++) {
      [grid[y][0], grid[y][w-1]].forEach(id => edgeIds[id] = (edgeIds[id] || 0) + 1);
    }
    const bg = Object.entries(edgeIds).sort((a,b) => b[1] - a[1])[0]?.[0];
    if (!bg) return grid;
    // Find lightest palette color as the "removed bg" replacement
    let lightest = palette[0]; let lightVal = -1;
    for (const p of palette) {
      const v = p.rgb.r + p.rgb.g + p.rgb.b;
      if (v > lightVal) { lightVal = v; lightest = p; }
    }
    // Flood-fill from the edges, replacing only `bg` cells (and adjacent
    // bg-ish cells) so we don't wipe internal areas that happen to match.
    const out = grid.map(row => row.slice());
    const seen = Array.from({length: h}, () => new Array(w).fill(false));
    const stack = [];
    for (let x = 0; x < w; x++) { stack.push([x, 0]); stack.push([x, h-1]); }
    for (let y = 0; y < h; y++) { stack.push([0, y]); stack.push([w-1, y]); }
    while (stack.length) {
      const [x, y] = stack.pop();
      if (x < 0 || y < 0 || x >= w || y >= h || seen[y][x]) continue;
      if (out[y][x] !== bg) continue;
      seen[y][x] = true;
      out[y][x] = lightest.id;
      stack.push([x+1,y],[x-1,y],[x,y+1],[x,y-1]);
    }
    return out;
  }
  window.removeBackgroundLite = removeBackgroundLite;

  // Smooth single-pixel noise: replace solitary pixels with the modal color
  // of their 4-neighborhood.
  function denoise(grid) {
    const h = grid.length, w = grid[0].length;
    const out = grid.map(row => row.slice());
    for (let y = 0; y < h; y++) {
      for (let x = 0; x < w; x++) {
        const me = grid[y][x];
        const neighbors = [
          y > 0 ? grid[y-1][x] : me,
          y < h-1 ? grid[y+1][x] : me,
          x > 0 ? grid[y][x-1] : me,
          x < w-1 ? grid[y][x+1] : me,
        ];
        if (!neighbors.includes(me)) {
          // pick modal neighbor
          const counts = {};
          for (const n of neighbors) counts[n] = (counts[n] || 0) + 1;
          out[y][x] = Object.entries(counts).sort((a,b) => b[1] - a[1])[0][0];
        }
      }
    }
    return out;
  }
  window.denoise = denoise;

  // Render a grid to a preview canvas/dataURL (used for cart previews etc).
  function gridToDataURL(grid, scale = 8) {
    const h = grid.length, w = grid[0].length;
    const canvas = document.createElement('canvas');
    canvas.width = w * scale;
    canvas.height = h * scale;
    const ctx = canvas.getContext('2d');
    for (let y = 0; y < h; y++) {
      for (let x = 0; x < w; x++) {
        const b = window.BEAD_BY_ID[grid[y][x]];
        ctx.fillStyle = b ? b.hex : '#FFFFFF';
        ctx.fillRect(x * scale, y * scale, scale, scale);
      }
    }
    return canvas.toDataURL('image/png');
  }
  window.gridToDataURL = gridToDataURL;

  // Bead count for a generated grid.
  function countBeads(grid) {
    const counts = {};
    for (const row of grid) {
      for (const id of row) counts[id] = (counts[id] || 0) + 1;
    }
    return counts;
  }
  window.countBeadsFromGrid = countBeads;

  // Estimated kit price for an MVP. See PRD pricing rules.
  //   base kit fee  = $9.99
  //   bead cost     = totalBeads / 1000 * brand.pricePer1000
  //   handling      = colorCount * $0.15
  function calculateEstimatedKitPrice(beadCounts, palette) {
    const totalBeads = Object.values(beadCounts).reduce((s, n) => s + n, 0);
    const colorCount = Object.keys(beadCounts).length;
    // Use the brand's price-per-1000 if all beads come from one brand,
    // else fall back to the most-used brand's price.
    let pricePer1000 = 4.99;
    if (palette && palette.length) pricePer1000 = palette[0].pricePer1000 || 4.99;
    const base = 9.99;
    const beadCost = (totalBeads / 1000) * pricePer1000;
    const handling = colorCount * 0.15;
    const total = base + beadCost + handling;
    return Math.round(total * 100) / 100;
  }
  window.calculateEstimatedKitPrice = calculateEstimatedKitPrice;

  // The main pipeline. Accepts either an HTMLImageElement (already loaded)
  // or a File. Returns a GeneratedPattern.
  async function generatePatternFromImage(input, options) {
    const {
      brand = 'Mard',
      gridSize = 50,
      colorLimit = null,
      removeBg = false,
      denoise: doDenoise = true,
    } = options || {};

    const brandDef = window.BEAD_BRANDS[brand];
    if (!brandDef) throw new Error('Unknown brand: ' + brand);
    const palette = brandDef.palette;

    // 1. Load image
    let img, url;
    if (input instanceof File) {
      const r = await loadImageFromFile(input);
      img = r.img; url = r.url;
    } else if (input instanceof HTMLImageElement) {
      img = input;
    } else {
      throw new Error('Unsupported input');
    }

    // 2. Downsample to grid
    const { imageData, canvas: smallCanvas } = sampleImageToGrid(img, gridSize);
    const { data, width, height } = imageData;

    // 3. Map every pixel to the nearest bead color
    const grid = [];
    for (let y = 0; y < height; y++) {
      const row = [];
      for (let x = 0; x < width; x++) {
        const i = (y * width + x) * 4;
        const r = data[i], g = data[i+1], b = data[i+2], a = data[i+3];
        // Treat fully-transparent pixels as background
        if (a < 32) {
          row.push(palette[0].id);
        } else {
          const nearest = findNearestBeadColor({ r, g, b }, palette);
          row.push(nearest.id);
        }
      }
      grid.push(row);
    }

    // 4. Optional cleanup steps
    let processed = grid;
    if (doDenoise) processed = denoise(processed);
    if (removeBg) processed = removeBackgroundLite(processed, palette);
    if (colorLimit) processed = applyColorLimit(processed, palette, colorLimit);

    // 5. Bead counts + pricing
    const beadCounts = countBeads(processed);
    const totalBeads = Object.values(beadCounts).reduce((s, n) => s + n, 0);
    const colorCount = Object.keys(beadCounts).length;
    const estimatedPrice = calculateEstimatedKitPrice(beadCounts, palette);

    // 6. Preview image (data URL of the rendered bead grid)
    const previewImageUrl = gridToDataURL(processed, 8);

    const pattern = {
      id: 'pat-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8),
      sourceImageUrl: url || null,
      previewImageUrl,
      brand,
      gridSize,
      width,
      height,
      grid: processed,
      beadCounts,
      totalBeads,
      colorCount,
      estimatedPrice,
      createdAt: new Date().toISOString(),
    };
    return pattern;
  }
  window.generatePatternFromImage = generatePatternFromImage;

  // Pre-baked demo pattern from a built-in seeded synthetic image.
  // Used so the canvas isn't blank on first render.
  function buildDemoPattern(brand = 'Mard', gridSize = 50) {
    const palette = window.BEAD_BRANDS[brand].palette;
    // A face-like procedural grid using the brand palette directly.
    const grid = [];
    const cx = gridSize / 2, cy = gridSize / 2 - 1;
    // pick reasonable colors by name
    const find = (re) => palette.find(p => re.test(p.name))?.id || palette[0].id;
    const bg     = find(/cream|white|eggshell|cloud/i);
    const hair   = find(/charcoal|cocoa|brown|coffee/i);
    const hairD  = find(/black|off black|espresso|slate/i);
    const skin   = find(/skin|sand|toasted|bone/i);
    const skinH  = find(/pearl|cream|buttercream|eggshell/i);
    const skinS  = find(/clay|tan|beige|khaki|coral/i);
    const eyes   = find(/black|off black/i);
    const cheek  = find(/bubble|bubblegum|pink|rose/i);
    const mouth  = find(/crimson|red|wine|tomato/i);
    const accent = find(/yellow|arcade|sun/i);
    const accent2= find(/cyan|electric|sky/i);
    for (let y = 0; y < gridSize; y++) {
      const row = [];
      for (let x = 0; x < gridSize; x++) {
        const dx = (x - cx) / (gridSize * 0.32);
        const dy = (y - cy) / (gridSize * 0.38);
        const r = Math.sqrt(dx*dx + dy*dy);
        let id = bg;
        if (y < 3 && (x % 6 === 0)) id = accent;
        if (y === gridSize-1 && x % 4 === 0) id = accent2;
        if (r < 1.05 && dy < -0.15) { id = hair; if (dx < -0.3 && dy < -0.5) id = hairD; }
        if (r < 0.95 && dy >= -0.15) {
          id = skin;
          if (dx > 0.2 && dy < 0.2) id = skinH;
          if (dy > 0.45 || dx < -0.45) id = skinS;
        }
        const eyeY = Math.round(cy + 1);
        if ((y === eyeY || y === eyeY + 1) && (x === Math.round(cx - 4) || x === Math.round(cx + 4))) id = eyes;
        if (y === eyeY + 3 && (x === Math.round(cx - 5) || x === Math.round(cx + 5))) id = cheek;
        if (y === eyeY + 5 && x >= Math.round(cx - 2) && x <= Math.round(cx + 2)) id = mouth;
        row.push(id);
      }
      grid.push(row);
    }
    const beadCounts = countBeads(grid);
    const totalBeads = Object.values(beadCounts).reduce((s,n) => s + n, 0);
    const colorCount = Object.keys(beadCounts).length;
    return {
      id: 'demo-' + brand + '-' + gridSize,
      sourceImageUrl: null,
      previewImageUrl: null,
      brand, gridSize, width: gridSize, height: gridSize,
      grid, beadCounts, totalBeads, colorCount,
      estimatedPrice: calculateEstimatedKitPrice(beadCounts, palette),
      createdAt: new Date().toISOString(),
      isDemo: true,
    };
  }
  window.buildDemoPattern = buildDemoPattern;

})();
