// Options for Preact.
import './runtime/catchError';
import './runtime/debounceRendering';
import './runtime/vnode';
import './runtime/unmount';

import { Component } from 'preact';

import {
  VNODE_COMPONENT,
  NAMESPACE,
  HOOKS_LIST,
  EFFECTS_LIST,
  COMPONENT_HOOKS,
  HOOK_ARGS,
  HOOK_VALUE,
  HOOK_CLEANUP,
} from './constants';
import { computeKey } from './computeKey';
import { vnodesForComponent, mappedVNodes } from './runtime/vnodesForComponent';
import { signaturesForType } from './runtime/signaturesForType';

let typesById = new Map();
let pendingUpdates = [];

function sign(type, key, forceReset, getCustomHooks, status) {
  if (type) {
    let signature = signaturesForType.get(type);
    if (status === 'begin') {
      signaturesForType.set(type, {
        type,
        key,
        forceReset,
        getCustomHooks: getCustomHooks || (() => []),
      });

      return 'needsHooks';
    } else if (status === 'needsHooks') {
      signature.fullKey = computeKey(signature);
    }
  }
}

function replaceComponent(OldType, NewType, resetHookState) {
  const vnodes = vnodesForComponent.get(OldType);
  if (!vnodes) return;

  // migrate the list to our new constructor reference
  vnodesForComponent.delete(OldType);
  vnodesForComponent.set(NewType, vnodes);

  mappedVNodes.set(OldType, NewType);

  pendingUpdates = pendingUpdates.filter(p => p[0] !== OldType);

  vnodes.forEach(vnode => {
    if (!vnode.__c || !vnode.__c.__P) return;
    // update the type in-place to reference the new component
    vnode.type = NewType;

    if (vnode[VNODE_COMPONENT]) {
      vnode[VNODE_COMPONENT].constructor = vnode.type;

      try {
        if (vnode[VNODE_COMPONENT] instanceof OldType) {
          const oldInst = vnode[VNODE_COMPONENT];

          const newInst = new NewType(
            vnode[VNODE_COMPONENT].props,
            vnode[VNODE_COMPONENT].context
          );

          vnode[VNODE_COMPONENT] = newInst;
          // copy old properties onto the new instance.
          //   - Objects (including refs) in the new instance are updated with their old values
          //   - Missing or null properties are restored to their old values
          //   - Updated Functions are not reverted
          //   - Scalars are copied
          for (let i in oldInst) {
            const type = typeof oldInst[i];
            if (!(i in newInst)) {
              newInst[i] = oldInst[i];
            } else if (type !== 'function' && typeof newInst[i] === type) {
              if (
                type === 'object' &&
                newInst[i] != null &&
                newInst[i].constructor === oldInst[i].constructor
              ) {
                Object.assign(newInst[i], oldInst[i]);
              } else {
                newInst[i] = oldInst[i];
              }
            }
          }
        }
      } catch (e) {
        /* Functional component */
        vnode[VNODE_COMPONENT].constructor = NewType;
      }

      if (resetHookState) {
        if (
          vnode[VNODE_COMPONENT][COMPONENT_HOOKS] &&
          vnode[VNODE_COMPONENT][COMPONENT_HOOKS][HOOKS_LIST] &&
          vnode[VNODE_COMPONENT][COMPONENT_HOOKS][HOOKS_LIST].length
        ) {
          vnode[VNODE_COMPONENT][COMPONENT_HOOKS][HOOKS_LIST].forEach(
            possibleEffect => {
              if (
                possibleEffect[HOOK_CLEANUP] &&
                typeof possibleEffect[HOOK_CLEANUP] === 'function'
              ) {
                possibleEffect[HOOK_CLEANUP]();
                possibleEffect[HOOK_CLEANUP] = undefined;
              } else if (
                possibleEffect[HOOK_ARGS] &&
                possibleEffect[HOOK_VALUE] &&
                Object.keys(possibleEffect).length === 3
              ) {
                const cleanupKey = Object.keys(possibleEffect).find(
                  key => key !== HOOK_ARGS && key !== HOOK_VALUE
                );
                if (
                  cleanupKey &&
                  typeof possibleEffect[cleanupKey] == 'function'
                ) {
                  possibleEffect[cleanupKey]();
                  possibleEffect[cleanupKey] = undefined;
                }
              }
            }
          );
        }

        vnode[VNODE_COMPONENT][COMPONENT_HOOKS] = {
          [HOOKS_LIST]: [],
          [EFFECTS_LIST]: [],
        };
      } else if (
        vnode[VNODE_COMPONENT][COMPONENT_HOOKS] &&
        vnode[VNODE_COMPONENT][COMPONENT_HOOKS][HOOKS_LIST] &&
        vnode[VNODE_COMPONENT][COMPONENT_HOOKS][HOOKS_LIST].length
      ) {
        vnode[VNODE_COMPONENT][COMPONENT_HOOKS][HOOKS_LIST].forEach(
          possibleEffect => {
            if (
              possibleEffect[HOOK_CLEANUP] &&
              typeof possibleEffect[HOOK_CLEANUP] === 'function'
            ) {
              possibleEffect[HOOK_CLEANUP]();
              possibleEffect[HOOK_CLEANUP] = undefined;
            } else if (
              possibleEffect[HOOK_ARGS] &&
              possibleEffect[HOOK_VALUE] &&
              Object.keys(possibleEffect).length === 3
            ) {
              const cleanupKey = Object.keys(possibleEffect).find(
                key => key !== HOOK_ARGS && key !== HOOK_VALUE
              );
              if (cleanupKey && typeof possibleEffect[cleanupKey] == 'function')
                possibleEffect[cleanupKey]();
              possibleEffect[cleanupKey] = undefined;
            }
          }
        );

        vnode[VNODE_COMPONENT][COMPONENT_HOOKS][HOOKS_LIST].forEach(hook => {
          if (hook.__H && Array.isArray(hook.__H)) {
            hook.__H = undefined;
          }
        });
      }

      Component.prototype.forceUpdate.call(vnode[VNODE_COMPONENT]);
    }
  });
}

self[NAMESPACE] = {
  getSignature: type => signaturesForType.get(type),
  register: (type, id) => {
    if (typeof type !== 'function') return;

    if (typesById.has(id)) {
      const existing = typesById.get(id);
      if (existing !== type) {
        pendingUpdates.push([existing, type]);
        typesById.set(id, type);
      }
    } else {
      typesById.set(id, type);
    }

    if (!signaturesForType.has(type)) {
      signaturesForType.set(type, {
        getCustomHooks: () => [],
        type,
      });
    }
  },
  getPendingUpdates: () => pendingUpdates,
  flush: () => {
    pendingUpdates = [];
  },
  replaceComponent,
  sign,
  computeKey,
};
