import { uniq } from 'lodash-es';
import { useCallback, useEffect, useMemo, useState } from 'react';

import { createContext, useContextSelector } from '@/hooks/useContextSelector';
import useDismount from '@/hooks/useDismount';

import StudioContext from './StudioContext';
import { getSelectedClipCreationIntents } from './selectors';
import sortStudioClips from './sortStudioClips';
import { StudioProjectState } from './types';
import useAlignedClips, { AlignedClip } from './useAlignedClips';

export type GeneratingAlternateEntry = {
  status: 'generating';
  clipId: string;
};

export type ReadyAlternateEntry = {
  status: 'ready';
  alignedClip: AlignedClip;
};

export const getAlternateClipId = (entry: AlternateEntry) => {
  if (entry.status === 'ready') {
    return entry.alignedClip.clip.id;
  } else if (entry.status === 'generating') {
    return entry.clipId;
  } else {
    throw new Error('Unknown alternate entry status');
  }
};

type AlternateEntry = GeneratingAlternateEntry | ReadyAlternateEntry;

type ListAndSelectionHash = {
  list: string[];
  selectionHash: string;
};

const EMPTY_ARRAY: string[] = [];

export const getSelectionHash = (selection: {
  anchorBeats: number;
  focusBeats: number;
  focusedTrackId: string | null;
}) => {
  return JSON.stringify({
    ssb: Math.min(selection.anchorBeats, selection.focusBeats),
    seb: Math.max(selection.anchorBeats, selection.focusBeats),
    fti: selection.focusedTrackId,
  });
};

const expectedClipsBySelectionHash = new Map<string, string[]>();

export default function useAlternateController(
  nonPreviewState: StudioProjectState
) {
  useDismount(
    useCallback(() => {
      expectedClipsBySelectionHash.clear();
    }, []),
    250
  );

  const selectedClipCreationIntents =
    getSelectedClipCreationIntents(nonPreviewState);
  const generatingClipIds = useMemo(() => {
    return selectedClipCreationIntents.flatMap((c) => c.possibleClipIds);
  }, [selectedClipCreationIntents]);

  const currentSelectionHash = useMemo(() => {
    return getSelectionHash(nonPreviewState.selection);
  }, [nonPreviewState.selection]);

  const [expectedListAndSelectionHash, setExpectedListAndSelectionHash] =
    useState<ListAndSelectionHash>(() => ({
      list:
        expectedClipsBySelectionHash.get(currentSelectionHash) || EMPTY_ARRAY,
      selectionHash: currentSelectionHash,
    }));

  const expectedClipIds = useMemo(
    () =>
      uniq([...expectedListAndSelectionHash.list, ...generatingClipIds]).sort(
        (a, b) => a.localeCompare(b)
      ),
    [expectedListAndSelectionHash.list, generatingClipIds]
  );

  const alignedClips = useAlignedClips(nonPreviewState, expectedClipIds);
  const alignedClipIds = useMemo(() => {
    return new Set(alignedClips.map((c) => c.clip.id));
  }, [alignedClips.map((c) => c.clip.id).join(',')]);

  useEffect(() => {
    setExpectedListAndSelectionHash((prev) => ({
      ...prev,
      list: prev.list.filter((clipId) => !alignedClipIds.has(clipId)),
    }));
  }, [alignedClipIds]);

  const expect = useCallback(
    (selectionHash: string, clipId: string) => {
      if (selectionHash !== currentSelectionHash) {
        return;
      }

      setExpectedListAndSelectionHash((prev) =>
        prev.list.includes(clipId)
          ? prev
          : {
              ...prev,
              list: [...prev.list, clipId],
            }
      );
    },
    [currentSelectionHash]
  );

  useEffect(() => {
    expectedClipsBySelectionHash.set(
      expectedListAndSelectionHash.selectionHash,
      expectedListAndSelectionHash.list
    );
  }, [expectedListAndSelectionHash]);

  const clear = useCallback(() => {
    setExpectedListAndSelectionHash({
      list:
        expectedClipsBySelectionHash.get(currentSelectionHash) || EMPTY_ARRAY,
      selectionHash: currentSelectionHash,
    });
  }, [currentSelectionHash]);

  useEffect(() => {
    clear();
  }, [clear]);

  const list = useMemo(() => {
    if (expectedListAndSelectionHash.selectionHash !== currentSelectionHash) {
      return alignedClips.map(
        (c): ReadyAlternateEntry => ({
          status: 'ready',
          alignedClip: c,
        })
      );
    }

    const expectedEntries = expectedClipIds.filter(
      (clipId) =>
        !alignedClips.some((c) => {
          return c.clip.id === clipId;
        })
    );

    return [
      ...alignedClips
        .map(
          (c): ReadyAlternateEntry => ({
            status: 'ready',
            alignedClip: c,
          })
        )
        .sort((a, b) =>
          sortStudioClips(a.alignedClip.clip, b.alignedClip.clip)
        ),
      ...expectedEntries.map(
        (c): GeneratingAlternateEntry => ({
          status: 'generating',
          clipId: c,
        })
      ),
    ];
  }, [alignedClips, expectedClipIds]);

  return useMemo(
    () => ({
      list,
      expect,
      clear,
    }),
    [list, expect, clear]
  );
}

export const AlternateControllerContext = createContext<
  ReturnType<typeof useAlternateController>
>(undefined as never);

export const AlternateControllerContextProvider = ({
  children,
}: {
  children: React.ReactNode;
}) => {
  const state = useContextSelector(StudioContext, (context) => context.state);
  return (
    <AlternateControllerContext.Provider value={useAlternateController(state)}>
      {children}
    </AlternateControllerContext.Provider>
  );
};
