import { AlignmentSpec } from '@/hooks/useGenerate';

import { EditTiming } from '../edit2025/types';
import { getAbsoluteBeatsFromSeconds } from './getAbsolutePoint';
import getGridOffsetBeats from './getGridOffsetBeats';
import { StudioClip } from './types';
import {
  isFutureOnly,
  isHistoryAndFuture,
  isHistoryOnly,
} from './useAlignedClips';

export type AlignmentMatch = {
  replacementStartBeats: number;
  replacementEndBeats: number;
  historyClipGridOffset: number | null;
  futureClipGridOffset: number | null;
};

export const pruneAlignmentMatches = (
  matches: AlignmentMatch[]
): AlignmentMatch[] => {
  const result: AlignmentMatch[] = [];
  const nonZeroDurationMatches = matches.filter(
    (m) => m.replacementEndBeats > m.replacementStartBeats
  );
  const zeroDurationMatches = matches.filter(
    (m) => m.replacementEndBeats === m.replacementStartBeats
  );

  // Add all non-zero duration matches
  result.push(...nonZeroDurationMatches);

  // Add zero-duration matches that aren't too close to edges of non-zero matches
  const TOLERANCE = 0.00001;
  const validZeroMatches = zeroDurationMatches.filter((zeroMatch) => {
    const point = zeroMatch.replacementStartBeats;
    return !nonZeroDurationMatches.some(
      (nonZeroMatch) =>
        Math.abs(point - nonZeroMatch.replacementStartBeats) < TOLERANCE ||
        Math.abs(point - nonZeroMatch.replacementEndBeats) < TOLERANCE
    );
  });

  result.push(...validZeroMatches);
  return result;
};

const findHistoryOnlyAlignmentMatches = (
  timing: EditTiming,
  alignmentSpec: AlignmentSpec,
  studioClips: StudioClip[]
): AlignmentMatch[] => {
  const { historyEndSeconds, historyClipId } = alignmentSpec;
  const historyClips = studioClips.filter((c) => c.clipId === historyClipId);
  const lastClipEndBeats = Math.max(...historyClips.map((c) => c.endBeats));
  return historyClips.map((c) => ({
    replacementStartBeats: getAbsoluteBeatsFromSeconds(
      timing,
      c,
      historyEndSeconds!,
      true
    ),
    replacementEndBeats: lastClipEndBeats,
    historyClipGridOffset: getGridOffsetBeats(c),
    futureClipGridOffset: null,
  }));
};

const findFutureOnlyAlignmentMatches = (
  timing: EditTiming,
  alignmentSpec: AlignmentSpec,
  studioClips: StudioClip[]
): AlignmentMatch[] => {
  const { futureStartSeconds, futureClipId } = alignmentSpec;
  const futureClips = studioClips.filter((c) => c.clipId === futureClipId);
  const firstClipStartBeats = Math.min(...futureClips.map((c) => c.startBeats));
  return futureClips.map((c) => ({
    replacementStartBeats: firstClipStartBeats,
    replacementEndBeats: getAbsoluteBeatsFromSeconds(
      timing,
      c,
      futureStartSeconds!,
      true
    ),
    futureClipGridOffset: getGridOffsetBeats(c),
    historyClipGridOffset: null,
  }));
};

const findHistoryAndFutureAlignmentMatches = (
  timing: EditTiming,
  alignmentSpec: AlignmentSpec,
  studioClips: StudioClip[],
  clipBoundToleranceBeats: number = 1
): AlignmentMatch[] => {
  const { historyEndSeconds, futureStartSeconds, historyClipId, futureClipId } =
    alignmentSpec;
  const historyClips = studioClips.filter((c) => c.clipId === historyClipId);
  const futureClips = studioClips.filter((c) => c.clipId === futureClipId);

  const result: AlignmentMatch[] = [];
  result.push(
    ...historyClips.flatMap(
      (hc) =>
        futureClips
          .filter((fc) => fc.startBeats >= hc.startBeats)
          .map((fc) => {
            const result = {
              replacementStartBeats: getAbsoluteBeatsFromSeconds(
                timing,
                hc,
                historyEndSeconds!,
                true
              ),
              replacementEndBeats: getAbsoluteBeatsFromSeconds(
                timing,
                fc,
                futureStartSeconds!,
                true
              ),
              historyClipGridOffset: getGridOffsetBeats(hc),
              futureClipGridOffset: getGridOffsetBeats(fc),
            };

            if (result.replacementStartBeats > result.replacementEndBeats) {
              return null;
            }

            if (
              result.replacementStartBeats <
                hc.startBeats - clipBoundToleranceBeats ||
              result.replacementStartBeats >
                hc.endBeats + clipBoundToleranceBeats
            ) {
              return null;
            }

            if (
              result.replacementEndBeats >
                fc.endBeats + clipBoundToleranceBeats ||
              result.replacementEndBeats <
                fc.startBeats - clipBoundToleranceBeats
            ) {
              return null;
            }

            return result;
          })
          .filter(Boolean) as AlignmentMatch[]
    )
  );

  result.push(
    ...(historyClips
      .map((hc) => {
        const replacementStartBeats = getAbsoluteBeatsFromSeconds(
          timing,
          hc,
          historyEndSeconds!,
          true
        );

        if (
          replacementStartBeats > hc.endBeats ||
          replacementStartBeats < hc.startBeats
        ) {
          return null;
        }

        return {
          replacementStartBeats,
          replacementEndBeats: replacementStartBeats,
          historyClipGridOffset: getGridOffsetBeats(hc),
          futureClipGridOffset: null,
        };
      })
      .filter(Boolean) as AlignmentMatch[])
  );

  result.push(
    ...(futureClips
      .map((fc) => {
        const replacementEndBeats = getAbsoluteBeatsFromSeconds(
          timing,
          fc,
          futureStartSeconds!,
          true
        );

        if (
          replacementEndBeats > fc.endBeats ||
          replacementEndBeats < fc.startBeats
        ) {
          return null;
        }

        return {
          replacementStartBeats: replacementEndBeats,
          replacementEndBeats,
          futureClipGridOffset: getGridOffsetBeats(fc),
          historyClipGridOffset: null,
        };
      })
      .filter(Boolean) as AlignmentMatch[])
  );

  return result;
};

const findAlignmentMatches = (
  timing: EditTiming,
  alignmentSpec: AlignmentSpec,
  studioClips: StudioClip[]
): AlignmentMatch[] => {
  if (isHistoryOnly(alignmentSpec)) {
    return findHistoryOnlyAlignmentMatches(timing, alignmentSpec, studioClips);
  } else if (isFutureOnly(alignmentSpec)) {
    return findFutureOnlyAlignmentMatches(timing, alignmentSpec, studioClips);
  } else if (isHistoryAndFuture(alignmentSpec)) {
    return findHistoryAndFutureAlignmentMatches(
      timing,
      alignmentSpec,
      studioClips
    );
  } else {
    console.error(alignmentSpec);
    throw new Error('Invalid alignment spec');
  }
};

export default findAlignmentMatches;
