import { portsOf, DIRS, pathSpec, pieceById } from './pieces.js';
import { paletteOf } from './palettes.js';
import { ParticleSystem } from './particles.js';

export function fitCanvas(canvas, w, h, maxDpr = 2) {
  const dpr = Math.min(window.devicePixelRatio || 1, maxDpr);
  const pw = Math.max(1, Math.floor(w * dpr));
  const ph = Math.max(1, Math.floor(h * dpr));
  if (canvas.width !== pw || canvas.height !== ph) {
    canvas.width = pw;
    canvas.height = ph;
  }
  canvas.style.width = `${Math.floor(w)}px`;
  canvas.style.height = `${Math.floor(h)}px`;
  const ctx = canvas.getContext('2d');
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  return { ctx, dpr, w, h };
}

function roundRect(ctx, x, y, w, h, r) {
  const rr = Math.min(r, w / 2, h / 2);
  ctx.beginPath();
  ctx.moveTo(x + rr, y);
  ctx.arcTo(x + w, y, x + w, y + h, rr);
  ctx.arcTo(x + w, y + h, x, y + h, rr);
  ctx.arcTo(x, y + h, x, y, rr);
  ctx.arcTo(x, y, x + w, y, rr);
  ctx.closePath();
}

export class View {
  constructor(boardCanvas, queueCanvas) {
    this.boardCanvas = boardCanvas;
    this.queueCanvas = queueCanvas;
    this.particles = new ParticleSystem();
    this.layout = { x: 0, y: 0, size: 48, pad: 0, w: 0, h: 0 };
    this.hover = null;
    this.time = 0;
    this.bgTime = 0;
  }

  setPalette(id) {
    this.pal = paletteOf(id);
  }

  resize(cssW, cssH) {
    this.layout.w = cssW;
    this.layout.h = cssH;
    fitCanvas(this.boardCanvas, cssW, cssH);
  }

  gridAt(mx, my, game) {
    const { x, y, size } = this.layout;
    const gx = Math.floor((mx - x) / size);
    const gy = Math.floor((my - y) / size);
    if (gx < 0 || gy < 0 || gx >= game.cols || gy >= game.rows) return null;
    return { x: gx, y: gy };
  }

  cellCenter(gx, gy) {
    const { x, y, size } = this.layout;
    return { x: x + gx * size + size / 2, y: y + gy * size + size / 2 };
  }

  layoutBoard(game) {
    const w = this.layout.w;
    const h = this.layout.h;
    const pad = 18;
    const size = Math.floor(
      Math.min((w - pad * 2) / game.cols, (h - pad * 2) / game.rows)
    );
    const bw = size * game.cols;
    const bh = size * game.rows;
    this.layout.size = size;
    this.layout.pad = pad;
    this.layout.x = Math.floor((w - bw) / 2);
    this.layout.y = Math.floor((h - bh) / 2);
    return this.layout;
  }

  update(dt, game) {
    this.time += dt;
    this.particles.enabled = game.settings.particles !== false;
    this.particles.update(dt);
    if (game.phase === 'flowing' || game.phase === 'finishing') {
      for (const head of game.heads) {
        const c = this.cellCenter(head.x, head.y);
        if (Math.random() < dt * 14) {
          this.particles.drip(c.x, c.y, this.pal.flooz);
        }
      }
    }
    if (game.leak && game.leak.t < 0.05 && game.leak.t + dt >= 0.05) {
      const c = this.cellCenter(game.leak.x, game.leak.y);
      const ang = game.leak.dir ? DIRS[game.leak.dir].ang : 0;
      this.particles.spawn(c.x, c.y, 'splash', {
        n: 36,
        angle: ang,
        spread: 1.4,
        speed: 160,
        color: game.leak.success ? this.pal.gold : this.pal.flooz,
        g: 420,
      });
    }
  }

  burstPlace(gx, gy, replace) {
    const c = this.cellCenter(gx, gy);
    this.particles.spawn(c.x, c.y, 'spark', {
      n: replace ? 18 : 10,
      speed: replace ? 140 : 70,
      color: replace ? this.pal.danger : this.pal.accent,
      g: 30,
      life: 0.4,
    });
  }

  burstWin(game) {
    for (let i = 0; i < 6; i++) {
      const c = this.cellCenter(
        Math.floor(Math.random() * game.cols),
        Math.floor(Math.random() * game.rows)
      );
      this.particles.spawn(c.x, c.y, 'win', {
        n: 16,
        color: Math.random() > 0.5 ? this.pal.flooz : this.pal.gold,
        g: 60,
        speed: 120,
      });
    }
  }

  draw(game) {
    const pal = this.pal;
    const { ctx, w, h } = {
      ctx: this.boardCanvas.getContext('2d'),
      w: this.layout.w,
      h: this.layout.h,
    };
    ctx.setTransform(
      Math.min(window.devicePixelRatio || 1, 2),
      0,
      0,
      Math.min(window.devicePixelRatio || 1, 2),
      0,
      0
    );
    ctx.clearRect(0, 0, w, h);

    let ox = 0;
    let oy = 0;
    if (game.shake > 0 && game.settings.shake && !game.settings.reducedMotion) {
      ox = (Math.random() - 0.5) * game.shake;
      oy = (Math.random() - 0.5) * game.shake;
    }
    ctx.save();
    ctx.translate(ox, oy);

    this.layoutBoard(game);
    this._drawFrame(ctx, game, pal);
    this._drawCells(ctx, game, pal);
    if (game.settings.ghostPath && game.phase === 'countdown') {
      this._drawGhost(ctx, game, pal);
    }
    this._drawPipes(ctx, game, pal);
    this._drawFlooz(ctx, game, pal);
    if (this.hover && game.hoverValid(this.hover.x, this.hover.y)) {
      this._drawGhostPiece(ctx, game, pal);
    }
    this.particles.draw(ctx);
    this._drawPopup(ctx, game);
    ctx.restore();

    if (game.flash > 0) {
      ctx.fillStyle = `rgba(255,70,80,${game.flash * 0.28})`;
      if (game.phase === 'won' || (game.leak && game.leak.success)) {
        ctx.fillStyle = `rgba(92,255,212,${game.flash * 0.22})`;
      }
      ctx.fillRect(0, 0, w, h);
    }
  }

  _drawFrame(ctx, game, pal) {
    const { x, y, size } = this.layout;
    const bw = size * game.cols;
    const bh = size * game.rows;
    roundRect(ctx, x - 10, y - 10, bw + 20, bh + 20, 16);
    ctx.fillStyle = pal.panel;
    ctx.fill();
    ctx.strokeStyle = 'rgba(255,255,255,0.06)';
    ctx.lineWidth = 1;
    ctx.stroke();

    roundRect(ctx, x - 3, y - 3, bw + 6, bh + 6, 10);
    ctx.fillStyle = pal.bg;
    ctx.fill();

    for (let gy = 0; gy < game.rows; gy++) {
      for (let gx = 0; gx < game.cols; gx++) {
        const cx = x + gx * size;
        const cy = y + gy * size;
        ctx.fillStyle = (gx + gy) % 2 === 0 ? pal.grid : shade(pal.grid, 1.08);
        ctx.fillRect(cx, cy, size, size);
        ctx.fillStyle = 'rgba(255,255,255,0.015)';
        ctx.fillRect(cx + 1, cy + 1, size - 2, 2);
      }
    }
    ctx.strokeStyle = pal.gridLine;
    ctx.lineWidth = 1;
    ctx.beginPath();
    for (let i = 0; i <= game.cols; i++) {
      ctx.moveTo(x + i * size + 0.5, y);
      ctx.lineTo(x + i * size + 0.5, y + bh);
    }
    for (let i = 0; i <= game.rows; i++) {
      ctx.moveTo(x, y + i * size + 0.5);
      ctx.lineTo(x + bw, y + i * size + 0.5);
    }
    ctx.stroke();

    if (game.wrap) {
      ctx.save();
      ctx.strokeStyle = pal.accent;
      ctx.globalAlpha = 0.35;
      ctx.setLineDash([6, 6]);
      ctx.lineDashOffset = -this.time * 20;
      ctx.lineWidth = 2;
      roundRect(ctx, x + 2, y + 2, bw - 4, bh - 4, 8);
      ctx.stroke();
      ctx.restore();
    }
  }

  _drawCells(ctx, game, pal) {
    const { x, y, size } = this.layout;
    for (let gy = 0; gy < game.rows; gy++) {
      for (let gx = 0; gx < game.cols; gx++) {
        const c = game.board[gy][gx];
        const cx = x + gx * size;
        const cy = y + gy * size;
        if (c.kind === 'wall') this._wall(ctx, cx, cy, size, pal);
        if (this.hover && this.hover.x === gx && this.hover.y === gy) {
          ctx.fillStyle = 'rgba(62,224,255,0.07)';
          ctx.fillRect(cx, cy, size, size);
        }
        if (c.replaceFlash > 0) {
          ctx.fillStyle = `rgba(255,68,85,${c.replaceFlash})`;
          ctx.fillRect(cx, cy, size, size);
        }
      }
    }
  }

  _wall(ctx, x, y, s, pal) {
    const p = Math.max(4, s * 0.08);
    roundRect(ctx, x + p, y + p, s - p * 2, s - p * 2, 6);
    ctx.fillStyle = '#07090d';
    ctx.fill();
    ctx.save();
    ctx.clip();
    ctx.strokeStyle = 'rgba(90,110,140,0.18)';
    ctx.lineWidth = 2;
    for (let i = -s; i < s * 2; i += Math.max(7, s * 0.12)) {
      ctx.beginPath();
      ctx.moveTo(x + i, y);
      ctx.lineTo(x + i + s, y + s);
      ctx.stroke();
    }
    ctx.restore();
    roundRect(ctx, x + p, y + p, s - p * 2, s - p * 2, 6);
    ctx.strokeStyle = pal.wallEdge;
    ctx.lineWidth = 2;
    ctx.stroke();
    ctx.fillStyle = 'rgba(180,200,220,0.2)';
    const b = Math.max(4, s * 0.08);
    ctx.fillRect(x + s * 0.25, y + s * 0.25, b, b);
    ctx.fillRect(x + s * 0.68, y + s * 0.62, b, b);
  }

  _drawPipes(ctx, game, pal) {
    for (let gy = 0; gy < game.rows; gy++) {
      for (let gx = 0; gx < game.cols; gx++) {
        const c = game.board[gy][gx];
        if (!c.pipe) continue;
        const { x, y } = this.cellCenter(gx, gy);
        const s = this.layout.size;
        let scale = 1;
        const age = this.time - (c.placeT || 0);
        if (age < 0.22 && !game.settings.reducedMotion && c.kind === 'pipe') {
          const t = age / 0.22;
          scale = 0.72 + 0.28 * easeOutBack(t);
        }
        ctx.save();
        ctx.translate(x, y);
        ctx.scale(scale, scale);
        this.drawPipeBody(ctx, c.pipe, 0, 0, s, pal, c.kind);
        ctx.restore();
      }
    }
  }

  drawPipeBody(ctx, piece, x, y, size, pal, kind = 'pipe') {
    const r = size * 0.22;
    const isTank = piece.id && String(piece.id).startsWith('tank');
    const isStart = kind === 'start';
    const isEnd = kind === 'end';

    this._strokePorts(ctx, piece.mask, x, y, size, r * 2.15, pal.metal0, 'round');
    this._strokePorts(ctx, piece.mask, x, y, size, r * 1.78, pal.metal1, 'round');
    this._strokePorts(ctx, piece.mask, x, y, size, r * 1.35, pal.metal2, 'round');
    this._strokePorts(ctx, piece.mask, x, y, size, r * 0.92, pal.channel, 'round');

    if (isTank) {
      roundRect(ctx, x - size * 0.28, y - size * 0.28, size * 0.56, size * 0.56, 8);
      ctx.fillStyle = pal.metal1;
      ctx.fill();
      roundRect(ctx, x - size * 0.2, y - size * 0.2, size * 0.4, size * 0.4, 6);
      ctx.fillStyle = pal.channel;
      ctx.fill();
      ctx.strokeStyle = pal.metal2;
      ctx.lineWidth = 2;
      ctx.stroke();
    } else {
      ctx.beginPath();
      ctx.arc(x, y, r * 0.95, 0, Math.PI * 2);
      ctx.fillStyle = pal.metal1;
      ctx.fill();
      ctx.beginPath();
      ctx.arc(x, y, r * 0.55, 0, Math.PI * 2);
      ctx.fillStyle = pal.channel;
      ctx.fill();
    }

    if (piece.independent) {
      ctx.strokeStyle = pal.gold;
      ctx.globalAlpha = 0.55;
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.moveTo(x - r * 0.3, y - r * 0.3);
      ctx.lineTo(x + r * 0.3, y + r * 0.3);
      ctx.moveTo(x + r * 0.3, y - r * 0.3);
      ctx.lineTo(x - r * 0.3, y + r * 0.3);
      ctx.stroke();
      ctx.globalAlpha = 1;
    }

    if (piece.oneWay && kind !== 'start') {
      this._arrow(ctx, x, y, DIRS[piece.oneWay].ang, size * 0.16, pal.gold);
    }

    if (isStart) {
      ctx.beginPath();
      ctx.arc(x, y, r * 1.15, 0, Math.PI * 2);
      ctx.strokeStyle = pal.start;
      ctx.shadowColor = pal.start;
      ctx.shadowBlur = 12;
      ctx.lineWidth = 3;
      ctx.stroke();
      ctx.shadowBlur = 0;
      this._arrow(ctx, x, y, DIRS[piece.oneWay || 'E'].ang, size * 0.18, pal.start);
    }
    if (isEnd) {
      ctx.beginPath();
      ctx.arc(x, y, r * 1.15, 0, Math.PI * 2);
      ctx.strokeStyle = pal.end;
      ctx.shadowColor = pal.end;
      ctx.shadowBlur = 12;
      ctx.lineWidth = 3;
      ctx.stroke();
      ctx.shadowBlur = 0;
      ctx.fillStyle = pal.end;
      ctx.globalAlpha = 0.35;
      ctx.beginPath();
      ctx.arc(x, y, r * 0.45, 0, Math.PI * 2);
      ctx.fill();
      ctx.globalAlpha = 1;
    }
  }

  _strokePorts(ctx, mask, x, y, size, width, color, cap) {
    ctx.strokeStyle = color;
    ctx.lineWidth = width;
    ctx.lineCap = cap;
    ctx.lineJoin = 'round';
    ctx.beginPath();
    const half = size / 2;
    const pts = {
      N: [x, y - half],
      E: [x + half, y],
      S: [x, y + half],
      W: [x - half, y],
    };
    for (const d of portsOf(mask)) {
      ctx.moveTo(x, y);
      ctx.lineTo(pts[d][0], pts[d][1]);
    }
    ctx.stroke();
  }

  _arrow(ctx, x, y, ang, s, color) {
    ctx.save();
    ctx.translate(x, y);
    ctx.rotate(ang);
    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.moveTo(s * 1.1, 0);
    ctx.lineTo(-s * 0.6, s * 0.7);
    ctx.lineTo(-s * 0.6, -s * 0.7);
    ctx.closePath();
    ctx.fill();
    ctx.restore();
  }

  _drawFlooz(ctx, game, pal) {
    for (let gy = 0; gy < game.rows; gy++) {
      for (let gx = 0; gx < game.cols; gx++) {
        const c = game.board[gy][gx];
        if (!c.pipe) continue;
        const chans = c.pipe.independent ? ['ns', 'ew'] : ['main'];
        for (const ch of chans) {
          const prog = c.fill[ch];
          if (prog <= 0) continue;
          const entered = c.entered[ch];
          if (!entered) continue;
          const { x, y } = this.cellCenter(gx, gy);
          this._floozStroke(ctx, c.pipe, x, y, this.layout.size, entered, prog, pal);
        }
      }
    }
    if ((game.phase === 'flowing' || game.phase === 'finishing') && game.heads) {
      for (const head of game.heads) {
        const p = this._headPos(game, head);
        if (!p) continue;
        const pulse = 0.75 + Math.sin(this.time * 12) * 0.25;
        ctx.save();
        ctx.shadowColor = pal.floozCore;
        ctx.shadowBlur = 18;
        ctx.fillStyle = pal.floozCore;
        ctx.beginPath();
        ctx.arc(p.x, p.y, this.layout.size * 0.11 * pulse, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
      }
    }
    if (game.leak) {
      const c = this.cellCenter(game.leak.x, game.leak.y);
      const t = Math.min(1, game.leak.t / 0.5);
      ctx.save();
      ctx.globalAlpha = 0.55 * (1 - t * 0.4);
      ctx.fillStyle = game.leak.success ? pal.gold : pal.flooz;
      ctx.beginPath();
      ctx.arc(c.x, c.y, this.layout.size * (0.2 + t * 0.45), 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();
    }
  }

  _floozStroke(ctx, piece, x, y, size, entered, prog, pal) {
    const spec = pathSpec(piece, entered, size);
    const w = size * 0.2;
    ctx.save();
    ctx.translate(x, y);
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    ctx.shadowColor = pal.flooz;
    ctx.shadowBlur = 12;
    const draw = (width, color, p) => {
      ctx.strokeStyle = color;
      ctx.lineWidth = width;
      ctx.setLineDash([Math.max(0.001, spec.length * p), 10000]);
      ctx.beginPath();
      this._traceSpec(ctx, spec);
      ctx.stroke();
    };
    draw(w * 1.55, pal.floozHot, prog);
    draw(w * 0.85, pal.flooz, prog);
    draw(w * 0.35, pal.floozCore, Math.max(0, prog - 0.04));
    if (piece.id && piece.id.startsWith('tank') && prog > 0.15) {
      const fill = Math.min(1, (prog - 0.15) / 0.7);
      roundRect(ctx, -size * 0.18, -size * 0.18, size * 0.36, size * 0.36, 5);
      ctx.fillStyle = pal.flooz;
      ctx.globalAlpha = 0.35 + fill * 0.5;
      ctx.fill();
      ctx.globalAlpha = 1;
    }
    ctx.restore();
  }

  _traceSpec(ctx, spec) {
    ctx.moveTo(spec.from[0], spec.from[1]);
    if (spec.kind === 'tank' || spec.kind === 'corner') {
      ctx.lineTo(0, 0);
      ctx.lineTo(spec.to[0], spec.to[1]);
    } else {
      ctx.lineTo(spec.to[0], spec.to[1]);
    }
  }

  _headPos(game, head) {
    const cell = game.cell(head.x, head.y);
    if (!cell?.pipe) return this.cellCenter(head.x, head.y);
    const spec = pathSpec(cell.pipe, head.enteredFrom, this.layout.size);
    const t = Math.min(1, head.progress);
    const c = this.cellCenter(head.x, head.y);
    const from = spec.from;
    const to = spec.to;
    if (spec.kind === 'tank' || spec.kind === 'corner') {
      if (t < 0.5) {
        const u = t / 0.5;
        return { x: c.x + from[0] * (1 - u), y: c.y + from[1] * (1 - u) };
      }
      const u = (t - 0.5) / 0.5;
      return { x: c.x + to[0] * u, y: c.y + to[1] * u };
    }
    return {
      x: c.x + from[0] + (to[0] - from[0]) * t,
      y: c.y + from[1] + (to[1] - from[1]) * t,
    };
  }

  _drawGhost(ctx, game, pal) {
    const path = game.tracePreview();
    if (path.length < 2) return;
    ctx.save();
    ctx.strokeStyle = pal.accent;
    ctx.globalAlpha = 0.22;
    ctx.lineWidth = 3;
    ctx.setLineDash([5, 6]);
    ctx.lineDashOffset = -this.time * 24;
    ctx.lineCap = 'round';
    ctx.beginPath();
    path.forEach((p, i) => {
      const c = this.cellCenter(p.x, p.y);
      if (i === 0) ctx.moveTo(c.x, c.y);
      else ctx.lineTo(c.x, c.y);
    });
    ctx.stroke();
    ctx.restore();
  }

  _drawGhostPiece(ctx, game, pal) {
    const id = game.queue[0];
    if (!id || !this.hover) return;
    const { x, y } = this.cellCenter(this.hover.x, this.hover.y);
    ctx.save();
    ctx.globalAlpha = 0.42;
    this.drawPipeBody(ctx, pieceById(id), x, y, this.layout.size, pal, 'pipe');
    ctx.restore();
  }

  _drawPopup(ctx, game) {
    if (!game.popup) return;
    const t = game.popup.t;
    const a = t > 0.85 ? (1 - t) / 0.15 : t < 0.25 ? t / 0.25 : 1;
    ctx.save();
    ctx.globalAlpha = Math.max(0, a);
    ctx.font = `800 ${Math.floor(this.layout.w * 0.038)}px Orbitron, sans-serif`;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillStyle = game.popup.color;
    ctx.shadowColor = game.popup.color;
    ctx.shadowBlur = 18;
    ctx.fillText(game.popup.text, this.layout.w / 2, this.layout.h * 0.18);
    ctx.restore();
  }

  drawQueue(game) {
    const canvas = this.queueCanvas;
    if (!canvas) return;
    const w = canvas.clientWidth || 120;
    const h = canvas.clientHeight || 420;
    const { ctx } = fitCanvas(canvas, w, h);
    ctx.clearRect(0, 0, w, h);
    const pal = this.pal;
    const n = game.queue.length;
    const slotH = Math.min(88, (h - 16) / n);
    for (let i = 0; i < n; i++) {
      const y = 8 + i * slotH;
      const active = i === 0;
      roundRect(ctx, 8, y, w - 16, slotH - 8, 10);
      ctx.fillStyle = active ? 'rgba(62,224,255,0.12)' : 'rgba(255,255,255,0.04)';
      ctx.fill();
      ctx.strokeStyle = active ? pal.accent : 'rgba(255,255,255,0.06)';
      ctx.lineWidth = active ? 2 : 1;
      ctx.stroke();
      const pid = game.queue[i];
      const s = Math.min(slotH - 20, w - 40);
      this.drawPipeBody(ctx, pieceById(pid), w / 2, y + (slotH - 8) / 2, s, pal, 'pipe');
      if (active) {
        ctx.fillStyle = pal.accent;
        ctx.font = '700 9px Outfit, sans-serif';
        ctx.textAlign = 'center';
        ctx.fillText('NEXT', w / 2, y + 12);
      }
    }
  }

  drawMini(ctx, pieceId, w, h, pal) {
    ctx.clearRect(0, 0, w, h);
    this.drawPipeBody(ctx, pieceById(pieceId), w / 2, h / 2, Math.min(w, h) * 0.9, pal, 'pipe');
  }
}

function shade(hex, f) {
  const n = parseInt(hex.slice(1), 16);
  let r = (n >> 16) & 255;
  let g = (n >> 8) & 255;
  let b = n & 255;
  r = Math.min(255, Math.round(r * f));
  g = Math.min(255, Math.round(g * f));
  b = Math.min(255, Math.round(b * f));
  return `rgb(${r},${g},${b})`;
}

function easeOutBack(t) {
  const c1 = 1.70158;
  const c3 = c1 + 1;
  return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
}


