import { CanvasRegion } from '@/components/edit2025/canvasRenderer/CanvasRenderer';
import { snapWithEvent } from '@/utils/snap';

import { StudioContextType } from '../StudioContext';
import { combineActions } from '../actions/combineActions';
import focusTimeline from '../actions/focusTimeline';
import updateClips from '../actions/updateClips';
import updateSelection, {
  setBeatRangeSelection,
} from '../actions/updateSelection';
import getCanvasRelativeRect from '../getCanvasRelativeRect';
import { inFlightDragKeys } from '../hooks/useInFlightDrags';
import { getStudioClipsByTrackId } from '../selectors';
import { EDGE_TOUCH_TARGET_WIDTH } from './makeStudioSelectionRegions';

const START_END_REGION_WIDTH = 80;
const EPSILON = 0.000001; // ~0.1 samples at 120bpm

export default function makeGapRegions(studioContext: StudioContextType): {
  aboveSelectionRegions: CanvasRegion[];
  belowSelectionRegions: CanvasRegion[];
} {
  if (
    studioContext.lyricsEditController.replacingLyrics ||
    !!studioContext.previewController.previewingOnTimeline
  ) {
    return {
      aboveSelectionRegions: [],
      belowSelectionRegions: [],
    };
  }

  const aboveSelectionRegions: CanvasRegion[] = [];
  const belowSelectionRegions: CanvasRegion[] = [];
  studioContext.state.tracks.forEach((track) => {
    const trackHeader =
      studioContext.timelineController.trackHeadersRef.current[track.id];
    if (!trackHeader) return [];

    const trackRect = getCanvasRelativeRect(
      trackHeader.getBoundingClientRect(),
      studioContext.timelineController.getWrapperRect()
    );

    const clips = track.clips;
    const gaps: { startBeats: number; endBeats: number }[] = clips[0]
      ? [
          {
            startBeats: -Infinity,
            endBeats: clips[0].startBeats,
          },
        ]
      : [];

    for (let i = 0; i < track.clips.length; i++) {
      const currentClip = track.clips[i];
      const nextClip = track.clips[i + 1];

      if (nextClip && currentClip.endBeats < nextClip.startBeats - EPSILON) {
        gaps.push({
          startBeats: currentClip.endBeats,
          endBeats:
            nextClip.startBeats +
            studioContext.inFlightDrags.get(
              inFlightDragKeys.gapEnd(currentClip.endBeats, track.id)
            ),
        });
      } else if (!nextClip) {
        gaps.push({
          startBeats: currentClip.endBeats,
          endBeats: Infinity,
        });
      }
    }

    return gaps.forEach((g) => {
      let startX = 0;
      let endX = 0;
      if (!Number.isFinite(g.startBeats)) {
        if (!studioContext.oneTrackMode) {
          return {};
        }
        // endX = studioContext.beatsToCanvasX(g.endBeats);
        // startX = endX - START_END_REGION_WIDTH;
        return {}; // TODO: add intro
      } else if (!Number.isFinite(g.endBeats)) {
        if (!studioContext.oneTrackMode) {
          return {};
        }
        startX = studioContext.beatsToCanvasX(g.startBeats);
        endX = startX + START_END_REGION_WIDTH;
      } else {
        startX = studioContext.beatsToCanvasX(g.startBeats);
        endX = studioContext.beatsToCanvasX(g.endBeats);
      }

      const y = trackRect.top;
      const h = trackRect.height;

      if (
        studioContext.inFlightDrags.hasMatching((key) =>
          key.startsWith('gapEnd')
        ) &&
        !studioContext.inFlightDrags.has(
          inFlightDragKeys.gapEnd(g.startBeats, track.id)
        )
      ) {
        return [];
      }

      belowSelectionRegions.push({
        touchTarget: {
          bounds: {
            top: y,
            left: startX,
            bottom: y + h,
            right: endX,
          },
          hoverCursor: 'pointer',
          onMouseDown: () => {
            studioContext.setEndSelectionMode('extend');
            studioContext.setState(
              updateSelection((prev) => ({
                ...prev,
                anchorBeats: Number.isFinite(g.startBeats)
                  ? g.startBeats
                  : g.endBeats,
                focusBeats: Number.isFinite(g.endBeats)
                  ? g.endBeats
                  : Math.min(prev.anchorBeats, prev.focusBeats),
              }))
            );
          },
        },
        render: (ctx: CanvasRenderingContext2D, hovered: boolean) => {
          const hoveredAndNotDragging =
            hovered &&
            !studioContext.inFlightDrags.has(
              inFlightDragKeys.gapEnd(g.startBeats, track.id)
            );
          ctx.save();
          ctx.beginPath();
          ctx.fillStyle = '#000000';
          ctx.strokeStyle = '#333';
          ctx.roundRect(startX, y, endX - startX, h, 10);
          ctx.lineCap = 'round';
          ctx.fill();
          ctx.stroke();
          if (!Number.isFinite(g.endBeats)) {
            // Extend arrow
            ctx.beginPath();
            ctx.lineWidth = 2;
            ctx.strokeStyle = hoveredAndNotDragging ? '#fff' : '#666';
            ctx.moveTo((endX + startX) / 2 - 12, y + h / 2);
            ctx.lineTo((endX + startX) / 2 + 8, y + h / 2);
            ctx.moveTo((endX + startX) / 2 + 3, y + h / 2 - 5);
            ctx.lineTo((endX + startX) / 2 + 8, y + h / 2);
            ctx.moveTo((endX + startX) / 2 + 3, y + h / 2 + 5);
            ctx.lineTo((endX + startX) / 2 + 8, y + h / 2);
            ctx.moveTo((endX + startX) / 2 + 12, y + h / 2 - 6);
            ctx.lineTo((endX + startX) / 2 + 12, y + h / 2 + 6);
            ctx.stroke();
          }
          ctx.restore();
        },
      });

      if (Number.isFinite(g.endBeats)) {
        aboveSelectionRegions.push({
          touchTarget: {
            bounds: {
              top: y,
              left: endX - EDGE_TOUCH_TARGET_WIDTH / 2,
              bottom: y + h,
              right: endX + EDGE_TOUCH_TARGET_WIDTH / 2,
            },
            hoverCursor: 'ew-resize',
            dragCursor: 'ew-resize',
            onMouseDown: studioContext.handleClickDrag(({ downBeats }) => {
              const originalGapSize = g.endBeats - g.startBeats;
              const affectedClipIds = getStudioClipsByTrackId(
                studioContext.state
              )
                [track.id]?.filter((c) => c.startBeats >= g.startBeats)
                .map((c) => c.id);
              studioContext.inFlightDrags.update(
                inFlightDragKeys.gapEnd(g.startBeats, track.id),
                0
              );
              studioContext.inFlightDrags.update(
                inFlightDragKeys.selectionEdge('focus'),
                0
              );
              affectedClipIds.forEach((clipId) => {
                studioContext.inFlightDrags.update(
                  inFlightDragKeys.clip(clipId),
                  0
                );
              });
              studioContext.setState(
                combineActions(
                  focusTimeline,
                  setBeatRangeSelection(g.startBeats, g.endBeats)
                )
              );
              return {
                onMouseMove: ({ moveBeats, moveEvent }) => {
                  const delta = Math.max(
                    -originalGapSize,
                    snapWithEvent(
                      moveEvent,
                      moveBeats,
                      studioContext.gridSizeRef.current,
                      null,
                      studioContext.getMaxShift()
                    ) - downBeats
                  );

                  affectedClipIds.forEach((clipId) => {
                    studioContext.inFlightDrags.update(
                      inFlightDragKeys.clip(clipId),
                      delta
                    );
                  });
                  studioContext.inFlightDrags.update(
                    inFlightDragKeys.gapEnd(g.startBeats, track.id),
                    delta
                  );
                  studioContext.inFlightDrags.update(
                    inFlightDragKeys.selectionEdge('focus'),
                    delta
                  );
                },
                onMouseUp: ({ upBeats, upEvent }) => {
                  studioContext.inFlightDrags.finishAll();
                  const delta = Math.max(
                    -originalGapSize,
                    snapWithEvent(
                      upEvent,
                      upBeats,
                      studioContext.gridSizeRef.current,
                      null,
                      studioContext.getMaxShift()
                    ) - downBeats
                  );

                  studioContext.setState(
                    combineActions(
                      focusTimeline,
                      setBeatRangeSelection(g.startBeats, g.endBeats + delta),
                      updateClips(affectedClipIds, (c) => {
                        return {
                          ...c,
                          startBeats: c.startBeats + delta,
                          endBeats: c.endBeats + delta,
                        };
                      })
                    )
                  );
                },
              };
            }),
          },
          render(ctx: CanvasRenderingContext2D, hovered: boolean) {
            ctx.save();
            ctx.beginPath();
            ctx.fillStyle = hovered ? '#ffffff' : '#ffffff88';
            ctx.roundRect(endX - 3, y + 10, 6, h - 20, 10);
            ctx.fill();
            ctx.restore();
          },
        });
      }
    });
  });

  return { aboveSelectionRegions, belowSelectionRegions };
}
