import {
  GOT_BUTTERCHURN_PRESETS,
  GOT_BUTTERCHURN,
  SELECT_PRESET_AT_INDEX,
  RESOLVE_PRESET_AT_INDEX,
  PRESET_REQUESTED,
  TOGGLE_RANDOMIZE_PRESETS,
  TOGGLE_PRESET_CYCLING,
  SCHEDULE_MILKDROP_MESSAGE,
} from "../actionTypes";
import * as Selectors from "../selectors";
import {
  TransitionType,
  Preset,
  ButterchurnOptions,
  StatePreset,
  Thunk,
  Action,
} from "../types";
import * as FileUtils from "../fileUtils";

function normalizePresetTypes(preset: Preset): StatePreset {
  const { name } = preset;
  if ("butterchurnPresetObject" in preset) {
    return {
      type: "RESOLVED",
      name,
      preset: preset.butterchurnPresetObject,
    };
  } else if ("getButterchrunPresetObject" in preset) {
    return {
      type: "UNRESOLVED",
      name,
      getPreset: preset.getButterchrunPresetObject,
    };
  } else if ("butterchurnPresetUrl" in preset) {
    return {
      type: "UNRESOLVED",
      name,
      getPreset: async () => {
        const resp = await fetch(preset.butterchurnPresetUrl);
        return resp.json();
      },
    };
  }
  throw new Error("Invalid preset object");
}

export function initializePresets(presetOptions: ButterchurnOptions): Thunk {
  return async dispatch => {
    const { getPresets, importButterchurn } = presetOptions;
    importButterchurn().then(butterchurn => {
      dispatch({ type: GOT_BUTTERCHURN, butterchurn: butterchurn.default });
    });

    const presets = await getPresets();
    const normalizePresets = presets.map(normalizePresetTypes);
    dispatch(loadPresets(normalizePresets));
  };
}

export function loadPresets(presets: StatePreset[]): Thunk {
  return (dispatch, getState) => {
    const state = getState();
    const presetsLength = state.milkdrop.presets.length;
    dispatch({ type: GOT_BUTTERCHURN_PRESETS, presets });
    if (presetsLength === 0 && Selectors.getRandomizePresets(state)) {
      dispatch(selectRandomPreset());
    } else {
      dispatch(
        requestPresetAtIndex(presetsLength, TransitionType.IMMEDIATE, true)
      );
    }
  };
}

export function appendPresetFileList(fileList: FileList): Thunk {
  return async (dispatch, getState, { convertPreset }) => {
    const presets: StatePreset[] = Array.from(fileList)
      .map(file => {
        const JSON_EXT = ".json";
        const MILK_EXT = ".milk";
        const filename = file.name.toLowerCase();
        if (filename.endsWith(MILK_EXT)) {
          if (convertPreset == null) {
            throw new Error("Invalid type");
          }
          return {
            type: "UNRESOLVED",
            name: file.name.slice(0, file.name.length - MILK_EXT.length),
            getPreset: () => convertPreset(file),
          } as StatePreset;
        } else if (filename.endsWith(JSON_EXT)) {
          return {
            type: "UNRESOLVED",
            name: file.name.slice(0, file.name.length - JSON_EXT.length),
            getPreset: async () => {
              const str = await FileUtils.genStringFromFileReference(file);
              // TODO: How should we handle the case where json parsing fails?
              return JSON.parse(str);
            },
          } as StatePreset;
        }
        console.error("Invalid type preset when loading directory");

        return null as never;
      })
      .filter(Boolean);
    dispatch(loadPresets(presets));
  };
}

export function selectNextPreset(
  transitionType: TransitionType = TransitionType.DEFAULT
): Thunk {
  return (dispatch, getState) => {
    const state = getState();
    if (Selectors.getRandomizePresets(state)) {
      return dispatch(selectRandomPreset(transitionType));
    }
    const currentPresetIndex = Selectors.getCurrentPresetIndex(state);
    if (currentPresetIndex == null) {
      return;
    }
    const nextPresetIndex = currentPresetIndex + 1;
    dispatch(requestPresetAtIndex(nextPresetIndex, transitionType, true));
  };
}

export function selectPreviousPreset(
  transitionType: TransitionType = TransitionType.DEFAULT
): Thunk {
  return (dispatch, getState) => {
    const state = getState();
    const { presetHistory } = state.milkdrop;
    if (presetHistory.length < 1) {
      return;
    }
    // Awkward. We do -2 becuase the the last track is the current track.
    const lastPresetIndex = presetHistory[presetHistory.length - 2];

    dispatch(requestPresetAtIndex(lastPresetIndex, transitionType, false));
  };
}

export function selectRandomPreset(
  transitionType: TransitionType = TransitionType.DEFAULT
): Thunk {
  return (dispatch, getState) => {
    const state = getState();
    // TODO: Make this a selector.
    const randomIndex = Math.floor(
      Math.random() * state.milkdrop.presets.length
    );
    dispatch(requestPresetAtIndex(randomIndex, transitionType, true));
  };
}

// TODO: Technically there's a race here. If you request two presets in a row, the
// first one may resolve before the second.
export function requestPresetAtIndex(
  index: number,
  transitionType: TransitionType,
  addToHistory: boolean
): Thunk {
  return async (dispatch, getState) => {
    const state = getState();
    const preset = state.milkdrop.presets[index];
    if (preset == null) {
      // Index might be out of range.
      return;
    }
    dispatch({ type: PRESET_REQUESTED, index, addToHistory });
    switch (preset.type) {
      case "RESOLVED":
        dispatch({ type: SELECT_PRESET_AT_INDEX, index, transitionType });
        return;
      case "UNRESOLVED":
        const json = await preset.getPreset();
        // TODO: Ensure that this works correctly even if requests resolve out of order
        dispatch({ type: RESOLVE_PRESET_AT_INDEX, index, json });
        dispatch({ type: SELECT_PRESET_AT_INDEX, index, transitionType });
        return;
    }
  };
}

export function handlePresetDrop(e: React.DragEvent): Thunk {
  return appendPresetFileList(e.dataTransfer.files);
}

export function toggleRandomizePresets(): Action {
  return { type: TOGGLE_RANDOMIZE_PRESETS };
}

export function togglePresetCycling(): Action {
  return { type: TOGGLE_PRESET_CYCLING };
}

export function scheduleMilkdropMessage(message: string): Action {
  return { type: SCHEDULE_MILKDROP_MESSAGE, message };
}
