import {
  Group,
  Undo,
  Redo,
  Delegate,
  CalculateState,
  DoNStatesExist
} from './interfaces/internal';

import {
  Action,
  Reducer,
  Comparator,
  UndoxState,
  Limit
} from './interfaces/public';

import {
  UndoxTypes,
  GroupAction,
  RedoAction,
  UndoAction,
  UndoxAction
} from './undox.action';


// helper used to flatten the history if grouped actions are in it
const flatten = <T> (x: (T | T[])[]) => [].concat(...x as any) as T[]

// actions can be an array of arrays because of grouped actions, so we flatten it first
const calculateState: CalculateState = (reducer, actions, state) => flatten(actions).reduce(reducer, state)

const getFutureActions = <S, A extends Action>(state: UndoxState<S, A>) => state.history.slice(state.index + 1)

const getPastActionsWithPresent = <S, A extends Action>(state: UndoxState<S, A>, isPastLimitExceeded: boolean) => {
  const startIdx = isPastLimitExceeded ? 2 : 1
  return [state.history[0],...state.history.slice(startIdx, state.index + 1)]
}

const getPastActions = <S, A extends Action>(state: UndoxState<S, A>) => state.history.slice(0, state.index)

const getPresentState = <S, A extends Action>(state: UndoxState<S, A>) => state.present

export const createSelectors = <S, A extends Action>(reducer: Reducer<S, A>) => {

  return {
    getPastStates : (state: UndoxState<S, A>): S[] =>
      getPastActions(state) 
        .reduce(
          (states, a, i) =>
            Array.isArray(a)
              ? [ ...states, a.reduce(reducer, states[i - 1]) ]
              : [ ...states, reducer(states[i - 1], a) ]
          , [ ] as S[]
        ),

    getFutureStates : (state: UndoxState<S, A>): S[] =>
      getFutureActions(state)
        .reduce(
          (states, a, i) =>
            Array.isArray(a)
              ? [ ...states, a.reduce(reducer, states[i]) ]
              : [ ...states, reducer(states[i], a) ]
          , [ getPresentState(state) ]
        ).slice(1),

    getPresentState,
    getPastActions : (state: UndoxState<S, A>): A[] => flatten(getPastActions(state)),
    getPresentAction : (state: UndoxState<S, A>): A | A[] => state.history[state.index],
    getFutureActions : (state: UndoxState<S, A>): A[] => flatten(getFutureActions(state))
  }

}


const doNPastStatesExist   : DoNStatesExist = ({ index }, nStates) => index >= nStates
const doNFutureStatesExist : DoNStatesExist = ({ history, index }, nStates) => history.length - 1 - index >= nStates


const group: Group = (state, action, reducer, comparator, limit) => {

  const presentState = getPresentState(state)
  const nextState = action.payload.reduce(reducer, state.present)

  if (comparator(presentState, nextState))
    return state

  const isPastLimitExceeded = pastLimitExceeded(state.index, limit)

  return {
    ...state,
    history : [ ...getPastActionsWithPresent(state, isPastLimitExceeded), action.payload ],
    index   : state.index + 1,
    present : nextState
  }

}


const undo: Undo = (reducer, state, { payload = 1 }, limit) => {

  const nPastStatesExist = doNPastStatesExist(state, payload)
  const index = nPastStatesExist ? state.index - payload : 0
  const futureExceeded = (state.history.length - state.index) > limit.future
  const history = futureExceeded ? state.history.slice(0, -payload) : state.history
  const isPastLimitExceeded = pastLimitExceeded(state.index, limit)

  const newState = {
    ...state,
    history,
    index
  }

  return {
    ...newState,
    present : calculateState(reducer as any, getPastActionsWithPresent(!isPastLimitExceeded ? newState : {...newState, index: state.index }, isPastLimitExceeded), newState.initial)
  }

}

const redo: Redo = (reducer, state, { payload = 1 }) => {

  const latestFuture = state.history.slice(state.index + 1, state.index + 1 + payload)

  return {
    ...state,
    index   : doNFutureStatesExist(state, payload) ? state.index + payload : state.history.length - 1, 
    present : calculateState(reducer, latestFuture, getPresentState(state))
  }

}

const pastLimitExceeded = (index: number, limit: Limit) => index >= limit.past

const delegate: Delegate = (state, action, reducer, comparator, limit) => {
  const nextPresent = reducer(state.present, action)
  const index = state.index
  const isPastLimitExceeded = pastLimitExceeded(index, limit)

  if (comparator(state.present, nextPresent))
    return state

  const newHistory = [...getPastActionsWithPresent(state, isPastLimitExceeded), action]

  const newState = {
    ...state,
    history : newHistory,
    index   : isPastLimitExceeded ? state.index : state.index + 1,
    present : nextPresent,
    initial : isPastLimitExceeded ? reducer(state.initial, state.history[1] as any) : state.initial
  }
  return newState

}


export const undox = <S, A extends Action>(
  reducer: Reducer<S, A>,
  initAction = { type: 'undox/INIT' } as any,
  comparator: Comparator<S> = (s1, s2) => s1 === s2,
  limit?: Limit
  ) => {

  const noLimit = { past: Infinity, future: Infinity }
   
  const initial = reducer(undefined, initAction)

  const initialState: UndoxState<S, A> = {
    history : [ initAction ],
    present : initial,
    index   : 0,
    initial
  }

  return (state = initialState, action: UndoxAction<A>) => {

    switch (action.type) {

      case UndoxTypes.UNDO:
        return undo(reducer, state, action as UndoAction, limit || noLimit)

      case UndoxTypes.REDO:
        return redo(reducer, state, action as RedoAction, limit || noLimit)

      case UndoxTypes.GROUP:
        return group(state, action as GroupAction<A>, reducer, comparator, limit || noLimit)

      default:
        return delegate(state, action as A, reducer, comparator, limit || noLimit)

    }

  }

}
