/**
 * Stroke pipeline — paints one segment (last-position → current
 * position) onto the active layer (or its active mask sub-layer).
 *
 * `strokeTo` dispatches by tool:
 *   - clone  → cloneStrokeTo (custom stamp-based paint loop)
 *   - brush  → source-over with opacity × flow + softness blur
 *   - eraser → destination-out with opacity × flow + softness blur
 *   - inpaint → source-over (paint) or destination-out (erase) with
 *               full alpha on the mask canvas
 *
 * If the active parent has an active mask sub-layer, brush / eraser /
 * inpaint target the mask canvas instead of the layer's pixel canvas.
 *
 * @param {{
 *   activeLayer:          () => object | null,
 *   getActiveMaskLayer:   () => object | null,
 *   composite:            () => void,
 * }} deps
 */
import { state } from './state.js';

export function createStrokePipeline({ activeLayer, getActiveMaskLayer, composite }) {
  function cloneStrokeTo(x, y, layer) {
    if (!state.cloneSourceSnapshot) return;
    const off = state.layerOffsets.get(layer.id) || { x: 0, y: 0 };
    const dx = x - state.cloneStrokeStartX;
    const dy = y - state.cloneStrokeStartY;
    const srcX = state.cloneSourceX + dx;
    const srcY = state.cloneSourceY + dy;
    const ctx = layer.ctx;
    const radius = Math.max(1, state.brushSize / 2);
    // Walk last → current in roughly half-brush steps so stamps
    // overlap into a continuous brush trail.
    const lastSrcX = state.cloneSourceX + (state.lastX - state.cloneStrokeStartX);
    const lastSrcY = state.cloneSourceY + (state.lastY - state.cloneStrokeStartY);
    const dist = Math.hypot(x - state.lastX, y - state.lastY);
    const step = Math.max(1, radius * 0.5);
    const steps = Math.max(1, Math.ceil(dist / step));
    const stampSize = Math.max(2, Math.ceil(radius * 2));
    const stampRadius = stampSize / 2;
    const stamp = document.createElement('canvas');
    stamp.width = stampSize;
    stamp.height = stampSize;
    const stampCtx = stamp.getContext('2d');
    const softness = Math.max(0, Math.min(1, state.cloneSoftness / 300));
    const hardStop = stampRadius * (1 - softness);
    ctx.save();
    ctx.globalAlpha = (state.cloneOpacity / 100) * (state.cloneFlow / 100);
    for (let i = 1; i <= steps; i++) {
      const t = i / steps;
      const px = state.lastX + (x - state.lastX) * t - off.x;
      const py = state.lastY + (y - state.lastY) * t - off.y;
      const sx = lastSrcX + (srcX - lastSrcX) * t;
      const sy = lastSrcY + (srcY - lastSrcY) * t;
      stampCtx.clearRect(0, 0, stampSize, stampSize);
      stampCtx.globalCompositeOperation = 'source-over';
      stampCtx.drawImage(
        state.cloneSourceSnapshot,
        sx - stampRadius, sy - stampRadius, stampSize, stampSize,
        0, 0, stampSize, stampSize,
      );
      stampCtx.globalCompositeOperation = 'destination-in';
      const mask = stampCtx.createRadialGradient(stampRadius, stampRadius, hardStop, stampRadius, stampRadius, stampRadius);
      mask.addColorStop(0, 'rgba(0,0,0,1)');
      mask.addColorStop(1, 'rgba(0,0,0,0)');
      stampCtx.fillStyle = mask;
      stampCtx.fillRect(0, 0, stampSize, stampSize);
      ctx.drawImage(stamp, px - stampRadius, py - stampRadius);
    }
    ctx.restore();
    state.lastX = x;
    state.lastY = y;
    composite();
  }

  function strokeTo(x, y) {
    const layer = activeLayer();
    if (!layer) return;
    // Clone uses a stamp-based paint loop, not the line-stroke
    // pipeline below.
    if (state.tool === 'clone') return cloneStrokeTo(x, y, layer);

    // If the active parent has an active mask sub-layer, brush /
    // eraser / inpaint paint the mask canvas instead of the layer's
    // pixel canvas. Brush adds to the mask, Eraser carves it away,
    // Inpaint still works (its mask plumbing was already pointed at
    // the same canvas).
    const activeMask = getActiveMaskLayer();
    const paintingMask = !!activeMask &&
      (state.tool === 'brush' || state.tool === 'eraser' || state.tool === 'inpaint');
    const ctx = paintingMask
      ? activeMask.ctx
      : (state.tool === 'inpaint' ? state.maskCtx : layer.ctx);
    const off = state.layerOffsets.get(layer.id) || { x: 0, y: 0 };

    ctx.save();
    ctx.lineWidth = state.brushSize;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';

    if (state.tool === 'eraser') {
      ctx.globalCompositeOperation = 'destination-out';
      // Effective alpha = opacity × flow. Opacity = max strength a
      // stroke can reach; flow = how much erases per pass.
      ctx.globalAlpha = (state.eraserOpacity / 100) * (state.eraserFlow / 100);
      ctx.strokeStyle = 'rgba(0,0,0,1)';
      if (state.eraserSoftness > 0) {
        const blurPx = (state.eraserSoftness / 100) * (state.brushSize / 2);
        ctx.filter = `blur(${blurPx.toFixed(2)}px)`;
      }
    } else if (state.tool === 'brush') {
      // Brush — state.color onto the layer (or white onto an active
      // mask sub-layer). Mask painting forces full alpha so masks
      // stay a clean binary by default (a sub-100% brush would
      // silently paint partial-strength mask pixels).
      ctx.globalCompositeOperation = 'source-over';
      ctx.strokeStyle = paintingMask ? 'rgba(255,255,255,1)' : state.color;
      if (paintingMask) {
        ctx.globalAlpha = 1;
      } else {
        ctx.globalAlpha = (state.brushOpacity / 100) * (state.brushFlow / 100);
        if (state.brushSoftness > 0) {
          const blurPx = (state.brushSoftness / 100) * (state.brushSize / 2);
          ctx.filter = `blur(${blurPx.toFixed(2)}px)`;
        }
      }
    } else if (state.tool === 'inpaint') {
      if (state.inpaintEraseStroke) {
        ctx.globalCompositeOperation = 'destination-out';
        ctx.strokeStyle = 'rgba(0,0,0,1)';
      } else {
        ctx.globalCompositeOperation = 'source-over';
        // Diffusion server expects white = inpaint area. The red
        // overlay is rendered separately in composite() for the user.
        ctx.strokeStyle = 'rgba(255,255,255,1)';
      }
    } else {
      ctx.globalCompositeOperation = 'source-over';
      ctx.strokeStyle = state.color;
    }

    // Mask canvases are always full-image (no per-layer offset), so
    // painting onto a mask uses canvas-coord origin too — same as
    // inpaint.
    const onMaskOrInpaint = paintingMask || state.tool === 'inpaint';
    const drawX = onMaskOrInpaint ? 0 : off.x;
    const drawY = onMaskOrInpaint ? 0 : off.y;

    ctx.beginPath();
    ctx.moveTo(state.lastX - drawX, state.lastY - drawY);
    ctx.lineTo(x - drawX, y - drawY);
    ctx.stroke();
    ctx.restore();

    state.lastX = x;
    state.lastY = y;
    composite();
  }

  return { strokeTo, cloneStrokeTo };
}
