<script lang="ts" setup>
import type {
	IWorkflow,
	XYPosition,
	ConnectStartEvent,
	CanvasNodeMoveEvent,
	IWorkflowNode
} from '../Interface'
import type {
	NodeMouseEvent,
	Connection,
	NodeDragEvent,
	CoordinateExtent,
	CoordinateExtentRange,
	ValidConnectionFunc,
	ViewportTransform
} from '@vue-flow/core'

import { ref, computed, toRef } from 'vue'
import { VueFlow, useVueFlow, MarkerType } from '@vue-flow/core'
import { MiniMap } from '@vue-flow/minimap'
import { useThrottleFn } from '@vueuse/core'

import CanvasNode from './elements/nodes/CanvasNode.vue'
import CanvasEdge from './elements/edges/CanvasEdge.vue'
import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue'
import CanvasBackground from './elements/background/CanvasBackground.vue'
import CanvasControlBar from './elements/control-bar/CanvasControlBar.vue'
import { provideVueFlowContext } from '../hooks/useVueFlowContext'
import { getEdgeExecutionStatus } from '../utils/execution-status'

defineOptions({
	name: 'workflow-canvas',
	inheritAttrs: false
})

type CreateNodeSource = { type: string } | string

const emit = defineEmits<{
	'update:node:size': [id: string, size: { width: number; height: number }]
	'update:node:position': [id: string, position: XYPosition]
	'update:nodes:position': [events: CanvasNodeMoveEvent[]]
	'update:node:activated': [id: string, event?: MouseEvent]
	'update:node:deactivated': [id: string]
	'update:node:enabled': [id: string]
	'update:node:selected': [id?: string]
	'update:node:name': [id: string]
	'update:node:parameters': [id: string, parameters: Record<string, unknown>]
	'update:node:inputs': [id: string]
	'update:node:outputs': [id: string]
	'update:node:attrs': [id: string, attrs: Record<string, unknown>]
	'update:logs-open': [open?: boolean]
	'update:logs:input-open': [open?: boolean]
	'update:logs:output-open': [open?: boolean]
	'update:has-range-selection': [isActive: boolean]
	'click:node': [id: string, position: XYPosition]
	'dblclick:node': [id: string, position: XYPosition]
	'click:node:add': [
		payload: { nodeId: string; handle: string; position: XYPosition; event?: MouseEvent }
	]
	'initialized:nodes': []
	'run:node': [id: string]
	'copy:production:url': [id: string]
	'copy:test:url': [id: string]
	'delete:node': [id: string]
	'replace:node': [id: string]
	'create:node': [source: CreateNodeSource]
	'create:sticky': []
	'delete:nodes': [ids: string[]]
	'update:nodes:enabled': [ids: string[]]
	'copy:nodes': [ids: string[]]
	'duplicate:nodes': [ids: string[]]
	'cut:nodes': [ids: string[]]
	'drag-and-drop': [position: XYPosition, event: DragEvent]
	dragover: [event: DragEvent]
	dragleave: [event: DragEvent]
	'delete:connection': [connection: Connection]
	'create:connection:start': [handle: ConnectStartEvent]
	'create:connection': [connection: Connection]
	'create:connection:end': [connection: Connection, event?: MouseEvent]
	'create:connection:cancelled': [
		payload: {
			handle: ConnectStartEvent
			position: XYPosition
			event?: MouseEvent
			parentId?: string
		}
	]
	/**
	 * 连线节点添加
	 */
	'click:connection:add': [connection: Connection, parentId?: string]
	/**
	 * 视图切换
	 */
	'viewport:change': [viewport: ViewportTransform]
}>()

const props = withDefaults(
	defineProps<{
		id?: string
		nodes: IWorkflow['nodes']
		edges: IWorkflow['edges']
		hideChildNode?: boolean
		readOnly?: boolean
		nodeMap: Record<string, any>
		showControlBar?: boolean
		autoFit?: boolean
		translateExtent?: CoordinateExtent
		nodeExtent?: CoordinateExtent | CoordinateExtentRange
		defaultViewport?: ViewportTransform
	}>(),
	{
		id: 'canvas',
		readOnly: false,
		nodes: () => [],
		edges: () => [],
		showControlBar: true,
		autoFit: true
	}
)

const getNodes = computed(() =>
	props.hideChildNode ? props.nodes.filter((node) => !node?.parentId) : props.nodes
)

const getEdges = computed(() =>
	props.edges
		.filter((edge) => {
			const { source, target } = edge
			return getNodes.value.find((node) => node.id === source || node.id === target)
		})
		.map((edge) => {
			const sourceNode = nodeById.value[edge.source]
			const targetNode = nodeById.value[edge.target]
			const sourceParentId = sourceNode?.parentId || ''
			const targetParentId = targetNode?.parentId || ''
			const executionStatus = getEdgeExecutionStatus(sourceNode, targetNode)

			if (sourceParentId && sourceParentId !== targetParentId) {
				return {
					...edge,
					source: sourceParentId,
					data: {
						...(edge.data || {}),
						executionStatus
					}
				}
			}

			if (targetParentId && targetParentId !== sourceParentId) {
				return {
					...edge,
					target: targetParentId,
					data: {
						...(edge.data || {}),
						executionStatus
					}
				}
			}

			return {
				...edge,
				data: {
					...(edge.data || {}),
					executionStatus
				}
			}
		})
)

const defaultViewport = computed(() => props.defaultViewport || { x: 0, y: 0, zoom: 1 })
const projectExtent = computed<CoordinateExtent | undefined>(() => {
	return Array.isArray(props.translateExtent) ? props.translateExtent : undefined
})
const isValidConnection: ValidConnectionFunc = (connection) => {
	if (!connection.source || !connection.target) {
		return true
	}

	return connection.source !== connection.target
}

const clampPositionToExtent = (position: XYPosition, extent?: CoordinateExtent): XYPosition => {
	if (!extent) {
		return position
	}

	const [[minX, minY], [maxX, maxY]] = extent

	return {
		x: Math.min(Math.max(position.x, minX), maxX),
		y: Math.min(Math.max(position.y, minY), maxY)
	}
}

const showMinimap = ref(false)
const vueFlow = useVueFlow(props.id)

const {
	viewport,
	viewportRef,
	project,
	zoomIn,
	zoomOut,
	fitView,
	setViewport,
	zoomTo,
	onNodeMouseEnter,
	onNodeMouseLeave,
	onEdgeMouseEnter,
	onEdgeMouseLeave,
	onEdgeMouseMove
} = vueFlow

const nodeById = computed((): Record<string, IWorkflowNode> => {
	return props.nodes.reduce<Record<string, IWorkflowNode>>((acc, node) => {
		acc[node.id] = node
		return acc
	}, {})
})

/**
 * Edge and Nodes Hovering
 */

const edgesHoveredById = ref<Record<string, boolean>>({})
const edgesBringToFrontById = ref<Record<string, boolean>>({})

onEdgeMouseEnter(({ edge }) => {
	edgesBringToFrontById.value = { [edge.id]: true }
	edgesHoveredById.value = { [edge.id]: true }
})

onEdgeMouseMove(
	useThrottleFn(({ edge, event }) => {
		const type = edge.data?.source?.type
		if (type !== 'ai_tool') {
			return
		}

		if (!edge.data?.maxConnections || edge.data.maxConnections > 1) {
			const projectedPosition = getProjectedPosition(event)
			const yDiff = projectedPosition.y - edge.targetY
			if (yDiff < 4 * 16) {
				edgesBringToFrontById.value = { [edge.id]: false }
			} else {
				edgesBringToFrontById.value = { [edge.id]: true }
			}
		}
	}, 100)
)

onEdgeMouseLeave(({ edge }) => {
	edgesBringToFrontById.value = { [edge.id]: false }
	edgesHoveredById.value = { [edge.id]: false }
})

function onUpdateEdgeLabelHovered(id: string, hovered: boolean) {
	edgesBringToFrontById.value = { [id]: true }
	edgesHoveredById.value[id] = hovered
}

const nodesHoveredById = ref<Record<string, boolean>>({})

onNodeMouseEnter(({ node }) => {
	nodesHoveredById.value = { [node.id]: true }
})

onNodeMouseLeave(({ node }) => {
	nodesHoveredById.value = { [node.id]: false }
})

/**
 * Returns the position of a mouse or touch event
 */
const getMousePosition = (event: MouseEvent | TouchEvent): XYPosition => {
	const x = (event && 'clientX' in event ? event.clientX : event?.touches?.[0]?.clientX) ?? 0
	const y = (event && 'clientY' in event ? event.clientY : event?.touches?.[0]?.clientY) ?? 0

	return { x, y }
}

function getProjectedPosition(event?: MouseEvent | TouchEvent) {
	const bounds = viewportRef.value?.getBoundingClientRect() ?? { left: 0, top: 0 }
	const { x, y } = event ? getMousePosition(event) : { x: 0, y: 0 }

	return clampPositionToExtent(
		project({
			x: x - bounds.left,
			y: y - bounds.top
		}),
		projectExtent.value
	)
}

/**
 * Events
 */

const onNodeClick = ({ node, event }: NodeMouseEvent) => {
	emit('click:node', node.id, getProjectedPosition(event))
}

const onNodeDoubleClick = ({ node, event }: NodeMouseEvent) => {
	emit('dblclick:node', node.id, getProjectedPosition(event))
}

function onDrop(event: DragEvent) {
	const position = getProjectedPosition(event)
	emit('drag-and-drop', position, event)
}

function onDragOver(event: DragEvent) {
	emit('dragover', event)
}

function onDragLeave(event: DragEvent) {
	emit('dragleave', event)
}

const onZoomIn = () => {
	zoomIn()
}

const onZoomOut = () => {
	zoomOut()
}

const onZoomToFit = () => {
	fitView()
}

const onResetZoom = () => {
	zoomTo(1)
}
const onAddNode = (value: CreateNodeSource) => {
	emit('create:node', value)
}

const onToggleMinimap = () => {
	showMinimap.value = !showMinimap.value
}

function onUpdateNodesPosition(events: CanvasNodeMoveEvent[]) {
	emit('update:nodes:position', events)
}

function onUpdateNodePosition(id: string, position: XYPosition) {
	emit('update:node:position', id, position)
	emit('update:nodes:position', [{ id, position }])
}

function onNodeDragStop(event: NodeDragEvent) {
	onUpdateNodesPosition(event.nodes.map(({ id, position }) => ({ id, position })))
}

function onUpdateNodeAttrs(id: string, attrs: Record<string, unknown>) {
	emit('update:node:attrs', id, attrs)
}

function onViewportChange(viewport: ViewportTransform) {
	emit('viewport:change', viewport)
}

/**
 * Connections / Edges
 */

const connectionCreated = ref(false)
const connectingHandle = ref<ConnectStartEvent>()
const connectedHandle = ref<Connection>()

function onConnectStart(handle: ConnectStartEvent) {
	emit('create:connection:start', handle)
	connectingHandle.value = handle
	connectionCreated.value = false
}

function onConnect(connection: Connection) {
	emit('create:connection', connection)

	connectedHandle.value = connection
	connectionCreated.value = true
}

function onConnectEnd(event?: MouseEvent) {
	if (connectedHandle.value) {
		emit('create:connection:end', connectedHandle.value, event)
	} else if (connectingHandle.value) {
		emit('create:connection:cancelled', {
			handle: connectingHandle.value,
			position: getProjectedPosition(event),
			event
		})
	}

	connectedHandle.value = undefined
	connectingHandle.value = undefined
}

function onDeleteConnection(connection: Connection) {
	emit('delete:connection', connection)
}

function onClickConnectionAdd(connection: Connection) {
	emit('click:connection:add', connection)
}

function onDeleteNode(id: string) {
	emit('delete:node', id)
}

function onRunNode(id: string) {
	emit('run:node', id)
}

let loaded = false
async function onNodesInitialized() {
	if (!loaded) {
		if (props.readOnly) {
			await fitView({
				maxZoom: 1
			})
		} else if (props.defaultViewport) {
			await setViewport(props.defaultViewport)
		} else if (props.autoFit) {
			await fitView({
				maxZoom: 1
			})
		} else {
			await setViewport(defaultViewport.value)
		}
		loaded = true
		emit('initialized:nodes')
	}
}

provideVueFlowContext({
	id: toRef(props, 'id'),
	nodes: toRef(props, 'nodes'),
	edges: toRef(props, 'edges'),
	vueFlow,
	nodeMap: toRef(props, 'nodeMap'),
	connectingHandle
})

defineExpose({
	vueFlow
})
</script>

<template>
	<VueFlow
		:id="id"
		:nodes="getNodes"
		:edges="getEdges"
		:default-viewport="defaultViewport"
		:is-valid-connection="isValidConnection"
		:translate-extent="translateExtent"
		:node-extent="nodeExtent"
		:connection-line-options="{ markerEnd: MarkerType.ArrowClosed }"
		:connection-radius="60"
		snap-to-grid
		:snap-grid="[16, 16]"
		@node-click="onNodeClick"
		@node-double-click="onNodeDoubleClick"
		@node-drag-stop="onNodeDragStop"
		@drop="onDrop"
		@dragover="onDragOver"
		@dragleave="onDragLeave"
		@connect="onConnect"
		@connect-start="onConnectStart"
		@connect-end="onConnectEnd"
		@nodes-initialized="onNodesInitialized"
		@viewport-change="onViewportChange"
		v-bind="$attrs"
	>
		<template #node-canvas-node="nodeProps">
			<slot name="node" v-bind="{ nodeProps }">
				<CanvasNode
					v-bind="nodeProps"
					:node="nodeById[nodeProps.id]!"
					:read-only="readOnly"
					:hovered="nodesHoveredById[nodeProps.id]"
					@click:node="(id, position) => emit('click:node', id, position)"
					@dblclick:node="(id, position) => emit('dblclick:node', id, position)"
					@move="onUpdateNodePosition"
					@update="onUpdateNodeAttrs"
					@add-node="
						emit('click:node:add', {
							nodeId: $event.nodeId,
							handle: $event.handleId,
							position: getProjectedPosition($event.event),
							event: $event.event
						})
					"
					@add-inner-node="
						emit('click:node:add', {
							nodeId: nodeProps.id,
							handle: 'inner',
							position: nodeProps.position,
							event: undefined
						})
					"
					@add-inner-edge="emit('create:connection:end', $event)"
					@create-connection-cancelled="
						(p) => emit('create:connection:cancelled', { ...p, parentId: nodeProps.id })
					"
					@delete="onDeleteNode"
					@run="onRunNode"
					@delete:connection="onDeleteConnection"
					@update:nodes:position="onUpdateNodesPosition"
					@edge:add:click="onClickConnectionAdd"
					@loop:child:click:node="(id, position) => emit('click:node', id, position)"
					@loop:child:dblclick:node="(id, position) => emit('dblclick:node', id, position)"
					@loop:child:click:node:add="emit('click:node:add', $event)"
					@loop:child:create:connection:end="
						(connection, event) => emit('create:connection:end', connection, event)
					"
					@loop:child:update:node:attrs="(id, attrs) => emit('update:node:attrs', id, attrs)"
					@loop:child:update:nodes:position="emit('update:nodes:position', $event)"
					@loop:child:create:connection:cancelled="
						(p) => emit('create:connection:cancelled', { ...p, parentId: nodeProps.id })
					"
					@loop:child:run:node="emit('run:node', $event)"
					@loop:child:delete:node="emit('delete:node', $event)"
					@loop:child:delete:connection="emit('delete:connection', $event)"
					@loop:child:click:connection:add="emit('click:connection:add', $event, nodeProps.id)"
				/>
			</slot>
		</template>

		<template #edge-canvas-edge="edgeProps">
			<slot name="edge" v-bind="{ edgeProps }">
				<CanvasEdge
					v-bind="edgeProps"
					:read-only="readOnly"
					:hovered="edgesHoveredById[edgeProps.id]"
					marker-end="url(#custom-arrow-head-marker)"
					@add="onClickConnectionAdd"
					@delete="onDeleteConnection"
					@update:label:hovered="onUpdateEdgeLabelHovered(edgeProps.id, $event)"
				/>
			</slot>
		</template>

		<template #background>
			<rect width="100%" height="100%" fill="#f0f0f0" />
		</template>

		<MiniMap
			v-show="showMinimap"
			:height="120"
			:width="180"
			:node-border-radius="16"
			class="bottom-40px! bg-#f5f5f5 border border-solid border-gray-300"
			position="bottom-left"
			pannable
			zoomable
		/>

		<CanvasControlBar
			v-if="showControlBar"
			@zoom-in="onZoomIn"
			@zoom-out="onZoomOut"
			@zoom-to-fit="onZoomToFit"
			@reset-zoom="onResetZoom"
			@add-node="onAddNode"
			@toggle-minimap="onToggleMinimap"
		/>

		<slot name="canvas-background" v-bind="{ viewport }">
			<CanvasBackground :viewport="viewport" :striped="readOnly" />
		</slot>

		<CanvasArrowHeadMarker id="custom-arrow-head-marker" />
	</VueFlow>
</template>

<style>
@import '@vue-flow/core/dist/style.css';
@import '@vue-flow/core/dist/theme-default.css';
</style>
