/**
 * Interactive Multi-track Timeline & Waveform Editor
 */
class KTVTimeline {
  constructor(state, player) {
    this.state = state;
    this.player = player;
    this.container = document.querySelector(".timeline-canvas-container");
    this.canvas = document.getElementById("timeline-canvas");
    this.ctx = this.canvas.getContext("2d");
    
    this.pixelsPerSecond = 80;
    this.rulerHeight = 24;
    this.waveformHeight = 60;
    this.trackHeight = 46;
    this.totalHeight = 180;
    
    // Drag state
    this.dragTarget = null; // { type: 'move'|'resize-left'|'resize-right'|'scrub', sIdx, wIdx, initialStart, initialEnd, mouseStartX }
    this.hoverWord = null;
    
    this.initEvents();
    this.resizeCanvas();
    this.startRenderLoop();
  }

  initEvents() {
    window.addEventListener("resize", () => this.resizeCanvas());

    this.state.on("videoLoaded", () => this.resizeCanvas());
    this.state.on("sentencesChanged", () => this.render());
    this.state.on("timingUpdated", () => this.render());

    // Mouse events for drag, resize and scrub
    this.canvas.addEventListener("mousedown", (e) => this.onMouseDown(e));
    window.addEventListener("mousemove", (e) => this.onMouseMove(e));
    window.addEventListener("mouseup", (e) => this.onMouseUp(e));

    // Wheel event for zoom with Ctrl or horizontal scroll
    this.container.addEventListener("wheel", (e) => {
      if (e.ctrlKey) {
        e.preventDefault();
        const factor = e.deltaY < 0 ? 1.15 : 0.85;
        this.setZoom(this.pixelsPerSecond * factor);
      }
    }, { passive: false });

    // Zoom buttons
    const btnZoomIn = document.getElementById("btn-zoom-in");
    const btnZoomOut = document.getElementById("btn-zoom-out");
    if (btnZoomIn) btnZoomIn.addEventListener("click", () => this.setZoom(this.pixelsPerSecond * 1.25));
    if (btnZoomOut) btnZoomOut.addEventListener("click", () => this.setZoom(this.pixelsPerSecond * 0.8));
  }

  setZoom(pps) {
    this.pixelsPerSecond = Math.max(30, Math.min(250, pps));
    this.resizeCanvas();
    this.render();
  }

  resizeCanvas() {
    const duration = Math.max(10, this.state.videoDuration || 10);
    const totalWidth = Math.max(this.container.clientWidth, duration * this.pixelsPerSecond + 100);
    
    const dpr = window.devicePixelRatio || 1;
    this.canvas.width = totalWidth * dpr;
    this.canvas.height = this.totalHeight * dpr;
    this.canvas.style.width = `${totalWidth}px`;
    this.canvas.style.height = `${this.totalHeight}px`;
    this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    this.render();
  }

  startRenderLoop() {
    const loop = () => {
      this.render();
      requestAnimationFrame(loop);
    };
    requestAnimationFrame(loop);
  }

  render() {
    const ctx = this.ctx;
    const duration = Math.max(10, this.state.videoDuration || 10);
    const totalWidth = parseFloat(this.canvas.style.width) || 1200;
    const height = this.totalHeight;
    const pps = this.pixelsPerSecond;
    const curTime = (this.player && this.player.video && !isNaN(this.player.video.currentTime))
      ? this.player.video.currentTime
      : (this.state.currentTime || 0);

    ctx.clearRect(0, 0, totalWidth, height);

    // 1. Draw Background & Grid
    ctx.fillStyle = "#0c1322";
    ctx.fillRect(0, 0, totalWidth, height);

    // 2. Draw Waveform Track
    this.drawWaveform(ctx, totalWidth, pps);

    // 3. Draw Time Ruler
    this.drawRuler(ctx, totalWidth, pps, duration);

    // 4. Draw Character Blocks Track
    this.drawWordBlocks(ctx, pps);

    // 5. Draw Playhead
    const playheadX = curTime * pps;
    ctx.strokeStyle = "#ff0055";
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(playheadX, 0);
    ctx.lineTo(playheadX, height);
    ctx.stroke();

    // Playhead head handle
    ctx.fillStyle = "#ff0055";
    ctx.beginPath();
    ctx.moveTo(playheadX - 6, 0);
    ctx.lineTo(playheadX + 6, 0);
    ctx.lineTo(playheadX, 10);
    ctx.closePath();
    ctx.fill();

    // Auto follow playhead during playback if it reaches edge of view
    if (this.player && this.player.video && !this.player.video.paused && !this.dragTarget) {
      const scrollLeft = this.container.scrollLeft;
      const clientWidth = this.container.clientWidth;
      if (playheadX > scrollLeft + clientWidth - 30) {
        this.container.scrollLeft = playheadX - 100;
      } else if (playheadX < scrollLeft) {
        this.container.scrollLeft = Math.max(0, playheadX - 50);
      }
    }
  }

  drawRuler(ctx, totalWidth, pps, duration) {
    ctx.fillStyle = "#162238";
    ctx.fillRect(0, 0, totalWidth, this.rulerHeight);
    ctx.strokeStyle = "#273957";
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(0, this.rulerHeight);
    ctx.lineTo(totalWidth, this.rulerHeight);
    ctx.stroke();

    ctx.fillStyle = "#94a3b8";
    ctx.font = "10px monospace";
    ctx.textBaseline = "middle";

    const step = pps < 50 ? 5 : pps < 100 ? 1 : 0.5;
    for (let t = 0; t <= duration + 5; t += step) {
      const x = t * pps;
      const isMajor = Math.abs(t % 5) < 0.001;
      const tickH = isMajor ? 12 : 6;

      ctx.beginPath();
      ctx.moveTo(x, this.rulerHeight - tickH);
      ctx.lineTo(x, this.rulerHeight);
      ctx.stroke();

      if (isMajor || pps >= 100) {
        const m = Math.floor(t / 60);
        const s = Math.floor(t % 60);
        ctx.fillText(`${m}:${String(s).padStart(2, '0')}`, x + 3, this.rulerHeight / 2);
      }
    }
  }

  drawWaveform(ctx, totalWidth, pps) {
    const peaks = this.state.waveformData || [];
    const yCenter = this.rulerHeight + this.waveformHeight / 2;
    const maxHalfH = this.waveformHeight / 2 - 4;

    ctx.fillStyle = "#0f1a2c";
    ctx.fillRect(0, this.rulerHeight, totalWidth, this.waveformHeight);

    if (peaks.length === 0) return;

    ctx.fillStyle = "#1e3a5f";
    ctx.strokeStyle = "#38bdf8";
    ctx.lineWidth = 1.5;

    const duration = this.state.videoDuration || 1;
    const peakWidth = totalWidth / peaks.length;

    ctx.beginPath();
    for (let i = 0; i < peaks.length; i++) {
      const x = (i / peaks.length) * duration * pps;
      const h = peaks[i] * maxHalfH;
      ctx.moveTo(x, yCenter - h);
      ctx.lineTo(x, yCenter + h);
    }
    ctx.stroke();
  }

  drawWordBlocks(ctx, pps) {
    const sentences = this.state.sentences || [];
    const trackY = this.rulerHeight + this.waveformHeight + 8;
    const blockH = 34;

    sentences.forEach((s, sIdx) => {
      const words = s.words || [];
      const singer = (s.singer || "duet").toLowerCase();

      // Singer theme color
      let baseColor = "rgba(0, 230, 118, 0.25)";
      let strokeColor = "#00e676";
      if (singer === "male" || singer === "男") {
        baseColor = "rgba(0, 210, 255, 0.25)";
        strokeColor = "#00d2ff";
      } else if (singer === "female" || singer === "女") {
        baseColor = "rgba(255, 64, 129, 0.25)";
        strokeColor = "#ff4081";
      }

      words.forEach((w, wIdx) => {
        const start = w.startTime || 0;
        const end = w.endTime || 0;
        const isRecorded = (end > start);

        // Untimed / unrecorded words have no timestamp on the audio track; do NOT draw at 0.0s!
        if (!isRecorded) {
          return;
        }

        const x = start * pps;
        const wPx = Math.max(16, (end - start) * pps);

        // Word background box
        ctx.fillStyle = baseColor;
        ctx.strokeStyle = strokeColor;
        ctx.lineWidth = 1.5;
        this.roundRect(ctx, x, trackY, wPx, blockH, 4, true, true);

        // Resize handles visual cue if hovered
        const isHovered = (this.hoverWord && this.hoverWord.sIdx === sIdx && this.hoverWord.wIdx === wIdx);
        if (isHovered) {
          ctx.fillStyle = "#ffffff";
          // Left handle
          ctx.fillRect(x + 1, trackY + 8, 3, blockH - 16);
          // Right handle
          ctx.fillRect(x + wPx - 4, trackY + 8, 3, blockH - 16);
        }

        // Word Text
        ctx.fillStyle = "#ffffff";
        ctx.font = "bold 13px 'Microsoft JhengHei', sans-serif";
        ctx.textBaseline = "middle";
        ctx.textAlign = "center";
        ctx.fillText(w.text, x + wPx / 2, trackY + blockH / 2);
      });
    });
  }

  roundRect(ctx, x, y, width, height, radius, fill, stroke) {
    ctx.beginPath();
    ctx.moveTo(x + radius, y);
    ctx.lineTo(x + width - radius, y);
    ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
    ctx.lineTo(x + width, y + height - radius);
    ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
    ctx.lineTo(x + radius, y + height);
    ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
    ctx.lineTo(x, y + radius);
    ctx.quadraticCurveTo(x, y, x + radius, y);
    ctx.closePath();
    if (fill) ctx.fill();
    if (stroke) ctx.stroke();
  }

  /**
   * Hit test word block at (x, y)
   */
  findWordAt(clientX, clientY) {
    const rect = this.canvas.getBoundingClientRect();
    const mouseX = clientY !== undefined ? (clientX - rect.left) : clientX;
    const mouseY = clientY !== undefined ? (clientY - rect.top) : 0;
    const pps = this.pixelsPerSecond;
    const trackY = this.rulerHeight + this.waveformHeight + 8;
    const blockH = 34;

    if (mouseY < trackY || mouseY > trackY + blockH) {
      return null;
    }

    const sentences = this.state.sentences || [];
    for (let sIdx = 0; sIdx < sentences.length; sIdx++) {
      const words = sentences[sIdx].words || [];
      for (let wIdx = 0; wIdx < words.length; wIdx++) {
        const w = words[wIdx];
        const start = w.startTime || 0;
        const end = w.endTime || 0;
        if (end <= start) continue;
        const x = start * pps;
        const wPx = Math.max(16, (end - start) * pps);

        if (mouseX >= x && mouseX <= x + wPx) {
          const handleMargin = 6;
          let part = "body";
          if (mouseX <= x + handleMargin) part = "left";
          else if (mouseX >= x + wPx - handleMargin) part = "right";

          return { sIdx, wIdx, word: w, part, x, wPx };
        }
      }
    }
    return null;
  }

  onMouseDown(e) {
    const rect = this.canvas.getBoundingClientRect();
    const mouseX = Math.max(0, e.clientX - rect.left);
    const mouseY = e.clientY - rect.top;
    const maxDur = (this.player.video.duration && !isNaN(this.player.video.duration) && this.player.video.duration > 0)
      ? this.player.video.duration
      : (this.state.videoDuration || 9999);

    // 1. Direct hit on the red playhead line (allow dragging it directly from anywhere)
    const curTime = (this.player && this.player.video && !isNaN(this.player.video.currentTime))
      ? this.player.video.currentTime
      : (this.state.currentTime || 0);
    const playheadX = curTime * this.pixelsPerSecond;
    if (Math.abs(mouseX - playheadX) <= 10) {
      const seekTime = Math.max(0, Math.min(maxDur, mouseX / this.pixelsPerSecond));
      this.player.seek(seekTime);
      this.dragTarget = { type: "scrub", mouseStartX: mouseX };
      return;
    }

    // 2. Click on ruler or waveform: seek video & scrub
    if (mouseY < this.rulerHeight + this.waveformHeight) {
      const seekTime = Math.max(0, Math.min(maxDur, mouseX / this.pixelsPerSecond));
      this.player.seek(seekTime);
      this.dragTarget = { type: "scrub", mouseStartX: mouseX };
      return;
    }

    // 3. Click on word block: start move or resize if it has timing
    const hit = this.findWordAt(e.clientX, e.clientY);
    if (hit) {
      const hasTiming = (hit.word.startTime > 0 || (hit.word.endTime && hit.word.endTime > 0));
      if (hasTiming) {
        this.dragTarget = {
          type: hit.part === "left" ? "resize-left" : hit.part === "right" ? "resize-right" : "move",
          sIdx: hit.sIdx,
          wIdx: hit.wIdx,
          initialStart: hit.word.startTime,
          initialEnd: hit.word.endTime,
          mouseStartX: mouseX
        };
        this.player.seek(hit.word.startTime);
        return;
      }
    }

    // 4. Click on empty space or untimed word in track area: seek video
    const seekTime = Math.max(0, Math.min(maxDur, mouseX / this.pixelsPerSecond));
    this.player.seek(seekTime);
    this.dragTarget = { type: "scrub", mouseStartX: mouseX };
  }

  onMouseMove(e) {
    if (this.dragTarget && e.buttons === 0) {
      this.dragTarget = null;
    }

    const rect = this.canvas.getBoundingClientRect();
    const mouseX = Math.max(0, e.clientX - rect.left);
    const mouseY = e.clientY - rect.top;
    const maxDur = (this.player.video.duration && !isNaN(this.player.video.duration) && this.player.video.duration > 0)
      ? this.player.video.duration
      : (this.state.videoDuration || 9999);

    if (this.dragTarget) {
      if (this.dragTarget.type === "scrub") {
        const seekTime = Math.max(0, Math.min(maxDur, mouseX / this.pixelsPerSecond));
        this.player.seek(seekTime);
      } else if (this.dragTarget.type === "move") {
        const dx = (mouseX - this.dragTarget.mouseStartX) / this.pixelsPerSecond;
        const sIdx = this.dragTarget.sIdx;
        const wIdx = this.dragTarget.wIdx;
        const dur = this.dragTarget.initialEnd - this.dragTarget.initialStart;
        const newStart = Math.max(0, this.dragTarget.initialStart + dx);
        const newEnd = newStart + dur;
        this.state.updateWordTiming(sIdx, wIdx, newStart, newEnd);
        this.player.seek(newStart);
      } else if (this.dragTarget.type === "resize-left") {
        const dx = (mouseX - this.dragTarget.mouseStartX) / this.pixelsPerSecond;
        const sIdx = this.dragTarget.sIdx;
        const wIdx = this.dragTarget.wIdx;
        const newStart = Math.min(this.dragTarget.initialEnd - 0.05, Math.max(0, this.dragTarget.initialStart + dx));
        this.state.updateWordTiming(sIdx, wIdx, newStart, this.dragTarget.initialEnd);
        this.player.seek(newStart);
      } else if (this.dragTarget.type === "resize-right") {
        const dx = (mouseX - this.dragTarget.mouseStartX) / this.pixelsPerSecond;
        const sIdx = this.dragTarget.sIdx;
        const wIdx = this.dragTarget.wIdx;
        const newEnd = Math.max(this.dragTarget.initialStart + 0.05, this.dragTarget.initialEnd + dx);
        this.state.updateWordTiming(sIdx, wIdx, this.dragTarget.initialStart, newEnd);
      }
      return;
    }

    // Hover cursor updates
    const curTime = (this.player && this.player.video && !isNaN(this.player.video.currentTime))
      ? this.player.video.currentTime
      : (this.state.currentTime || 0);
    const playheadX = curTime * this.pixelsPerSecond;
    if (Math.abs(mouseX - playheadX) <= 8) {
      this.canvas.style.cursor = "col-resize";
      return;
    }

    const hit = this.findWordAt(e.clientX, e.clientY);
    this.hoverWord = hit;
    if (hit) {
      if (hit.part === "left" || hit.part === "right") {
        this.canvas.style.cursor = "ew-resize";
      } else {
        this.canvas.style.cursor = "move";
      }
    } else if (mouseY < this.rulerHeight + this.waveformHeight) {
      this.canvas.style.cursor = "pointer";
    } else {
      this.canvas.style.cursor = "default";
    }
  }

  onMouseUp() {
    this.dragTarget = null;
  }
}

window.KTVTimeline = KTVTimeline;
