<script setup lang="ts">
import { computed } from 'vue'
import { Position, type Connection } from '@vue-flow/core'

import CanvasHandle from '../handles/CanvasHandle.vue'
import NodeRenderer from './render-types/NodeRenderer.vue'
import CanvasNodeToolBar from './CanvasNodeToolBar.vue'

import type { NodeProps } from '@vue-flow/core'
import type {
	ConnectStartEvent,
	IWorkflowNode,
	IWorkflowEdge,
	XYPosition,
	CanvasNodeMoveEvent,
	CanvasConnectionPort,
	CanvasElementPortWithRenderData
} from '../../../Interface'
import { provideCanvasNodeContext } from '../../../hooks/useCanvasNodeContext'
import { useVueFlowContext } from '../../../hooks/useVueFlowContext'

type Props = NodeProps<IWorkflowNode> & {
	readOnly?: boolean
	hovered?: boolean
	node: IWorkflowNode
}

const props = defineProps<Props>()
const { nodeMap, edges } = useVueFlowContext()

const emit = defineEmits<{
	update: [id: string, parameters: Record<string, unknown>]
	move: [id: string, position: { x: number; y: number }]
	delete: [id: string]
	run: [id: string]
	'click:node': [id: string, position: XYPosition]
	'dblclick:node': [id: string, position: XYPosition]
	'add-node': [payload: { nodeId: string; handleId: string; event: MouseEvent }]
	'add-inner-node': []
	'add-inner-edge': [connection: Connection]
	'create-connection-cancelled': [
		payload: { handle: ConnectStartEvent; position: XYPosition; event?: MouseEvent }
	]
	'loop:child:click:node': [id: string, position: XYPosition]
	'loop:child:dblclick:node': [id: string, position: XYPosition]
	'loop:child:click:node:add': [
		payload: { nodeId: string; handle: string; position: XYPosition; event?: MouseEvent }
	]
	'loop:child:create:connection:end': [connection: Connection, event?: MouseEvent]
	'loop:child:update:node:attrs': [id: string, attrs: Record<string, unknown>]
	'loop:child:update:nodes:position': [events: CanvasNodeMoveEvent[]]
	'loop:child:create:connection:cancelled': [
		payload: { handle: ConnectStartEvent; position: XYPosition; event?: MouseEvent }
	]
	'loop:child:run:node': [id: string]
	'loop:child:delete:node': [id: string]
	'loop:child:delete:connection': [connection: Connection]
	'loop:child:click:connection:add': [connection: Connection]
}>()

/**
 * 处理节点
 */
const createEndpoint = (data: {
	port: CanvasConnectionPort
	index: number
	count: number
	connectionsCount: number
	offsetAxis: 'top' | 'left'
	position: Position
	type: 'source' | 'target'
}): CanvasElementPortWithRenderData => {
	const { port, index, count, connectionsCount, offsetAxis, position, type } = data

	return {
		...port,
		handleId: port?.id || props.node.id + '-' + type,
		position,
		connectionsCount,
		isConnecting: false,
		offset: {
			[offsetAxis]: `${(100 / (count + 1)) * (index + 1)}%`
		}
	}
}

const getConnectionCount = (
	connectionType: 'source' | 'target',
	handleId: string,
	nodeEdges: IWorkflowEdge[]
) => {
	return nodeEdges.filter((edge) => {
		if (connectionType === 'source') {
			const edgeHandleId = edge.sourceHandle || 'source'
			return edge.source === props.id && edgeHandleId === handleId
		}

		const edgeHandleId = edge.targetHandle || 'target'
		return edge.target === props.id && edgeHandleId === handleId
	}).length
}

/**
 * Inputs
 */
const inputs = computed(() => {
	const getInputs = nodeMap.value?.[props.node?.data.nodeType]?.inputs
	const inputs =
		typeof getInputs === 'function' ? getInputs(props.node?.data) : getInputs || ['main']

	return (inputs as CanvasConnectionPort[]).map((target, index) =>
		createEndpoint({
			port: target,
			index,
			count: inputs.length,
			connectionsCount: getConnectionCount(
				'target',
				target?.id || `${props.node?.id}-target`,
				edges.value
			),
			offsetAxis: 'top',
			position: Position.Left,
			type: 'target'
		})
	)
})

/**
 * Outputs
 */
const outputs = computed(() => {
	const getOutputs = nodeMap.value?.[props.node?.data.nodeType]?.outputs
	const outputs =
		typeof getOutputs === 'function' ? getOutputs(props.node?.data) : getOutputs || ['main']

	return (outputs as CanvasConnectionPort[]).map((target, index) =>
		createEndpoint({
			port: target,
			index,
			count: outputs.length,
			connectionsCount: getConnectionCount(
				'source',
				target?.id || `${props.node?.id}-source`,
				edges.value
			),
			offsetAxis: 'top',
			position: Position.Right,
			type: 'source'
		})
	)
})

const hideToolBar = computed(() => nodeMap.value?.[props.node?.data.nodeType]?.hideToolBar)
const isPendingNode = computed(() => !!props.node?.data?.pending)

const onUpdate = (prop: Record<string, unknown>) => {
	emit('update', props.id, prop)
}

const onDelete = () => {
	emit('delete', props.id)
}

const onRun = () => {
	emit('run', props.id)
}

provideCanvasNodeContext({
	props: computed(() => ({
		...props,
		data: props.node?.data
	})),
	inputs,
	outputs
})
</script>

<template>
	<div class="relative w-full h-full">
		<NodeRenderer
			@update="onUpdate"
			@click:node="(id, position) => emit('click:node', id, position)"
			@dblclick:node="(id, position) => emit('dblclick:node', id, position)"
			@click:node:add="emit('add-node', $event)"
			@add-inner-node="emit('add-inner-node')"
			@add-inner-edge="emit('add-inner-edge', $event)"
			@create:connection:cancelled="emit('create-connection-cancelled', $event)"
			@loop:child:click:node="(id, position) => emit('loop:child:click:node', id, position)"
			@loop:child:dblclick:node="(id, position) => emit('loop:child:dblclick:node', id, position)"
			@loop:child:click:node:add="emit('loop:child:click:node:add', $event)"
			@loop:child:create:connection:end="
				(connection, event) => emit('loop:child:create:connection:end', connection, event)
			"
			@loop:child:update:node:attrs="(id, attrs) => emit('loop:child:update:node:attrs', id, attrs)"
			@loop:child:update:nodes:position="emit('loop:child:update:nodes:position', $event)"
			@loop:child:create:connection:cancelled="
				emit('loop:child:create:connection:cancelled', $event)
			"
			@loop:child:run:node="emit('loop:child:run:node', $event)"
			@loop:child:delete:node="emit('loop:child:delete:node', $event)"
			@loop:child:delete:connection="emit('loop:child:delete:connection', $event)"
			@loop:child:click:connection:add="emit('loop:child:click:connection:add', $event)"
		/>

		<template
			v-if="!isPendingNode"
			v-for="target in inputs"
			:key="'handle-inputs-port' + target.index"
		>
			<CanvasHandle v-bind="target" type="target" />
		</template>

		<template
			v-if="!isPendingNode"
			v-for="source in outputs"
			:key="'handle-outputs-port' + source.index"
		>
			<CanvasHandle
				v-bind="source"
				:node-id="props.id"
				type="source"
				@add="
					emit('add-node', { nodeId: props.id, handleId: $event.handleId, event: $event.event })
				"
			/>
		</template>

		<CanvasNodeToolBar v-if="!hideToolBar" @delete="onDelete" @run="onRun" :hovered="hovered" />
	</div>
</template>

<style>
.canvas-node-status-shell {
	transition:
		border-color 180ms ease,
		box-shadow 180ms ease,
		filter 180ms ease;
	isolation: isolate;
}

.canvas-node-status-shell[data-execution-status='running'] {
	border-color: #3b82f6 !important;
	box-shadow:
		0 0 0 1px rgb(59 130 246 / 20%),
		0 0 18px rgb(59 130 246 / 22%);
	animation: canvas-node-status-pulse 1.8s ease-in-out infinite;
}

.canvas-node-status-shell[data-execution-status='running']::before {
	content: '';
	position: absolute;
	top: 0;
	left: 8%;
	width: 28%;
	height: 3px;
	border-radius: 999px;
	background: linear-gradient(
		90deg,
		rgb(59 130 246 / 0%),
		rgb(147 197 253 / 95%),
		rgb(59 130 246 / 0%)
	);
	box-shadow: 0 0 10px rgb(59 130 246 / 45%);
	pointer-events: none;
	animation: canvas-node-status-scan 1.6s ease-in-out infinite;
}

.canvas-node-status-shell[data-execution-status='running']::after {
	content: '';
	position: absolute;
	inset: 0;
	border-radius: inherit;
	background: linear-gradient(
		120deg,
		rgb(255 255 255 / 0%),
		rgb(255 255 255 / 10%),
		rgb(255 255 255 / 0%)
	);
	opacity: 0.65;
	pointer-events: none;
	animation: canvas-node-status-sheen 2.2s ease-in-out infinite;
}

.canvas-node-status-shell[data-execution-status='success'] {
	border-color: #22c55e !important;
}

.canvas-node-status-shell[data-execution-status='warning'] {
	border-color: #f59e0b !important;
}

.canvas-node-status-shell[data-execution-status='suspended'] {
	border-color: #64748b !important;
	box-shadow:
		0 0 0 1px rgb(100 116 139 / 20%),
		0 0 14px rgb(100 116 139 / 22%);
}

.canvas-node-status-shell[data-pending='true'] {
	opacity: 0.7;
	border-color: #409eff !important;
	filter: none !important;
}

.canvas-node-status-shell[data-pending='true']::before {
	content: '';
	position: absolute;
	inset: 0;
	border-radius: inherit;
	padding: 1.5px;
	background: repeating-conic-gradient(
		from var(--pending-dash-angle),
		rgba(64, 158, 255, 0.95) 0deg 10deg,
		transparent 10deg 20deg
	);
	-webkit-mask:
		linear-gradient(#000 0 0) content-box,
		linear-gradient(#000 0 0);
	-webkit-mask-composite: xor;
	mask-composite: exclude;
	pointer-events: none;
	animation: canvas-node-pending-dash-flow 8s linear infinite;
}

@keyframes canvas-node-status-pulse {
	0%,
	100% {
		box-shadow:
			0 0 0 1px rgb(59 130 246 / 18%),
			0 0 12px rgb(59 130 246 / 14%);
	}

	50% {
		box-shadow:
			0 0 0 1px rgb(59 130 246 / 26%),
			0 0 22px rgb(59 130 246 / 26%);
	}
}

@keyframes canvas-node-status-scan {
	from {
		transform: translateX(-10%);
		opacity: 0.35;
	}

	to {
		transform: translateX(250%);
		opacity: 1;
	}
}

@keyframes canvas-node-status-sheen {
	0%,
	100% {
		opacity: 0.2;
	}

	50% {
		opacity: 0.7;
	}
}

@property --pending-dash-angle {
	syntax: '<angle>';
	inherits: false;
	initial-value: 0deg;
}

@keyframes canvas-node-pending-dash-flow {
	from {
		--pending-dash-angle: 0deg;
	}
	to {
		--pending-dash-angle: 360deg;
	}
}
</style>
