import {
  getClipBeatsToClipContentSecondsSegments,
  getTimelineBeatsToClipBeatsSegments,
  getTimelineBeatsToUnwarpedClipContentSeconds,
} from '@suno/studiokit/timeMapping';
import { getWarpEnabledAndPopulated } from '@suno/studiokit/warpUtils';

import { darken } from '@/utils/colorBlending';

import { RULER_HEIGHT } from './canvasRegions/makeStudioBeatGridRegion';
import { getDerivedTiming } from './selectors';
import shouldShowTakeLanes from './shouldShowTakeLanes';
import type { StudioProjectState } from './types';

export interface WaveformTrack {
  id: string;
  top: number; // CSS px from the overlay's own origin
  height: number; // CSS px
  color?: string | null;
  display?: 'split' | 'mono';
  clips: Array<{
    // StudioClip id to allow downstream lookups (e.g., amplitude)
    id?: string;
    startBeats: number;
    endBeats: number;
    readStartBeats?: number;
    assetId?: string | null;
    uploadId?: string | null;
    srcUrl?: string | null;
    color?: string | null;
    warpEnabled?: boolean;
    segments?: Array<{
      timelineStartBeats: number;
      timelineEndBeats: number;
      contentStartSeconds: number;
      contentEndSeconds: number;
    }>;
  }>;
}

/**
 * Build tracks for the overlay. Timewarping is ON by default (can be disabled).
 * `visibleStartBeats` / `visibleEndBeats` allow us to pre-clip segments to the viewport.
 * `bufferLengthSecondsByClipId` is optional; if provided, we clamp segments to real audio.
 */
export function buildWaveformTracks(
  state: StudioProjectState,
  opts?: {
    timewarpEnabled?: boolean;
    bufferLengthSecondsByClipId?: Record<string, number>;
    /** Optional: pre-clip output segments to the visible beats window to reduce work downstream */
    visibleStartBeats?: number;
    visibleEndBeats?: number;
  }
): WaveformTrack[] {
  const _timewarpEnabled = opts?.timewarpEnabled ?? true; // currently unused; segments are always computed
  const bufferLenById = opts?.bufferLengthSecondsByClipId ?? {};
  const timing = getDerivedTiming(state);
  const visStart = opts?.visibleStartBeats ?? -Infinity;
  const visEnd = opts?.visibleEndBeats ?? +Infinity;
  const bps = timing.bps || 2;

  const wvTracks: WaveformTrack[] = [];
  let yOffset = RULER_HEIGHT + 1; // Start after the ruler

  // Helper to build warp segments for a clip
  const buildClipData = (clip: any) => {
    // Replicate EXACT color logic from makeStudioClipRegions.ts lines 444-472
    const isUploading = !!clip.uploadId;
    const isMuted = !!clip.mute; // Use clip mute state as visual mute state (treat like track mute)
    const isPreviewClip = false;
    const hasInFlightDrag = false; // We also don't have drag state
    const lifted = false;

    const isUnliftedCopy = !lifted && hasInFlightDrag;
    // Avoid string concat when no opacity suffix is needed
    const color =
      isUploading || isMuted || (false && !isPreviewClip)
        ? darken(clip.color, 0.5) // Apply track mute style (darken by 0.5) for muted clips
        : isUnliftedCopy
          ? clip.color + '99'
          : clip.color;

    const base = {
      id: clip.id,
      startBeats: clip.startBeats,
      endBeats: clip.endBeats,
      readStartBeats: clip.readStartBeats || 0,
      assetId: clip.clipId,
      uploadId: clip.uploadId,
      streaming: clip.streaming,
      srcUrl: undefined as string | undefined,
      color: color,
    };

    // Build time-warp segments that map timeline beats → content seconds
    const segments: Array<{
      timelineStartBeats: number;
      timelineEndBeats: number;
      contentStartSeconds: number;
      contentEndSeconds: number;
    }> = [];

    try {
      // Buffer length for this clip, with finite fallback to avoid infinite ranges
      const bufferLenSec =
        bufferLenById[clip.clipId!] ??
        (clip.endBeats - clip.startBeats) / Math.max(1e-9, bps);

      // If the clip has effective warp markers, use precise clip‑beats mapping.
      // Otherwise (no markers / downbeats 404), switch to UNWARPED timeline mapping
      if (clip.warp && getWarpEnabledAndPopulated(clip.warp)) {
        // 1) Combine looping/placement (timeline→clip beats) with clip warp (clip beats→content seconds)
        const loopSegments = getTimelineBeatsToClipBeatsSegments(clip);
        const contentSegments = getClipBeatsToClipContentSecondsSegments(
          clip,
          bufferLenSec /* provide finite duration when possible */
        );

        // O(N+M) merge over clip-beats domain
        let i = 0; // loopSegments
        let j = 0; // contentSegments
        while (i < loopSegments.length && j < contentSegments.length) {
          const L = loopSegments[i];
          const W = contentSegments[j];
          const loopTL0 = L.sourceRange.start; // timeline beats (start)
          const loopCB0 = L.targetRange.start; // clip beats (start)
          const loopCB1 = L.targetRange.end; // clip beats (end)
          const wCB0 = W.sourceRange.start; // clip beats (start)
          const wCB1 = W.sourceRange.end; // clip beats (end)

          // If no overlap in CLIP-BEATS domain, advance the pointer that ends first.
          if (wCB1 <= loopCB0) {
            j++;
            continue;
          }
          if (wCB0 >= loopCB1) {
            i++;
            continue;
          }

          // Overlap in CLIP-BEATS
          let u0 = Math.max(wCB0, loopCB0);
          let u1 = Math.min(wCB1, loopCB1);
          if (u1 > u0) {
            // Map overlapped clip-beats [u0,u1] into TIMELINE beats using loop placement (slope 1)
            let tStart = loopTL0 + (u0 - loopCB0);
            let tEnd = loopTL0 + (u1 - loopCB0);

            // Clip to clip bounds and (optionally) to visible window
            const hardL = Math.max(clip.startBeats, visStart);
            const hardR = Math.min(clip.endBeats, visEnd);
            if (tEnd > hardL && tStart < hardR) {
              // Apply timeline clipping; adjust corresponding clip-beats (1:1 slope)
              if (tStart < hardL) {
                u0 += hardL - tStart;
                tStart = hardL;
              }
              if (tEnd > hardR) {
                u1 -= tEnd - hardR;
                tEnd = hardR;
              }
              if (tEnd > tStart) {
                // Map to CONTENT seconds inside W (affine)
                const denom = Math.max(1e-9, wCB1 - wCB0);
                const wT0 = (u0 - wCB0) / denom;
                const wT1 = (u1 - wCB0) / denom;
                const wS0 = W.targetRange.start;
                const wS1 = W.targetRange.end;
                const s0 = wS0 + (wS1 - wS0) * wT0;
                const s1 = wS0 + (wS1 - wS0) * wT1;
                if (Number.isFinite(s0) && Number.isFinite(s1) && s1 > s0) {
                  segments.push({
                    timelineStartBeats: tStart,
                    timelineEndBeats: tEnd,
                    contentStartSeconds: s0,
                    contentEndSeconds: s1,
                  });
                }
              }
            }
          }
          // Advance whichever segment finishes first in clip-beats space
          if (loopCB1 <= wCB1) i++;
          else j++;
        }
      } else {
        // No warp markers → use UNWARPED timeline mapping
        const toSecUnwarped = getTimelineBeatsToUnwarpedClipContentSeconds(
          timing,
          clip as any,
          bufferLenSec
        );
        // Use clip range ∩ visible window
        const visibleClipStart = Math.max(clip.startBeats, visStart);
        const visibleClipEnd = Math.min(clip.endBeats, visEnd);

        if (visibleClipEnd > visibleClipStart) {
          const startResult = toSecUnwarped(visibleClipStart);
          const endResult = toSecUnwarped(visibleClipEnd);
          if (startResult && endResult) {
            const s0 = startResult[1];
            const s1 = endResult[0];
            if (Number.isFinite(s0) && Number.isFinite(s1) && s1 > s0) {
              segments.push({
                timelineStartBeats: visibleClipStart,
                timelineEndBeats: visibleClipEnd,
                contentStartSeconds: s0,
                contentEndSeconds: s1,
              });
            }
          }
        }
      }
    } catch (error) {
      // If warp computation fails, fall back to linear
      console.warn('Warp computation failed for clip', clip.clipId, error);
      return { ...base, warpEnabled: false };
    }

    return {
      ...base,
      warpEnabled: true,
      segments,
    };
  };

  for (const track of state.tracks) {
    const trackTop = yOffset;
    const trackHeight = track.height;

    // Main track
    wvTracks.push({
      id: track.id,
      top: trackTop,
      height: trackHeight,
      display: 'mono',
      color: track.color,
      clips: track.clips.map(buildClipData),
    });

    yOffset += trackHeight + 2; // Add some spacing

    // Add take lanes if they should be shown
    if (shouldShowTakeLanes(track)) {
      for (const takeLane of track.takeLanes) {
        const takeLaneTop = yOffset;
        const takeLaneHeight = takeLane.height;

        wvTracks.push({
          id: takeLane.id,
          top: takeLaneTop,
          height: takeLaneHeight,
          display: 'mono',
          color: track.color, // Take lanes use the parent track's color
          clips: takeLane.clips.map(buildClipData),
        });

        yOffset += takeLaneHeight + 2;
      }
    }
  }

  return wvTracks;
}
