import { fail, nextTick } from '@turbox3d/shared';
import { ctx } from '../const/config';
import { Reaction } from './reactive';
import { HistoryCollectorPayload, TimeTravel, History } from './time-travel';
import { actionTypeChain, ActionType } from './store';
import { EDepState, ECollectType, ESpecialReservedKey } from '../const/enums';
import { Action } from './action';
import { KeyPathType, rawCache } from './domain';
import { isDomain } from '../utils/common';
import { EEventName, emitter, SetPropertyEvent } from '../utils/event';
import { materialCallStack } from '../utils/materialCallStack';

export type ReactionId = any | Reaction;

export type DepNodeAssembly = Map<any, Set<ReactionId>>;

export type DepNodeStatus = Map<any, EDepState>;

const isInBlackList = (propKey: string) => {
  const blackList = {
    constructor: true,
    $$turboxProperties: true,
    context: true,
    currentTarget: true,
    originalArrayLength: true,
    reactorConfigMap: true,
    computedProperties: true,
    propertyGet: true,
    propertySet: true,
    proxyDeleteProperty: true,
    proxyOwnKeys: true,
    proxySet: true,
    proxyGet: true,
    proxyReactive: true,
    $update: true,
    illegalAssignmentCheck: true,
    dispatch: true,
  };

  return !!blackList[propKey];
};

/**
 * collect relation map of the dep key and the reaction ids
 */
class DepCollector {
  public dependencyGraph = new Map<object, DepNodeAssembly>();
  public reactionIdDeps = new Map<ReactionId, Map<object, DepNodeStatus>>();
  private reactionIdStack: ReactionId[] = [];

  start(id: ReactionId) {
    this.reactionIdStack.push(id);
  }

  collect(target: object, propertyKey: any) {
    if (isInBlackList(propertyKey)) {
      return;
    }
    const stackLength = this.reactionIdStack.length;
    if (stackLength === 0) {
      return;
    }
    const currentReactionId = this.reactionIdStack[stackLength - 1];
    const depNodeAssembly = this.dependencyGraph.get(target);
    if (depNodeAssembly !== void 0) {
      const depNode = depNodeAssembly.get(propertyKey);
      if (depNode !== void 0) {
        depNode.add(currentReactionId);
      } else {
        depNodeAssembly.set(propertyKey, new Set([currentReactionId]));
      }
    } else {
      const map = new Map([[propertyKey, new Set([currentReactionId])]]);
      this.dependencyGraph.set(target, map);
    }
    const reactionDeps = this.reactionIdDeps.get(currentReactionId);
    if (reactionDeps !== void 0) {
      const depNodeStatusAssembly = reactionDeps.get(target);
      if (depNodeStatusAssembly !== void 0) {
        depNodeStatusAssembly.set(propertyKey, EDepState.LATEST);
      } else {
        reactionDeps.set(target, new Map([[
          propertyKey, EDepState.LATEST,
        ]]));
      }
    } else {
      this.reactionIdDeps.set(currentReactionId, new Map([[target, new Map([[
        propertyKey, EDepState.LATEST,
      ]])]]));
    }
  }

  end() {
    const currentReactionId = this.reactionIdStack.pop();
    if (!currentReactionId) {
      return;
    }
    const map = this.reactionIdDeps.get(currentReactionId);
    if (!map) {
      return;
    }
    map.forEach((depNode, target) => {
      depNode.forEach((value, key) => {
        // remove stale deps
        if (value === EDepState.OBSERVED) {
          const depNodeAssembly = this.dependencyGraph.get(target);
          if (depNodeAssembly) {
            const idSet = depNodeAssembly.get(key);
            idSet && idSet.delete(currentReactionId);
            depNode.set(key, EDepState.NOT_OBSERVED);
          }
        }
        // reset dep status
        if (value === EDepState.LATEST) {
          depNode.set(key, EDepState.OBSERVED);
        }
      });
    });
  }

  clear(id: ReactionId) {
    const map = this.reactionIdDeps.get(id);
    if (!map) {
      return;
    }
    map.forEach((depNode, target) => {
      depNode.forEach((value, key) => {
        const depNodeAssembly = this.dependencyGraph.get(target);
        if (depNodeAssembly) {
          const idSet = depNodeAssembly.get(key);
          idSet && idSet.delete(id);
        }
      });
    });
    this.reactionIdDeps.delete(id);
  }

  isObserved(targetKey: object, propKey: string) {
    const map = this.dependencyGraph.get(targetKey);
    return map !== void 0 && map.has(propKey) && (map.get(propKey) || new Set()).size > 0;
  }
}

export const depCollector = new DepCollector();

class TriggerCollector {
  public waitTriggerIds: Set<ReactionId> = new Set();

  trigger(
    target: object,
    key: any,
    payload: HistoryCollectorPayload,
    isNeedRecord = true,
    devToolInfo?: {
      keyPath: Array<{ type: KeyPathType; value: string }>;
      domain: string;
    }
  ) {
    const { type, beforeUpdate, didUpdate } = payload;
    if (ctx.devTool && devToolInfo) {
      try {
        const stringify = (window && (window as any).__TURBOX_DEVTOOL_GOLBAL_STRINGIFY__) ?? JSON.stringify;
        const event: SetPropertyEvent = {
          time: Date.now(),
          stackId: materialCallStack.currentStack?.stackId,
          domain: devToolInfo.domain,
          keyPath: devToolInfo.keyPath,
          newValue: stringify(didUpdate),
          oldValue: stringify(beforeUpdate),
          type
        };
        emitter.emit(EEventName.setProperty, event);
        console.log(event);
      } catch (e) {
        fail(e);
      }
    }
    const enhanceKey = type === ECollectType.ADD ? ESpecialReservedKey.ITERATE : key;
    this.collectComponentId(target, enhanceKey);
    if (
      !ctx.timeTravel.isActive ||
      !TimeTravel.currentTimeTravel ||
      !isNeedRecord ||
      TimeTravel.processing ||
      enhanceKey === ESpecialReservedKey.ITERATE ||
      enhanceKey === ESpecialReservedKey.COMPUTED
    ) {
      return;
    }
    if (Action.context) {
      this.recordDiff(target, enhanceKey, payload, Action.context.historyNode.history);
      return;
    }
    this.recordDiff(target, enhanceKey, payload, TimeTravel.currentTimeTravel.currentHistory);
  }

  endBatch(isClearHistory = true, action?: Action) {
    if (!action) {
      this.waitTriggerIds.clear();
    }
    if (!ctx.timeTravel.isActive || !TimeTravel.currentTimeTravel || !isClearHistory) {
      return;
    }
    if (action) {
      action.historyNode.actionChain.length = 0;
      action.historyNode.history.clear();
      nextTick(() => {
        const ctt = TimeTravel.currentTimeTravel;
        ctt && ctt.onChange(ctt.undoable, ctt.redoable, 'clear', action);
      });
      return;
    }
    actionTypeChain.length = 0;
    TimeTravel.currentTimeTravel.currentHistory.clear();
  }

  save(action?: Action) {
    if (!ctx.timeTravel.isActive || !TimeTravel.currentTimeTravel || Action.context) {
      return;
    }
    const ctt = TimeTravel.currentTimeTravel;
    if (action) {
      this.saveHistory(ctt, action.historyNode.actionChain, action.historyNode.history, action);
      return;
    }
    this.saveHistory(ctt, actionTypeChain, ctt.currentHistory);
  }

  private collectComponentId(target: object, enhanceKey: any) {
    const depNodeAssembly = depCollector.dependencyGraph.get(target);
    if (depNodeAssembly !== void 0) {
      const idSet = depNodeAssembly.get(enhanceKey);
      if (idSet !== void 0 && idSet.size > 0) {
        idSet.forEach(id => this.waitTriggerIds.add(id));
      }
    }
  }

  private recordDiff(target: object, enhanceKey: any, payload: HistoryCollectorPayload, history: History) {
    const { type, beforeUpdate, didUpdate } = payload;
    let proxyTarget: object | undefined;
    if (isDomain(target)) {
      proxyTarget = target;
    } else {
      proxyTarget = rawCache.get(target);
    }
    if (!proxyTarget) {
      return;
    }
    const keyToDiffChangeMap = history.get(proxyTarget);
    if (keyToDiffChangeMap !== void 0) {
      const diffInfo = keyToDiffChangeMap.get(enhanceKey);
      if (diffInfo !== void 0) {
        diffInfo.didUpdate = didUpdate;
      } else {
        keyToDiffChangeMap.set(enhanceKey, {
          type,
          beforeUpdate,
          didUpdate,
        });
      }
    } else {
      history.set(proxyTarget, new Map([[enhanceKey, {
        type,
        beforeUpdate,
        didUpdate,
      }]]));
    }
  }

  private saveHistory(ctt: TimeTravel, actionChain: ActionType[], history?: History, action?: Action) {
    if (!history) {
      return;
    }
    if (history.size === 0) {
      return;
    }
    const clonedChain = ctx.timeTravel.keepActionChain ? actionChain.slice() : [];
    const clonedHistory = new Map(history);
    if (ctt.cursor === ctt.transactionHistories.length - 1) {
      ctt.transactionHistories.push({
        actionChain: clonedChain,
        history: clonedHistory,
      });
      ctt.cursor += 1;
    } else if (ctt.cursor < ctt.transactionHistories.length - 1) {
      ctt.transactionHistories = ctt.transactionHistories.slice(0, ctt.cursor + 1);
      ctt.transactionHistories.push({
        actionChain: clonedChain,
        history: clonedHistory,
      });
      ctt.cursor += 1;
    }

    if (ctt.transactionHistories.length > ctx.timeTravel.maxStepNumber!) {
      ctt.transactionHistories.shift();
      ctt.cursor -= 1;
    }
    nextTick(() => {
      ctt.onChange(ctt.undoable, ctt.redoable, 'save', action);
    });
  }
}

export const triggerCollector = new TriggerCollector();
