import { difference } from 'lodash-es';
import { reaction, runInAction } from 'mobx';
import { v4 as uuidv4 } from 'uuid';

import { tagsToArray } from '@/components/song/songUtils';
import { toast } from '@/components/toast/Toast';
import { fetchClip } from '@/hooks/useClip';
import formatLyricsGenerationErrorForToast from '@/lib/formatLyricsGenerationErrorForToast';
import { Clip, GenerationType } from '@/state/clipStore';
import { getClipTitle, getExtendTask } from '@/utils/clip';
import {
  isFeatureEnabledForPlan,
  isHybridModelAvailable,
  isNegativeTagsFeatureEnabled,
} from '@/utils/session';
import {
  DEFAULT_AUK_MODEL_NAME,
  DEFAULT_BLUEJAY_MODEL_NAME,
  DEFAULT_CROW_MODEL_NAME,
  DEFAULT_V3_MODEL_NAME,
  DEFAULT_V4_MODEL_NAME,
  getDefaultModel,
  getRandomSubset,
  quantize,
} from '@/utils/utils';

import {
  COEXISTING_STYLES_NUM,
  MIN_INFILL_SECONDS,
  RECOMMEND_STYLES_NUM,
  randomModifiedGenre,
  randomOneBoxPrompt,
} from '../utils/constants';
import { getRecommendedStyles } from '../utils/recommendedStyles';
import { RootStore, Substore } from './rootStore';
import { ModelType, PlanFeature } from './sessionStore';
import { makeAutoObservableSubstore } from './utils';

// the current codec freq is 25 HZ hence quantize value is a multiple of 0.04
export const QUANTIZE_VALUE = 0.2;
const CREATE_STATE_STORAGE_KEY = 'create-state';

export type TaskLiteral =
  | 'extend'
  | 'upload_extend'
  | 'infill'
  | 'artist_consistency'
  | 'cover'
  | 'infill_intro'
  | 'infill_outro'
  | 'artist_cover'
  | 'cover_extend'
  | 'artist_extend'
  | 'artist_cover_extend'
  | 'multi_artist_consistency'
  | 'playlist_condition'
  | 'underpainting'
  | 'overpainting'
  | null;

export class GenerateFormStore implements Substore {
  lyrics: string = '';
  lyricsPrompt: string = '';
  fullLyrics: string | null = null;
  lyricsModel: string = 'default';
  lastLyricsGeneration: {
    lyricsModel: string;
    prompt: string;
    lyrics: string;
    title: string;
    tags?: string;
  } | null = null;
  lastTagsGeneration: {
    requestId: string;
    generatedTags: string;
  } | null = null;
  createSessionToken: string = uuidv4();
  style: string = '';
  negativeTags: string = '';

  defaultStyles: string[] = [];
  coexistingStylesDict: Record<string, Record<string, number>> = {};

  /**
   * Styles recommended because they are related to the current selected styles
   */
  relatedStyles: string[] = [];
  /**
   * The displayed suggestions including the recommended styles for the current
   * input as well as any random padding options
   */
  recommendedStyles: string[] = [];

  description: string = '';
  title: string = '';

  // model version
  mv: string = DEFAULT_V3_MODEL_NAME; // the currently set model version (what displays in ModelSelect)
  mvUserPreference: string = DEFAULT_V3_MODEL_NAME; // the last model version explicitly chosen by the user

  placeholder: string = randomOneBoxPrompt();
  instrumental: boolean = false;
  enableExcludeStyle: boolean = false;

  reusePromptClipId: string | null = null;
  reusePromptClipUserId: string | null | undefined = undefined;
  reusePromptClipIsPublic: boolean | undefined | null = undefined;

  continueClipId: string | null = null;
  continueClipDuration: number | null = null;
  continueAtSeconds: number | null = null;
  continueClipUserId: string | null | undefined = undefined;
  continueClipIsPublic: boolean | undefined | null = undefined;
  continuedAlignedPrompt: string | null = null;

  artistClipId: string | null = null;
  artistStartSeconds: number | null = null;
  artistEndSeconds: number | null = null;

  coverClipId: string | null = null;

  infillFromSeconds: number | null = null;
  infillToSeconds: number | null = null;
  infillContextStartSeconds: number | null = null;
  infillContextEndSeconds: number | null = null;
  infillFixDuration: boolean = true;

  isSimple: boolean = true;
  isCustomModeFocused: boolean = false;
  isLoading = false;
  showLyrics = false;

  isLoadingLyrics = false;
  currentLyricsRequestId: string | null = null;
  isGenerateLyricsUsed = false;
  isRecommendStyleUsed = false;
  isMadLibs = false;

  isMobile = false;

  lyricsRef: any;

  selectedClip: Clip | null = null;

  shouldOpenMobileCreate: boolean | undefined = false;

  imageData: { [key: string]: string | null } = {};
  videoUploadId: string | null = null;

  generationType: GenerationType = 'TEXT';

  artist: string = '';

  cover: string = '';

  configurations: { [key: string]: any } = {};
  advancedParams: { [field: string]: any } | null = null;
  advancedDiffusionParams: { [field: string]: any } | null = null;

  task: TaskLiteral = null;

  personaClipId: string | null = null;
  personaId: string | null = null;

  hasSeenCrow: boolean = false;
  hasSeenBluejay: boolean = false;
  hasSeenCrowModal: boolean = false;
  hasLoadedLocalStorage: boolean = false;

  cachedState: any = {};

  hasRunSavedPrompt: boolean = false;

  isRemixCreate: boolean = false;

  stylesLyricsClipId: string | null = null;

  speedClipId: string | null = null;

  isMumbleMode: boolean = false;

  readonly root: RootStore;
  get apiClient() {
    return this.root.apiClient;
  }
  get queryClient() {
    return this.root.queryClient;
  }

  // DO NOT ADD INIT LOGIC IN constructor. this will run on every page! considering adding any it in the component init logic instead
  constructor(root: RootStore) {
    this.root = root;
    makeAutoObservableSubstore(this);
  }

  // this will run on every page! considering adding any it in the component init logic instead
  initialize() {
    this.loadFromLocalStorage();
    this.initLocalStorageUpdate();
    const cleanUpLocalStorageReaction = reaction(
      () => this.root.session.userId,
      (userId) => {
        if (!!userId) {
          this.loadFromLocalStorage();
          this.initLocalStorageUpdate();
          cleanUpLocalStorageReaction();
        }
      },
      { fireImmediately: true }
    );
  }

  /**
   * Initializes recommended styles
   */
  async initStyles() {
    try {
      const { data, error } = await this.apiClient.GET(
        '/api/generate/get_recommend_styles'
      );
      if (error) throw error;

      runInAction(() => {
        if (data) {
          if (data.default_styles) {
            this.defaultStyles = data.default_styles;
          }
          if (data.co_existing_styles_dict) {
            this.coexistingStylesDict = data.co_existing_styles_dict;
          }
          // Pick random recommended styles
          this.setRecommendedStyles(this.getRandomDefaultStyles());
        }
      });
    } catch (error) {
      console.error('Failed to fetch default styles:', error);
    }
  }

  getRandomDefaultStyles(count = RECOMMEND_STYLES_NUM) {
    return getRandomSubset(this.defaultStyles, count);
  }

  setGenerationType = (generationType: GenerationType) => {
    this.generationType = generationType;
  };

  setTask = (task: TaskLiteral) => {
    this.task = task;
  };

  setLyricsPrompt = (lyricsPrompt: string) => {
    this.lyricsPrompt = lyricsPrompt;
  };

  setLyrics = (lyrics: string) => {
    this.lyrics = lyrics;
  };

  setShowLyrics = (showLyrics: boolean) => {
    this.showLyrics = showLyrics;
  };

  setMv = (mv: string) => {
    this.mv = mv;
    // If user leaves the Bluejay model while lyrics editor is in Mumble mode, fall back to Custom mode
    if (mv !== 'chirp-bluejay') {
      const createV2 = this.root.createV2 as any;
      if (
        createV2.activeLyricsMode === 'mumble_mode' ||
        createV2.activeLyricsModeTab === 'mumble_mode'
      ) {
        createV2.setMumbleMode(false);
        createV2.setActiveLyricsMode('custom');
        createV2.setActiveLyricsModeTab('custom');
      }
      createV2.resetPlaylistConditioning();
      createV2.resetPainting();
    }
  };

  resetMvToUserPreference = () => {
    this.setMv(this.mvUserPreference);
  };

  /*
  sets the model version based on user input, which will also set the mvUserPreference
  (which gets memoized into local storage state)
  */
  setMvUserPreference = (mv: string) => {
    this.mvUserPreference = mv;
    this.setMv(mv);
  };

  setTitle = (title: string) => {
    this.title = title;
  };

  setReusePromptClip = (clip: Clip) => {
    this.reusePromptClipId = clip.id;
    this.reusePromptClipUserId = clip.user_id;
    this.reusePromptClipIsPublic = clip.is_public;
  };

  setContinueClip(clip: Clip | null) {
    this.infillFromSeconds = null;
    this.infillToSeconds = null;
    if (clip === null) {
      this.continueClipId = null;
      this.continueClipDuration = null;
      this.continueAtSeconds = null;
      this.continueClipUserId = null;
      this.continueClipIsPublic = undefined;
      return;
    } else {
      this.continueClipId = clip.id;
      this.continueClipUserId = clip.user_id;
      this.continueClipIsPublic = clip.is_public;
      this.continueClipDuration = clip.metadata?.duration || null;

      this.continueAtSeconds = clip.metadata?.duration
        ? quantize(clip.metadata?.duration - QUANTIZE_VALUE, QUANTIZE_VALUE)
        : null;
    }
    this.isSimple = false;
    // to handle the case of the create toolbar where the task is already pre-set,
    // infer the task name ('upload_extend' | 'extend') here
    if (this.task === 'extend') {
      this.task = getExtendTask(clip);
    }
  }

  matchClipModel = (clip: Clip) => {
    // if session.models hasn't loaded yet, optimistically set to v4
    // and let it be down-graded by ModelSelect effect
    if (clip.metadata?.type === 'upload') {
      // for uploads, just leave as whatever's already selected
      return;
    }
    if (
      (clip?.model_name || '').includes('crow') &&
      !!this.root.session.billingModels.find(
        (model: ModelType) => model.external_key === DEFAULT_CROW_MODEL_NAME
      )?.can_use
    ) {
      this.mv = DEFAULT_CROW_MODEL_NAME;
    } else if (
      (clip?.model_name || '').includes('bluejay') &&
      !!this.root.session.billingModels.find(
        (model: ModelType) => model.external_key === DEFAULT_BLUEJAY_MODEL_NAME
      )?.can_use
    ) {
      this.mv = DEFAULT_BLUEJAY_MODEL_NAME;
    } else if (
      (clip?.model_name || '').includes('auk') &&
      this.root.session.isAukEnabled()
    ) {
      this.mv = DEFAULT_AUK_MODEL_NAME;
    } else if (
      clip.major_model_version === 'v4' &&
      (isHybridModelAvailable(this.root.session) ||
        !this.root.session.getViewableModels())
    ) {
      this.mv = DEFAULT_V4_MODEL_NAME;
    } else {
      this.mv = DEFAULT_V3_MODEL_NAME;
    }
  };

  resetContinueClip = () => {
    this.setContinueClip(null);
    if (this.task === 'extend' || this.task === 'upload_extend') {
      this.setTask(null);
    }
    // since the mv might diverge from the user's preferred model version during an extend, reset it back here
    this.mv = this.mvUserPreference;
  };

  setContinueAtSeconds = (startAt: number | null) => {
    this.continueAtSeconds = startAt;
  };

  setInfillFromSeconds = (startAt: number | null) => {
    this.infillFromSeconds = startAt;
  };

  setInfillToSeconds = (endAt: number | null) => {
    this.infillToSeconds = endAt;
  };

  setArtistClip = (clip: Clip | null) => {
    if (clip === null) {
      this.artistClipId = null;
      this.artistStartSeconds = null;
      this.artistEndSeconds = null;
      return;
    } else {
      this.artistClipId = clip.id;
      this.artistStartSeconds = 0;
      const clipDuration = clip.metadata?.duration || 0;

      if (this.root.session?.flags?.['auk-model']) {
        this.artistEndSeconds = Math.min(clipDuration, 60); // 1 minute when auk-model is enabled
      } else {
        this.artistEndSeconds = Math.min(clipDuration, 120); // 2 minutes otherwise
      }

      // Only reset cover if auk-model flag is not enabled
      if (!this.root.session?.flags?.['auk-model']) {
        this.resetCoverClip();
      }

      this.infillFromSeconds = null;
      this.infillToSeconds = null;
    }
    this.isSimple = false;
  };

  resetArtistClip = () => {
    this.artistClipId = null;
    this.artistStartSeconds = null;
    this.artistEndSeconds = null;
  };

  isArtistValid = () => {
    if (this.artistClipId == null) {
      return true;
    }

    if (this.artistStartSeconds == null || this.artistEndSeconds == null) {
      return false;
    }

    if (
      this.artistStartSeconds < 0 ||
      (this.artistEndSeconds != null &&
        this.artistStartSeconds > this.artistEndSeconds)
    ) {
      return false;
    }

    return true;
  };

  setCoverClip = (clip: Clip | null) => {
    if (clip === null) {
      this.coverClipId = null;
      return;
    } else {
      this.coverClipId = clip.id;
      this.setLyrics(clip.metadata?.prompt || '');

      const currentClipTitle = getClipTitle(clip);
      const coverTitle =
        currentClipTitle && currentClipTitle.length > 0
          ? `${currentClipTitle} (Cover)`
          : '';
      this.setTitle(coverTitle);

      if (!this.root.session?.flags?.['auk-model']) {
        this.resetArtistClip();
      }
      this.infillFromSeconds = null;
      this.infillToSeconds = null;
    }
    this.isSimple = false;
  };

  resetCoverClip = () => {
    this.coverClipId = null;
    if (this.task === 'cover') {
      this.setTask(null);
    }
    if (this.title && this.title.length > 0 && this.title.includes('(Cover)')) {
      this.setTitle('');
    }
  };

  resetArtistCover = () => {
    this.coverClipId = null;
    this.setPersona(null);
    this.setPersonaClipId(null);
    this.setArtistClip(null);
    if (this.task === 'artist_cover') {
      this.setTask(null);
    }
    if (this.title && this.title.length > 0 && this.title.includes('(Cover)')) {
      this.setTitle('');
    }
  };

  setPersonaClipId = (clipId: string | null) => {
    if (!clipId || clipId.length <= 0) {
      this.personaClipId = null;
      return;
    }

    if (!isFeatureEnabledForPlan(this.root.session, PlanFeature.Auk)) {
      this.resetContinueClip();
      this.resetCoverClip();
    }

    this.personaClipId = clipId;
    this.infillFromSeconds = null;
    this.infillToSeconds = null;

    this.isSimple = false;
  };

  resetPersona = () => {
    this.setPersonaClipId(null);
    this.setPersona(null);
  };

  resetInfill = () => {
    this.infillFromSeconds = null;
    this.infillToSeconds = null;
    this.infillContextStartSeconds = null;
    this.infillContextEndSeconds = null;
  };

  setPersona = (personaId: string | null) => {
    this.personaId = personaId;
  };

  setIsMobile = (isMobile: boolean) => {
    this.isMobile = isMobile;
  };

  resetContinueAtSeconds = () => {
    this.continueAtSeconds = this.continueClipDuration;
  };

  isContinueValid = () => {
    // Not using continue
    if (this.continueClipId === null) {
      return true;
    }

    if (this.continueAtSeconds === null) {
      return false;
    }

    if (
      this.continueAtSeconds <= 0 ||
      (this.continueClipDuration != null &&
        this.continueAtSeconds > this.continueClipDuration)
    ) {
      return false;
    }

    return true;
  };

  isInfillValid = () => {
    // Not using continue
    if (this.continueClipId === null) {
      return true;
    }

    if (
      this.infillFromSeconds === null ||
      this.infillToSeconds === null ||
      this.infillFromSeconds === this.infillToSeconds ||
      (this.infillToSeconds - this.infillFromSeconds < MIN_INFILL_SECONDS &&
        !(this.task === 'infill_intro' || this.task === 'infill_outro') &&
        !this.root.session.flags?.['edit-mode-intro']) ||
      !isFeatureEnabledForPlan(this.root.session, PlanFeature.EditMode)
    ) {
      return false;
    }
    return true;
  };

  setSelectedClip = (clip: Clip | null) => {
    this.selectedClip = clip;
  };

  setStyle = (style: string) => {
    this.style = style;
    this.isRecommendStyleUsed = true;

    // Normalize the input into an array of trimmed style tags
    const styles = tagsToArray(this.style);

    // Determine the recommendations for the currnet input
    const relatedStyles = getRecommendedStyles(
      styles,
      this.coexistingStylesDict,
      COEXISTING_STYLES_NUM
    );

    // If related styles for the input have changed, update displayed recommendations after half a second
    if (this.relatedStyles.join(',') !== relatedStyles.join(',')) {
      this.relatedStyles = relatedStyles;
      const recommendedStyles = !styles.length
        ? this.getRandomDefaultStyles(RECOMMEND_STYLES_NUM)
        : [
            ...relatedStyles,
            ...getRandomSubset(
              difference(
                Object.keys(this.coexistingStylesDict),
                styles,
                relatedStyles
              ),
              RECOMMEND_STYLES_NUM - relatedStyles.length
            ),
          ];
      if (recommendedStyles.length) {
        this.setRecommendedStyles(recommendedStyles);
      }
    }
  };

  setNegativeTags = (negativeTags: string) => {
    this.negativeTags = negativeTags;
  };

  setIsMadLibs = (isMadLibs: boolean) => {
    this.isMadLibs = isMadLibs;
  };

  setRecommendedStyles = (recommendedStyles: string[]) => {
    this.recommendedStyles = recommendedStyles;
  };

  setDescription = (description: string) => {
    this.description = description;
  };

  setLyricsModel = (lyricsModel: string) => {
    this.lyricsModel = lyricsModel;
  };

  setIsSimple = (isSimple: boolean) => {
    this.isSimple = isSimple;
    if (isSimple) {
      this.setContinueClip(null);
    }
  };

  setIsLoading = (isLoading: boolean) => {
    this.isLoading = isLoading;
  };

  setInstrumental = (instrumental: boolean) => {
    this.instrumental = instrumental;
  };

  setEnableExcludeStyle = (enableExcludeStyle: boolean) => {
    this.enableExcludeStyle = enableExcludeStyle;
  };

  resetEnableExcludeStyle = () => {
    this.enableExcludeStyle =
      this.negativeTags && this.negativeTags.length > 0 ? true : false;
  };

  setImageData = (imageData: string | null, index: number) => {
    this.imageData[index] = imageData;
  };

  setVideoUploadId = (videoUploadId: string) => {
    this.videoUploadId = videoUploadId;
  };

  rerollPlaceholder = (generator: () => string = randomOneBoxPrompt) => {
    this.placeholder = generator();
  };

  reuseClip = (clip: Clip) => {
    this.setReusePromptClip(clip);
    this.setClipMetadata(clip);
    this.isSimple = false;
  };

  setClipMetadata = (clip: Clip) => {
    this.setTitle(clip.title || '');
    this.setLyrics(clip.metadata?.prompt || '');
    this.setStyle(clip.metadata?.tags || '');
    if (isNegativeTagsFeatureEnabled(this.root.session)) {
      this.setNegativeTags(clip.metadata?.negative_tags || '');
      this.enableExcludeStyle = !!(
        clip.metadata?.negative_tags && clip.metadata?.negative_tags.length > 0
      );
    }
    this.setDescription(clip.metadata?.gpt_description_prompt || '');
  };

  cancelGenerateLyrics = () => {
    if (this.isLoadingLyrics && this.currentLyricsRequestId) {
      this.isLoadingLyrics = false;
      this.currentLyricsRequestId = null;
    }
  };

  generateLyrics = async (
    prompt?: string,
    updateCreateLyricsState: boolean = true
  ) => {
    this.isLoadingLyrics = true;
    this.currentLyricsRequestId = null;
    this.isGenerateLyricsUsed = true;
    const capturedPrompt = prompt ?? (this.lyrics || '');
    const capturedLyricsModel = this.lyricsModel;

    const { data, error } = await this.apiClient.POST('/api/generate/lyrics/', {
      body: {
        prompt: capturedPrompt,
        lyrics_model: capturedLyricsModel,
        create_session_token: this.createSessionToken,
      },
    });

    if (error || !data.id) {
      console.error('Generate Lyrics Error:', error);
      toast({
        title: 'Please wait a few seconds.',
        description: 'Please wait a few seconds before generating more lyrics.',
        status: 'error',
        duration: 4000,
        isClosable: true,
      });
      this.isLoadingLyrics = false;
      return;
    }

    const requestId = data.id;
    this.currentLyricsRequestId = requestId;

    for (let i = 0; i < 10; i++) {
      await new Promise((resolve) => setTimeout(resolve, 4000));

      // don't break - we do not want to fall through to the normal "request finished" logic that would follow this loop.
      if (requestId !== this.currentLyricsRequestId) return;

      const { data, error } = await this.apiClient.GET(
        '/api/generate/lyrics/{lyrics_id}',
        {
          params: {
            path: { lyrics_id: requestId },
          },
        }
      );

      if (requestId !== this.currentLyricsRequestId) return;

      if (error) {
        // this can only be an internal error
        toast({
          ...formatLyricsGenerationErrorForToast(),
          status: 'error',
          duration: 5000,
          isClosable: true,
        });
        break;
      }

      if (data?.status === 'running') {
        continue;
      } else if (data?.status === 'error') {
        toast({
          ...formatLyricsGenerationErrorForToast(data.error_message),
          status: 'error',
          duration: 4000,
          isClosable: true,
        });
        break;
      } else {
        if (updateCreateLyricsState) {
          this.setLyrics(data.text);
          this.setTitle(data.title);
        }
        this.lastLyricsGeneration = {
          lyricsModel: capturedLyricsModel,
          prompt: capturedPrompt,
          lyrics: data.text,
          title: data.title,
          tags: (data.tags || []).join(', '),
        };
        break;
      }
    }
    this.isLoadingLyrics = false;
  };

  generateStyle = () => {
    this.setStyle(randomModifiedGenre());
  };

  storageObject = () => {
    return {
      mv: this.mv,
      mvUserPreference: this.mvUserPreference,
      isSimple: this.isSimple,
      lyricsModel: this.lyricsModel,
      hasSeenCrow: this.hasSeenCrow,
      hasSeenBluejay: this.hasSeenBluejay,
      hasSeenCrowModal: this.hasSeenCrowModal,
      /*lyrics: this.lyrics,
      style: this.style,
      description: this.description,
      title: this.title,
      instrumental: this.instrumental,
      ,*/
    };
  };

  initLocalStorageUpdate = () => {
    reaction(this.storageObject, () => {
      if (this.root.isLocalStorageAvailable && !!this.root.session.userId) {
        const storageStr = JSON.stringify(this.storageObject());
        localStorage.setItem(
          `${CREATE_STATE_STORAGE_KEY}-${this.root.session.userId}`,
          storageStr
        );
      }
    });
  };

  loadFromLocalStorage = () => {
    runInAction(() => {
      if (this.root.isLocalStorageAvailable && !!this.root.session.userId) {
        const storageStr = localStorage.getItem(
          `${CREATE_STATE_STORAGE_KEY}-${this.root.session.userId}`
        );
        try {
          const stateJson = JSON.parse(storageStr || '');
          if (stateJson) {
            if (stateJson.mvUserPreference) {
              this.mv = stateJson.mvUserPreference;
              this.mvUserPreference = stateJson.mvUserPreference;
            } else if (stateJson.mv) {
              // allow for migrating localStorage to the new write key
              this.mv = stateJson.mv;
              this.mvUserPreference = stateJson.mv;
            }
            this.isSimple =
              stateJson.isSimple !== undefined ? stateJson.isSimple : true;
            this.lyricsModel = stateJson.lyricsModel || 'default';
            this.hasSeenCrow = stateJson.hasSeenCrow || false;
            this.hasSeenBluejay = stateJson.hasSeenBluejay || false;
            this.hasSeenCrowModal = stateJson.hasSeenCrowModal || false;
            /*
          this.lyrics = stateJson.lyrics || '';
          this.style = stateJson.style || '';
          this.description = stateJson.description || '';
          this.title = stateJson.title || '';
          this.instrumental = stateJson.instrumental || false;
          */
          }
        } catch (e) {
          console.log('Local Storage loading failed', e);
        }
        this.hasLoadedLocalStorage = true;
      }
    });
  };

  hasExistingLocalStoragePreferences = () => {
    if (!this.root.isLocalStorageAvailable || !this.root.session.userId) {
      return false;
    }

    const storageStr = localStorage.getItem(
      `${CREATE_STATE_STORAGE_KEY}-${this.root.session.userId}`
    );

    return storageStr !== null;
  };

  loadAdvancedParams = async () => {
    if (this.advancedParams !== null) {
      return;
    }
    const { data } = await this.apiClient.GET('/api/generate/matrix');
    this.advancedParams = data?.params.reduce(
      (acc: any, val: { field_name: string; type: string }) => {
        acc[val.field_name] = { type: val.type };
        return acc;
      },
      {}
    );
    this.advancedDiffusionParams =
      data?.diffusion_params?.reduce(
        (
          acc: { [field: string]: any },
          val: {
            field_name: string;
            type: 'float' | 'int';
            min?: number | null;
            max?: number | null;
          }
        ) => {
          acc[val.field_name] = {
            type: val.type,
            min: val.min ?? undefined,
            max: val.max ?? undefined,
          };
          return acc;
        },
        {}
      ) ?? null;
  };

  getAdvancedParams = () => {
    return this.advancedParams;
  };

  cacheState = () => {
    this.cachedState = {
      task: this.task,
      lyrics: this.lyrics,
      lyricsModel: this.lyricsModel,
      lyricsPrompt: this.lyricsPrompt,
      style: this.style,
      negativeTags: this.negativeTags,
      description: this.description,
      title: this.title,
      mv: this.mv,
      mvUserPreference: this.mvUserPreference,
      instrumental: this.instrumental,
      continueClipDuration: this.continueClipDuration,
      continueAtSeconds: this.continueAtSeconds,
      videoUploadId: this.videoUploadId,
      imageData: this.imageData,
      generationType: this.generationType,
      enableExcludeStyle: this.enableExcludeStyle,
      continueClipId: this.continueClipId,
      coverClipId: this.coverClipId,
      personaClipId: this.personaClipId,
      artistClipId: this.artistClipId,
      personaId: this.personaId,
      hasSeenCrow: this.hasSeenCrow,
      hasSeenBluejay: this.hasSeenBluejay,
      hasSeenCrowModal: this.hasSeenCrowModal,
    };
  };

  restoreCachedState = () => {
    if (Object.keys(this.cachedState).length === 0) {
      this.reset();
      return;
    }

    this.task = this.cachedState.task;
    this.lyrics = this.cachedState.lyrics;
    this.lyricsModel = this.cachedState.lyricsModel;
    this.lyricsPrompt = this.cachedState.lyricsPrompt;
    this.style = this.cachedState.style;
    this.negativeTags = this.cachedState.negativeTags;
    this.description = this.cachedState.description;
    this.title = this.cachedState.title;
    this.mv = this.cachedState.mv;
    this.mvUserPreference = this.cachedState.mvUserPreference;
    this.instrumental = this.cachedState.instrumental;
    this.continueClipDuration = this.cachedState.continueClipDuration;
    this.continueAtSeconds = this.cachedState.continueAtSeconds;
    this.videoUploadId = this.cachedState.videoUploadId;
    this.imageData = this.cachedState.imageData;
    this.generationType = this.cachedState.generationType;
    this.enableExcludeStyle = this.cachedState.enableExcludeStyle;
    this.continueClipId = this.cachedState.continueClipId;
    this.coverClipId = this.cachedState.coverClipId;
    this.personaClipId = this.cachedState.personaClipId;
    this.artistClipId = this.cachedState.artistClipId;
    this.personaId = this.cachedState.personaId;
    this.hasSeenCrow = this.cachedState.hasSeenCrow;
    this.hasSeenBluejay = this.cachedState.hasSeenBluejay;
    this.hasSeenCrowModal = this.cachedState.hasSeenCrowModal;
  };

  reset = () => {
    this.task = null;
    this.lyrics = '';
    this.lyricsPrompt = '';
    this.style = '';
    this.negativeTags = '';
    this.description = '';
    this.title = '';
    this.mv = getDefaultModel(this.root.session.getViewableModels());
    this.mvUserPreference = getDefaultModel(
      this.root.session.getViewableModels()
    );
    this.instrumental = false;
    this.continueClipDuration = null;
    this.continueAtSeconds = null;
    this.videoUploadId = null;
    this.imageData = {};
    this.generationType = 'TEXT';
    this.enableExcludeStyle = false;

    this.resetArtistClip();
    this.resetPersona();
    this.resetCoverClip();
    this.resetContinueClip();
    this.resetStylesLyricsClip();
    this.resetCreateToken();
  };

  resetRemixClipConditions = () => {
    this.resetCoverClip();
    this.resetContinueClip();
    this.resetStylesLyricsClip();
    this.resetSpeedClip();

    if (this.title && this.title.trim().endsWith('(Remix)')) {
      this.setTitle(this.title.trim().replace(/\s*\(Remix\)$/, ''));
    }
  };

  resetConfigurations = () => {
    this.configurations = {};
  };

  resetCreateToken = () => {
    this.createSessionToken = uuidv4();
  };

  isSetToDefaults = () => {
    return (
      this.lyrics === '' &&
      this.style === '' &&
      this.negativeTags === '' &&
      this.description === '' &&
      this.title === '' &&
      this.mv === getDefaultModel(this.root.session.getViewableModels()) &&
      this.instrumental === false &&
      this.continueClipId === null &&
      this.continueClipDuration === null &&
      this.continueAtSeconds === null &&
      this.videoUploadId === null &&
      Object.keys(this.imageData).length === 0 &&
      this.generationType === 'TEXT' &&
      this.enableExcludeStyle === false
    );
  };

  setIsRemixCreate = (value: boolean) => {
    this.isRemixCreate = value;
  };

  setStylesLyricsClip = async (clipId: string | null) => {
    if (clipId === null) {
      this.stylesLyricsClipId = null;
      return;
    } else {
      this.stylesLyricsClipId = clipId;
      const clip = await fetchClip(this.root.clips, clipId);

      if (this.stylesLyricsClipId !== clipId) {
        // exit if the clip has changed since we started fetching it
        return;
      }

      if (!clip) {
        console.error('Clip not found', clipId);
        return;
      }

      this.setLyrics(clip.metadata?.prompt || '');
      this.setStyle(clip.metadata?.tags || '');

      if (clip.metadata?.negative_tags) {
        this.setNegativeTags(clip.metadata?.negative_tags || '');
        this.setEnableExcludeStyle(true);
      }

      this.infillFromSeconds = null;
      this.infillToSeconds = null;
    }
    this.isSimple = false;
  };

  resetStylesLyricsClip = () => {
    this.stylesLyricsClipId = null;
  };

  setSpeedClipId(clipId: string | null) {
    this.speedClipId = clipId;
    this.setStylesLyricsClip(null);
    this.setCoverClip(null);
    this.setContinueClip(null);
  }

  resetSpeedClip = () => {
    this.speedClipId = null;
  };

  setMumbleMode = (mumbleMode: boolean) => {
    this.isMumbleMode = mumbleMode;
  };
}
