import type { IAnimation } from '@antv/g';
import { Graph as Graphlib } from '@antv/graphlib';
import { isLayoutWithIterations } from '@antv/layout';
import { deepMix } from '@antv/util';
import { COMBO_KEY, GraphEvent, TREE_KEY } from '../constants';
import { BaseLayout } from '../layouts';
import { AntVLayout } from '../layouts/types';
import { getExtension } from '../registry/get';
import type { GraphData, LayoutOptions, NodeData } from '../spec';
import type { STDLayoutOptions } from '../spec/layout';
import type { DrawData } from '../transforms/types';
import type { ID, TreeData } from '../types';
import { getAnimationOptions } from '../utils/animation';
import { isCollapsed } from '../utils/collapsibility';
import { isToBeDestroyed } from '../utils/element';
import { emit, GraphLifeCycleEvent } from '../utils/event';
import { createTreeStructure } from '../utils/graphlib';
import { idOf } from '../utils/id';
import { isLegacyAntVLayout, isTreeLayout, layoutAdapter, legacyLayoutAdapter } from '../utils/layout';
import { print } from '../utils/print';
import { dfs } from '../utils/traverse';
import type { RuntimeContext } from './types';

export class LayoutController {
  private context: RuntimeContext;

  private instance?: BaseLayout;

  private instances: BaseLayout[] = [];

  private animationResult?: IAnimation | null;

  private get presetOptions() {
    return {
      animation: !!getAnimationOptions(this.context.options, true),
    };
  }

  private get options() {
    const { options } = this.context;
    return options.layout;
  }

  constructor(context: RuntimeContext) {
    this.context = context;
  }

  public getLayoutInstance(): BaseLayout[] {
    return this.instances;
  }

  /**
   * <zh/> 前布局，即在绘制前执行布局
   *
   * <en/> Pre-layout, that is, perform layout before drawing
   * @param data - <zh/> 绘制数据 | <en/> Draw data
   * @remarks
   * <zh/> 前布局应该只在首次绘制前执行，后续更新不会触发
   *
   * <en/> Pre-layout should only be executed before the first drawing, and subsequent updates will not trigger
   */
  public async preLayout(data: DrawData) {
    const { graph, model } = this.context;

    const { add } = data;
    emit(graph, new GraphLifeCycleEvent(GraphEvent.BEFORE_LAYOUT, { type: 'pre' }));
    const simulate = await this.context.layout?.simulate();
    simulate?.nodes?.forEach((l) => {
      const id = idOf(l);
      const node = add.nodes.get(id);
      model.syncNodeLikeDatum(l);
      if (node) Object.assign(node.style!, l.style);
    });
    simulate?.edges?.forEach((l) => {
      const id = idOf(l);
      const edge = add.edges.get(id);
      model.syncEdgeDatum(l);
      if (edge) Object.assign(edge.style!, l.style);
    });
    simulate?.combos?.forEach((l) => {
      const id = idOf(l);
      const combo = add.combos.get(id);
      model.syncNodeLikeDatum(l);
      if (combo) Object.assign(combo.style!, l.style);
    });
    emit(graph, new GraphLifeCycleEvent(GraphEvent.AFTER_LAYOUT, { type: 'pre' }));
    this.transformDataAfterLayout('pre', data);
  }

  /**
   * <zh/> 后布局，即在完成绘制后执行布局
   *
   * <en/> Post layout, that is, perform layout after drawing
   * @param layoutOptions - <zh/> 布局配置项 | <en/> Layout options
   */
  public async postLayout(layoutOptions: LayoutOptions | undefined = this.options) {
    if (!layoutOptions) return;
    const pipeline = Array.isArray(layoutOptions) ? layoutOptions : [layoutOptions];
    const { graph } = this.context;
    emit(graph, new GraphLifeCycleEvent(GraphEvent.BEFORE_LAYOUT, { type: 'post' }));

    for (let index = 0; index < pipeline.length; index++) {
      const options = pipeline[index];
      const data = this.getLayoutData(options);
      const opts = { ...this.presetOptions, ...options };

      emit(graph, new GraphLifeCycleEvent(GraphEvent.BEFORE_STAGE_LAYOUT, { options: opts, index }));
      const result = await this.stepLayout(data, opts, index);
      emit(graph, new GraphLifeCycleEvent(GraphEvent.AFTER_STAGE_LAYOUT, { options: opts, index }));

      if (!options.animation) {
        this.updateElementPosition(result, false);
      }
    }
    emit(graph, new GraphLifeCycleEvent(GraphEvent.AFTER_LAYOUT, { type: 'post' }));
    this.transformDataAfterLayout('post');
  }

  private transformDataAfterLayout(type: 'pre' | 'post', data?: DrawData) {
    const transforms = this.context.transform.getTransformInstance();
    // @ts-expect-error skip type check
    Object.values(transforms).forEach((transform) => transform.afterLayout(type, data));
  }

  /**
   * <zh/> 模拟布局
   *
   * <en/> Simulate layout
   * @param options - <zh/> 布局配置项 | <en/> Layout options
   * @returns <zh/> 模拟布局结果 | <en/> Simulated layout result
   */
  public async simulate(options: LayoutOptions | undefined = this.options): Promise<GraphData> {
    if (!options) return {};
    const pipeline = Array.isArray(options) ? options : [options];

    let simulation: GraphData = {};

    for (let index = 0; index < pipeline.length; index++) {
      const options = pipeline[index];

      const data = this.getLayoutData(options);
      const result = await this.stepLayout(data, { ...this.presetOptions, ...options, animation: false }, index);

      simulation = result;
    }

    return simulation;
  }

  public async stepLayout(data: GraphData, options: STDLayoutOptions, index: number): Promise<GraphData> {
    if (isTreeLayout(options)) return await this.treeLayout(data, options, index);
    return await this.graphLayout(data, options, index);
  }

  private async graphLayout(data: GraphData, options: STDLayoutOptions, index: number): Promise<GraphData> {
    const { animation, iterations = 300 } = options;

    const layout = this.initGraphLayout(options);
    if (!layout) return {};

    this.instances[index] = layout;
    this.instance = layout;

    if (isLayoutWithIterations(layout)) {
      // 有动画，基于布局迭代 tick 更新位置 / Update position based on layout iteration tick
      if (animation) {
        return await layout.execute(data, {
          animate: true,
          maxIteration: iterations,
          onTick: (tickData: GraphData) => this.updateElementPosition(tickData, false),
        });
      }

      // 无动画，直接返回终态位置 / No animation, return final position directly
      layout.execute(data);
      layout.stop();
      return layout.tick(iterations);
    }

    // 无迭代的布局，直接返回终态位置 / Layout without iteration, return final position directly
    const layoutResult = await layout.execute(data);

    if (animation) {
      const animationResult = this.updateElementPosition(layoutResult, animation);
      await animationResult?.finished;
    }
    return layoutResult;
  }

  private async treeLayout(data: GraphData, options: STDLayoutOptions, index: number): Promise<GraphData> {
    const { type, animation } = options;
    // @ts-expect-error @antv/hierarchy 布局格式与 @antv/layout 不一致，其导出的是一个方法，而非 class
    // The layout format of @antv/hierarchy is inconsistent with @antv/layout, it exports a method instead of a class
    const layout = getExtension('layout', type) as (tree: TreeData, options: STDLayoutOptions) => TreeData;
    if (!layout) return {};

    const { nodes = [], edges = [] } = data;

    const model = new Graphlib({
      nodes: nodes.map((node) => ({ id: idOf(node), data: node.data || {} })),
      edges: edges.map((edge) => ({ id: idOf(edge), source: edge.source, target: edge.target, data: edge.data || {} })),
    });

    createTreeStructure(model);

    const layoutPreset: GraphData = { nodes: [], edges: [] };
    const layoutResult: GraphData = { nodes: [], edges: [] };

    const roots = model.getRoots(TREE_KEY) as unknown as TreeData[];
    roots.forEach((root) => {
      dfs(
        root,
        (node) => {
          node.children = model.getSuccessors(node.id) as TreeData[];
        },
        (node) => model.getSuccessors(node.id) as TreeData[],
        'TB',
      );

      const result = layout(root, options);
      const { x: rx, y: ry, z: rz = 0 } = result;
      // 将布局结果转化为 LayoutMapping 格式 / Convert the layout result to LayoutMapping format
      dfs(
        result,
        (node) => {
          const { id, x, y, z = 0 } = node;
          layoutPreset.nodes!.push({ id, style: { x: rx, y: ry, z: rz } });
          layoutResult.nodes!.push({ id, style: { x, y, z } });
        },
        (node) => node.children,
        'TB',
      );
    });

    const offset = this.inferTreeLayoutOffset(layoutResult);
    applyTreeLayoutOffset(layoutResult, offset);

    if (animation) {
      // 先将所有节点移动到根节点位置 / Move all nodes to the root node position first
      applyTreeLayoutOffset(layoutPreset, offset);
      this.updateElementPosition(layoutPreset, false);

      const animationResult = this.updateElementPosition(layoutResult, animation);
      await animationResult?.finished;
    }

    return layoutResult;
  }

  private inferTreeLayoutOffset(data: GraphData) {
    let [minX, maxX] = [Infinity, -Infinity];
    let [minY, maxY] = [Infinity, -Infinity];

    data.nodes?.forEach((node) => {
      const { x = 0, y = 0 } = node.style || {};
      minX = Math.min(minX, x);
      maxX = Math.max(maxX, x);
      minY = Math.min(minY, y);
      maxY = Math.max(maxY, y);
    });

    const { canvas } = this.context;
    const canvasSize = canvas.getSize();
    const [x1, y1] = canvas.getCanvasByViewport([0, 0]);
    const [x2, y2] = canvas.getCanvasByViewport(canvasSize);

    if (minX >= x1 && maxX <= x2 && minY >= y1 && maxY <= y2) return [0, 0] as [number, number];

    const cx = (x1 + x2) / 2;
    const cy = (y1 + y2) / 2;

    return [cx - (minX + maxX) / 2, cy - (minY + maxY) / 2] as [number, number];
  }

  public stopLayout() {
    if (this.instance && isLayoutWithIterations(this.instance)) {
      this.instance.stop();
      this.instance = undefined;
    }

    if (this.animationResult) {
      this.animationResult.finish();
      this.animationResult = undefined;
    }
  }

  public getLayoutData(options: STDLayoutOptions): GraphData {
    const {
      nodeFilter = () => true,
      comboFilter = () => true,
      preLayout = false,
      isLayoutInvisibleNodes = false,
    } = options;
    const { nodes, edges, combos } = this.context.model.getData();

    const { element, model } = this.context;
    const getElement = (id: ID) => element!.getElement(id);

    const filterFn = preLayout
      ? (node: NodeData) => {
          if (!isLayoutInvisibleNodes) {
            if (node.style?.visibility === 'hidden') return false;
            if (model.getAncestorsData(node.id, TREE_KEY).some(isCollapsed)) return false;
            if (model.getAncestorsData(node.id, COMBO_KEY).some(isCollapsed)) return false;
          }
          return nodeFilter(node);
        }
      : (node: NodeData) => {
          const id = idOf(node);
          const element = getElement(id);
          if (!element) return false;
          if (isToBeDestroyed(element)) return false;
          return nodeFilter(node);
        };

    const nodesToLayout = nodes.filter(filterFn);
    const combosToLayout = combos.filter(comboFilter);

    const nodeLikeIdsMap = new Map<ID, NodeData>(nodesToLayout.map((node) => [idOf(node), node]));
    combosToLayout.forEach((combo) => nodeLikeIdsMap.set(idOf(combo), combo));

    const edgesToLayout = edges.filter(({ source, target }) => {
      return nodeLikeIdsMap.has(source) && nodeLikeIdsMap.has(target);
    });

    return {
      nodes: nodesToLayout,
      edges: edgesToLayout,
      combos: combosToLayout,
    };
  }

  /**
   * <zh/> 创建布局实例
   *
   * <en/> Create layout instance
   * @param options - <zh/> 布局配置项 | <en/> Layout options
   * @returns <zh/> 布局对象 | <en/> Layout object
   */
  private initGraphLayout(options: STDLayoutOptions) {
    const { element, viewport } = this.context;
    const { type, animation, iterations, ...restOptions } = options;

    const [width, height] = viewport!.getCanvasSize();
    const center = [width / 2, height / 2];

    const nodeSize: number | ((node: NodeData) => number) =
      (options?.nodeSize as number) ??
      ((node) => {
        const nodeElement = element?.getElement(node.id);
        if (nodeElement) return nodeElement.attributes.size;
        return element?.getElementComputedStyle('node', node).size;
      });

    const Ctor = getExtension('layout', type);
    if (!Ctor) return print.warn(`The layout of ${type} is not registered.`);

    const STDCtor =
      Object.getPrototypeOf(Ctor.prototype) === BaseLayout.prototype
        ? Ctor
        : isLegacyAntVLayout(Ctor)
          ? legacyLayoutAdapter(Ctor, this.context)
          : layoutAdapter(Ctor as new (options?: Record<string, unknown>) => AntVLayout, this.context);

    const layout = new STDCtor(this.context);

    const config = { nodeSize, width, height, center };

    switch (layout.id) {
      case 'd3-force':
      case 'd3-force-3d':
        Object.assign(config, {
          center: { x: width / 2, y: height / 2, z: 0 },
        });
        break;
      default:
        break;
    }

    deepMix(layout.options, config, restOptions);
    return layout as unknown as BaseLayout;
  }

  private updateElementPosition(layoutResult: GraphData, animation: boolean) {
    const { model, element } = this.context;
    if (!element) return null;
    model.updateData(layoutResult);

    return element.draw({ animation, silence: true });
  }

  public destroy() {
    this.stopLayout();
    // @ts-expect-error force delete
    this.context = {};
    this.instance = undefined;
    this.instances = [];
    this.animationResult = undefined;
  }
}

/**
 * <zh/> 对树形布局结果应用偏移
 *
 * <en/> Apply offset to tree layout result
 * @param data - <zh/> 布局数据 | <en/> Layout data
 * @param offset - <zh/> 偏移量 | <en/> Offset
 */
const applyTreeLayoutOffset = (data: GraphData, offset: [number, number]) => {
  const [ox, oy] = offset;
  data.nodes?.forEach((node) => {
    if (node.style) {
      const { x = 0, y = 0 } = node.style;
      node.style.x = x + ox;
      node.style.y = y + oy;
    } else {
      node.style = { x: ox, y: oy };
    }
  });
};
