<script setup lang="ts">
import { nextTick, onMounted, reactive, ref, watch, onBeforeUnmount, computed } from 'vue'
import { useLocalEditorTheme } from '@/utils/useLocalEditorTheme'
import type { editor } from 'monaco-editor'
import type { CSSProperties } from 'vue'
import { ElMessage } from 'element-plus'
import * as monaco from 'monaco-editor'
import { IconButton } from '@repo/ui'
import { debounce } from 'lodash-es'
import { useI18n } from '@/composables/useI18n'
import {
	clearCodeEditorCompletionConfig,
	shouldTriggerCodeEditorCompletion,
	syncCodeEditorCompletionConfig,
	type CodeEditorCompletionConfig
} from './codeEditorCompletion'

interface CodeEditorType {
	// 是否展示工具栏
	tools?: boolean
	// 是否展示全屏工具
	allowFullscreen?: boolean
	// 是否展示复制工具
	copyCode?: boolean
	// 是否自动切换主题
	autoToggleTheme?: boolean
	// 挂载DOM
	appendTo?: string | HTMLElement
	// 内容
	modelValue?: any
	// 语法
	language?: string
	// 主题
	theme?: 'vs-light' | 'vs-dark'
	// 是否只读
	readOnly?: boolean
	// 是否显示行号
	lineNumbers?: 'on' | 'off'
	// 设置代码模块高度，总高度多40px
	height?: number
	// 返回结果为string,也可转化为json
	formatValue?: 'string' | 'json'
	// 插件内部配置
	config?: editor.IStandaloneEditorConstructionOptions
	// 是否可以切换语言
	allowChangeLanguage?: boolean
	completionConfig?: CodeEditorCompletionConfig
	// 自定义样式
	style?: CSSProperties
	// 自定义class
	class?: string
}

const props = withDefaults(defineProps<CodeEditorType>(), {
	//默认配置
	tools: true,
	copyCode: true,
	readOnly: false,
	allowFullscreen: true,
	autoToggleTheme: true,
	allowChangeLanguage: true,
	language: 'javascript',
	lineNumbers: 'on',
	theme: 'vs-light',
	formatValue: 'string',
	height: 150,
	completionConfig: () => ({}),
	config: () => ({
		minimap: { enabled: false },
		selectOnLineNumbers: true
	})
})

const emit = defineEmits(['update:modelValue', 'update:language'])
const { t } = useI18n()

let monacoEditor: monaco.editor.IStandaloneCodeEditor | null = null
let model: editor.ITextModel | null = null
let layoutFrame = 0
const editContainer = ref<HTMLElement>()
const bodyHeight = ref(props.height)
const resizeState = reactive({
	startY: 0,
	startHeight: props.height,
	isDragging: false
})
// json文件配置
const jsonCompletionConfig = ref<CodeEditorCompletionConfig>()

const isFullScreen = ref(false)
const { monacoTheme, themeClass, toggleTheme } = useLocalEditorTheme({
	defaultTheme: props.theme,
	autoToggleTheme: props.autoToggleTheme
})

let componentConfig = reactive({
	language: props.language
})

const languageSource = [
	{ id: 'javascript', name: 'javascript' },
	{ id: 'python', name: 'python' },
	{ id: 'json', name: 'json' },
	{ id: 'java', name: 'java' }
]

const getCompletionConfig = computed((): CodeEditorCompletionConfig => {
	return {
		...(jsonCompletionConfig.value || {}),
		...props.completionConfig
	}
})

/**
 * @description: formatValue为json 格式，需要转换处理
 * @return
 */
const formatValue = (value: any) => {
	if (props.formatValue === 'json') {
		return JSON.stringify(value ?? {}, null, 2)
	}
	return value ?? ''
}

const syncCompletionConfig = () => {
	if (!model) return
	syncCodeEditorCompletionConfig(model, componentConfig.language, getCompletionConfig.value)
}

const getJsonCompletionConfig = () => {
	fetch('/Content/Lib/monacoEditor/completion/java.json')
		.then((res) => res.json())
		.then((data) => {
			jsonCompletionConfig.value = data?.java
			syncCompletionConfig()
		})
		.catch(() => {
			console.warn('Failed to load Java completion config.')
		})
}

getJsonCompletionConfig()

onMounted(() => {
	// 处理代码转换
	model = monaco.editor.createModel(formatValue(props.modelValue), componentConfig.language)
	syncCompletionConfig()

	// 接入配置
	monacoEditor = monaco.editor.create(editContainer.value!, {
		model,
		wordWrap: 'on',
		automaticLayout: true,
		theme: monacoTheme.value,
		readOnly: props.readOnly,
		lineNumbers: props.lineNumbers,
		...props.config
	})

	//  添加空格
	monacoEditor.onKeyDown((e) => {
		if (e.keyCode === monaco.KeyCode.Space) {
			e.preventDefault()
			monacoEditor?.trigger('keyboard', 'type', { text: ' ' })
		}
	})

	// 监听代码输入
	monacoEditor.onDidChangeModelContent(updateModelValue)
	monacoEditor.onDidChangeModelContent((event) => {
		if (!monacoEditor || !model) return

		const lastChange = event.changes[event.changes.length - 1]
		if (!lastChange?.text) return

		const triggerCharacters = new Set(['.'])
		const languageRules = getCompletionConfig.value?.[componentConfig.language] || []
		languageRules.forEach((rule) => {
			;(rule.triggerCharacters || []).forEach((char) => triggerCharacters.add(char))
		})

		if (![...triggerCharacters].some((char) => lastChange.text.endsWith(char))) {
			return
		}

		const position = monacoEditor.getPosition()
		if (!position) return

		if (
			shouldTriggerCodeEditorCompletion(
				model,
				position,
				componentConfig.language,
				getCompletionConfig.value
			)
		) {
			monacoEditor.trigger('completion', 'editor.action.triggerSuggest', {})
		}
	})
})

// 读取传值数据
watch(
	() => props.modelValue,
	(value) => {
		nextTick(() => {
			if (!model) return
			const next = formatValue(value)
			if (model.getValue() !== next) {
				model.setValue(next)
			}
		})
	},
	{ immediate: true }
)

// 切换语法，触发回调
watch(
	() => componentConfig.language,
	(lang) => {
		nextTick(() => {
			if (!model) return
			monaco.editor.setModelLanguage(model, lang)
			syncCompletionConfig()
			emit('update:language', lang)
		})
	},
	{ immediate: true }
)

watch(
	() => getCompletionConfig.value,
	() => {
		syncCompletionConfig()
	},
	{ deep: true }
)

// 监听全屏, 重新计算layout
watch(isFullScreen, () => {
	nextTick(() => {
		monacoEditor?.layout()
	})
})

watch(
	() => props.height,
	(height) => {
		if (bodyHeight.value < height) {
			bodyHeight.value = height
		}
		nextTick(() => {
			monacoEditor?.layout()
		})
	}
)

/**
 * @description: 发送回调(formatValue:json，需要转换处理)
 * @return {*}
 */
const updateModelValue = debounce(() => {
	if (!model) return
	const value = model.getValue()

	if (props.formatValue === 'json') {
		try {
			emit('update:modelValue', JSON.parse(value))
		} catch {
			return ElMessage.warning(t('common.nodeBase.codeEditor.jsonSyntaxError'))
		}
	} else {
		emit('update:modelValue', value)
	}
}, 1000)

/**
 * @description: 复制代码到粘贴板
 * @return {*}
 */
const copyCode = () => {
	const text = model?.getValue()
	if (!text) return
	try {
		navigator.clipboard.writeText(text).then(() => {
			ElMessage.success(t('common.nodeBase.codeEditor.copySuccess'))
		})
	} catch {
		const textarea = document.createElement('textarea')
		textarea.value = text
		document.body.appendChild(textarea)
		textarea.select()
		document.execCommand('copy')
		textarea.remove()
		ElMessage.success(t('common.nodeBase.codeEditor.copySuccess'))
	}
}

/**
 * @description: 设置全屏样式
 * @return {*}
 */
const fullScreenStyle = computed(() => {
	if (isFullScreen.value) return { ...props.style }

	return {
		height: `${bodyHeight.value + 40}px`,
		...props.style
	}
})
/**
 * @description: 切换主题， 并设置对应样式
 * @return {*}
 */
const toolTipClass = computed(() => {
	return `editor-tooltip editor-tooltip--${themeClass.value}`
})
const onToggleTheme = () => {
	toggleTheme()
}

const syncEditorLayout = () => {
	if (layoutFrame) {
		cancelAnimationFrame(layoutFrame)
	}
	layoutFrame = requestAnimationFrame(() => {
		layoutFrame = 0
		monacoEditor?.layout()
	})
}

const stopResize = () => {
	if (!resizeState.isDragging) return
	resizeState.isDragging = false
	document.body.style.userSelect = ''
	document.body.style.cursor = ''
	window.removeEventListener('mousemove', onResize)
	window.removeEventListener('mouseup', stopResize)
}

const onResize = (event: MouseEvent) => {
	if (!resizeState.isDragging || isFullScreen.value) return
	const nextHeight = resizeState.startHeight + event.clientY - resizeState.startY
	const targetHeight = Math.max(props.height, nextHeight)
	if (targetHeight === bodyHeight.value) return
	bodyHeight.value = targetHeight
	syncEditorLayout()
}

const onResizeStart = (event: MouseEvent) => {
	if (isFullScreen.value) return
	resizeState.startY = event.clientY
	resizeState.startHeight = bodyHeight.value
	resizeState.isDragging = true
	document.body.style.userSelect = 'none'
	document.body.style.cursor = 'ns-resize'
	window.addEventListener('mousemove', onResize)
	window.addEventListener('mouseup', stopResize)
}

/**
 * 设置文本
 * @param text
 */
function setValue(text: string) {
	monacoEditor?.setValue(text || '')
}

/**
 * 光标处插入文本
 * @param text
 */
function insertText(text: string) {
	// 获取光标位置
	const position = monacoEditor?.getPosition()
	// 未获取到光标位置信息
	if (!position) {
		return
	}
	// 插入
	monacoEditor?.executeEdits('', [
		{
			range: new monaco.Range(
				position.lineNumber,
				position.column,
				position.lineNumber,
				position.column
			),
			text
		}
	])
	// 设置新的光标位置
	monacoEditor?.setPosition({
		...position,
		column: position.column + text.length
	})
	// 重新聚焦
	monacoEditor?.focus()
}

// 销毁model
onBeforeUnmount(() => {
	stopResize()
	if (layoutFrame) {
		cancelAnimationFrame(layoutFrame)
	}
	if (model) {
		clearCodeEditorCompletionConfig(model)
	}
	monacoEditor?.dispose()
	model?.dispose()
})

defineExpose({
	insertText,
	setValue
})
</script>
<template>
	<Teleport :disabled="!appendTo" :to="appendTo">
		<div
			class="monacoEditor !m-0"
			:class="[
				themeClass,
				{ 'is-fullscreen': isFullScreen, 'is-resizing': resizeState.isDragging }
			]"
			:style="fullScreenStyle"
		>
			<div class="tools h-[33px] flex items-center justify-between gap-2" v-if="props.tools">
				<!-- 语法切换 -->
				<ElTooltip
					:effect="themeClass"
					:popper-class="toolTipClass"
					placement="top"
					:content="t('common.nodeBase.codeEditor.switchLanguage')"
				>
					<div class="w-1/3">
						<ElSelect v-if="allowChangeLanguage" v-model="componentConfig.language">
							<ElOption v-for="value in languageSource" :label="value.name" :value="value.id" />
						</ElSelect>
					</div>
				</ElTooltip>
				<div class="flex-1 flex items-center justify-end gap-1">
					<!-- 放大/缩小 -->
					<ElTooltip
						:effect="themeClass"
						placement="top"
						:popper-class="toolTipClass"
						:content="
							!isFullScreen
								? t('common.nodeBase.codeEditor.enterFullscreen')
								: t('common.nodeBase.codeEditor.exitFullscreen')
						"
					>
						<div
							class="cursor-pointer text-xl text-center px-2"
							@click="isFullScreen = !isFullScreen"
							v-if="props.allowFullscreen"
						>
							<IconButton
								:icon="isFullScreen ? 'lucide:minimize' : 'lucide:fullscreen'"
								link
								class="fullscreen"
							/>
						</div>
					</ElTooltip>

					<!-- copy -->
					<ElTooltip
						:effect="themeClass"
						:popper-class="toolTipClass"
						placement="top"
						:content="t('common.nodeBase.codeEditor.copy')"
					>
						<div class="copy text-center px-2" @click="copyCode" v-if="props.copyCode">
							<IconButton icon="lucide:copy" link class="copyIcon"></IconButton>
						</div>
					</ElTooltip>

					<!-- 主题 -->
					<ElTooltip
						:effect="themeClass"
						:popper-class="toolTipClass"
						placement="top"
						:content="t('common.nodeBase.codeEditor.theme')"
					>
						<div class="copy text-center px-2" @click="onToggleTheme">
							<IconButton
								:icon="themeClass === 'dark' ? 'lucide:moon' : 'lucide:sun'"
								link
								class="themeIcon"
							>
							</IconButton>
						</div>
					</ElTooltip>
				</div>
			</div>
			<!-- 编辑器 -->
			<div class="editor-wrapper">
				<div ref="editContainer" class="code-editor"></div>
				<div v-if="!isFullScreen" class="resize-handle" @mousedown.prevent="onResizeStart"></div>
			</div>
		</div>
	</Teleport>
</template>
<style lang="less">
.editor-tooltip {
	border-radius: 6px;
	font-size: 12px;
	padding: 6px 10px;
}

// light
.editor-tooltip--light {
	background-color: var(--bg-base) !important;
	color: #1e1e1e !important;
	border: 1px solid #e5e7eb;

	.el-popper__arrow::before {
		background: var(--bg-base) !important;
		border: 1px solid #e5e7eb;
	}
}

// dark
.editor-tooltip--dark {
	background-color: #1e1e1e !important;
	color: var(--bg-base) !important;

	.el-popper__arrow::before {
		background: #1e1e1e !important;
	}
}
</style>
<style lang="less" scoped>
.monacoEditor {
	border: 1px solid #dcdfe6;
	border-radius: 4px;
	overflow: hidden;
	display: flex;
	flex-direction: column;
	transition:
		height 0.25s ease,
		inset 0.25s ease,
		background-color 0.2s;

	&.is-resizing {
		transition:
			inset 0.25s ease,
			background-color 0.2s;
	}

	&.is-fullscreen {
		position: absolute;
		inset: 0;
		z-index: 1000;
	}

	.tools {
		border-bottom: 1px solid #eeeeee83;
		padding-bottom: 6px;

		::v-deep(.el-select .el-select__wrapper) {
			border: none;
			box-shadow: none;
			background-color: var(--bg-base);
		}

		::v-deep(.el-select .el-select__placeholder) {
			color: #1e1e1e;
		}
	}

	.editor-wrapper {
		flex: 1;
		overflow: hidden;
		position: relative;
		.resize-handle {
			position: absolute;
			bottom: 2px;
			left: 50%;
			transform: translateX(-50%);
			width: 18px;
			height: 4px;
			background-color: #d0d5dd;
			border-radius: 8px;
			cursor: ns-resize;
		}
	}

	.code-editor {
		width: 100%;
		height: 100%;

		::v-deep(.monaco-editor) {
			height: 100%;
			background-color: #ffffff;
		}

		&.bordered {
			border: 1px solid var(--epic-border-color);
		}
	}
}

.light {
	background-color: #ffffff;
	color: #1e1e1e;

	&:hover {
		color: #1e1e1e;
	}

	.tools {
		::v-deep(.el-select .el-select__wrapper) {
			border: none;
			box-shadow: none;
			background-color: #ffffff;
		}

		::v-deep(.el-select .el-select__placeholder) {
			color: #1e1e1e;
		}

		.fullscreen,
		.fullscreen-exit,
		.themeIcon,
		.copyIcon {
			color: #1e1e1e;
		}
	}
}

.dark {
	background-color: #1e1e1e;
	color: #ffffff;

	&:hover {
		color: #ffffff;
	}

	.tools {
		::v-deep(.el-select .el-select__wrapper) {
			border: none;
			box-shadow: none;
			background-color: #1e1e1e;
		}

		::v-deep(.el-select .el-select__placeholder) {
			color: #ffffff;
		}

		.fullscreen,
		.fullscreen-exit,
		.themeIcon,
		.copyIcon {
			color: #ffffff;
		}
	}
}
</style>
