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

import { ref, onMounted, computed, provide } from 'vue'
import { VueFlow, useVueFlow, MarkerType } from '@vue-flow/core'
import { MiniMap } from '@vue-flow/minimap'

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 ConditionNode from './elements/node-temp/ConditionNode.vue'
import StartNode from './elements/node-temp/StartNode.vue'
import HttpNode from './elements/node-temp/HttpNode1.vue'
import EndNode from './elements/node-temp/EndNode.vue'
import CodeNode from './elements/node-temp/CodeNode.vue'
import DataBaseNode from './elements/node-temp/DataBaseNode.vue'

defineOptions({
	name: 'workflow-canvas'
})

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]
	'click:node:add': [id: string, handle: string]
	'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: any]
	'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]
	'delete:connection': [connection: Connection]
	'create:connection:start': [handle: ConnectStartEvent]
	'create:connection': [connection: Connection]
	'create:connection:end': [connection: Connection, event?: MouseEvent]
	'create:connection:cancelled': [
		handle: ConnectStartEvent,
		position: XYPosition,
		event?: MouseEvent
	]
	'click:connection:add': [connection: Connection]
	run: []
}>()

const props = withDefaults(
	defineProps<{
		id?: string
		nodes: IWorkflow['nodes']
		edges: IWorkflow['edges']
		readOnly?: boolean
	}>(),
	{
		id: 'canvas',
		readOnly: false,
		nodes: () => [],
		edges: () => []
	}
)

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

const { viewport, viewportRef, project, zoomIn, zoomOut, fitView, zoomTo } = vueFlow

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

/**
 * 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 project({
		x: x - bounds.left,
		y: y - bounds.top
	})
}

/**
 * Events
 */

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

function onDrop(event: DragEvent) {
	const position = getProjectedPosition(event)

	emit('drag-and-drop', position, event)
}

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

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

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

const onResetZoom = () => {
	zoomTo(1)
}
const onAddNode = (value: SourceType | string) => {
	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)
}

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)
}

/**
 * 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', connectingHandle.value, 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)
}

let loaded = false
function onNodesInitialized() {
	if (!loaded) {
		onZoomToFit()
		loaded = true
		emit('initialized:nodes')
	}
}

/**
 * Handle
 */
const handleRun = () => {
	emit('run')
}

onMounted(() => {
	fitView()
})

provide('vueflow', {
	id: props.id,
	nodes: props.nodes,
	edges: props.edges,
	vueFlow
})

defineExpose({
	vueFlow
})
</script>

<template>
	<VueFlow
		:id="id"
		:nodes="nodes"
		:edges="edges"
		:connection-line-options="{ markerEnd: MarkerType.ArrowClosed }"
		:connection-radius="60"
		snap-to-grid
		:snap-grid="[16, 16]"
		@node-click="onNodeClick"
		@node-drag-stop="onNodeDragStop"
		@drop="onDrop"
		@connect="onConnect"
		@connect-start="onConnectStart"
		@connect-end="onConnectEnd"
		@nodes-initialized="onNodesInitialized"
		v-bind="$attrs"
	>
		<template #node-canvas-node="nodeProps">
			<CanvasNode
				v-bind="nodeProps"
				:data="nodeDataById[nodeProps.id]!"
				@move="onUpdateNodePosition"
				@update="onUpdateNodeAttrs"
			/>
		</template>

		<template #node-start-node="nodeProps">
			<StartNode v-bind="nodeProps" :data="nodeDataById[nodeProps.id]" />
		</template>

		<template #node-end-node="nodeProps">
			<EndNode v-bind="nodeProps" :data="nodeDataById[nodeProps.id]" />
		</template>

		<template #node-http-node="nodeProps">
			<HttpNode v-bind="nodeProps" :data="nodeDataById[nodeProps.id]!" />
		</template>

		<template #node-code-node="nodeProps">
			<CodeNode v-bind="nodeProps" :data="nodeDataById[nodeProps.id]!" />
		</template>

		<template #node-database-node="nodeProps">
			<DataBaseNode v-bind="nodeProps" :data="nodeDataById[nodeProps.id]!" />
		</template>

		<template #node-condition-node="nodeProps">
			<ConditionNode v-bind="nodeProps" :data="nodeDataById[nodeProps.id]!" />
		</template>

		<template #edge-canvas-edge="edgeProps">
			<CanvasEdge
				v-bind="edgeProps"
				marker-end="url(#custom-arrow-head-marker)"
				@add="onClickConnectionAdd"
				@delete="onDeleteConnection"
			/>
		</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
			@zoom-in="onZoomIn"
			@zoom-out="onZoomOut"
			@zoom-to-fit="onZoomToFit"
			@reset-zoom="onResetZoom"
			@add-node="onAddNode"
			@run="handleRun"
			@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>
