<template>
	<div @keydown.stop :class="parameterInputClasses">
		<expression-edit
			:dialogVisible="expressionEditDialogVisible"
			:modelValue="
				isResourceLocatorParameter && typeof modelValue !== 'string'
					? modelValue
						? modelValue.value
						: ''
					: modelValue
			"
			:parameter="parameter"
			:path="path"
			:eventSource="eventSource || 'ndv'"
			:isReadOnly="isReadOnly"
			:redactValues="shouldRedactValue"
			@closeDialog="closeExpressionEditDialog"
			@update:modelValue="expressionUpdated"
		></expression-edit>
		<div class="parameter-input ignore-key-press" :style="parameterInputWrapperStyle">
			<resource-locator
				v-if="isResourceLocatorParameter"
				ref="resourceLocator"
				:parameter="parameter"
				:modelValue="modelValue"
				:dependentParametersValues="dependentParametersValues"
				:displayTitle="displayTitle"
				:expressionDisplayValue="expressionDisplayValue"
				:expressionComputedValue="expressionEvaluated"
				:isValueExpression="isValueExpression"
				:isReadOnly="isReadOnly"
				:parameterIssues="getIssues"
				:droppable="droppable"
				:node="node"
				:path="path"
				:event-bus="eventBus"
				@update:modelValue="valueChanged"
				@modalOpenerClick="openExpressionEditorModal"
				@focus="setFocus"
				@blur="onBlur"
				@drop="onResourceLocatorDrop"
			/>
			<ExpressionParameterInput
				v-else-if="isValueExpression || forceShowExpression"
				:modelValue="expressionDisplayValue"
				:title="displayTitle"
				:isReadOnly="isReadOnly"
				:isSingleLine="isSingleLine"
				:path="path"
				:additional-expression-data="additionalExpressionData"
				:class="{ 'ph-no-capture': shouldRedactValue }"
				@update:modelValue="expressionUpdated"
				@modalOpenerClick="openExpressionEditorModal"
				@focus="setFocus"
				@blur="onBlur"
				ref="inputField"
			/>
			<div
				v-else-if="
					['json', 'string'].includes(parameter.type) ||
					remoteParameterOptionsLoadingIssues !== null
				"
			>
				<el-dialog
					v-if="codeEditDialogVisible"
					:modelValue="true"
					append-to-body
					:close-on-click-modal="false"
					width="80%"
					:title="`${i18n.baseText('codeEdit.edit')} ${$locale
						.nodeText()
						.inputLabelDisplayName(parameter, path)}`"
					:before-close="closeCodeEditDialog"
				>
					<div class="ignore-key-press">
						<code-node-editor
							:modelValue="modelValue"
							:defaultValue="parameter.default"
							:language="editorLanguage"
							:isReadOnly="isReadOnly"
							@update:modelValue="expressionUpdated"
						/>
					</div>
				</el-dialog>

				<text-edit
					:dialogVisible="textEditDialogVisible"
					:modelValue="modelValue"
					:parameter="parameter"
					:path="path"
					:isReadOnly="isReadOnly"
					@closeDialog="closeTextEditDialog"
					@update:modelValue="expressionUpdated"
				></text-edit>

				<code-node-editor
					v-if="editorType === 'codeNodeEditor' && isCodeNode(node)"
					:mode="node.parameters.mode"
					:modelValue="modelValue"
					:defaultValue="parameter.default"
					:language="editorLanguage"
					:isReadOnly="isReadOnly"
					:rows="getArgument('rows')"
					:aiButtonEnabled="settingsStore.isCloudDeployment"
					@update:modelValue="valueChangedDebounced"
				/>

				<html-editor
					v-else-if="editorType === 'htmlEditor'"
					:modelValue="modelValue"
					:isReadOnly="isReadOnly"
					:rows="getArgument('rows')"
					:disableExpressionColoring="!isHtmlNode(node)"
					:disableExpressionCompletions="!isHtmlNode(node)"
					@update:modelValue="valueChangedDebounced"
				/>

				<sql-editor
					v-else-if="editorType === 'sqlEditor'"
					:modelValue="modelValue"
					:dialect="getArgument('sqlDialect')"
					:isReadOnly="isReadOnly"
					:rows="getArgument('rows')"
					@update:modelValue="valueChangedDebounced"
				/>

				<code-node-editor
					v-else-if="editorType === 'json' && !isExecuteWorkflowNode(node)"
					:mode="node.parameters.mode"
					:modelValue="modelValue"
					:defaultValue="parameter.default"
					:language="editorLanguage"
					:isReadOnly="isReadOnly"
					:aiButtonEnabled="false"
					@update:modelValue="valueChangedDebounced"
					:rows="getArgument('rows')"
				/>

				<div v-else-if="editorType" class="readonly-code clickable" @click="displayEditDialog()">
					<code-node-editor
						v-if="!codeEditDialogVisible"
						:modelValue="modelValue"
						:language="editorLanguage"
						:isReadOnly="true"
						:rows="getArgument('rows')"
					/>
				</div>

				<n8n-input
					v-else
					v-model="tempValue"
					ref="inputField"
					:class="{ 'input-with-opener': true, 'ph-no-capture': shouldRedactValue }"
					:size="inputSize"
					:type="getStringInputType"
					:rows="getArgument('rows')"
					:disabled="isReadOnly"
					@update:modelValue="valueChanged($event) && onUpdateTextInput($event)"
					@keydown.stop
					@focus="setFocus"
					@blur="onBlur"
					:title="displayTitle"
					:placeholder="getPlaceholder()"
				>
					<template #suffix>
						<n8n-icon
							v-if="!isReadOnly && !isSecretParameter"
							icon="external-link-alt"
							size="xsmall"
							class="edit-window-button textarea-modal-opener"
							:class="{
								focused: isFocused,
								invalid: !isFocused && getIssues.length > 0 && !isValueExpression,
							}"
							:title="i18n.baseText('parameterInput.openEditWindow')"
							@click="displayEditDialog()"
							@focus="setFocus"
						/>
					</template>
				</n8n-input>
			</div>

			<div v-else-if="parameter.type === 'color'" ref="inputField" class="color-input">
				<el-color-picker
					size="small"
					class="color-picker"
					:modelValue="displayValue"
					:disabled="isReadOnly"
					@focus="setFocus"
					@blur="onBlur"
					@update:modelValue="valueChanged"
					:title="displayTitle"
					:show-alpha="getArgument('showAlpha')"
				/>
				<n8n-input
					v-model="tempValue"
					:size="inputSize"
					type="text"
					:disabled="isReadOnly"
					@update:modelValue="valueChanged"
					@keydown.stop
					@focus="setFocus"
					@blur="onBlur"
					:title="displayTitle"
				/>
			</div>

			<el-date-picker
				v-else-if="parameter.type === 'dateTime'"
				v-model="tempValue"
				ref="inputField"
				type="datetime"
				valueFormat="YYYY-MM-DDTHH:mm:ss"
				:size="inputSize"
				:modelValue="displayValue"
				:title="displayTitle"
				:disabled="isReadOnly"
				:placeholder="
					parameter.placeholder
						? getPlaceholder()
						: i18n.baseText('parameterInput.selectDateAndTime')
				"
				:picker-options="dateTimePickerOptions"
				:class="{ 'ph-no-capture': shouldRedactValue }"
				@update:modelValue="valueChanged"
				@focus="setFocus"
				@blur="onBlur"
				@keydown.stop
			/>

			<n8n-input-number
				v-else-if="parameter.type === 'number'"
				ref="inputField"
				:size="inputSize"
				:modelValue="displayValue"
				:controls="false"
				:max="getArgument('maxValue')"
				:min="getArgument('minValue')"
				:precision="getArgument('numberPrecision')"
				:disabled="isReadOnly"
				:class="{ 'ph-no-capture': shouldRedactValue }"
				@update:modelValue="onUpdateTextInput"
				@focus="setFocus"
				@blur="onBlur"
				@keydown.stop
				:title="displayTitle"
				:placeholder="parameter.placeholder"
			/>

			<credentials-select
				v-else-if="parameter.type === 'credentialsSelect' || parameter.name === 'genericAuthType'"
				ref="inputField"
				:parameter="parameter"
				:node="node"
				:activeCredentialType="activeCredentialType"
				:inputSize="inputSize"
				:displayValue="displayValue"
				:isReadOnly="isReadOnly"
				:displayTitle="displayTitle"
				@credentialSelected="credentialSelected"
				@update:modelValue="valueChanged"
				@setFocus="setFocus"
				@onBlur="onBlur"
			>
				<template #issues-and-options>
					<parameter-issues :issues="getIssues" />
				</template>
			</credentials-select>

			<n8n-select
				v-else-if="parameter.type === 'options'"
				ref="inputField"
				:size="inputSize"
				filterable
				:modelValue="displayValue"
				:placeholder="
					parameter.placeholder ? getPlaceholder() : i18n.baseText('parameterInput.select')
				"
				:loading="remoteParameterOptionsLoading"
				:disabled="isReadOnly || remoteParameterOptionsLoading"
				:title="displayTitle"
				@update:modelValue="valueChanged"
				@keydown.stop
				@focus="setFocus"
				@blur="onBlur"
			>
				<n8n-option
					v-for="option in parameterOptions"
					:value="option.value"
					:key="option.value"
					:label="getOptionsOptionDisplayName(option)"
				>
					<div class="list-option">
						<div
							class="option-headline"
							:class="{ 'remote-parameter-option': isRemoteParameterOption(option) }"
						>
							{{ getOptionsOptionDisplayName(option) }}
						</div>
						<div
							v-if="option.description"
							class="option-description"
							v-html="getOptionsOptionDescription(option)"
						></div>
					</div>
				</n8n-option>
			</n8n-select>

			<n8n-select
				v-else-if="parameter.type === 'multiOptions'"
				ref="inputField"
				:size="inputSize"
				filterable
				multiple
				:modelValue="displayValue"
				:loading="remoteParameterOptionsLoading"
				:disabled="isReadOnly || remoteParameterOptionsLoading"
				:title="displayTitle"
				:placeholder="i18n.baseText('parameterInput.select')"
				@update:modelValue="valueChanged"
				@keydown.stop
				@focus="setFocus"
				@blur="onBlur"
			>
				<n8n-option
					v-for="option in parameterOptions"
					:value="option.value"
					:key="option.value"
					:label="getOptionsOptionDisplayName(option)"
				>
					<div class="list-option">
						<div class="option-headline">{{ getOptionsOptionDisplayName(option) }}</div>
						<div
							v-if="option.description"
							class="option-description"
							v-html="getOptionsOptionDescription(option)"
						></div>
					</div>
				</n8n-option>
			</n8n-select>

			<!-- temporary state of booleans while data is mapped -->
			<n8n-input
				v-else-if="parameter.type === 'boolean' && droppable"
				:size="inputSize"
				:modelValue="JSON.stringify(displayValue)"
				:disabled="isReadOnly"
				:title="displayTitle"
			/>
			<el-switch
				v-else-if="parameter.type === 'boolean'"
				:class="{ 'switch-input': true, 'ph-no-capture': shouldRedactValue }"
				ref="inputField"
				active-color="#13ce66"
				:modelValue="displayValue"
				:disabled="isReadOnly"
				@update:modelValue="valueChanged"
			/>
		</div>

		<parameter-issues
			v-if="parameter.type !== 'credentialsSelect' && !isResourceLocatorParameter"
			:issues="getIssues"
		/>
	</div>
</template>

<script lang="ts">
/* eslint-disable prefer-spread */
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';

import { get } from 'lodash-es';

import type { INodeUi, INodeUpdatePropertiesInformation } from '@/Interface';
import type {
	ILoadOptions,
	INodeParameters,
	INodePropertyOptions,
	Workflow,
	INodeProperties,
	INodePropertyCollection,
	NodeParameterValueType,
	IParameterLabel,
	EditorType,
	CodeNodeEditorLanguage,
	IDataObject,
} from 'n8n-workflow';
import { NodeHelpers, CREDENTIAL_EMPTY_VALUE } from 'n8n-workflow';

import CredentialsSelect from '@/components/CredentialsSelect.vue';
import ExpressionEdit from '@/components/ExpressionEdit.vue';
import ParameterIssues from '@/components/ParameterIssues.vue';
import ResourceLocator from '@/components/ResourceLocator/ResourceLocator.vue';
import ExpressionParameterInput from '@/components/ExpressionParameterInput.vue';
import TextEdit from '@/components/TextEdit.vue';
import CodeNodeEditor from '@/components/CodeNodeEditor/CodeNodeEditor.vue';
import HtmlEditor from '@/components/HtmlEditor/HtmlEditor.vue';
import SqlEditor from '@/components/SqlEditor/SqlEditor.vue';

import { workflowHelpers } from '@/mixins/workflowHelpers';
import { hasExpressionMapping, isValueExpression } from '@/utils/nodeTypesUtils';
import { isResourceLocatorValue } from '@/utils/typeGuards';

import {
	CUSTOM_API_CALL_KEY,
	EXECUTE_WORKFLOW_NODE_TYPE,
	HTML_NODE_TYPE,
	NODES_USING_CODE_NODE_EDITOR,
} from '@/constants';

import type { PropType } from 'vue';
import { debounceHelper } from '@/mixins/debounce';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useCredentialsStore } from '@/stores/credentials.store';
import { useSettingsStore } from '@/stores/settings.store';
import { htmlEditorEventBus } from '@/event-bus';
import type { EventBus } from 'n8n-design-system/utils';
import { createEventBus } from 'n8n-design-system/utils';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useI18n } from '@/composables/useI18n';
import type { N8nInput } from 'n8n-design-system';
import { isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
import { useExternalHooks } from '@/composables/useExternalHooks';

type Picker = { $emit: (arg0: string, arg1: Date) => void };

export default defineComponent({
	name: 'parameter-input',
	mixins: [workflowHelpers, debounceHelper],
	components: {
		CodeNodeEditor,
		HtmlEditor,
		SqlEditor,
		ExpressionEdit,
		ExpressionParameterInput,
		CredentialsSelect,
		ParameterIssues,
		ResourceLocator,
		TextEdit,
	},
	props: {
		additionalExpressionData: {
			type: Object as PropType<IDataObject>,
			default: () => ({}),
		},
		isReadOnly: {
			type: Boolean,
		},
		isSingleLine: {
			type: Boolean,
		},
		parameter: {
			type: Object as PropType<INodeProperties>,
		},
		path: {
			type: String,
		},
		modelValue: {
			type: [String, Number, Boolean, Array, Object] as PropType<NodeParameterValueType>,
		},
		hideLabel: {
			type: Boolean,
		},
		droppable: {
			type: Boolean,
		},
		activeDrop: {
			type: Boolean,
		},
		forceShowExpression: {
			type: Boolean,
		},
		hint: {
			type: String as PropType<string | undefined>,
		},
		inputSize: {
			type: String,
		},
		hideIssues: {
			type: Boolean,
		},
		documentationUrl: {
			type: String as PropType<string | undefined>,
		},
		errorHighlight: {
			type: Boolean,
		},
		isForCredential: {
			type: Boolean,
		},
		eventSource: {
			type: String,
		},
		expressionEvaluated: {
			type: String as PropType<string | undefined>,
		},
		label: {
			type: Object as PropType<IParameterLabel>,
			default: () => ({
				size: 'small',
			}),
		},
		eventBus: {
			type: Object as PropType<EventBus>,
			default: () => createEventBus(),
		},
	},
	setup() {
		const externalHooks = useExternalHooks();
		const i18n = useI18n();
		const nodeHelpers = useNodeHelpers();

		return {
			externalHooks,
			i18n,
			nodeHelpers,
		};
	},
	data() {
		return {
			codeEditDialogVisible: false,
			nodeName: '',
			expressionAddOperation: 'set' as 'add' | 'set',
			expressionEditDialogVisible: false,
			remoteParameterOptions: [] as INodePropertyOptions[],
			remoteParameterOptionsLoading: false,
			remoteParameterOptionsLoadingIssues: null as string | null,
			textEditDialogVisible: false,
			editDialogClosing: false,
			tempValue: '', //  el-date-picker and el-input does not seem to work without v-model so add one
			CUSTOM_API_CALL_KEY,
			activeCredentialType: '',
			dateTimePickerOptions: {
				shortcuts: [
					{
						text: 'Today', // TODO

						onClick(picker: Picker) {
							picker.$emit('pick', new Date());
						},
					},
					{
						text: 'Yesterday', // TODO

						onClick(picker: Picker) {
							const date = new Date();
							date.setTime(date.getTime() - 3600 * 1000 * 24);
							picker.$emit('pick', date);
						},
					},
					{
						text: 'A week ago', // TODO

						onClick(picker: Picker) {
							const date = new Date();
							date.setTime(date.getTime() - 3600 * 1000 * 24 * 7);
							picker.$emit('pick', date);
						},
					},
				],
			},
			isFocused: false,
		};
	},
	watch: {
		async dependentParametersValues() {
			// Reload the remote parameters whenever a parameter
			// on which the current field depends on changes
			await this.loadRemoteParameterOptions();
		},
		modelValue() {
			if (this.parameter.type === 'color' && this.getArgument('showAlpha') === true) {
				// Do not set for color with alpha else wrong value gets displayed in field
				return;
			}
			this.tempValue = this.displayValue as string;
		},
	},
	computed: {
		...mapStores(
			useCredentialsStore,
			useNodeTypesStore,
			useNDVStore,
			useWorkflowsStore,
			useSettingsStore,
		),
		expressionDisplayValue(): string {
			if (this.forceShowExpression) {
				return '';
			}

			const value = isResourceLocatorValue(this.modelValue)
				? this.modelValue.value
				: this.modelValue;
			if (typeof value === 'string' && value.startsWith('=')) {
				return value.slice(1);
			}

			return `${this.displayValue ?? ''}`;
		},
		isValueExpression(): boolean {
			return isValueExpression(this.parameter, this.modelValue);
		},
		codeAutocomplete(): string | undefined {
			return this.getArgument('codeAutocomplete') as string | undefined;
		},
		dependentParametersValues(): string | null {
			const loadOptionsDependsOn = this.getArgument('loadOptionsDependsOn') as string[] | undefined;

			if (loadOptionsDependsOn === undefined) {
				return null;
			}

			// Get the resolved parameter values of the current node
			const currentNodeParameters = this.ndvStore.activeNode?.parameters;
			try {
				const resolvedNodeParameters = this.resolveParameter(currentNodeParameters);

				const returnValues: string[] = [];
				for (const parameterPath of loadOptionsDependsOn) {
					returnValues.push(get(resolvedNodeParameters, parameterPath) as string);
				}

				return returnValues.join('|');
			} catch (error) {
				return null;
			}
		},
		node(): INodeUi {
			return this.ndvStore.activeNode!;
		},
		displayTitle(): string {
			const interpolation = { interpolate: { shortPath: this.shortPath } };

			if (this.getIssues.length && this.isValueExpression) {
				return this.i18n.baseText('parameterInput.parameterHasIssuesAndExpression', interpolation);
			} else if (this.getIssues.length && !this.isValueExpression) {
				return this.i18n.baseText('parameterInput.parameterHasIssues', interpolation);
			} else if (!this.getIssues.length && this.isValueExpression) {
				return this.i18n.baseText('parameterInput.parameterHasExpression', interpolation);
			}

			return this.i18n.baseText('parameterInput.parameter', interpolation);
		},
		displayValue(): string | number | boolean | null {
			if (this.remoteParameterOptionsLoading) {
				// If it is loading options from server display
				// to user that the data is loading. If not it would
				// display the user the key instead of the value it
				// represents
				return this.i18n.baseText('parameterInput.loadingOptions');
			}

			// if the value is marked as empty return empty string, to prevent displaying the asterisks
			if (this.modelValue === CREDENTIAL_EMPTY_VALUE) {
				return '';
			}

			let returnValue;
			if (!this.isValueExpression) {
				returnValue = this.isResourceLocatorParameter
					? isResourceLocatorValue(this.modelValue)
						? this.modelValue.value
						: ''
					: this.modelValue;
			} else {
				returnValue = this.expressionEvaluated;
			}

			if (this.parameter.type === 'credentialsSelect' && typeof this.modelValue === 'string') {
				const credType = this.credentialsStore.getCredentialTypeByName(this.modelValue);
				if (credType) {
					returnValue = credType.displayName;
				}
			}

			if (
				Array.isArray(returnValue) &&
				this.parameter.type === 'color' &&
				this.getArgument('showAlpha') === true &&
				returnValue.charAt(0) === '#'
			) {
				// Convert the value to rgba that el-color-picker can display it correctly
				const bigint = parseInt(returnValue.slice(1), 16);
				const h = [];
				h.push((bigint >> 24) & 255);
				h.push((bigint >> 16) & 255);
				h.push((bigint >> 8) & 255);
				h.push(((255 - bigint) & 255) / 255);

				returnValue = 'rgba(' + h.join() + ')';
			}

			if (returnValue !== undefined && returnValue !== null && this.parameter.type === 'string') {
				const rows = this.getArgument('rows');
				if (rows === undefined || rows === 1) {
					returnValue = returnValue.toString().replace(/\n/, '|');
				}
			}

			return returnValue;
		},
		getStringInputType() {
			if (this.getArgument('password') === true) {
				return 'password';
			}

			const rows = this.getArgument('rows');
			if (rows !== undefined && rows > 1) {
				return 'textarea';
			}

			if (this.editorType === 'code') {
				return 'textarea';
			}

			return 'text';
		},
		getIssues(): string[] {
			if (this.hideIssues || this.node === null) {
				return [];
			}

			const newPath = this.shortPath.split('.');
			newPath.pop();

			const issues = NodeHelpers.getParameterIssues(
				this.parameter,
				this.node.parameters,
				newPath.join('.'),
				this.node,
			);

			if (this.parameter.type === 'credentialsSelect' && this.displayValue === '') {
				issues.parameters = issues.parameters || {};

				const issue = this.i18n.baseText('parameterInput.selectACredentialTypeFromTheDropdown');

				issues.parameters[this.parameter.name] = [issue];
			} else if (
				['options', 'multiOptions'].includes(this.parameter.type) &&
				!this.remoteParameterOptionsLoading &&
				this.remoteParameterOptionsLoadingIssues === null &&
				this.parameterOptions
			) {
				// Check if the value resolves to a valid option
				// Currently it only displays an error in the node itself in
				// case the value is not valid. The workflow can still be executed
				// and the error is not displayed on the node in the workflow
				const validOptions = this.parameterOptions.map(
					(options) => (options as INodePropertyOptions).value,
				);

				const checkValues: string[] = [];

				if (!this.skipCheck(this.displayValue)) {
					if (Array.isArray(this.displayValue)) {
						checkValues.push.apply(checkValues, this.displayValue);
					} else {
						checkValues.push(this.displayValue as string);
					}
				}

				for (const checkValue of checkValues) {
					if (checkValue === null || !validOptions.includes(checkValue)) {
						if (issues.parameters === undefined) {
							issues.parameters = {};
						}

						const issue = this.i18n.baseText('parameterInput.theValueIsNotSupported', {
							interpolate: { checkValue },
						});

						issues.parameters[this.parameter.name] = [issue];
					}
				}
			} else if (this.remoteParameterOptionsLoadingIssues !== null && !this.isValueExpression) {
				if (issues.parameters === undefined) {
					issues.parameters = {};
				}
				issues.parameters[this.parameter.name] = [
					`There was a problem loading the parameter options from server: "${this.remoteParameterOptionsLoadingIssues}"`,
				];
			}

			if (issues?.parameters?.[this.parameter.name] !== undefined) {
				return issues.parameters[this.parameter.name];
			}

			return [];
		},
		editorType(): EditorType {
			return this.getArgument('editor') as EditorType;
		},
		editorLanguage(): CodeNodeEditorLanguage {
			if (this.editorType === 'json' || this.parameter.type === 'json') return 'json';
			return (this.getArgument('editorLanguage') as CodeNodeEditorLanguage) ?? 'javaScript';
		},
		parameterOptions():
			| Array<INodePropertyOptions | INodeProperties | INodePropertyCollection>
			| undefined {
			if (!this.hasRemoteMethod) {
				// Options are already given
				return this.parameter.options;
			}

			// Options get loaded from server
			return this.remoteParameterOptions;
		},
		parameterInputClasses() {
			const classes: { [c: string]: boolean } = {
				droppable: this.droppable,
				activeDrop: this.activeDrop,
			};

			const rows = this.getArgument('rows');
			const isTextarea = this.parameter.type === 'string' && rows !== undefined;
			const isSwitch = this.parameter.type === 'boolean' && !this.isValueExpression;

			if (!isTextarea && !isSwitch) {
				classes['parameter-value-container'] = true;
			}

			if (
				!this.droppable &&
				!this.activeDrop &&
				(this.getIssues.length > 0 || this.errorHighlight) &&
				!this.isValueExpression
			) {
				classes['has-issues'] = true;
			}

			return classes;
		},
		parameterInputWrapperStyle() {
			let deductWidth = 0;
			const styles = {
				width: '100%',
			};
			if (this.parameter.type === 'credentialsSelect' || this.isResourceLocatorParameter) {
				return styles;
			}
			if (this.getIssues.length) {
				deductWidth += 20;
			}

			if (deductWidth !== 0) {
				styles.width = `calc(100% - ${deductWidth}px)`;
			}

			return styles;
		},
		hasRemoteMethod(): boolean {
			return !!this.getArgument('loadOptionsMethod') || !!this.getArgument('loadOptions');
		},
		shortPath(): string {
			const shortPath = this.path.split('.');
			shortPath.shift();
			return shortPath.join('.');
		},
		workflow(): Workflow {
			return this.getCurrentWorkflow();
		},
		isResourceLocatorParameter(): boolean {
			return this.parameter.type === 'resourceLocator';
		},
		isSecretParameter(): boolean {
			return this.getArgument('password') === true;
		},
		remoteParameterOptionsKeys(): string[] {
			return (this.remoteParameterOptions || []).map((o) => o.name);
		},
		shouldRedactValue(): boolean {
			return this.getStringInputType === 'password' || this.isForCredential;
		},
	},
	methods: {
		isRemoteParameterOption(option: INodePropertyOptions) {
			return this.remoteParameterOptionsKeys.includes(option.name);
		},
		credentialSelected(updateInformation: INodeUpdatePropertiesInformation) {
			// Update the values on the node
			this.workflowsStore.updateNodeProperties(updateInformation);

			const node = this.workflowsStore.getNodeByName(updateInformation.name);

			if (node) {
				// Update the issues
				this.nodeHelpers.updateNodeCredentialIssues(node);
			}

			void this.externalHooks.run('nodeSettings.credentialSelected', { updateInformation });
		},
		/**
		 * Check whether a param value must be skipped when collecting node param issues for validation.
		 */
		skipCheck(value: string | number | boolean | null) {
			return typeof value === 'string' && value.includes(CUSTOM_API_CALL_KEY);
		},
		getPlaceholder(): string {
			return this.isForCredential
				? this.i18n.credText().placeholder(this.parameter)
				: this.i18n.nodeText().placeholder(this.parameter, this.path);
		},
		getOptionsOptionDisplayName(option: INodePropertyOptions): string {
			return this.isForCredential
				? this.i18n.credText().optionsOptionDisplayName(this.parameter, option)
				: this.i18n.nodeText().optionsOptionDisplayName(this.parameter, option, this.path);
		},
		getOptionsOptionDescription(option: INodePropertyOptions): string {
			return this.isForCredential
				? this.i18n.credText().optionsOptionDescription(this.parameter, option)
				: this.i18n.nodeText().optionsOptionDescription(this.parameter, option, this.path);
		},

		async loadRemoteParameterOptions() {
			if (
				this.node === null ||
				!this.hasRemoteMethod ||
				this.remoteParameterOptionsLoading ||
				!this.parameter
			) {
				return;
			}
			this.remoteParameterOptionsLoadingIssues = null;
			this.remoteParameterOptionsLoading = true;
			this.remoteParameterOptions.length = 0;

			// Get the resolved parameter values of the current node

			try {
				const currentNodeParameters = (this.ndvStore.activeNode as INodeUi).parameters;
				const resolvedNodeParameters = this.resolveRequiredParameters(
					this.parameter,
					currentNodeParameters,
				) as INodeParameters;
				const loadOptionsMethod = this.getArgument('loadOptionsMethod') as string | undefined;
				const loadOptions = this.getArgument('loadOptions') as ILoadOptions | undefined;

				const options = await this.nodeTypesStore.getNodeParameterOptions({
					nodeTypeAndVersion: {
						name: this.node.type,
						version: this.node.typeVersion,
					},
					path: this.path,
					methodName: loadOptionsMethod,
					loadOptions,
					currentNodeParameters: resolvedNodeParameters,
					credentials: this.node.credentials,
				});

				this.remoteParameterOptions.push.apply(this.remoteParameterOptions, options);
			} catch (error) {
				this.remoteParameterOptionsLoadingIssues = error.message;
			}

			this.remoteParameterOptionsLoading = false;
		},
		closeCodeEditDialog() {
			this.codeEditDialogVisible = false;

			this.editDialogClosing = true;
			void this.$nextTick(() => {
				this.editDialogClosing = false;
			});
		},
		closeExpressionEditDialog() {
			this.expressionEditDialogVisible = false;
		},
		trackExpressionEditOpen() {
			if (!this.node) {
				return;
			}

			if (this.node.type.startsWith('n8n-nodes-base') || isCredentialOnlyNodeType(this.node.type)) {
				this.$telemetry.track('User opened Expression Editor', {
					node_type: this.node.type,
					parameter_name: this.parameter.displayName,
					parameter_field_type: this.parameter.type,
					new_expression: !this.isValueExpression,
					workflow_id: this.workflowsStore.workflowId,
					session_id: this.ndvStore.sessionId,
					source: this.eventSource || 'ndv',
				});
			}
		},
		closeTextEditDialog() {
			this.textEditDialogVisible = false;

			this.editDialogClosing = true;
			void this.$nextTick(() => {
				this.$refs.inputField?.blur?.();
				this.editDialogClosing = false;
			});
		},
		displayEditDialog() {
			if (this.editDialogClosing) {
				return;
			}

			if (this.editorType) {
				this.codeEditDialogVisible = true;
			} else {
				this.textEditDialogVisible = true;
			}
		},
		getArgument(argumentName: string): string | number | boolean | undefined {
			return this.parameter.typeOptions?.[argumentName];
		},
		expressionUpdated(value: string) {
			const val: NodeParameterValueType = this.isResourceLocatorParameter
				? { __rl: true, value, mode: this.modelValue.mode }
				: value;
			this.valueChanged(val);
		},
		openExpressionEditorModal() {
			if (!this.isValueExpression) return;

			this.expressionEditDialogVisible = true;
			this.trackExpressionEditOpen();
		},
		onBlur() {
			this.$emit('blur');
			this.isFocused = false;
		},
		onResourceLocatorDrop(data: string) {
			this.$emit('drop', data);
		},
		async setFocus(event: MouseEvent) {
			if (['json'].includes(this.parameter.type) && this.getArgument('alwaysOpenEditWindow')) {
				this.displayEditDialog();
				return;
			}

			if (this.node !== null) {
				// When an event like mouse-click removes the active node while
				// editing is active it does not know where to save the value to.
				// For that reason do we save the node-name here. We could probably
				// also just do that once on load but if Vue decides for some reason to
				// reuse the input it could have the wrong value so lets set it everytime
				// just to be sure
				this.nodeName = this.node.name;
			}

			await this.$nextTick();

			// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
			const inputRef = this.$refs.inputField as InstanceType<N8nInput> | undefined;
			if (inputRef?.$el) {
				if (inputRef.focusOnInput) {
					inputRef.focusOnInput();
				} else if (inputRef.focus) {
					inputRef.focus();
				}

				this.isFocused = true;
			}

			this.$emit('focus');
		},
		isCodeNode(node: INodeUi): boolean {
			return NODES_USING_CODE_NODE_EDITOR.includes(node.type);
		},
		isHtmlNode(node: INodeUi): boolean {
			return node.type === HTML_NODE_TYPE;
		},
		isExecuteWorkflowNode(node: INodeUi): boolean {
			return node.type === EXECUTE_WORKFLOW_NODE_TYPE;
		},
		rgbaToHex(value: string): string | null {
			// Convert rgba to hex from: https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
			const valueMatch = value.match(/^rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+(\.\d+)?)\)$/);
			if (valueMatch === null) {
				// TODO: Display something if value is not valid
				return null;
			}
			const [r, g, b, a] = valueMatch.splice(1, 4).map((v) => Number(v));
			return (
				'#' +
				((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1) +
				((1 << 8) + Math.floor((1 - a) * 255)).toString(16).slice(1)
			);
		},
		onTextInputChange(value: string) {
			const parameterData = {
				node: this.node !== null ? this.node.name : this.nodeName,
				name: this.path,
				value,
			};

			this.$emit('textInput', parameterData);
		},
		valueChangedDebounced(value: NodeParameterValueType | {} | Date) {
			void this.callDebounced('valueChanged', { debounceTime: 100 }, value);
		},
		onUpdateTextInput(value: string) {
			this.valueChanged(value);
			this.onTextInputChange(value);
		},
		valueChanged(value: NodeParameterValueType | {} | Date) {
			if (this.parameter.name === 'nodeCredentialType') {
				this.activeCredentialType = value as string;
			}

			if (value instanceof Date) {
				value = value.toISOString();
			}

			if (
				this.parameter.type === 'color' &&
				this.getArgument('showAlpha') === true &&
				value !== null &&
				value !== undefined &&
				value.toString().charAt(0) !== '#'
			) {
				const newValue = this.rgbaToHex(value as string);
				if (newValue !== null) {
					this.tempValue = newValue;
					value = newValue;
				}
			}

			const parameterData = {
				node: this.node !== null ? this.node.name : this.nodeName,
				name: this.path,
				value,
			};

			this.$emit('update', parameterData);

			if (this.parameter.name === 'operation' || this.parameter.name === 'mode') {
				this.$telemetry.track('User set node operation or mode', {
					workflow_id: this.workflowsStore.workflowId,
					node_type: this.node && this.node.type,
					resource: this.node && this.node.parameters.resource,
					is_custom: value === CUSTOM_API_CALL_KEY,
					session_id: this.ndvStore.sessionId,
					parameter: this.parameter.name,
				});
			}
		},
		async optionSelected(command: string) {
			const prevValue = this.modelValue;

			if (command === 'resetValue') {
				this.valueChanged(this.parameter.default);
			} else if (command === 'addExpression') {
				if (this.isResourceLocatorParameter) {
					if (isResourceLocatorValue(this.modelValue)) {
						this.valueChanged({
							__rl: true,
							value: `=${this.modelValue.value}`,
							mode: this.modelValue.mode,
						});
					} else {
						this.valueChanged({ __rl: true, value: `=${this.modelValue}`, mode: '' });
					}
				} else if (
					this.parameter.type === 'number' &&
					(!this.modelValue || this.modelValue === '[Object: null]')
				) {
					this.valueChanged('={{ 0 }}');
				} else if (this.parameter.type === 'number' || this.parameter.type === 'boolean') {
					this.valueChanged(`={{ ${this.modelValue} }}`);
				} else {
					this.valueChanged(`=${this.modelValue}`);
				}

				await this.setFocus();
			} else if (command === 'removeExpression') {
				let value: NodeParameterValueType = this.expressionEvaluated;

				this.isFocused = false;

				if (this.parameter.type === 'multiOptions' && typeof value === 'string') {
					value = (value || '')
						.split(',')
						.filter((value) =>
							(this.parameterOptions || []).find(
								(option) => (option as INodePropertyOptions).value === value,
							),
						);
				}

				if (this.isResourceLocatorParameter && isResourceLocatorValue(this.modelValue)) {
					this.valueChanged({ __rl: true, value, mode: this.modelValue.mode });
				} else {
					let newValue = typeof value !== 'undefined' ? value : null;

					if (this.parameter.type === 'string') {
						// Strip the '=' from the beginning
						newValue = this.modelValue ? this.modelValue.toString().substring(1) : null;
					}

					this.valueChanged(newValue);
				}
			} else if (command === 'refreshOptions') {
				if (this.isResourceLocatorParameter) {
					this.eventBus.emit('refreshList');
				}
				void this.loadRemoteParameterOptions();
			} else if (command === 'formatHtml') {
				htmlEditorEventBus.emit('format-html');
			}

			if (this.node && (command === 'addExpression' || command === 'removeExpression')) {
				const telemetryPayload = {
					node_type: this.node.type,
					parameter: this.path,
					old_mode: command === 'addExpression' ? 'fixed' : 'expression',
					new_mode: command === 'removeExpression' ? 'fixed' : 'expression',
					was_parameter_empty: prevValue === '' || prevValue === undefined,
					had_mapping: hasExpressionMapping(prevValue),
					had_parameter: typeof prevValue === 'string' && prevValue.includes('$parameter'),
				};
				this.$telemetry.track('User switched parameter mode', telemetryPayload);
				void this.externalHooks.run('parameterInput.modeSwitch', telemetryPayload);
			}
		},
	},
	async updated() {
		await this.$nextTick();
		const remoteParameterOptions = this.$el.querySelectorAll('.remote-parameter-option');

		if (remoteParameterOptions.length > 0) {
			void this.externalHooks.run('parameterInput.updated', { remoteParameterOptions });
		}
	},
	mounted() {
		this.eventBus.on('optionSelected', this.optionSelected);

		this.tempValue = this.displayValue as string;
		if (this.node !== null) {
			this.nodeName = this.node.name;
		}

		if (this.node && this.node.parameters.authentication === 'predefinedCredentialType') {
			this.activeCredentialType = this.node.parameters.nodeCredentialType as string;
		}

		if (
			this.parameter.type === 'color' &&
			this.getArgument('showAlpha') === true &&
			this.displayValue !== null &&
			this.displayValue.toString().charAt(0) !== '#'
		) {
			const newValue = this.rgbaToHex(this.displayValue as string);
			if (newValue !== null) {
				this.tempValue = newValue;
			}
		}

		if (this.hasRemoteMethod && this.node !== null) {
			// Make sure to load the parameter options
			// directly and whenever the credentials change
			this.$watch(
				() => this.node?.credentials,
				() => {
					void this.loadRemoteParameterOptions();
				},
				{ immediate: true },
			);
		}

		void this.externalHooks.run('parameterInput.mount', {
			parameter: this.parameter,
			inputFieldRef: this.$refs.inputField as InstanceType<typeof N8nInput>,
		});
	},
	beforeUnmount() {
		this.eventBus.off('optionSelected', this.optionSelected);
	},
});
</script>

<style scoped lang="scss">
.readonly-code {
	font-size: var(--font-size-xs);
}

.switch-input {
	margin: var(--spacing-5xs) 0 var(--spacing-2xs) 0;
}

.parameter-value-container {
	display: flex;
	align-items: center;
}

.parameter-actions {
	display: inline-flex;
	align-items: center;
}

.parameter-input {
	display: inline-block;

	:deep(.color-input) {
		display: flex;

		.el-color-picker__trigger {
			border: none;
		}
	}
}
</style>

<style lang="scss">
.ql-editor {
	padding: 6px;
	line-height: 26px;
	background-color: #f0f0f0;
}

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

	textarea,
	input,
	.cm-editor {
		border-width: 1.5px;
	}
}

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

	textarea,
	input {
		cursor: grabbing !important;
		border-width: 1px;
	}
}

.has-issues {
	--input-border-color: var(--color-danger);
}

.el-dropdown {
	color: var(--color-text-light);
}

.list-option {
	margin: 6px 0;
	white-space: normal;
	padding-right: 20px;

	.option-headline {
		font-weight: var(--font-weight-bold);
		line-height: var(--font-line-height-regular);
		overflow-wrap: break-word;
	}

	.option-description {
		margin-top: 2px;
		font-size: var(--font-size-2xs);
		font-weight: var(--font-weight-regular);
		line-height: var(--font-line-height-xloose);
		color: $custom-font-very-light;
	}
}

.edit-window-button {
	display: none;
}

.parameter-input:hover .edit-window-button {
	display: inline;
}

.expand-input-icon-container {
	display: flex;
	height: 100%;
	align-items: center;
}

.input-with-opener .el-input__suffix {
	right: 0;
}

.el-input--suffix .el-input__inner {
	padding-right: 0;
}

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

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

		&:hover {
			color: var(--color-primary);
		}
	}
}

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

.invalid {
	border-color: var(--color-danger);
}
</style>
