<script setup lang="ts">
import type { AgentDraftState, AgentOption } from '../../types';
import type {
  AgentStudioCanvasSize,
  AgentStudioConnectionView,
  AgentStudioNodeData,
  AgentStudioNodeView,
  AgentStudioViewport,
} from './types';

import {
  computed,
  h,
  nextTick,
  onBeforeUnmount,
  onMounted,
  ref,
  render as renderVueVNode,
  shallowRef,
  watch,
} from 'vue';

import { usePreferences } from '@easyflow/preferences';

import { Tinyflow } from '@tinyflow-ai/vue';

import { useAgentStudioLayout } from '../../composables/agent-studio/useAgentStudioLayout';
import { useAgentStudioModel } from '../../composables/agent-studio/useAgentStudioModel';
import AgentStudioBaseNode from './AgentStudioBaseNode.vue';
import AgentStudioCapabilityNode from './AgentStudioCapabilityNode.vue';
import AgentStudioEdgeLayer from './AgentStudioEdgeLayer.vue';

import '@tinyflow-ai/vue/dist/index.css';

const props = withDefaults(
  defineProps<{
    active?: boolean;
    knowledgeOptions?: AgentOption[];
    mcpOptions?: AgentOption[];
    pluginOptions?: AgentOption[];
    selectedNodeId: string;
    state: AgentDraftState;
    workflowOptions?: AgentOption[];
  }>(),
  {
    active: true,
    knowledgeOptions: () => [],
    mcpOptions: () => [],
    pluginOptions: () => [],
    workflowOptions: () => [],
  },
);

const emit = defineEmits<{
  select: [nodeId: string];
}>();

const { isDark } = usePreferences();
const { capture, layout } = useAgentStudioLayout(props.state);
const canvasRef = ref<HTMLElement>();
const canvasSize = ref<AgentStudioCanvasSize>();
const canvasModel = useAgentStudioModel(
  props.state,
  () => props.selectedNodeId,
  layout,
  () => canvasSize.value,
  () => props.knowledgeOptions || [],
  () => ({
    mcp: props.mcpOptions || [],
    plugin: props.pluginOptions || [],
    workflow: props.workflowOptions || [],
  }),
);
const liveNodes = shallowRef<AgentStudioNodeView[]>([]);
const liveViewport = shallowRef<AgentStudioViewport>({
  x: 250,
  y: 100,
  zoom: 1,
});
function buildFlowData() {
  const liveNodeMap = new Map(liveNodes.value.map((node) => [node.id, node]));
  return {
    edges: canvasModel.value.edges,
    nodes: canvasModel.value.nodes.map((node) => {
      const liveNode = liveNodeMap.get(node.id);
      const position = liveNode?.position || node.position;
      if (position === node.position) {
        return node;
      }
      return {
        ...node,
        position,
      };
    }),
    viewport: layout.viewport ? { ...layout.viewport } : undefined,
  };
}
let lastCapturedEdges: unknown;
let lastCapturedNodes: unknown;
let lastCapturedViewport: unknown;

const customNodes = computed(() => ({
  agentStudioBase: {
    presentation: 'plain',
    render: (parent: HTMLElement, node: any) => {
      renderVueNode(parent, AgentStudioBaseNode, node.data);
    },
    onUpdate: (parent: HTMLElement, node: any) => {
      renderVueNode(parent, AgentStudioBaseNode, node.data);
    },
    onDestroy: unmountVueNode,
  },
  agentStudioCapability: {
    presentation: 'plain',
    render: (parent: HTMLElement, node: any) => {
      renderVueNode(parent, AgentStudioCapabilityNode, node.data);
    },
    onUpdate: (parent: HTMLElement, node: any) => {
      renderVueNode(parent, AgentStudioCapabilityNode, node.data);
    },
    onDestroy: unmountVueNode,
  },
}));

const connections = computed<AgentStudioConnectionView[]>(() => {
  const nodes =
    liveNodes.value.length > 0
      ? liveNodes.value
      : (canvasModel.value.nodes as AgentStudioNodeView[]);
  const nodeMap = new Map(nodes.map((node) => [node.id, node]));
  return canvasModel.value.edges
    .map((edge) => {
      const source = nodeMap.get(edge.source);
      const target = nodeMap.get(edge.target);
      if (!source || !target) return undefined;
      return {
        active: false,
        id: edge.id,
        path: buildConnectionPath(source, target),
        sourceId: edge.source,
        targetId: edge.target,
      };
    })
    .filter(Boolean) as AgentStudioConnectionView[];
});

function renderVueNode(
  parent: HTMLElement,
  component: any,
  data: AgentStudioNodeData,
) {
  const current = (parent as any).__agentStudioNodeView as
    | undefined
    | {
        component: any;
        source: AgentStudioNodeData;
      };
  if (current && current.component === component) {
    if (current.source === data) {
      return;
    }
    current.source = data;
  }

  renderVueVNode(h(component, { data }), parent);
  (parent as any).__agentStudioNodeView = {
    component,
    source: data,
  };
}

function unmountVueNode(parent: HTMLElement) {
  renderVueVNode(null, parent);
  delete (parent as any).__agentStudioNodeView;
}

function getNodeSize(node: AgentStudioNodeView) {
  return {
    height: node.height || (node.data.kind === 'base' ? 104 : 78),
    width: node.width || (node.data.kind === 'base' ? 268 : 212),
  };
}

function buildConnectionPath(
  source: AgentStudioNodeView,
  target: AgentStudioNodeView,
) {
  const sourceSize = getNodeSize(source);
  const targetSize = getNodeSize(target);
  const sourceCenter = {
    x: source.position.x + sourceSize.width / 2,
    y: source.position.y + sourceSize.height / 2,
  };
  const targetCenter = {
    x: target.position.x + targetSize.width / 2,
    y: target.position.y + targetSize.height / 2,
  };
  const sourceOnLeft = targetCenter.x < sourceCenter.x;
  const start = {
    x: source.position.x + (sourceOnLeft ? 0 : sourceSize.width),
    y: sourceCenter.y,
  };
  const end = {
    x: target.position.x + (sourceOnLeft ? targetSize.width : 0),
    y: targetCenter.y,
  };
  const midX = (start.x + end.x) / 2;
  return [
    `M ${start.x} ${start.y}`,
    `L ${midX} ${start.y}`,
    `L ${midX} ${end.y}`,
    `L ${end.x} ${end.y}`,
  ].join(' ');
}

function handleDataChange(data: any) {
  if (!props.active) {
    return;
  }
  const edges = Array.isArray(data?.edges) ? data.edges : undefined;
  const nodes = Array.isArray(data?.nodes)
    ? (data.nodes as AgentStudioNodeView[])
    : undefined;
  const viewport = data?.viewport as AgentStudioViewport | undefined;
  if (
    edges === lastCapturedEdges &&
    nodes === lastCapturedNodes &&
    viewport === lastCapturedViewport
  ) {
    return;
  }
  lastCapturedEdges = edges;
  lastCapturedNodes = nodes;
  lastCapturedViewport = viewport;

  if (nodes) {
    liveNodes.value = nodes;
  }
  if (viewport) {
    liveViewport.value = viewport;
  }
  const selected = nodes?.find((node: any) => node.selected);
  if (selected?.id && selected.id !== props.selectedNodeId) {
    emit('select', selected.id);
  }
}

function handleDataCommit(data: any) {
  if (!props.active) {
    return;
  }
  capture({
    nodes: Array.isArray(data?.nodes)
      ? (data.nodes as AgentStudioNodeView[])
      : undefined,
    viewport: data?.viewport as AgentStudioViewport | undefined,
  });
}

function shallowEqual(left: object, right: object) {
  if (left === right) {
    return true;
  }
  const leftRecord = left as Record<string, unknown>;
  const rightRecord = right as Record<string, unknown>;
  const leftKeys = Object.keys(leftRecord);
  if (leftKeys.length !== Object.keys(rightRecord).length) {
    return false;
  }
  return leftKeys.every((key) => Object.is(leftRecord[key], rightRecord[key]));
}

function canvasModelsEqual(
  previous: typeof canvasModel.value,
  next: typeof canvasModel.value,
  comparePositions: boolean,
) {
  if (
    previous.nodes.length !== next.nodes.length ||
    previous.edges.length !== next.edges.length
  ) {
    return false;
  }
  const nodesEqual = previous.nodes.every((node, index) => {
    const nextNode = next.nodes[index];
    return (
      node.id === nextNode?.id &&
      node.type === nextNode.type &&
      node.width === nextNode.width &&
      node.height === nextNode.height &&
      shallowEqual(node.data, nextNode.data) &&
      (!comparePositions ||
        (node.position.x === nextNode.position.x &&
          node.position.y === nextNode.position.y))
    );
  });
  if (!nodesEqual) {
    return false;
  }
  return previous.edges.every((edge, index) => {
    const nextEdge = next.edges[index];
    return nextEdge ? shallowEqual(edge, nextEdge) : false;
  });
}

const stableFlowData = shallowRef(buildFlowData());
let lastCanvasModel = canvasModel.value;
let resizeObserver: ResizeObserver | undefined;

function syncFlowData(nextModel: typeof canvasModel.value) {
  const comparePositions = liveNodes.value.length === 0;
  if (canvasModelsEqual(lastCanvasModel, nextModel, comparePositions)) {
    return;
  }
  lastCanvasModel = nextModel;
  stableFlowData.value = buildFlowData();
}

watch(canvasModel, syncFlowData);

function updateCanvasSize() {
  const rect = canvasRef.value?.getBoundingClientRect();
  if (!rect) return;
  const next = {
    height: rect.height,
    width: rect.width,
  };
  if (
    canvasSize.value?.height === next.height &&
    canvasSize.value.width === next.width
  ) {
    return;
  }
  canvasSize.value = next;
}

function stopCanvasObservation() {
  resizeObserver?.disconnect();
  resizeObserver = undefined;
}

function startCanvasObservation() {
  stopCanvasObservation();
  if (!props.active) {
    return;
  }
  updateCanvasSize();
  if (typeof ResizeObserver === 'undefined' || !canvasRef.value) return;
  resizeObserver = new ResizeObserver(updateCanvasSize);
  resizeObserver.observe(canvasRef.value);
}

onMounted(() => {
  startCanvasObservation();
});

onBeforeUnmount(() => {
  stopCanvasObservation();
});

watch(
  () => props.active,
  async (active) => {
    if (!active) {
      stopCanvasObservation();
      return;
    }
    await nextTick();
    startCanvasObservation();
  },
);
</script>

<template>
  <div ref="canvasRef" class="agent-studio-canvas">
    <Tinyflow
      class="agent-studio-canvas__flow"
      :data="stableFlowData"
      :theme="isDark ? 'dark' : 'light'"
      :custom-nodes="customNodes"
      :hide-bottom-dock="true"
      :hide-edge-panel="true"
      :hide-mini-map="true"
      :hide-node-handles="true"
      :hide-node-picker="true"
      :hide-node-setting="true"
      :hide-node-toolbar="true"
      :hide-edge-markers="true"
      :edge-animated="false"
      :edge-interaction-width="0"
      :nodes-draggable="true"
      :nodes-connectable="false"
      :elements-selectable="true"
      :drop-enabled="false"
      :on-data-change="handleDataChange"
      :on-data-commit="handleDataCommit"
    />
    <AgentStudioEdgeLayer :connections="connections" :viewport="liveViewport" />
  </div>
</template>

<style scoped>
.agent-studio-canvas {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

.agent-studio-canvas__flow {
  width: 100%;
  height: 100%;
}

.agent-studio-canvas :deep(.tinyflow),
.agent-studio-canvas :deep(.agentsflow) {
  width: 100%;
  height: 100% !important;
}

.agent-studio-canvas :deep(.svelte-flow__node) {
  z-index: 2 !important;
  border: 0;
  border-radius: 18px;
}

.agent-studio-canvas :deep(.svelte-flow__node::after) {
  display: none;
}

.agent-studio-canvas :deep(.svelte-flow__node:hover),
.agent-studio-canvas :deep(.svelte-flow__node.selectable.selected) {
  border: 0;
  box-shadow: none;
}

.agent-studio-canvas :deep(.svelte-flow__handle),
.agent-studio-canvas :deep(.svelte-flow__minimap),
.agent-studio-canvas :deep(.tf-left-dock),
.agent-studio-canvas :deep(.tf-top-dock),
.agent-studio-canvas :deep(.tf-bottom-dock),
.agent-studio-canvas :deep(.tf-node-toolbar) {
  display: none !important;
}

.agent-studio-canvas :deep(.tf-flow-line-path) {
  stroke: var(--el-border-color);
  stroke: color-mix(
    in srgb,
    var(--el-color-primary) 30%,
    var(--el-border-color)
  );
  stroke-width: 1.5;
}
</style>
