import { IComparer, sort } from 'fast-sort';
import FuzzySearch from 'fuzzy-search';
import { Nilable, Nullable } from 'tsdef';

import { createSelector } from '@reduxjs/toolkit';

import { OptionIds } from '../action-definitions/option-ids';
import { FileArray, FileData, FileFilter } from '../types/file.types';
import { RootState } from '../types/redux.types';
import { FileSortKeySelector, SortOrder } from '../types/sort.types';
import { FileHelper } from '../util/file-helper';

// Raw selectors
export const selectInstanceId = (state: RootState) => state.instanceId;
export const selectExternalFileActionHandler = (state: RootState) => state.externalFileActionHandler;

export const selectFileActionMap = (state: RootState) => state.fileActionMap;
export const selectFileActionIds = (state: RootState) => state.fileActionIds;
export const selectFileActionData = (fileActionId: string) => (state: RootState) =>
  selectFileActionMap(state)[fileActionId];
export const selectToolbarItems = (state: RootState) => state.toolbarItems;
export const selectContextMenuItems = (state: RootState) => state.contextMenuItems;

export const selectFolderChain = (state: RootState) => state.folderChain;
export const selectCurrentFolder = (state: RootState) => {
  const folderChain = selectFolderChain(state);
  const currentFolder = folderChain.length > 0 ? folderChain[folderChain.length - 1] : null;
  return currentFolder;
};
export const selectParentFolder = (state: RootState) => {
  const folderChain = selectFolderChain(state);
  const parentFolder = folderChain.length > 1 ? folderChain[folderChain.length - 2] : null;
  return parentFolder;
};

export const selectRawFiles = (state: RootState) => state.rawFiles;
export const selectFileMap = (state: RootState) => state.fileMap;
export const selectCleanFileIds = (state: RootState) => state.cleanFileIds;
export const selectFileData = (fileId: Nullable<string>) => (state: RootState) =>
  fileId ? selectFileMap(state)[fileId] : null;

export const selectHiddenFileIdMap = (state: RootState) => state.hiddenFileIdMap;
export const selectHiddenFileCount = (state: RootState) => Object.keys(selectHiddenFileIdMap(state)).length;

export const selectFocusSearchInput = (state: RootState) => state.focusSearchInput;
export const selectSearchString = (state: RootState) => state.searchString;
export const selectOnSearchInput = (state: RootState) => state.onSearchInput;
export const selectOnCancelSearch = (state: RootState) => state.onCancelSearch;

export const selectSelectionMap = (state: RootState) => state.selectionMap;
export const selectSelectedFileIds = (state: RootState) => Object.keys(selectSelectionMap(state));
export const selectSelectionSize = (state: RootState) => selectSelectedFileIds(state).length;
export const selectIsFileSelected = (fileId: Nullable<string>) => (state: RootState) =>
  !!fileId && !!selectSelectionMap(state)[fileId];
export const selectSelectedFiles = (state: RootState) => {
  const fileMap = selectFileMap(state);
  return Object.keys(selectSelectionMap(state)).map((id) => fileMap[id]);
};
export const selectSelectedFilesForAction = (fileActionId: string) => (state: RootState) => {
  const { fileActionMap } = state;
  const action = fileActionMap[fileActionId];
  if (!action || !action.requiresSelection) return undefined;

  return getSelectedFiles(state, action.fileFilter);
};
export const selectSelectedFilesForActionCount = (fileActionId: string) => (state: RootState) =>
  getSelectedFilesForAction(state, fileActionId)?.length;
export const selectDisableSelection = (state: RootState) => state.disableSelection;
export const selectDisableSimpleDeselection = (state: RootState) => state.disableSimpleDeselection;
export const selectForceEnableOpenParent = (state: RootState) => state.forceEnableOpenParent;
export const selectHideToolbarInfo = (state: RootState) => state.hideToolbarInfo;

export const selectFileViewConfig = (state: RootState) => state.fileViewConfig;

export const selectSortActionId = (state: RootState) => state.sortActionId;
export const selectSortOrder = (state: RootState) => state.sortOrder;

export const selectOptionMap = (state: RootState) => state.optionMap;
export const selectOptionValue = (optionId: string) => (state: RootState) => selectOptionMap(state)[optionId];

export const selectDoubleClickDelay = (state: RootState) => state.doubleClickDelay;
export const selectIsDnDDisabled = (state: RootState) => state.disableDragAndDrop;
export const selectClearSelectionOnOutsideClick = (state: RootState) => state.clearSelectionOnOutsideClick;

export const selectContextMenuMounted = (state: RootState) => state.contextMenuMounted;
export const selectContextMenuConfig = (state: RootState) => state.contextMenuConfig;
export const selectContextMenuTriggerFile = (state: RootState) => {
  const config = selectContextMenuConfig(state);
  if (!config || !config.triggerFileId) return null;
  const fileMap = selectFileMap(state);
  return fileMap[config.triggerFileId] ?? null;
};

export const selectRenamingFileId = (state: RootState) => state.renamingFileId;
export const selectIsFileRenaming = (fileId: Nullable<string>) => (state: RootState) =>
  !!fileId && selectRenamingFileId(state) === fileId;

export const selectRenamingSanitizer = (state: RootState) => state.renamingSanitizer;

// Raw selectors
const getFileActionMap = (state: RootState) => state.fileActionMap;
const getOptionMap = (state: RootState) => state.optionMap;
const getFileMap = (state: RootState) => state.fileMap;
const getFileIds = (state: RootState) => state.fileIds;
const getCleanFileIds = (state: RootState) => state.cleanFileIds;
const getSortActionId = (state: RootState) => state.sortActionId;
const getSortOrder = (state: RootState) => state.sortOrder;
const getSearchString = (state: RootState) => state.searchString;
const _getLastClick = (state: RootState) => state.lastClick;
const getSortCollator = (state: RootState) => state.sortCollator;
const getSearchPredicate = (state: RootState) => state.searchPredicate;

// Memoized selectors
const makeGetAction = (fileActionSelector: (state: RootState) => Nullable<string>) =>
  createSelector([getFileActionMap, fileActionSelector], (fileActionMap, fileActionId) =>
    fileActionId && fileActionMap[fileActionId] ? fileActionMap[fileActionId] : null,
  );
const makeGetOptionValue = (optionId: string, defaultValue: any = undefined) =>
  createSelector([getOptionMap], (optionMap) => {
    const value = optionMap[optionId];
    if (value === undefined) {
      return defaultValue;
    }
    return value;
  });
const makeGetFiles = (fileIdsSelector: (state: RootState) => Nullable<string>[]) =>
  createSelector(
    [getFileMap, fileIdsSelector],
    (fileMap, fileIds): FileArray => fileIds.map((fileId) => (fileId && fileMap[fileId] ? fileMap[fileId] : null)),
  );
const getSortedFileIds = createSelector(
  [
    getFileIds,
    getSortOrder,
    getSortCollator,
    makeGetFiles(getFileIds),
    makeGetAction(getSortActionId),
    makeGetOptionValue(OptionIds.ShowFoldersFirst, false),
  ],
  (fileIds, sortOrder, sortCollator, files, sortAction, showFolderFirst) => {
    if (!sortAction) {
      // We allow users to set the sort action ID to `null` if they want to use their
      // own sorting mechanisms instead of relying on Chonky built-in sort.
      return fileIds;
    }

    const prepareSortKeySelector = (selector: FileSortKeySelector) => (file: Nullable<FileData>) => selector(file);

    const sortFunctions: {
      asc?: (file: FileData) => any;
      desc?: (file: FileData) => any;
      comparer?: IComparer;
    }[] = [];

    if (showFolderFirst) {
      // If option is undefined (relevant actions is not enabled), we don't show
      // folders first.
      sortFunctions.push({
        desc: prepareSortKeySelector(FileHelper.isDirectory),
      });
    }
    if (sortAction.sortKeySelector) {
      const configKeyName = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
      sortFunctions.push({
        [configKeyName]: prepareSortKeySelector(sortAction.sortKeySelector),
        comparer: sortCollator?.compare,
      });
    }
    if (sortFunctions.length === 0) return fileIds;

    // We copy the array because `fast-sort` mutates it
    const sortedFileIds = sort([...files])
      .by(sortFunctions as any)
      .map((file) => (file ? file.id : null));
    return sortedFileIds;
  },
);
const getSearcher = createSelector(
  [makeGetFiles(getCleanFileIds), getSearchPredicate],
  (cleanFiles, searchPredicate) => {
    const files = cleanFiles as FileData[];
    return searchPredicate
      ? { search: (needle: string) => files.filter((file) => searchPredicate(needle, file)) }
      : new FuzzySearch(files, ['name'], { caseSensitive: false });
  },
);
const getSearchFilteredFileIds = createSelector(
  [getCleanFileIds, getSearchString, getSearcher],
  (cleanFileIds, searchString, searcher) =>
    searchString ? searcher.search(searchString).map((f) => f.id) : cleanFileIds,
);
const getHiddenFileIdMap = createSelector(
  [getSearchFilteredFileIds, makeGetFiles(getCleanFileIds), makeGetOptionValue(OptionIds.ShowHiddenFiles)],
  (searchFilteredFileIds, cleanFiles, showHiddenFiles) => {
    const searchFilteredFileIdsSet = new Set(searchFilteredFileIds);
    const hiddenFileIdMap: any = {};
    cleanFiles.forEach((file) => {
      if (!file) return;
      else if (!searchFilteredFileIdsSet.has(file.id)) {
        // Hidden by seach
        hiddenFileIdMap[file.id] = true;
      } else if (!showHiddenFiles && FileHelper.isHidden(file)) {
        // Hidden by options
        hiddenFileIdMap[file.id] = true;
      }
    });
    return hiddenFileIdMap;
  },
);
const getDisplayFileIds = createSelector(
  [getSortedFileIds, getHiddenFileIdMap],
  /** Returns files that will actually be shown to the user. */
  (sortedFileIds, hiddenFileIdMap) => sortedFileIds.filter((id) => !id || !hiddenFileIdMap[id]),
);
const getLastClick = createSelector(
  [_getLastClick, getDisplayFileIds],
  /** Returns the last click index after ensuring it is actually still valid. */
  (lastClick, displayFileIds) => {
    if (
      !lastClick ||
      lastClick.index > displayFileIds.length - 1 ||
      lastClick.fileId != displayFileIds[lastClick.index]
    ) {
      return null;
    }
    return lastClick;
  },
);

export const selectors = {
  // Raw selectors
  getFileActionMap,
  getOptionMap,
  getFileMap,
  getFileIds,
  getCleanFileIds,
  getSortActionId,
  getSortOrder,
  getSearchString,
  _getLastClick,

  // Memoized selectors
  getSortedFileIds,
  getSearcher,
  getSearchFilteredFileIds,
  getHiddenFileIdMap,
  getDisplayFileIds,
  getLastClick,

  // Parametrized selectors
  makeGetAction,
  makeGetOptionValue,
  makeGetFiles,
};

// Selectors meant to be used outside of Redux code
export const getFileData = (state: RootState, fileId: Nullable<string>) =>
  fileId ? selectFileMap(state)[fileId] : null;
export const getIsFileSelected = (state: RootState, file: FileData) => {
  // !!! We deliberately don't use `FileHelper.isSelectable` here as we want to
  //     reflect the state of Redux store accurately.
  return !!selectSelectionMap(state)[file.id];
};
export const getSelectedFiles = (state: RootState, ...filters: Nilable<FileFilter>[]) => {
  const { fileMap, selectionMap } = state;

  const selectedFiles = Object.keys(selectionMap).map((id) => fileMap[id]);
  const filteredSelectedFiles = filters.reduce(
    (prevFiles, filter) => (filter ? prevFiles.filter(filter) : prevFiles),
    selectedFiles,
  );
  return filteredSelectedFiles;
};
export const getSelectedFilesForAction = (state: RootState, fileActionId: string) =>
  selectSelectedFilesForAction(fileActionId)(state);
