import {
  keymap,
  EditorView,
  ViewPlugin,
  ViewUpdate,
  placeholder,
} from "@codemirror/view"
import {
  Extension,
  EditorState,
  Compartment,
  EditorSelection,
} from "@codemirror/state"
import { Language, LanguageSupport } from "@codemirror/language"
import { defaultKeymap } from "@codemirror/commands"
import { Completion, autocompletion } from "@codemirror/autocomplete"
import { linter } from "@codemirror/lint"

import {
  watch,
  ref,
  Ref,
  onMounted,
  onBeforeUnmount,
} from "@nuxtjs/composition-api"

import { javascriptLanguage } from "@codemirror/lang-javascript"
import { jsonLanguage } from "@codemirror/lang-json"
import { GQLLanguage } from "@hoppscotch/codemirror-lang-graphql"
import { pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import { isJSONContentType } from "../utils/contenttypes"
import { Completer } from "./completion"
import { LinterDefinition } from "./linting/linter"
import { basicSetup, baseTheme, baseHighlightStyle } from "./themes/baseTheme"

type ExtendedEditorConfig = {
  mode: string
  placeholder: string
  readOnly: boolean
  lineWrapping: boolean
}

type CodeMirrorOptions = {
  extendedEditorConfig: Partial<ExtendedEditorConfig>
  linter: LinterDefinition | null
  completer: Completer | null
}

const hoppCompleterExt = (completer: Completer): Extension => {
  return autocompletion({
    override: [
      async (context) => {
        // Expensive operation! Disable on bigger files ?
        const text = context.state.doc.toJSON().join(context.state.lineBreak)

        const line = context.state.doc.lineAt(context.pos)
        const lineStart = line.from
        const lineNo = line.number - 1
        const ch = context.pos - lineStart

        // Only do trigger on type when typing a word token, else stop (unless explicit)
        if (!context.matchBefore(/\w+/) && !context.explicit)
          return {
            from: context.pos,
            options: [],
          }

        const result = await completer(text, { line: lineNo, ch })

        // Use more completion features ?
        const completions =
          result?.completions.map<Completion>((comp) => ({
            label: comp.text,
            detail: comp.meta,
          })) ?? []

        return {
          from: context.state.wordAt(context.pos)?.from ?? context.pos,
          options: completions,
        }
      },
    ],
  })
}

const hoppLinterExt = (hoppLinter: LinterDefinition): Extension => {
  return linter(async (view) => {
    // Requires full document scan, hence expensive on big files, force disable on big files ?
    const linterResult = await hoppLinter(
      view.state.doc.toJSON().join(view.state.lineBreak)
    )

    return linterResult.map((result) => {
      const startPos =
        view.state.doc.line(result.from.line + 1).from + result.from.ch
      const endPos = view.state.doc.line(result.to.line + 1).from + result.to.ch

      return {
        from: startPos,
        to: endPos,
        message: result.message,
        severity: result.severity,
      }
    })
  })
}

const hoppLang = (
  language: Language,
  linter?: LinterDefinition | undefined,
  completer?: Completer | undefined
) => {
  const exts: Extension[] = []

  if (linter) exts.push(hoppLinterExt(linter))
  if (completer) exts.push(hoppCompleterExt(completer))

  return new LanguageSupport(language, exts)
}

const getLanguage = (langMime: string): Language | null => {
  if (isJSONContentType(langMime)) {
    return jsonLanguage
  } else if (langMime === "application/javascript") {
    return javascriptLanguage
  } else if (langMime === "graphql") {
    return GQLLanguage
  }

  // None matched, so return null
  return null
}

const getEditorLanguage = (
  langMime: string,
  linter: LinterDefinition | undefined,
  completer: Completer | undefined
): Extension =>
  pipe(
    O.fromNullable(getLanguage(langMime)),
    O.map((lang) => hoppLang(lang, linter, completer)),
    O.getOrElseW(() => [])
  )

export function useCodemirror(
  el: Ref<any | null>,
  value: Ref<string>,
  options: CodeMirrorOptions
): { cursor: Ref<{ line: number; ch: number }> } {
  const language = new Compartment()
  const lineWrapping = new Compartment()
  const placeholderConfig = new Compartment()

  const cachedCursor = ref({
    line: 0,
    ch: 0,
  })
  const cursor = ref({
    line: 0,
    ch: 0,
  })

  const cachedValue = ref(value.value)

  const view = ref<EditorView>()

  const initView = (el: any) => {
    view.value = new EditorView({
      parent: el,
      state: EditorState.create({
        doc: value.value,
        extensions: [
          basicSetup,
          baseTheme,
          baseHighlightStyle,
          ViewPlugin.fromClass(
            class {
              update(update: ViewUpdate) {
                if (update.selectionSet) {
                  const cursorPos = update.state.selection.main.head

                  const line = update.state.doc.lineAt(cursorPos)

                  cachedCursor.value = {
                    line: line.number - 1,
                    ch: cursorPos - line.from,
                  }

                  cursor.value = {
                    line: cachedCursor.value.line,
                    ch: cachedCursor.value.ch,
                  }
                }
                if (update.docChanged) {
                  // Expensive on big files ?
                  cachedValue.value = update.state.doc
                    .toJSON()
                    .join(update.state.lineBreak)
                  if (!options.extendedEditorConfig.readOnly)
                    value.value = cachedValue.value
                }
              }
            }
          ),
          EditorState.changeFilter.of(
            () => !options.extendedEditorConfig.readOnly
          ),
          placeholderConfig.of(
            placeholder(options.extendedEditorConfig.placeholder ?? "")
          ),
          language.of(
            getEditorLanguage(
              options.extendedEditorConfig.mode ?? "",
              options.linter ?? undefined,
              options.completer ?? undefined
            )
          ),
          lineWrapping.of(
            options.extendedEditorConfig.lineWrapping
              ? [EditorView.lineWrapping]
              : []
          ),
          keymap.of(defaultKeymap),
        ],
      }),
    })
  }

  onMounted(() => {
    if (el.value) {
      if (!view.value) initView(el.value)
    }
  })

  watch(el, () => {
    if (el.value) {
      if (!view.value) initView(el.value)
    } else {
      view.value?.destroy()
      view.value = undefined
    }
  })

  onBeforeUnmount(() => {
    view.value?.destroy()
  })

  watch(value, (newVal) => {
    if (cachedValue.value !== newVal) {
      view.value?.dispatch({
        filter: false,
        changes: {
          from: 0,
          to: view.value.state.doc.length,
          insert: newVal,
        },
      })
    }
  })

  watch(
    () => [
      options.extendedEditorConfig.mode,
      options.linter,
      options.completer,
    ],
    () => {
      view.value?.dispatch({
        effects: language.reconfigure(
          getEditorLanguage(
            (options.extendedEditorConfig.mode as any) ?? "",
            options.linter ?? undefined,
            options.completer ?? undefined
          )
        ),
      })
    }
  )

  watch(
    () => options.extendedEditorConfig.lineWrapping,
    (newMode) => {
      view.value?.dispatch({
        effects: lineWrapping.reconfigure(
          newMode ? [EditorView.lineWrapping] : []
        ),
      })
    }
  )

  watch(
    () => options.extendedEditorConfig.placeholder,
    (newValue) => {
      view.value?.dispatch({
        effects: placeholderConfig.reconfigure(placeholder(newValue ?? "")),
      })
    }
  )

  watch(cursor, (newPos) => {
    if (view.value) {
      if (
        cachedCursor.value.line !== newPos.line ||
        cachedCursor.value.ch !== newPos.ch
      ) {
        const line = view.value.state.doc.line(newPos.line + 1)
        const selUpdate = EditorSelection.cursor(line.from + newPos.ch - 1)

        view.value?.focus()

        view.value.dispatch({
          scrollIntoView: true,
          selection: selUpdate,
          effects: EditorView.scrollTo.of(selUpdate),
        })
      }
    }
  })

  return {
    cursor,
  }
}
