import assert from "tiny-invariant"
import { enableMapSet, produce } from "immer"
import * as A from "./actions"
import type { File } from "~/shared/models"
import type { Action } from "./types"

enableMapSet()

export interface ReduxFile extends Pick<File, "id" | "tags" | "rules" | "sections"> {}

export interface RootState {
  /**
   * A list of opened files
   */
  tabs: ReduxFile[]

  /**
   * Marks a file as a preview tab.
   * @see https://github.com/Microsoft/vscode-docs/blob/vnext/release-notes/June_2016.md#preview-editors
   */
  previewFileId?: string

  /**
   * Designates the active file ID.
   */
  activeFileId?: string

  dirtyFileIds: Set<string>
}

/**
 * Array.prototype.map that returns the original array if no value is *referentially* different.
 */
function refEqualMap<T>(array: T[], mapFn: (value: T) => T): T[] {
  let modified = false
  const result: T[] = Array(array.length)
  for (let i = 0; i < array.length; i++) {
    const value = array[i]
    const mapped = mapFn(value)
    if (mapped !== value) {
      modified = true
    }
    result[i] = mapped
  }

  return modified ? result : array
}

const updateById = <T extends { id: string }>(
  array: T[],
  index: string,
  mapFn: (value: T) => T,
): T[] => refEqualMap(array, value => (value.id === index ? mapFn(value) : value))

const assertNever = (_: never): void => {}

function markDirty(fileId: string, state: RootState, isDirty: boolean): RootState {
  return {
    ...state,
    previewFileId: state.previewFileId === fileId ? undefined : state.previewFileId,
    dirtyFileIds: isDirty
      ? produce(state.dirtyFileIds, set => set.add(fileId))
      : produce(state.dirtyFileIds, set => void set.delete(fileId)),
  }
}

function updateActiveFile(
  state: RootState,
  recipe: (draft: ReduxFile) => void,
): RootState {
  const { tabs, activeFileId } = state
  assert(activeFileId != null)

  const newTabs = updateById(tabs, activeFileId, file => produce(file, recipe))
  if (newTabs === tabs) {
    return state
  }

  return {
    ...markDirty(activeFileId, state, true),
    tabs: newTabs,
  }
}

export function rootReducer(
  state: RootState = {
    tabs: [],
    dirtyFileIds: new Set(),
  },
  action: Action,
): RootState {
  const { tabs, activeFileId, previewFileId, dirtyFileIds } = state
  switch (action.type) {
    case A.OPEN_FILE: {
      const { file: style } = action
      // Tab already exists
      if (tabs.some(tab => tab.id === style.id)) {
        return {
          ...state,
          activeFileId: style.id,
        }
      }

      // There is an ephemeral tab, replace it
      if (previewFileId != null) {
        return {
          ...state,
          activeFileId: style.id,
          previewFileId: style.id,
          tabs: tabs.filter(x => x.id !== state.previewFileId).concat(style),
        }
      }

      // Append it to the end of tab list
      return {
        ...state,
        activeFileId: style.id,
        previewFileId: style.id,
        tabs: tabs.concat(style),
      }
    }

    case A.PATCH_FILES: {
      if (!action.patches.length) {
        return state
      }

      const patches = new Map(action.patches)

      return {
        ...state,
        tabs: refEqualMap(tabs, style => {
          const patch = patches.get(style.id)
          return patch ? { ...style, ...patch } : style
        }),
      }
    }

    case A.RENAME_FILE: {
      const { fileId: styleId, name } = action
      return {
        ...state,
        tabs: updateById(tabs, styleId, style => ({ ...style, name })),
      }
    }

    case A.UPDATE_FILE_CODE: {
      return updateActiveFile(state, draft => {
        draft.sections[0].code = action.code
      })
    }

    case A.CHANGE_DIRTY:
      return markDirty(action.fileId, state, action.dirty)

    case A.CLOSE_FILE: {
      const { fileId: styleId } = action
      return {
        ...state,
        tabs: tabs.filter(tab => tab.id !== styleId),
        activeFileId:
          activeFileId === styleId
            ? tabs.filter(tab => tab.id !== styleId)[0]?.id
            : activeFileId,
        previewFileId: previewFileId === styleId ? undefined : previewFileId,
        dirtyFileIds: produce(dirtyFileIds, set => void set.delete(styleId)),
      }
    }

    case A.SET_ACTIVE_FILE_ID:
      return {
        ...state,
        activeFileId: action.id,
      }

    case A.UPDATE_RULE: {
      const { index, rule } = action
      return updateActiveFile(state, draft => {
        draft.rules[index] = rule
      })
    }

    case A.DELETE_RULE: {
      const { index } = action
      return updateActiveFile(state, draft => {
        draft.rules.splice(index, 1)
      })
    }

    case A.ADD_RULE: {
      const { rule } = action
      return updateActiveFile(state, draft => {
        draft.rules.push(rule)
      })
    }

    case A.UPDATE_TAGS: {
      const { tags } = action
      return updateActiveFile(state, draft => {
        draft.tags = tags.slice()
      })
    }

    default:
      assertNever(action)
      return state
  }
}
