/* ============================================================
   PixelCanvas — The core 2D pixel art editing canvas.
   Supports: pencil, eraser, fill, eyedropper, line, rect, circle,
             select, move, mirror draw, symmetry, pixel grid,
             zoom, pan, transparency, alpha lock, layers.
   ============================================================ */

const PixelCanvasCore = {
  // Current tool
  tool: 'pencil',
  brushSize: 1,
  primaryColor: '#FFFFFFFF',
  secondaryColor: '#000000FF',
  showGrid: true,
  symmetryX: false,
  symmetryY: false,

  // View state
  zoom: 16, // pixel scale
  offsetX: 0,
  offsetY: 0,

  // Drawing state
  isDrawing: false,
  lastX: -1,
  lastY: -1,
  strokeStarted: false,

  // Selection
  selection: null, // { x, y, w, h }
  moveOffset: null,
  selectionPixels: null, // ImageData

  // Symmetry & shading helpers
  symmetryMode: 'none', // 'none' | 'horizontal' | 'vertical' | 'both'

  // Listeners for redraw
  _onChange: null,

  init(opts) {
    this.tool = opts.tool || 'pencil';
    this.primaryColor = opts.primaryColor || '#FFFFFFFF';
    this.showGrid = opts.showGrid !== false;
    this.zoom = opts.zoom || 16;
    this._onChange = opts.onChange || null;
  },

  setTool(t) { this.tool = t; },
  setPrimaryColor(c) { this.primaryColor = c; },
  setSymmetry(mode) { this.symmetryMode = mode; },
  setZoom(z) { this.zoom = Math.max(1, Math.min(128, z)); },
  zoomIn() { this.setZoom(this.zoom * 2); },
  zoomOut() { this.setZoom(Math.max(1, Math.floor(this.zoom / 2))); },
  resetView(w, h, containerW, containerH) {
    const maxZoom = Math.floor(Math.min(containerW / w, containerH / h));
    this.zoom = Math.max(1, maxZoom);
    this.offsetX = 0;
    this.offsetY = 0;
  },

  // Convert screen coords to pixel coords
  screenToPixel(sx, sy, canvasRect, zoom, offsetX, offsetY, width, height) {
    const cx = canvasRect.width / 2 + offsetX;
    const cy = canvasRect.height / 2 + offsetY;
    const px = Math.floor((sx - cx + (width * zoom) / 2) / zoom);
    const py = Math.floor((sy - cy + (height * zoom) / 2) / zoom);
    return { x: px, y: py };
  },

  // Draw composite of layers onto a canvas
  drawLayers(canvas, layers, width, height) {
    const ctx = canvas.getContext('2d');
    ctx.imageSmoothingEnabled = false;
    ctx.clearRect(0, 0, width, height);
    for (const layer of layers) {
      if (!layer.visible) continue;
      ctx.globalAlpha = layer.opacity;
      ctx.drawImage(layer.texture.canvas, 0, 0);
    }
    ctx.globalAlpha = 1;
  },

  // Get active layer
  getActiveLayer(layers, activeLayerId) {
    return layers.find(l => l.id === activeLayerId);
  },

  // ---------- Drawing Operations ----------

  // Put a single pixel, respecting symmetry, alpha lock
  putPixel(layer, x, y, color, width, height) {
    if (!layer || layer.locked) return;
    if (x < 0 || y < 0 || x >= width || y >= height) return;
    const ctx = layer.texture.canvas.getContext('2d');
    ctx.imageSmoothingEnabled = false;

    const drawAt = (px, py) => {
      if (px < 0 || py < 0 || px >= width || py >= height) return;
      if (layer.alphaLock) {
        // Only paint where there's already alpha
        const imgData = ctx.getImageData(px, py, 1, 1);
        if (imgData.data[3] === 0) return;
      }
      ctx.fillStyle = color;
      ctx.fillRect(px, py, 1, 1);
    };

    const rgba = this.hexToRgba(color);
    const baseAlpha = rgba.a / 255;

    // Brush size (circle-ish)
    const r = this.brushSize;
    for (let dy = -r + 1; dy < r; dy++) {
      for (let dx = -r + 1; dx < r; dx++) {
        if (r > 1 && dx * dx + dy * dy > r * r) continue;
        drawAt(x + dx, y + dy);
      }
    }

    // Symmetry
    if (this.symmetryMode === 'horizontal' || this.symmetryMode === 'both') {
      const sx = width - 1 - x;
      for (let dy = -r + 1; dy < r; dy++) {
        for (let dx = -r + 1; dx < r; dx++) {
          if (r > 1 && dx * dx + dy * dy > r * r) continue;
          drawAt(sx + dx, y + dy);
        }
      }
    }
    if (this.symmetryMode === 'vertical' || this.symmetryMode === 'both') {
      const sy = height - 1 - y;
      for (let dy = -r + 1; dy < r; dy++) {
        for (let dx = -r + 1; dx < r; dx++) {
          if (r > 1 && dx * dx + dy * dy > r * r) continue;
          drawAt(x + dx, sy + dy);
        }
      }
    }
    if (this.symmetryMode === 'both') {
      const sx = width - 1 - x;
      const sy = height - 1 - y;
      for (let dy = -r + 1; dy < r; dy++) {
        for (let dx = -r + 1; dx < r; dx++) {
          if (r > 1 && dx * dx + dy * dy > r * r) continue;
          drawAt(sx + dx, sy + dy);
        }
      }
    }
  },

  // Flood fill
  fill(layer, startX, startY, fillColor, width, height) {
    if (!layer || layer.locked) return;
    if (startX < 0 || startY < 0 || startX >= width || startY >= height) return;
    const ctx = layer.texture.canvas.getContext('2d');
    ctx.imageSmoothingEnabled = false;
    const imgData = ctx.getImageData(0, 0, width, height);
    const data = imgData.data;

    const fillRGBA = this.hexToRgba(fillColor);
    const startIdx = (startY * width + startX) * 4;
    const startR = data[startIdx], startG = data[startIdx+1], startB = data[startIdx+2], startA = data[startIdx+3];

    if (startR === fillRGBA.r && startG === fillRGBA.g && startB === fillRGBA.b && startA === fillRGBA.a) return;

    const stack = [[startX, startY]];
    const matches = (idx) => {
      return data[idx] === startR && data[idx+1] === startG && data[idx+2] === startB && data[idx+3] === startA;
    };

    while (stack.length > 0) {
      const [x, y] = stack.pop();
      let idx = (y * width + x) * 4;
      // Find left edge
      let lx = x;
      while (lx >= 0 && matches((y * width + lx) * 4)) lx--;
      lx++;
      // Scan right
      let rx = x;
      while (rx < width && matches((y * width + rx) * 4)) rx++;
      rx--;
      // Fill the line
      for (let px = lx; px <= rx; px++) {
        const i = (y * width + px) * 4;
        if (layer.alphaLock && data[i+3] === 0) continue;
        data[i] = fillRGBA.r;
        data[i+1] = fillRGBA.g;
        data[i+2] = fillRGBA.b;
        data[i+3] = fillRGBA.a;
      }
      // Check above and below
      if (y > 0) {
        for (let px = lx; px <= rx; px++) {
          if (matches(((y-1) * width + px) * 4)) {
            if (px === rx || !matches(((y-1) * width + px + 1) * 4)) {
              stack.push([px, y-1]);
            }
          }
        }
      }
      if (y < height - 1) {
        for (let px = lx; px <= rx; px++) {
          if (matches(((y+1) * width + px) * 4)) {
            if (px === rx || !matches(((y+1) * width + px + 1) * 4)) {
              stack.push([px, y+1]);
            }
          }
        }
      }
    }

    ctx.putImageData(imgData, 0, 0);
  },

  // Line (Bresenham)
  line(layer, x0, y0, x1, y1, color, width, height) {
    if (!layer || layer.locked) return;
    const dx = Math.abs(x1 - x0);
    const dy = -Math.abs(y1 - y0);
    const sx = x0 < x1 ? 1 : -1;
    const sy = y0 < y1 ? 1 : -1;
    let err = dx + dy;
    let x = x0, y = y0;
    while (true) {
      this.putPixel(layer, x, y, color, width, height);
      if (x === x1 && y === y1) break;
      const e2 = 2 * err;
      if (e2 >= dy) { err += dy; x += sx; }
      if (e2 <= dx) { err += dx; y += sy; }
    }
  },

  // Rectangle outline
  rect(layer, x0, y0, x1, y1, color, width, height) {
    if (!layer || layer.locked) return;
    const left = Math.min(x0, x1);
    const right = Math.max(x0, x1);
    const top = Math.min(y0, y1);
    const bottom = Math.max(y0, y1);
    this.line(layer, left, top, right, top, color, width, height);
    this.line(layer, right, top, right, bottom, color, width, height);
    this.line(layer, right, bottom, left, bottom, color, width, height);
    this.line(layer, left, bottom, left, top, color, width, height);
  },

  // Circle (midpoint)
  circle(layer, cx, cy, r, color, width, height) {
    if (!layer || layer.locked) return;
    if (r < 1) r = 1;
    let x = r, y = 0;
    let err = 1 - r;
    while (x >= y) {
      this.putPixel(layer, cx + x, cy + y, color, width, height);
      this.putPixel(layer, cx + y, cy + x, color, width, height);
      this.putPixel(layer, cx - y, cy + x, color, width, height);
      this.putPixel(layer, cx - x, cy + y, color, width, height);
      this.putPixel(layer, cx - x, cy - y, color, width, height);
      this.putPixel(layer, cx - y, cy - x, color, width, height);
      this.putPixel(layer, cx + y, cy - x, color, width, height);
      this.putPixel(layer, cx + x, cy - y, color, width, height);
      y++;
      if (err <= 0) {
        err += 2 * y + 1;
      } else {
        x--;
        err += 2 * y - 2 * x + 1;
      }
    }
  },

  // Eyedropper: pick color from composite
  eyedropper(layers, x, y, width, height) {
    if (x < 0 || y < 0 || x >= width || y >= height) return null;
    // Composite from bottom to top
    let r = 0, g = 0, b = 0, a = 0;
    for (const layer of layers) {
      if (!layer.visible) continue;
      const ctx = layer.texture.canvas.getContext('2d');
      const d = ctx.getImageData(x, y, 1, 1).data;
      const la = d[3] / 255 * layer.opacity;
      if (la === 0) continue;
      const outA = a + la * (1 - a);
      if (outA === 0) continue;
      r = (r * a + d[0] * la * (1 - a)) / outA;
      g = (g * a + d[1] * la * (1 - a)) / outA;
      b = (b * a + d[2] * la * (1 - a)) / outA;
      a = outA;
    }
    return `#${this.toHex(r)}${this.toHex(g)}${this.toHex(b)}${this.toHex(Math.round(a * 255))}`;
  },

  // ---------- Shading helpers ----------

  shadeColor(hex, amount) {
    // amount: -1 .. 1 (negative = darker, positive = lighter)
    const rgba = this.hexToRgba(hex);
    let r, g, b;
    if (amount >= 0) {
      r = rgba.r + (255 - rgba.r) * amount;
      g = rgba.g + (255 - rgba.g) * amount;
      b = rgba.b + (255 - rgba.b) * amount;
    } else {
      r = rgba.r * (1 + amount);
      g = rgba.g * (1 + amount);
      b = rgba.b * (1 + amount);
    }
    return `#${this.toHex(r)}${this.toHex(g)}${this.toHex(b)}${this.toHex(rgba.a)}`;
  },

  // ---------- Utilities ----------

  hexToRgba(hex) {
    // Supports #RRGGBB or #RRGGBBAA or #RGB
    let h = hex.replace('#', '');
    if (h.length === 3) h = h[0]+h[0]+h[1]+h[1]+h[2]+h[2]+'FF';
    else if (h.length === 6) h += 'FF';
    else if (h.length === 8) {}
    else h = '00000000';
    return {
      r: parseInt(h.substring(0, 2), 16),
      g: parseInt(h.substring(2, 4), 16),
      b: parseInt(h.substring(4, 6), 16),
      a: parseInt(h.substring(6, 8), 16),
    };
  },

  toHex(n) {
    const v = Math.max(0, Math.min(255, Math.round(n)));
    return v.toString(16).padStart(2, '0').toUpperCase();
  },

  // Pixel noise on selection or whole canvas
  applyNoise(layer, amount, x0, y0, w, h, width, height) {
    if (!layer || layer.locked) return;
    const ctx = layer.texture.canvas.getContext('2d');
    const imgData = ctx.getImageData(x0, y0, w, h);
    const d = imgData.data;
    for (let i = 0; i < d.length; i += 4) {
      if (layer.alphaLock && d[i+3] === 0) continue;
      const noise = (Math.random() - 0.5) * 2 * amount;
      d[i] = Math.max(0, Math.min(255, d[i] + noise));
      d[i+1] = Math.max(0, Math.min(255, d[i+1] + noise));
      d[i+2] = Math.max(0, Math.min(255, d[i+2] + noise));
    }
    ctx.putImageData(imgData, x0, y0);
  },

  // 1px outline (using current color) on non-transparent pixels' edges
  applyOutline(layer, outlineColor, width, height) {
    if (!layer || layer.locked) return;
    const ctx = layer.texture.canvas.getContext('2d');
    const imgData = ctx.getImageData(0, 0, width, height);
    const d = imgData.data;
    const newData = new Uint8ClampedArray(d);
    const rgba = this.hexToRgba(outlineColor);

    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        const i = (y * width + x) * 4;
        if (d[i+3] > 0) continue; // already opaque
        // Check neighbors
        let hasNeighbor = false;
        const dirs = [[-1,0],[1,0],[0,-1],[0,1]];
        for (const [dx, dy] of dirs) {
          const nx = x + dx, ny = y + dy;
          if (nx < 0 || ny < 0 || nx >= width || ny >= height) continue;
          const ni = (ny * width + nx) * 4;
          if (d[ni+3] > 0) { hasNeighbor = true; break; }
        }
        if (hasNeighbor) {
          newData[i] = rgba.r;
          newData[i+1] = rgba.g;
          newData[i+2] = rgba.b;
          newData[i+3] = rgba.a;
        }
      }
    }

    ctx.putImageData(new ImageData(newData, width, height), 0, 0);
  },

  // Mirror texture horizontally
  mirrorHorizontal(canvas, width, height) {
    const ctx = canvas.getContext('2d');
    const imgData = ctx.getImageData(0, 0, width, height);
    const d = imgData.data;
    const newData = new Uint8ClampedArray(d.length);
    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        const i = (y * width + x) * 4;
        const mi = (y * width + (width - 1 - x)) * 4;
        newData[i] = d[mi];
        newData[i+1] = d[mi+1];
        newData[i+2] = d[mi+2];
        newData[i+3] = d[mi+3];
      }
    }
    ctx.putImageData(new ImageData(newData, width, height), 0, 0);
  },

  // Rotate 90deg CW
  rotate90(canvas, width, height) {
    const ctx = canvas.getContext('2d');
    const imgData = ctx.getImageData(0, 0, width, height);
    const d = imgData.data;
    const newCanvas = document.createElement('canvas');
    newCanvas.width = height;
    newCanvas.height = width;
    const nctx = newCanvas.getContext('2d');
    nctx.imageSmoothingEnabled = false;
    const newData = nctx.createImageData(height, width);
    const nd = newData.data;
    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        const i = (y * width + x) * 4;
        const nx = height - 1 - y;
        const ny = x;
        const ni = (ny * height + nx) * 4;
        nd[ni] = d[i];
        nd[ni+1] = d[i+1];
        nd[ni+2] = d[i+2];
        nd[ni+3] = d[i+3];
      }
    }
    canvas.width = height;
    canvas.height = width;
    nctx.putImageData(newData, 0, 0);
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.drawImage(newCanvas, 0, 0);
  },

  // ---- Shading: apply highlight/midtone/shadow/deep shadow to selection or whole layer ----
  applyShading(canvas, mode, color, selection) {
    const ctx = canvas.getContext('2d');
    const w = canvas.width;
    const h = canvas.height;
    const imgData = ctx.getImageData(0, 0, w, h);
    const d = imgData.data;
    const rgba = this.hexToRgba(color);
    // Amount per mode (percentage of mix toward shade color)
    const amounts = { highlight: 0.35, midtone: 0.15, shadow: 0.35, deep: 0.55 };
    const amount = amounts[mode] || 0.2;
    for (let i = 0; i < d.length; i += 4) {
      if (d[i+3] === 0) continue; // skip fully transparent
      // Mix toward white (highlight) or black (shadow)
      let tr, tg, tb;
      if (mode === 'highlight' || mode === 'midtone') {
        tr = 255; tg = 255; tb = 255;
      } else {
        tr = 0; tg = 0; tb = 0;
      }
      d[i]   = Math.round(d[i]   * (1 - amount) + tr * amount);
      d[i+1] = Math.round(d[i+1] * (1 - amount) + tg * amount);
      d[i+2] = Math.round(d[i+2] * (1 - amount) + tb * amount);
    }
    ctx.putImageData(imgData, 0, 0);
  },

  // ---- Pixel Noise: add small color variation ----
  applyNoise(canvas, intensity = 15, selection) {
    const ctx = canvas.getContext('2d');
    const w = canvas.width;
    const h = canvas.height;
    const imgData = ctx.getImageData(0, 0, w, h);
    const d = imgData.data;
    for (let i = 0; i < d.length; i += 4) {
      if (d[i+3] === 0) continue;
      const n = (Math.random() - 0.5) * 2 * intensity;
      d[i]   = Math.max(0, Math.min(255, d[i]   + n));
      d[i+1] = Math.max(0, Math.min(255, d[i+1] + n));
      d[i+2] = Math.max(0, Math.min(255, d[i+2] + n));
    }
    ctx.putImageData(imgData, 0, 0);
  },

  // ---- Pixel Outline: add 1px outline around opaque pixels ----
  applyOutline(canvas, color = '#000000', mode = '1px') {
    const ctx = canvas.getContext('2d');
    const w = canvas.width;
    const h = canvas.height;
    const imgData = ctx.getImageData(0, 0, w, h);
    const d = imgData.data;
    const rgba = this.hexToRgba(color + (color.length === 7 ? 'FF' : ''));
    
    // Build a mask of opaque pixels
    const opaque = new Uint8Array(w * h);
    for (let i = 0; i < d.length; i += 4) {
      opaque[i/4] = d[i+3] > 0 ? 1 : 0;
    }
    
    // Find edge pixels (transparent with opaque neighbor)
    const newImgData = ctx.createImageData(w, h);
    const nd = newImgData.data;
    for (let y = 0; y < h; y++) {
      for (let x = 0; x < w; x++) {
        const idx = y * w + x;
        const i = idx * 4;
        if (opaque[idx]) {
          // Keep original pixel
          nd[i] = d[i]; nd[i+1] = d[i+1]; nd[i+2] = d[i+2]; nd[i+3] = d[i+3];
        } else {
          // Check neighbors
          let hasOpaqueNeighbor = false;
          if (x > 0 && opaque[idx-1]) hasOpaqueNeighbor = true;
          else if (x < w-1 && opaque[idx+1]) hasOpaqueNeighbor = true;
          else if (y > 0 && opaque[idx-w]) hasOpaqueNeighbor = true;
          else if (y < h-1 && opaque[idx+w]) hasOpaqueNeighbor = true;
          
          if (hasOpaqueNeighbor) {
            nd[i] = rgba.r; nd[i+1] = rgba.g; nd[i+2] = rgba.b; nd[i+3] = rgba.a;
          }
        }
      }
    }
    ctx.putImageData(newImgData, 0, 0);
  },
};

function syncPreviewCanvas(dest, src) {
  if (!dest || !src) return;
  if (dest.width !== src.width) dest.width = src.width;
  if (dest.height !== src.height) dest.height = src.height;
  const ctx = dest.getContext('2d');
  ctx.imageSmoothingEnabled = false;
  ctx.clearRect(0, 0, dest.width, dest.height);
  ctx.drawImage(src, 0, 0);
}

function markTextureDirty(tex) {
  if (!tex) return;
  tex.needsUpdate = true;
}

function observeRendererSize(container, camera, renderer) {
  const apply = () => {
    if (!container || !camera || !renderer) return;
    const w = container.clientWidth;
    const h = container.clientHeight;
    if (w < 2 || h < 2) return;
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
    renderer.setSize(w, h, false);
  };
  apply();
  if (typeof ResizeObserver === 'undefined') {
    return { disconnect() {}, apply };
  }
  const ro = new ResizeObserver(apply);
  ro.observe(container);
  return { disconnect() { ro.disconnect(); }, apply };
}

window.PixelCanvasCore = PixelCanvasCore;
window.syncPreviewCanvas = syncPreviewCanvas;
window.markTextureDirty = markTextureDirty;
window.observeRendererSize = observeRendererSize;
