import * as monaco from 'monaco-editor'

export type CodeEditorCompletionKind =
	| 'method'
	| 'function'
	| 'property'
	| 'field'
	| 'variable'
	| 'class'
	| 'keyword'

export interface CodeEditorCompletionSuggestion {
	label: string
	insertText?: string
	detail?: string
	documentation?: string
	kind?: CodeEditorCompletionKind
	snippet?: boolean
}

export interface CodeEditorCompletionTarget {
	object: string
	methods?: CodeEditorCompletionSuggestion[]
	properties?: CodeEditorCompletionSuggestion[]
	triggerCharacters?: string[]
}

export type CodeEditorCompletionConfig = Partial<Record<string, CodeEditorCompletionTarget[]>>

interface LanguageProviderState {
	disposable: monaco.IDisposable | null
	models: Map<string, CodeEditorCompletionTarget[]>
}

const completionKindMap: Record<CodeEditorCompletionKind, monaco.languages.CompletionItemKind> = {
	method: monaco.languages.CompletionItemKind.Method,
	function: monaco.languages.CompletionItemKind.Function,
	property: monaco.languages.CompletionItemKind.Property,
	field: monaco.languages.CompletionItemKind.Field,
	variable: monaco.languages.CompletionItemKind.Variable,
	class: monaco.languages.CompletionItemKind.Class,
	keyword: monaco.languages.CompletionItemKind.Keyword
}

const languageProviderStates = new Map<string, LanguageProviderState>()
const modelLanguageState = new Map<string, string>()

const getLanguageProviderState = (language: string): LanguageProviderState => {
	const current = languageProviderStates.get(language)
	if (current) {
		return current
	}

	const nextState: LanguageProviderState = {
		disposable: null,
		models: new Map()
	}

	languageProviderStates.set(language, nextState)
	return nextState
}

const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')

const isObjectContextMatch = (textBeforeCursor: string, objectName: string) => {
	return new RegExp(`(?:^|[^\\w$])${escapeRegExp(objectName)}\\.\\w*$`).test(textBeforeCursor)
}

const getTriggerCharacters = (models: Map<string, CodeEditorCompletionTarget[]>) => {
	const triggerCharacters = new Set<string>()

	for (const rules of models.values()) {
		for (const rule of rules) {
			const nextTriggerCharacters =
				rule.triggerCharacters && rule.triggerCharacters.length ? rule.triggerCharacters : ['.']

			nextTriggerCharacters.forEach((char) => triggerCharacters.add(char))
		}
	}

	return Array.from(triggerCharacters)
}

const toCompletionItem = (
	suggestion: CodeEditorCompletionSuggestion,
	range: monaco.IRange,
	fallbackKind: monaco.languages.CompletionItemKind
): monaco.languages.CompletionItem => {
	return {
		label: suggestion.label,
		kind: suggestion.kind ? completionKindMap[suggestion.kind] : fallbackKind,
		insertText: suggestion.insertText || suggestion.label,
		insertTextRules: suggestion.snippet
			? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
			: undefined,
		detail: suggestion.detail,
		documentation: suggestion.documentation,
		range
	}
}

const buildSuggestions = (
	rule: CodeEditorCompletionTarget,
	range: monaco.IRange
): monaco.languages.CompletionItem[] => {
	const methodSuggestions = (rule.methods || []).map((item) =>
		toCompletionItem(item, range, monaco.languages.CompletionItemKind.Method)
	)
	const propertySuggestions = (rule.properties || []).map((item) =>
		toCompletionItem(item, range, monaco.languages.CompletionItemKind.Property)
	)

	return [...methodSuggestions, ...propertySuggestions]
}

const registerLanguageProvider = (language: string) => {
	const state = getLanguageProviderState(language)

	state.disposable?.dispose()

	state.disposable = monaco.languages.registerCompletionItemProvider(language, {
		triggerCharacters: getTriggerCharacters(state.models),
		provideCompletionItems(model, position) {
			const rules = state.models.get(model.uri.toString())
			if (!rules?.length) {
				return { suggestions: [] }
			}

			const word = model.getWordUntilPosition(position)
			const range = {
				startLineNumber: position.lineNumber,
				endLineNumber: position.lineNumber,
				startColumn: word.startColumn,
				endColumn: word.endColumn
			}
			const textBeforeCursor = model.getValueInRange({
				startLineNumber: position.lineNumber,
				startColumn: 1,
				endLineNumber: position.lineNumber,
				endColumn: position.column
			})

			const suggestions = rules.flatMap((rule) =>
				isObjectContextMatch(textBeforeCursor, rule.object) ? buildSuggestions(rule, range) : []
			)

			return { suggestions }
		}
	})
}

export const clearCodeEditorCompletionConfig = (
	modelOrUri: monaco.editor.ITextModel | string
) => {
	const modelUri = typeof modelOrUri === 'string' ? modelOrUri : modelOrUri.uri.toString()
	const language = modelLanguageState.get(modelUri)

	if (!language) {
		return
	}

	const state = languageProviderStates.get(language)
	modelLanguageState.delete(modelUri)

	if (!state) {
		return
	}

	state.models.delete(modelUri)

	if (!state.models.size) {
		state.disposable?.dispose()
		languageProviderStates.delete(language)
		return
	}

	registerLanguageProvider(language)
}

export const syncCodeEditorCompletionConfig = (
	model: monaco.editor.ITextModel,
	language: string,
	config?: CodeEditorCompletionConfig
) => {
	const modelUri = model.uri.toString()

	clearCodeEditorCompletionConfig(modelUri)

	const rules = config?.[language] || []
	if (!rules.length) {
		return
	}

	const state = getLanguageProviderState(language)
	state.models.set(modelUri, rules)
	modelLanguageState.set(modelUri, language)
	registerLanguageProvider(language)
}

export const shouldTriggerCodeEditorCompletion = (
	model: monaco.editor.ITextModel,
	position: monaco.Position,
	language: string,
	config?: CodeEditorCompletionConfig
) => {
	const rules = config?.[language] || []
	if (!rules.length) {
		return false
	}

	const textBeforeCursor = model.getValueInRange({
		startLineNumber: position.lineNumber,
		startColumn: 1,
		endLineNumber: position.lineNumber,
		endColumn: position.column
	})

	return rules.some((rule) => isObjectContextMatch(textBeforeCursor, rule.object))
}
