import {
  ChecklistEditor,
  EditorPktEl,
  extractEditorProps,
  extractPktCommon,
  FormEditor,
  MediaEditor,
  NumericEditor,
  PktDateEditor,
  PktEl,
  PktExternalEditor,
  PktListEditor,
  PktTimeEditor,
  PktGpsEditor,
  PktLovibondEditor,
  PktAddressEditor,
  SignatureEditor,
  SpinnerEditor,
  TxtEditor,
  InfoEditor,
} from './PktEl';
import update from 'immutability-helper';
import {
  FormEditorAction,
  Path,
  savedPktFailure,
  savedPktSuccess,
} from './types';
import { nanoid } from '@reduxjs/toolkit';
import { saveNewPktRequest, updatePktRequest } from './requests';

export interface FormEditorState {
  loaded: boolean;
  topLevelEl: EditorPktEl;
  isEditMode: boolean;
  pktName?: string;
  pktId?: number;
  unsavedChanges: boolean;
  showDebugInfo: boolean;
  EditTime;
  saveStatus:
    | { state: 'unmodified' }
    | { state: 'unsavedChanges' }
    | { state: 'saving' }
    | { state: 'saved' }
    | { state: 'error'; error: Error & { request: { responseText: string } } };
}

function emptyForm(): EditorPktEl {
  return {
    typ: 'form',
    uuid: nanoid(),
    items: [],
  };
}

const init: FormEditorState = {
  loaded: false,
  topLevelEl: emptyForm(),
  isEditMode: true,
  unsavedChanges: false,
  showDebugInfo: false,
  EditTime: false,
  saveStatus: { state: 'unmodified' },
};

export default function reducer(
  state: FormEditorState = init,
  action: FormEditorAction
): FormEditorState {
  switch (action.type) {
    case 'LOAD_PKT_JSON':
      return {
        ...state,
        loaded: true,
        topLevelEl: addUuidsToPkt(action.pktJson),
        pktName: action.pktName,
        pktId: action.pktId,
      };
    case 'LOAD_EMPTY_FORM':
      return {
        ...state,
        loaded: true,
        topLevelEl: emptyForm(),
        pktName: undefined,
      };
    case 'TOGGLE_EDIT_MODE':
      return {
        ...state,
        isEditMode: !state.isEditMode,
      };
    case 'TOOGLE_TIME_CONTROL':
      return {
        ...state,
        EditTime: !state.EditTime,
      };
    case 'TOGGLE_DEBUG_INFO':
      return {
        ...state,
        showDebugInfo: !state.showDebugInfo,
      };
    case 'TOGGLE_REQUIRED':
      return {
        ...state,
        topLevelEl: applyUpdateToPath(state.topLevelEl, action.path, {
          $toggle: ['required'],
        }),
        saveStatus: { state: 'unsavedChanges' },
      };
    case 'INSERT_ELEMENT': 
      const newEl = makeNewEl(action.elTyp);
      return {
        ...state,
        topLevelEl: applyUpdateToPath(state.topLevelEl, action.path, {
          // To allow the insertion of elements into empty forms, the 'items'
          // or 'tmp' array needs to be created if not present.
          [action.itemType]: (items) =>
            update(items || [], { $splice: [[action.newItemIndex, 0, newEl]] }),
        }),
        saveStatus: { state: 'unsavedChanges' },
      };
    case 'DELETE_ELEMENT':
      // The path will in this case refer to the child element which should be
      // deleted:  [...pathToParent, 'items' | 'tmp', childIndex]
      const childIndex = action.path.slice(-1)[0];
      // This should be 'items' or 'tmp'
      const itemTypeMaybe = action.path.slice(-2)[0];
      let itemType: 'items' | 'tmp';
      if (itemTypeMaybe === 'items' || itemTypeMaybe === 'tmp') {
        itemType = itemTypeMaybe;
      } else {
        throw new Error('Error: Path to item does not have "items" or "tmp"');
      }
      const pathToParent = action.path.slice(0, -2);
      return {
        ...state,
        topLevelEl: applyUpdateToPath(state.topLevelEl, pathToParent, {
          [itemType]: { $splice: [[childIndex, 1]] },
        }),
        saveStatus: { state: 'unsavedChanges' },
      };
    case 'TOGGLE_EDITING':
      return {
        ...state,
        topLevelEl: applyUpdateToPath(state.topLevelEl, action.path, {
          $toggle: ['editing'],
        }),
      };
    case 'RENAME_ELEMENT':
      return {
        ...state,
        topLevelEl: applyUpdateToPath(state.topLevelEl, action.path, {
          name: { $set: action.newName },
        }),
        saveStatus: { state: 'unsavedChanges' },
      };

    case 'SET_SPINNER_OPTION_NAME':
    case 'SET_SPINNER_OPTION_VALUE':
    case 'SET_SPINNER_OPTION_REQUIRED': {
      const spinnerEl = getElFromPath(state.topLevelEl, action.spinnerPath);
      if (spinnerEl.typ !== 'spin' && spinnerEl.typ !== 'chk') {
        throw new Error(
          'The element at the given path is not a spinner or checklist'
        );
      }
      let operator;
      switch (action.type) {
        case 'SET_SPINNER_OPTION_NAME':
          operator = { text: { $set: action.newName } };
          break;
        case 'SET_SPINNER_OPTION_VALUE':
          operator = { value: { $set: action.newValue } };
          break;
        case 'SET_SPINNER_OPTION_REQUIRED':
          operator = { required: { $set: action.newValue } };
          break;
      }
      return {
        ...state,
        topLevelEl: applyUpdateToPath(state.topLevelEl, action.spinnerPath, {
          items: {
            [action.optionIndex]: operator,
          },
        }),
        saveStatus: { state: 'unsavedChanges' },
      };
    }
    case 'ADD_SPINNER_OPTION':
      return {
        ...state,
        topLevelEl: applyUpdateToPath(state.topLevelEl, action.spinnerPath, {
          items: {
            $splice: [
              [action.optionIndex, 0, { text: '', value: '', uuid: nanoid() }],
            ],
          },
        }),
        saveStatus: { state: 'unsavedChanges' },
      };
    case 'DELETE_SPINNER_OPTION':
      return {
        ...state,
        topLevelEl: applyUpdateToPath(state.topLevelEl, action.spinnerPath, {
          items: { $splice: [[action.optionIndex, 1]] },
        }),
        saveStatus: { state: 'unsavedChanges' },
      };
    case 'SET_HINT':
      return {
        ...state,
        topLevelEl: applyUpdateToPath(state.topLevelEl, action.path, {
          hint: { $set: action.newHint },
        }),
        saveStatus: { state: 'unsavedChanges' },
      };
    case 'SET_LISTSRC':
      return {
        ...state,
        topLevelEl: applyUpdateToPath(state.topLevelEl, action.path, {
          listsrc: { $set: action.newListsrc },
        }),
        saveStatus: { state: 'unsavedChanges' },
      };
    case 'SET_DEFAULT':
      return {
        ...state,
        topLevelEl: applyUpdateToPath(state.topLevelEl, action.path, {
          default: { $set: action.newDefault }
        }),
        saveStatus: { state: 'unsavedChanges' }
      };
    case 'SET_EL_TYPE': {
      // When changing the type of an element, we retain the properties that
      // are applicable to the new element while discarding those that do not
      // match.
      const oldEl = getElFromPath(state.topLevelEl, action.path);
      const commonProps = extractPktCommon(oldEl);
      const editorProps = extractEditorProps(oldEl);
      const newEl = makeNewEl(action.newType);
      if (
        (oldEl.typ === 'chk' && newEl.typ === 'spin') ||
        (oldEl.typ === 'spin' && newEl.typ === 'chk')
      ) {
        newEl.items = oldEl.items;
      }
      if (
        (oldEl.typ === 'txt' && newEl.typ === 'num') ||
        (oldEl.typ === 'num' && newEl.typ === 'txt')
      ) {
        newEl.hint = oldEl.hint;
      }
      return {
        ...state,
        topLevelEl: applyUpdateToPath(state.topLevelEl, action.path, {
          $set: { ...newEl, ...commonProps, ...editorProps },
        }),
        saveStatus: { state: 'unsavedChanges' },
      };
    }
    case 'TOGGLE_PROPERTY':
      return {
        ...state,
        topLevelEl: applyUpdateToPath(state.topLevelEl, action.path, {
          $toggle: [action.toggle],
        }),
        saveStatus: { state: 'unsavedChanges' },
      };
    case 'SET_PROPERTY':
      return {
        ...state,
        topLevelEl: applyUpdateToPath(state.topLevelEl, action.path, {
          [action.name]: { $set: action.value },
        }),
        saveStatus: { state: 'unsavedChanges' },
      };
    case 'REMOVE_PROPERTY':
      return {
        ...state,
        topLevelEl: applyUpdateToPath(state.topLevelEl, action.path, {
          $unset: [action.name],
        }),
        saveStatus: { state: 'unsavedChanges' },
      };
    case 'SET_FROM_DATE':
      return {
        ...state,
        topLevelEl: applyUpdateToPath(state.topLevelEl, action.path, {
          range: { $splice: [[0, 1, action.offset]] },
        }),
        saveStatus: { state: 'unsavedChanges' },
      };
    case 'SET_TO_DATE':
      return {
        ...state,
        topLevelEl: applyUpdateToPath(state.topLevelEl, action.path, {
          range: { $splice: [[1, 1, action.offset]] },
        }),
        saveStatus: { state: 'unsavedChanges' },
      };
    case 'SET_INPTYP':
      const operator =
        action.inpTyp === 'none'
          ? { $unset: ['inptyp'] }
          : { inptyp: { $set: action.inpTyp } };
      return {
        ...state,
        topLevelEl: applyUpdateToPath(state.topLevelEl, action.path, operator),
        saveStatus: { state: 'unsavedChanges' },
      };
    case 'SAVE_PKT':
      return { ...state, saveStatus: { state: 'saving' } };
    case 'SAVED_PKT_FAILURE':
      return { ...state, saveStatus: { state: 'error', error: action.error } };
    case 'SAVED_PKT_SUCCESS':
      return {
        ...state,
        saveStatus: { state: 'saved' },
        pktName: action.pktName,
        pktId: action.pktId,
      };
      case 'UPDATE_LIST_ELEMENT': {
        return {
          ...state,
          topLevelEl: applyUpdateToPath(state.topLevelEl, action.path, {
            $set: action.newValue
          }),
          unsavedChanges: true, // Setze unsavedChanges auf true, da eine Änderung vorgenommen wurde
          saveStatus: { state: 'unsavedChanges' }, // Aktualisiere den saveStatus, um die Änderung widerzuspiegeln
        };
      }
    default:
      return state;
  }
}

function addUuidsToPkt(el: PktEl): EditorPktEl {
  switch (el.typ) {
    case 'form':
      return {
        ...el,
        uuid: nanoid(),
        items: (el.items ?? []).map(addUuidsToPkt),
        tmp: (el.tmp ?? []).map(addUuidsToPkt),
      };
    case 'spin':
    case 'chk':
      return {
        ...el,
        uuid: nanoid(),
        items: el.items.map((spinnerItem) => {
          if (typeof spinnerItem === 'string') {
            return {
              text: spinnerItem,
              value: spinnerItem,
              uuid: nanoid(),
            };
          } else {
            return { ...spinnerItem, uuid: nanoid() };
          }
        }),
      };
    default:
      return { ...el, uuid: nanoid() };
  }
}
function makeNewEl(typ: PktEl['typ']): EditorPktEl {
  switch (typ) {
    case 'spin':
      return { typ: 'spin', items: [], uuid: nanoid() } as SpinnerEditor;
    case 'date':
      return {
        typ: 'date',
        range: [0, 0],
        d: { inp: '' },
        uuid: nanoid(),
      } as PktDateEditor;
    case 'ext':
      return {
        typ: 'ext',
        'by.id': '',
        d: { resid: '' },
        uuid: nanoid(),
      } as PktExternalEditor;
    case 'chk':
      return {
        typ: 'chk',
        items: [],
        d: { chk: [] },
        uuid: nanoid(),
      } as ChecklistEditor;
    case 'sign':
      return {
        typ: 'sign',
        d: { filename: '' },
        uuid: nanoid(),
      } as SignatureEditor;
    case 'txt':
      return { typ, uuid: nanoid() } as TxtEditor;
    case 'info':
      return { typ, uuid: nanoid() } as InfoEditor;
    case 'num':
      return { typ, uuid: nanoid() } as NumericEditor;
    case 'media':
      return { typ, uuid: nanoid() } as MediaEditor;
    case 'form':
      return { typ, uuid: nanoid() } as FormEditor;
    case 'list':
      return { typ, uuid: nanoid() } as PktListEditor;
    case 'time':
      return { typ, uuid: nanoid() } as PktTimeEditor;
    case 'gps':
      return { typ, uuid: nanoid() } as PktGpsEditor;
    case 'lovibond':
      return { typ, uuid: nanoid() } as PktLovibondEditor;
    case 'address':
      return { typ, uuid: nanoid() } as PktAddressEditor;
  }
}

function getElFromPath(topLevelEl: EditorPktEl, path: Path) {
  const pathCopy = [...path];
  let el = topLevelEl;
  while (pathCopy.length > 0) {
    const pathSegment = pathCopy.shift();
    el = el[pathSegment];
  }
  return el;
}

function applyUpdateToPath(
  el: EditorPktEl,
  path: Path,
  updateObject: object
): EditorPktEl {
  path = [...path].reverse();

  path.forEach((pathSegment) => {
    updateObject = { [pathSegment]: updateObject };
  });

  return update(el, updateObject);
}
function updateState(state: FormEditorState, path: any, newValue: any): FormEditorState {
  throw new Error('Function not implemented.');
}

export const savePKTAction = (pktName, pktJson) => async (dispatch, getState) => {
  const state = getState();
  const pktId = state.pktId;

  try {
    const result = pktId
        ? await updatePktRequest(pktName, pktJson, pktId)
        : await saveNewPktRequest(pktName, pktJson);
    dispatch({ type: 'SAVED_PKT_SUCCESS', pktName, pktId: pktId || result.data.pktId});
  } catch (err) {
    dispatch({ type: 'SAVED_PKT_FAILURE', err});
  }
};
