import { defineComponent } from 'vue';
import type { PropType } from 'vue';
import { mapStores } from 'pinia';

import type { INodeUi } from '@/Interface';
import { deviceSupportHelpers } from '@/mixins/deviceSupportHelpers';
import {
	NO_OP_NODE_TYPE,
	NODE_CONNECTION_TYPE_ALLOW_MULTIPLE,
	NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS,
	NODE_MIN_INPUT_ITEMS_COUNT,
} from '@/constants';

import { NodeHelpers, NodeConnectionType } from 'n8n-workflow';
import type {
	ConnectionTypes,
	INodeInputConfiguration,
	INodeTypeDescription,
	INodeOutputConfiguration,
	Workflow,
} from 'n8n-workflow';
import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import type { BrowserJsPlumbInstance } from '@jsplumb/browser-ui';
import type { Endpoint, EndpointOptions } from '@jsplumb/core';
import * as NodeViewUtils from '@/utils/nodeViewUtils';
import { useHistoryStore } from '@/stores/history.store';
import { useCanvasStore } from '@/stores/canvas.store';
import type { EndpointSpec } from '@jsplumb/common';

const createAddInputEndpointSpec = (
	connectionName: NodeConnectionType,
	color: string,
): EndpointSpec => {
	const multiple = NODE_CONNECTION_TYPE_ALLOW_MULTIPLE.includes(connectionName);

	return {
		type: 'N8nAddInput',
		options: {
			width: 24,
			height: 72,
			color,
			multiple,
		},
	};
};

const createDiamondOutputEndpointSpec = (): EndpointSpec => ({
	type: 'Rectangle',
	options: {
		height: 10,
		width: 10,
		cssClass: 'diamond-output-endpoint',
	},
});

export const nodeBase = defineComponent({
	mixins: [deviceSupportHelpers],
	mounted() {
		// Initialize the node
		if (this.data !== null) {
			try {
				this.__addNode(this.data);
			} catch (error) {
				// This breaks when new nodes are loaded into store but workflow tab is not currently active
				// Shouldn't affect anything
			}
		}
	},
	data() {
		return {
			inputs: [] as Array<ConnectionTypes | INodeInputConfiguration>,
			outputs: [] as Array<ConnectionTypes | INodeOutputConfiguration>,
		};
	},
	computed: {
		...mapStores(useNodeTypesStore, useUIStore, useCanvasStore, useWorkflowsStore, useHistoryStore),
		data(): INodeUi | null {
			return this.workflowsStore.getNodeByName(this.name);
		},
		nodeId(): string {
			return this.data?.id || '';
		},
	},
	props: {
		name: {
			type: String,
		},
		instance: {
			type: Object as PropType<BrowserJsPlumbInstance>,
		},
		isReadOnly: {
			type: Boolean,
		},
		isActive: {
			type: Boolean,
		},
		hideActions: {
			type: Boolean,
		},
		disableSelecting: {
			type: Boolean,
		},
		showCustomTooltip: {
			type: Boolean,
		},
		workflow: {
			type: Object as () => Workflow,
			required: true,
		},
	},
	methods: {
		__addEndpointTestingData(endpoint: Endpoint, type: string, inputIndex: number) {
			if (window?.Cypress && 'canvas' in endpoint.endpoint) {
				const canvas = endpoint.endpoint.canvas;
				this.instance.setAttribute(canvas, 'data-endpoint-name', this.data.name);
				this.instance.setAttribute(canvas, 'data-input-index', inputIndex.toString());
				this.instance.setAttribute(canvas, 'data-endpoint-type', type);
			}
		},
		__addInputEndpoints(node: INodeUi, nodeTypeData: INodeTypeDescription) {
			// Add Inputs
			const rootTypeIndexData: {
				[key: string]: number;
			} = {};
			const typeIndexData: {
				[key: string]: number;
			} = {};

			const inputs: Array<ConnectionTypes | INodeInputConfiguration> =
				NodeHelpers.getNodeInputs(this.workflow, this.data!, nodeTypeData) || [];
			this.inputs = inputs;

			const sortedInputs = [...inputs];
			sortedInputs.sort((a, b) => {
				if (typeof a === 'string') {
					return 1;
				} else if (typeof b === 'string') {
					return -1;
				}

				if (a.required && !b.required) {
					return -1;
				} else if (!a.required && b.required) {
					return 1;
				}

				return 0;
			});

			sortedInputs.forEach((value, i) => {
				let inputConfiguration: INodeInputConfiguration;
				if (typeof value === 'string') {
					inputConfiguration = {
						type: value,
					};
				} else {
					inputConfiguration = value;
				}

				const inputName: ConnectionTypes = inputConfiguration.type;

				const rootCategoryInputName =
					inputName === NodeConnectionType.Main ? NodeConnectionType.Main : 'other';

				// Increment the index for inputs with current name
				if (rootTypeIndexData.hasOwnProperty(rootCategoryInputName)) {
					rootTypeIndexData[rootCategoryInputName]++;
				} else {
					rootTypeIndexData[rootCategoryInputName] = 0;
				}

				if (typeIndexData.hasOwnProperty(inputName)) {
					typeIndexData[inputName]++;
				} else {
					typeIndexData[inputName] = 0;
				}

				const rootTypeIndex = rootTypeIndexData[rootCategoryInputName];
				const typeIndex = typeIndexData[inputName];

				const inputsOfSameRootType = inputs.filter((inputData) => {
					const thisInputName: string = typeof inputData === 'string' ? inputData : inputData.type;
					return inputName === NodeConnectionType.Main
						? thisInputName === NodeConnectionType.Main
						: thisInputName !== NodeConnectionType.Main;
				});

				const nonMainInputs = inputsOfSameRootType.filter((inputData) => {
					return inputData !== NodeConnectionType.Main;
				});
				const requiredNonMainInputs = nonMainInputs.filter((inputData) => {
					return typeof inputData !== 'string' && inputData.required;
				});
				const optionalNonMainInputs = nonMainInputs.filter((inputData) => {
					return typeof inputData !== 'string' && !inputData.required;
				});
				const spacerIndexes = this.getSpacerIndexes(
					requiredNonMainInputs.length,
					optionalNonMainInputs.length,
				);

				// Get the position of the anchor depending on how many it has
				const anchorPosition = NodeViewUtils.getAnchorPosition(
					inputName,
					'input',
					inputsOfSameRootType.length,
					spacerIndexes,
				)[rootTypeIndex];

				const scope = NodeViewUtils.getEndpointScope(inputName as NodeConnectionType);

				const newEndpointData: EndpointOptions = {
					uuid: NodeViewUtils.getInputEndpointUUID(this.nodeId, inputName, typeIndex),
					anchor: anchorPosition,
					// We potentially want to change that in the future to allow people to dynamically
					// activate and deactivate connected nodes
					maxConnections: inputConfiguration.maxConnections ?? -1,
					endpoint: 'Rectangle',
					paintStyle: NodeViewUtils.getInputEndpointStyle(
						nodeTypeData,
						'--color-foreground-xdark',
						inputName,
					),
					hoverPaintStyle: NodeViewUtils.getInputEndpointStyle(
						nodeTypeData,
						'--color-primary',
						inputName,
					),
					scope: NodeViewUtils.getScope(scope),
					source: inputName !== NodeConnectionType.Main,
					target: !this.isReadOnly && inputs.length > 1, // only enabled for nodes with multiple inputs.. otherwise attachment handled by connectionDrag event in NodeView,
					parameters: {
						connection: 'target',
						nodeId: this.nodeId,
						type: inputName,
						index: typeIndex,
					},
					enabled: !this.isReadOnly, // enabled in default case to allow dragging
					cssClass: 'rect-input-endpoint',
					dragAllowedWhenFull: true,
					hoverClass: 'rect-input-endpoint-hover',
					...this.__getInputConnectionStyle(inputName, nodeTypeData),
				};

				const endpoint = this.instance?.addEndpoint(
					this.$refs[this.data.name] as Element,
					newEndpointData,
				) as Endpoint;
				this.__addEndpointTestingData(endpoint, 'input', typeIndex);
				if (inputConfiguration.displayName || nodeTypeData.inputNames?.[i]) {
					// Apply input names if they got set
					endpoint.addOverlay(
						NodeViewUtils.getInputNameOverlay(
							inputConfiguration.displayName || nodeTypeData.inputNames[i],
							inputName,
							inputConfiguration.required,
						),
					);
				}
				if (!Array.isArray(endpoint)) {
					endpoint.__meta = {
						nodeName: node.name,
						nodeId: this.nodeId,
						index: typeIndex,
						totalEndpoints: inputsOfSameRootType.length,
						nodeType: node.type,
					};
				}

				// TODO: Activate again if it makes sense. Currently makes problems when removing
				//       connection on which the input has a name. It does not get hidden because
				//       the endpoint to which it connects when letting it go over the node is
				//       different to the regular one (have different ids). So that seems to make
				//       problems when hiding the input-name.

				// if (index === 0 && inputName === NodeConnectionType.Main) {
				// 	// Make the first main-input the default one to connect to when connection gets dropped on node
				// 	this.instance.makeTarget(this.nodeId, newEndpointData);
				// }
			});
			if (sortedInputs.length === 0) {
				this.instance.manage(this.$refs[this.data.name] as Element);
			}
		},
		getSpacerIndexes(
			leftGroupItemsCount: number,
			rightGroupItemsCount: number,
			insertSpacerBetweenGroups = NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS,
			minItemsCount = NODE_MIN_INPUT_ITEMS_COUNT,
		): number[] {
			const spacerIndexes = [];

			if (leftGroupItemsCount > 0 && rightGroupItemsCount > 0) {
				if (insertSpacerBetweenGroups) {
					spacerIndexes.push(leftGroupItemsCount);
				} else if (leftGroupItemsCount + rightGroupItemsCount < minItemsCount) {
					for (
						let spacerIndex = leftGroupItemsCount;
						spacerIndex < minItemsCount - rightGroupItemsCount;
						spacerIndex++
					) {
						spacerIndexes.push(spacerIndex);
					}
				}
			} else {
				if (
					leftGroupItemsCount > 0 &&
					leftGroupItemsCount < minItemsCount &&
					rightGroupItemsCount === 0
				) {
					for (
						let spacerIndex = 0;
						spacerIndex < minItemsCount - leftGroupItemsCount;
						spacerIndex++
					) {
						spacerIndexes.push(spacerIndex + leftGroupItemsCount);
					}
				} else if (
					leftGroupItemsCount === 0 &&
					rightGroupItemsCount > 0 &&
					rightGroupItemsCount < minItemsCount
				) {
					for (
						let spacerIndex = 0;
						spacerIndex < minItemsCount - rightGroupItemsCount;
						spacerIndex++
					) {
						spacerIndexes.push(spacerIndex);
					}
				}
			}

			return spacerIndexes;
		},
		__addOutputEndpoints(node: INodeUi, nodeTypeData: INodeTypeDescription) {
			const rootTypeIndexData: {
				[key: string]: number;
			} = {};
			const typeIndexData: {
				[key: string]: number;
			} = {};

			this.outputs = NodeHelpers.getNodeOutputs(this.workflow, this.data, nodeTypeData) || [];

			// TODO: There are still a lot of references of "main" in NodesView and
			//       other locations. So assume there will be more problems
			let maxLabelLength = 0;
			const outputConfigurations: INodeOutputConfiguration[] = [];
			this.outputs.forEach((value, i) => {
				let outputConfiguration: INodeOutputConfiguration;
				if (typeof value === 'string') {
					outputConfiguration = {
						type: value,
					};
				} else {
					outputConfiguration = value;
				}
				if (nodeTypeData.outputNames?.[i]) {
					outputConfiguration.displayName = nodeTypeData.outputNames[i];
				}

				if (outputConfiguration.displayName) {
					maxLabelLength =
						outputConfiguration.displayName.length > maxLabelLength
							? outputConfiguration.displayName.length
							: maxLabelLength;
				}

				outputConfigurations.push(outputConfiguration);
			});

			const endpointLabelLength = maxLabelLength < 4 ? 'short' : 'medium';

			this.outputs.forEach((value, i) => {
				const outputConfiguration = outputConfigurations[i];

				const outputName: ConnectionTypes = outputConfiguration.type;

				const rootCategoryOutputName =
					outputName === NodeConnectionType.Main ? NodeConnectionType.Main : 'other';

				// Increment the index for outputs with current name
				if (rootTypeIndexData.hasOwnProperty(rootCategoryOutputName)) {
					rootTypeIndexData[rootCategoryOutputName]++;
				} else {
					rootTypeIndexData[rootCategoryOutputName] = 0;
				}

				if (typeIndexData.hasOwnProperty(outputName)) {
					typeIndexData[outputName]++;
				} else {
					typeIndexData[outputName] = 0;
				}

				const rootTypeIndex = rootTypeIndexData[rootCategoryOutputName];
				const typeIndex = typeIndexData[outputName];

				const outputsOfSameRootType = this.outputs.filter((outputData) => {
					const thisOutputName: string =
						typeof outputData === 'string' ? outputData : outputData.type;
					return outputName === NodeConnectionType.Main
						? thisOutputName === NodeConnectionType.Main
						: thisOutputName !== NodeConnectionType.Main;
				});

				// Get the position of the anchor depending on how many it has
				const anchorPosition = NodeViewUtils.getAnchorPosition(
					outputName,
					'output',
					outputsOfSameRootType.length,
				)[rootTypeIndex];

				const scope = NodeViewUtils.getEndpointScope(outputName as NodeConnectionType);

				const newEndpointData: EndpointOptions = {
					uuid: NodeViewUtils.getOutputEndpointUUID(this.nodeId, outputName, typeIndex),
					anchor: anchorPosition,
					maxConnections: -1,
					endpoint: {
						type: 'Dot',
						options: {
							radius: nodeTypeData && outputsOfSameRootType.length > 2 ? 7 : 9,
						},
					},
					hoverPaintStyle: NodeViewUtils.getOutputEndpointStyle(nodeTypeData, '--color-primary'),
					scope,
					source: true,
					target: outputName !== NodeConnectionType.Main,
					enabled: !this.isReadOnly,
					parameters: {
						connection: 'source',
						nodeId: this.nodeId,
						type: outputName,
						index: typeIndex,
					},
					hoverClass: 'dot-output-endpoint-hover',
					connectionsDirected: true,
					dragAllowedWhenFull: false,
					...this.__getOutputConnectionStyle(outputName, outputConfiguration, nodeTypeData),
				};

				const endpoint = this.instance.addEndpoint(
					this.$refs[this.data.name] as Element,
					newEndpointData,
				);

				this.__addEndpointTestingData(endpoint, 'output', typeIndex);
				if (outputConfiguration.displayName) {
					// Apply output names if they got set
					const overlaySpec = NodeViewUtils.getOutputNameOverlay(
						outputConfiguration.displayName,
						outputName,
						outputConfiguration?.category,
					);
					endpoint.addOverlay(overlaySpec);
				}

				if (!Array.isArray(endpoint)) {
					endpoint.__meta = {
						nodeName: node.name,
						nodeId: this.nodeId,
						index: typeIndex,
						totalEndpoints: outputsOfSameRootType.length,
						endpointLabelLength,
					};
				}

				if (!this.isReadOnly && outputName === NodeConnectionType.Main) {
					const plusEndpointData: EndpointOptions = {
						uuid: NodeViewUtils.getOutputEndpointUUID(this.nodeId, outputName, typeIndex),
						anchor: anchorPosition,
						maxConnections: -1,
						endpoint: {
							type: 'N8nPlus',
							options: {
								dimensions: 24,
								connectedEndpoint: endpoint,
								showOutputLabel: this.outputs.length === 1,
								size: this.outputs.length >= 3 ? 'small' : 'medium',
								endpointLabelLength,
								hoverMessage: this.$locale.baseText('nodeBase.clickToAddNodeOrDragToConnect'),
							},
						},
						source: true,
						target: false,
						enabled: !this.isReadOnly,
						paintStyle: {
							outlineStroke: 'none',
						},
						hoverPaintStyle: {
							outlineStroke: 'none',
						},
						parameters: {
							connection: 'source',
							nodeId: this.nodeId,
							type: outputName,
							index: typeIndex,
							category: outputConfiguration?.category,
						},
						cssClass: 'plus-draggable-endpoint',
						dragAllowedWhenFull: false,
					};

					if (outputConfiguration?.category) {
						plusEndpointData.cssClass = `${plusEndpointData.cssClass} ${outputConfiguration?.category}`;
					}

					const plusEndpoint = this.instance.addEndpoint(
						this.$refs[this.data.name] as Element,
						plusEndpointData,
					);
					this.__addEndpointTestingData(plusEndpoint, 'plus', typeIndex);

					if (!Array.isArray(plusEndpoint)) {
						plusEndpoint.__meta = {
							nodeName: node.name,
							nodeId: this.nodeId,
							index: typeIndex,
							nodeType: node.type,
							totalEndpoints: outputsOfSameRootType.length,
						};
					}
				}
			});
		},
		__addNode(node: INodeUi) {
			const nodeTypeData = (this.nodeTypesStore.getNodeType(node.type, node.typeVersion) ??
				this.nodeTypesStore.getNodeType(NO_OP_NODE_TYPE)) as INodeTypeDescription;

			this.__addInputEndpoints(node, nodeTypeData);
			this.__addOutputEndpoints(node, nodeTypeData);
		},
		__getEndpointColor(connectionType: ConnectionTypes) {
			return `--node-type-${connectionType}-color`;
		},
		__getInputConnectionStyle(
			connectionType: ConnectionTypes,
			nodeTypeData: INodeTypeDescription,
		): EndpointOptions {
			if (connectionType === NodeConnectionType.Main) {
				return {
					paintStyle: NodeViewUtils.getInputEndpointStyle(
						nodeTypeData,
						this.__getEndpointColor(NodeConnectionType.Main),
						connectionType,
					),
				};
			}

			if (!Object.values(NodeConnectionType).includes(connectionType as NodeConnectionType)) {
				return {};
			}

			const createSupplementalConnectionType = (
				connectionName: ConnectionTypes,
			): EndpointOptions => ({
				endpoint: createAddInputEndpointSpec(
					connectionName as NodeConnectionType,
					this.__getEndpointColor(connectionName),
				),
			});

			return createSupplementalConnectionType(connectionType);
		},
		__getOutputConnectionStyle(
			connectionType: ConnectionTypes,
			outputConfiguration: INodeOutputConfiguration,
			nodeTypeData: INodeTypeDescription,
		): EndpointOptions {
			const type = 'output';

			const createSupplementalConnectionType = (
				connectionName: ConnectionTypes,
			): EndpointOptions => ({
				endpoint: createDiamondOutputEndpointSpec(),
				paintStyle: NodeViewUtils.getOutputEndpointStyle(
					nodeTypeData,
					this.__getEndpointColor(connectionName),
				),
				hoverPaintStyle: NodeViewUtils.getOutputEndpointStyle(
					nodeTypeData,
					this.__getEndpointColor(connectionName),
				),
			});

			if (connectionType === NodeConnectionType.Main) {
				if (outputConfiguration.category === 'error') {
					return {
						paintStyle: {
							...NodeViewUtils.getOutputEndpointStyle(
								nodeTypeData,
								this.__getEndpointColor(NodeConnectionType.Main),
							),
							fill: 'var(--node-error-output-color)',
						},
						cssClass: `dot-${type}-endpoint`,
					};
				}
				return {
					paintStyle: NodeViewUtils.getOutputEndpointStyle(
						nodeTypeData,
						this.__getEndpointColor(NodeConnectionType.Main),
					),
					cssClass: `dot-${type}-endpoint`,
				};
			}

			if (!Object.values(NodeConnectionType).includes(connectionType as NodeConnectionType)) {
				return {};
			}

			return createSupplementalConnectionType(connectionType);
		},
		touchEnd(e: MouseEvent) {
			if (this.isTouchDevice) {
				if (this.uiStore.isActionActive('dragActive')) {
					this.uiStore.removeActiveAction('dragActive');
				}
			}
		},
		mouseLeftClick(e: MouseEvent) {
			// @ts-ignore
			const path = e.path || (e.composedPath && e.composedPath());
			for (let index = 0; index < path.length; index++) {
				if (
					path[index].className &&
					typeof path[index].className === 'string' &&
					path[index].className.includes('no-select-on-click')
				) {
					return;
				}
			}

			if (!this.isTouchDevice) {
				if (this.uiStore.isActionActive('dragActive')) {
					this.uiStore.removeActiveAction('dragActive');
				} else {
					if (!this.isCtrlKeyPressed(e)) {
						this.$emit('deselectAllNodes');
					}

					if (this.uiStore.isNodeSelected(this.data.name)) {
						this.$emit('deselectNode', this.name);
					} else {
						this.$emit('nodeSelected', this.name);
					}
				}
			}
		},
	},
});
