<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue';
import { onClickOutside } from '@vueuse/core';

import ExpressionFunctionIcon from '@/components/ExpressionFunctionIcon.vue';
import InlineExpressionEditorInput from '@/components/InlineExpressionEditor/InlineExpressionEditorInput.vue';
import InlineExpressionEditorOutput from '@/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue';
import { useNDVStore } from '@/stores/ndv.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { createExpressionTelemetryPayload } from '@/utils/telemetryUtils';

import { useTelemetry } from '@/composables/useTelemetry';
import { dropInExpressionEditor } from '@/plugins/codemirror/dragAndDrop';
import type { Segment } from '@/types/expressions';
import { startCompletion } from '@codemirror/autocomplete';
import type { EditorState, SelectionRange } from '@codemirror/state';
import type { IDataObject } from 'n8n-workflow';
import { createEventBus, type EventBus } from 'n8n-design-system';

const isFocused = ref(false);
const segments = ref<Segment[]>([]);
const editorState = ref<EditorState>();
const selection = ref<SelectionRange>();
const inlineInput = ref<InstanceType<typeof InlineExpressionEditorInput>>();
const container = ref<HTMLDivElement>();

type Props = {
	path: string;
	modelValue: string;
	rows?: number;
	additionalExpressionData?: IDataObject;
	isReadOnly?: boolean;
	isAssignment?: boolean;
	eventBus?: EventBus;
};

const props = withDefaults(defineProps<Props>(), {
	rows: 5,
	isAssignment: false,
	isReadOnly: false,
	additionalExpressionData: () => ({}),
	eventBus: () => createEventBus(),
});

const emit = defineEmits<{
	'modal-opener-click': [];
	'update:model-value': [value: string];
	focus: [];
	blur: [];
}>();

const telemetry = useTelemetry();
const ndvStore = useNDVStore();
const workflowsStore = useWorkflowsStore();

const isDragging = computed(() => ndvStore.isDraggableDragging);

function focus() {
	if (inlineInput.value) {
		inlineInput.value.focus();
	}
}

function onFocus() {
	isFocused.value = true;
	emit('focus');
}

function onBlur(event?: FocusEvent | KeyboardEvent) {
	if (
		event?.target instanceof Element &&
		Array.from(event.target.classList).some((_class) => _class.includes('resizer'))
	) {
		return; // prevent blur on resizing
	}

	const wasFocused = isFocused.value;

	isFocused.value = false;

	if (wasFocused) {
		emit('blur');

		const telemetryPayload = createExpressionTelemetryPayload(
			segments.value,
			props.modelValue,
			workflowsStore.workflowId,
			ndvStore.pushRef,
			ndvStore.activeNode?.type ?? '',
		);

		telemetry.track('User closed Expression Editor', telemetryPayload);
	}
}

function onValueChange({ value, segments: newSegments }: { value: string; segments: Segment[] }) {
	segments.value = newSegments;

	if (isDragging.value) return;
	if (value === '=' + props.modelValue) return; // prevent report on change of target item

	emit('update:model-value', value);
}

function onSelectionChange({
	state: newState,
	selection: newSelection,
}: {
	state: EditorState;
	selection: SelectionRange;
}) {
	editorState.value = newState;
	selection.value = newSelection;
}

async function onDrop(value: string, event: MouseEvent) {
	if (!inlineInput.value) return;
	const { editor, setCursorPosition } = inlineInput.value;

	if (!editor) return;

	const droppedSelection = await dropInExpressionEditor(toRaw(editor), event, value);

	if (!ndvStore.isMappingOnboarded) ndvStore.setMappingOnboarded();

	if (!ndvStore.isAutocompleteOnboarded) {
		setCursorPosition((droppedSelection.ranges.at(0)?.head ?? 3) - 3);
		setTimeout(() => {
			startCompletion(editor);
		});
	}
}

async function onDropOnFixedInput() {
	await nextTick();

	if (!inlineInput.value) return;
	const { editor, setCursorPosition } = inlineInput.value;

	if (!editor || ndvStore.isAutocompleteOnboarded) return;

	setCursorPosition('lastExpression');
	setTimeout(() => {
		focus();
		startCompletion(editor);
	});
}

onMounted(() => {
	props.eventBus.on('drop', onDropOnFixedInput);
});

onBeforeUnmount(() => {
	props.eventBus.off('drop', onDropOnFixedInput);
});

watch(isDragging, (newIsDragging) => {
	if (newIsDragging) {
		onBlur();
	}
});

onClickOutside(container, (event) => onBlur(event));

defineExpose({ focus });
</script>

<template>
	<div ref="container" :class="$style['expression-parameter-input']" @keydown.tab="onBlur">
		<div
			:class="[
				$style['all-sections'],
				{ [$style.focused]: isFocused, [$style.assignment]: isAssignment },
			]"
		>
			<div :class="[$style['prepend-section'], 'el-input-group__prepend']">
				<span v-if="isAssignment">=</span>
				<ExpressionFunctionIcon v-else />
			</div>
			<DraggableTarget type="mapping" :disabled="isReadOnly" @drop="onDrop">
				<template #default="{ activeDrop, droppable }">
					<InlineExpressionEditorInput
						ref="inlineInput"
						:model-value="modelValue"
						:path="path"
						:is-read-only="isReadOnly"
						:rows="rows"
						:additional-data="additionalExpressionData"
						:class="{ [$style.activeDrop]: activeDrop, [$style.droppable]: droppable }"
						@focus="onFocus"
						@blur="onBlur"
						@update:model-value="onValueChange"
						@update:selection="onSelectionChange"
					/>
				</template>
			</DraggableTarget>
			<n8n-button
				v-if="!isDragging"
				square
				outline
				type="tertiary"
				icon="external-link-alt"
				size="xsmall"
				:class="$style['expression-editor-modal-opener']"
				data-test-id="expander"
				@click="emit('modal-opener-click')"
			/>
		</div>
		<InlineExpressionEditorOutput
			:unresolved-expression="modelValue"
			:selection="selection"
			:editor-state="editorState"
			:segments="segments"
			:is-read-only="isReadOnly"
			:visible="isFocused"
		/>
	</div>
</template>

<style lang="scss" module>
.expression-parameter-input {
	position: relative;

	:global(.cm-editor) {
		background-color: var(--color-code-background);
	}

	.all-sections {
		height: 30px;
		display: inline-table;
		width: 100%;
	}

	.prepend-section {
		padding: 0;
		padding-top: 2px;
		width: 22px;
		text-align: center;
	}
}

.assignment {
	.prepend-section {
		vertical-align: top;
		padding-top: 4px;
	}
}

.expression-editor-modal-opener {
	position: absolute;
	right: 0;
	bottom: 0;
	background-color: var(--color-code-background);
	padding: 3px;
	line-height: 9px;
	border: var(--input-border-color, var(--border-color-base))
		var(--input-border-style, var(--border-style-base))
		var(--input-border-width, var(--border-width-base));
	cursor: pointer;
	border-radius: 0;
	border-top-left-radius: var(--border-radius-base);

	&:hover {
		border: var(--input-border-color, var(--border-color-base))
			var(--input-border-style, var(--border-style-base))
			var(--input-border-width, var(--border-width-base));
	}

	svg {
		width: 9px !important;
		height: 9px;
		transform: rotate(270deg);
	}
}

.focused > .prepend-section {
	border-color: var(--color-secondary);
	border-bottom-left-radius: 0;
}

.focused :global(.cm-editor) {
	border-color: var(--color-secondary);
}

.focused > .expression-editor-modal-opener {
	border-color: var(--color-secondary);
	border-bottom-right-radius: 0;
	background-color: var(--color-code-background);
}

.droppable {
	--input-border-color: var(--color-ndv-droppable-parameter);
	--input-border-right-color: var(--color-ndv-droppable-parameter);
	--input-border-style: dashed;

	:global(.cm-editor) {
		border-width: 1.5px;
	}
}

.activeDrop {
	--input-border-color: var(--color-success);
	--input-border-right-color: var(--color-success);
	--input-background-color: var(--color-foreground-xlight);
	--input-border-style: solid;

	:global(.cm-editor) {
		cursor: grabbing !important;
		border-width: 1px;
	}
}
</style>
