<script setup lang="ts">
import { h } from 'vue';

import { tryOnUnmounted, useParentElement } from '@vueuse/core';
import { VxeUI } from 'vxe-pc-ui';

import { Button, notification } from 'ant-design-vue';
import { ArrowDownOutlined, ArrowRightOutlined, CloseOutlined, FullscreenExitOutlined, MenuOutlined, SearchOutlined, ToolOutlined } from '@ant-design/icons-vue';

import type { CallbackArgs } from '@logicflow/core';
import { ProximityConnect, InsertNodeInPolyline } from '@logicflow/extension';
import { Dagre } from '@logicflow/layout';
import VLogicFlowInit, { type LogicFlowInstance, type LogicFlowOptions, VLogicFlow, type VLogicFlowInstance } from '@/libs/logic-flow';

import { FlowNodeComponent, type FlowNodeProps, FlowNodeView, FlowNodeViewModel, Node } from './nodes';
import { type FlowRequestData, fromFlowRequestData, type Gather, toFlowRequestData } from './tool';
import RegisterPanel from './panel/RegisterPanel.vue';
import ReportPanel from './panel/ReportPanel.vue';

import { withResolvers } from '@/tools/promise';
import StartPanel from '@/pages/aio/flow-config/panel/StartPanel.vue';

defineOptions({
  name: 'FlowConfig',
});

const requestData = defineModel<FlowRequestData>('requestData', {
  default: () => ({}),
});

const options = reactive<LogicFlowOptions>({
  grid: { visible: true, size: 20, type: 'mesh', config: { color: '#ababab', thickness: 1 } },
  snapToGrid: true,
  snapline: false,
  textEdit: false,
  plugins: [Dagre, ProximityConnect, InsertNodeInPolyline],
  pluginsOptions: {
    proximityConnect: {
      enable: true, // 插件是否启用
      distance: 100, // 渐进连线阈值
      reverseDirection: true, // 连线方向
    },
  },
});
const nodes = ref<string[]>([]);
const nodeGroup = shallowRef<FlowNodeProps[][]>([]);

const parentRef = useParentElement();
const el = computed(() => parentRef.value?.querySelector(`.v-logic-flow`));

const scope = effectScope();
// 新加的 start
let instance!: VLogicFlowInstance;
let isUnmounted = false;
const hasInstance = () => !isUnmounted && !!instance && !!instance.lf;
// end
let oldValue: FlowRequestData;
const maxPanelHeight = ref(0);
const init = (lf: LogicFlowInstance): void => {
  setTimeout(() => {
    maxPanelHeight.value = lf.container.getBoundingClientRect().height - 80;
    openPanel.value = maxPanelHeight.value > 0;
  }, 100);
  instance = VLogicFlowInit(lf, {
    register: [{ category: 'node', type: 'FlowNode', view: FlowNodeView, model: FlowNodeViewModel }],
  });

  scope.run(() => {
    watchPostEffect(() => {
      const value = requestData.value;
      console.log('[AioFlowConfig] 接收 request-data 数据', value);
      if (!oldValue || oldValue?.timestamp !== value?.timestamp) update(value);
    });
  });

  instance.listener('connection:not-allowed', (event) => {
    notification.warning({
      key: 'connection:not-allowed',
      message: '无法连接节点',
      description: event.msg,
      top: '12px',
      getContainer: () => (el.value as HTMLElement) ?? document.body,
    });
  });
  // @ts-ignore
  instance.listener('node:click:300', (event) => openNodeConfig(event.data));
  instance.listener('node:properties-change', (event) => {
    if (event.keys.includes('requestData')) {
      // 更新面板属性
      for (const group of nodeGroup.value) {
        const node = group.find((node) => node.id === event.id);
        if (node) {
          Object.assign(node.properties!, event.properties);
          break;
        }
      }
    }
  });
  // @ts-ignore
  instance.listener('node:add,node:dnd-add', (event: CallbackArgs<'data'>) => {
    dragPanelNodeId.value = '';
    // 判断是否存在节点
    const length = instance.lf.getGraphRawData().nodes.filter((node) => Node.formatText(node) === Node.formatText(event.data)).length;
    if (length > 1) {
      notification.warning({
        key: 'node:add',
        message: '无法添加节点',
        description: `已存在 [${Node.formatText(event.data)}] 节点`,
        top: '12px',
        getContainer: () => (el.value as HTMLElement) ?? document.body,
      });
      instance.lf.deleteNode(event.data.id);
      return;
    }
    if (!nodes.value.includes(event.data.id)) nodes.value.push(event.data.id);
  });
  instance.listener('node:delete', (event) => {
    if ([Node.ID_Start, Node.ID_End].includes(event.data.id)) return instance.lf.addNode(event.data);

    const index = nodes.value.indexOf(event.data.id);
    if (index > -1) nodes.value.splice(index, 1);
  });
  // @ts-ignore
  /*instance.listener('edge:proximity-connect', () => {
    updateLayout();
  });*/
  // @ts-ignore
  instance.listener('node:insert', (event: CallbackArgs<'data'>) => {
    notification.success({
      key: 'connection:node:insert',
      message: '插入节点成功',
      description: () =>
        h(
          Button,
          {
            type: 'primary',
            size: 'small',
            onClick: () => {
              validate().catch();
              notification.close('connection:node:insert');
            },
          },
          { default: () => '检测连接' }
        ),
      top: '12px',
      getContainer: () => (el.value as HTMLElement) ?? document.body,
    });
  });
  // @ts-ignore
  instance.listener('connection:quick', (event: any) => {
    const add = (id: string) => {
      for (const group of nodeGroup.value) {
        const node = group.find((node) => node.id === id);
        if (node) return instance.lf.addNode(node);
      }
      return void 0;
    };
    const { sourceNodeId, targetNodeId } = event;
    const source = instance.lf.getNodeModelById(sourceNodeId) ?? add(sourceNodeId);
    const target = instance.lf.getNodeModelById(targetNodeId) ?? add(targetNodeId);
    if (source && target) {
      const sourceEdge = instance.lf.getNodeOutgoingEdge(sourceNodeId);
      const targetEdge = instance.lf.getNodeIncomingEdge(targetNodeId);
      for (const edge of sourceEdge) instance.lf.deleteEdge(edge.id);
      for (const edge of targetEdge) instance.lf.deleteEdge(edge.id);
      instance.lf.addEdge({ sourceNodeId, targetNodeId });
      updateLayout();
    }
  });
};

const updateLayout = (dir?: 'LR' | 'TB' | 'center') => {
  if (!hasInstance()) return;
  if (dir === 'center') {
    instance.lf.fitView(100, 0);
    instance.lf.translateCenter();
  } else {
    const dagre = instance?.getDagre();
    dagre?.layout({
      // @ts-ignore
      rankdir: dir ?? dagre?.option?.['rankdir'],
      align: 'UL',
      ranker: 'longest-path',
      nodesep: 60,
      ranksep: 80,
      acyclicer: 'greedy',
      isDefaultAnchor: true,
    });

    if (instance.lf.getGraphRawData().nodes.length > 8) instance.lf.fitView(24, 24);
    else instance.lf.translateCenter();
  }
};

const openPanel = ref(false);
const dragPanelNodeId = ref('');
const togglePanel = (open?: boolean) => {
  open ??= !openPanel.value;
  if (open && !(maxPanelHeight.value > 0)) maxPanelHeight.value = instance.lf.container.getBoundingClientRect().height - 80;
  openPanel.value = open;
};
const startDragPanelNode = (node: any, event: MouseEvent) => {
  let el = event.target as HTMLElement;
  do {
    if (el.id === node.id) break;
    el = el.parentElement as HTMLElement;
  } while (el);

  if (el.classList.contains('disabled')) {
    /*notification.warning({
      key: 'node:add',
      message: '无法添加节点',
      description: `已存在 [${node.text}] 节点`,
      top: '12px',
      getContainer: () => (el.value as HTMLElement) ?? document.body,
    });*/
    return;
  }

  dragPanelNodeId.value = node.id;
  instance.lf.dnd.startDrag(node);
  window.addEventListener(
    'click',
    () => {
      dragPanelNodeId.value = '';
    },
    { once: true }
  );
};
const getPanelNodeDisabled = (node: FlowNodeProps) => nodes.value.includes(node.id!);

const openNodeConfig = (node: FlowNodeProps) => {
  let panel;
  switch (node.id) {
    case Node.ID_Start:
      panel = StartPanel;
      break;
    case Node.ID_Register:
      panel = RegisterPanel;
      break;
    case Node.ID_Report_TongueAndFace:
    case Node.ID_Report_Health:
    case Node.ID_Scheme_Health:
      panel = ReportPanel;
      break;
  }
  if (panel) {
    VxeUI.modal.open({
      id: `node:config`,
      title: `${Node.formatText(node)} 节点`,
      escClosable: true,
      zIndex: 9999,
      slots: {
        default: () =>
          h(panel, {
            id: node.id,
            requestData: (node.properties?.requestData as string[]) ?? [],
            'onUpdate:requestData'(value: any) {
              if (!hasInstance()) return;
              instance.lf.getNodeModelById(node.id!)?.setProperties({ requestData: value });
              VxeUI.modal.close(`node:config`);
            },
          }),
      },
    });
  }
};

const update = (data?: FlowRequestData) => {
  if (!hasInstance()) return;
  const { graph, group } = fromFlowRequestData(data ?? requestData.value);
  nodes.value = graph.nodes?.map((node) => node.id!) ?? [];
  nodeGroup.value = group;

  instance.lf.renderRawData(graph);
  updateLayout('TB');
  if (graph.nodes && graph.nodes.length > 2) setTimeout(() => updateLayout('center'), 100);
};
const validate = (tips = true) => {
  if (!hasInstance()) return Promise.reject(new Error('LogicFlow 已销毁'));
  const preNodeRules = {
    [Node.ID_Report_Pulse]: Node.ID_Analysis_Pulse,
    [Node.ID_Report_TongueAndFace]: Node.ID_Analysis_TongueAndFace,
    [Node.ID_Analysis_Health]: Node.ID_Analysis_TongueAndFace,
    [Node.ID_Report_Alcohol]: Node.ID_Analysis_Health,
    [Node.ID_Report_Health]: Node.ID_Analysis_Health,
    [Node.ID_Scheme_Health]: Node.ID_Analysis_Health,
  };
  const map = (node: any, level = 0, gather: Gather = []) => {
    const sourceNodeId = node.id;
    const directNodes = instance.lf.getNodeOutgoingNode(sourceNodeId);
    const edges = instance.lf.getNodeOutgoingEdge(sourceNodeId);
    for (const directNode of directNodes) {
      const targetNodeId = directNode.id;

      const rule = preNodeRules[targetNodeId as keyof typeof preNodeRules];
      (typeof rule === 'function'
        ? rule
        : (node: any, gather: Gather) => {
            if (rule && !gather.find((item) => item.targetNodeId === rule)) {
              throw { gather, node, message: `目标 [${Node.formatText(node)}] 节点上流路径：必须存在 [${Node.formatText(rule)}] 节点` };
            }
            return;
          })(directNode, gather);

      // @ts-ignore
      gather.push({ level, sourceNodeId, targetNodeId, edgeId: edges.find((edge) => edge.targetNodeId === directNode.id)?.id });
      if (node.id !== Node.ID_End) map(directNode, level + 1, gather);
    }
    if (gather.length) return gather;
    else throw { gather, message: `请连接 [${Node.formatText(node)}] 节点` };
  };

  const start = instance.lf.getNodeModelById(Node.ID_Start);

  let gather: Gather;
  const { promise, resolve, reject } = withResolvers<{ gather: Gather; data?: FlowRequestData; message?: string }>();
  try {
    gather = map(start).sort((g1, g2) => g1.level - g2.level || +(g1.targetNodeId === Node.ID_Back) - +(g2.targetNodeId === Node.ID_Back));

    const data: Record<string, any> = {
      [Node.ID_Start]: instance.lf.getNodeModelById(Node.ID_Start)?.getProperties().requestData,
    };
    for (const group of nodeGroup.value) for (const node of group) if (node.id) data[node.id] = node.properties?.requestData;
    oldValue = toFlowRequestData(gather, data);
    requestData.value = oldValue;
    resolve({ gather, data: oldValue });
    console.log('[AioFlowConfig] 更新 request-data 数据: ', oldValue);
  } catch (error: any) {
    if (tips) {
      notification.error({
        key: 'graph:validate',
        message: '检测连接错误',
        description: error.message,
        top: '12px',
        getContainer: () => (el.value as HTMLElement) ?? document.body,
      });
    }
    gather = error.gather;
    reject(error);
  }

  if (Array.isArray(gather) && gather.length) {
    const last = gather.at(-1);
    const notOpenEdgeId = last.targetNodeId === Node.ID_Back ? last.edgeId : void 0;
    for (const { edgeId } of gather) {
      instance.lf.setProperties(edgeId, { isAnimation: true });
      if (notOpenEdgeId === edgeId) instance.lf.closeEdgeAnimation(notOpenEdgeId);
      else instance.lf.openEdgeAnimation(edgeId);
    }
  } else {
    for (const edge of instance.lf.getGraphRawData().edges) {
      const edgeId = edge.id;
      instance.lf.closeEdgeAnimation(edgeId);
    }
  }

  return promise;
};

tryOnUnmounted(() => {
  // 新加的
  isUnmounted = true;
  openPanel.value = false;
  notification.destroy();
  try { scope.stop(); } catch {}
});

defineExpose({
  validate,
  update,
});
</script>

<template>
  <v-logic-flow :class="{ 'flow-notification-container': el }" @loaded="init($event)" :options="options">
    <template #top>
      <div class="top-wrapper">{{ dragPanelNodeId ? `拖拽 [节点] 到画布进行配置` : `单击 [节点] 进行编辑, 双击 [节点][边] 进行删除;` }}</div>
    </template>
    <template #panel>
      <a-dropdown class="panel-wrapper" :trigger="['click']" :open="openPanel">
        <a-button type="primary" size="large" shape="circle" @click.prevent="togglePanel()">
          <template #icon>
            <CloseOutlined v-if="openPanel"></CloseOutlined>
            <MenuOutlined v-else />
          </template>
        </a-button>
        <template #overlay>
          <a-card size="small" style="width: 370px; overflow-y: auto;" :style="{ maxHeight: maxPanelHeight + 'px' }">
            <div class="flex justify-between m-y-2" v-for="(group, g) in nodeGroup" :key="g">
              <FlowNodeComponent
                :class="{ selected: dragPanelNodeId === node.id, disabled: getPanelNodeDisabled(node) }"
                v-for="node in group"
                :key="node.id"
                :id="node.id"
                :text="node.text"
                v-bind="node.properties"
                @mousedown="startDragPanelNode(node, $event)"
              />
            </div>
            <a-space direction="vertical" class="tips-wrapper">
              <div>添加流程：从左侧拖入</div>
              <div>编辑流程：单击</div>
              <div>删除流程和连线：双击</div>
              <div>连接流程：从上流节点中拖拽锚点到下流节点以连接</div>
            </a-space>
          </a-card>
        </template>
      </a-dropdown>
    </template>
    <template #float>
      <a-float-button-group trigger="hover" type="primary" :style="{ right: '24px' }">
        <template #icon>
          <ToolOutlined />
        </template>
        <a-float-button tooltip="检测连接" @click="validate().catch()">
          <template #icon><SearchOutlined /></template>
        </a-float-button>
        <a-float-button tooltip="居中显示" @click="updateLayout('center')">
          <template #icon><FullscreenExitOutlined /></template>
        </a-float-button>
        <a-float-button tooltip="布局从左到右" @click="updateLayout('LR')">
          <template #icon><ArrowRightOutlined /></template>
        </a-float-button>
        <a-float-button tooltip="布局从上到下" @click="updateLayout('TB')">
          <template #icon><ArrowDownOutlined /></template>
        </a-float-button>
      </a-float-button-group>
    </template>
  </v-logic-flow>
</template>

<style scoped lang="scss">
.top-wrapper {
  position: absolute;
  top: 12px;
  left: max(10%, 300px);
  right: max(10%, 300px);
  text-align: center;
  color: #bbbbbb;
}
.panel-wrapper {
  position: absolute;
  top: 24px;
  left: 24px;
  z-index: 1;
}
.tips-wrapper {
  width: 100%;
  padding: 4px;
  color: rgba(0, 0, 0, 0.8);
  border: 1px #bbbbbb dashed;
}
.ant-float-btn-group {
  position: absolute !important;
  bottom: 24px;
}
.flow-notification-container {
  :deep(.ant-notification) {
    position: absolute !important;
  }
}
:deep(.node-container) {
  --width: 160px;

  &.selected {
    .text-container {
      border-style: dashed;
      border-color: #1a82ca;
    }
  }

  &.disabled {
    opacity: 0.5;
    cursor: no-drop;
  }
}
</style>
