import Editor, { Monaco } from '@monaco-editor/react'
import useTheme from 'hooks/useTheme'
import libImport from './es5-lib'
import typesImport from './types-import'
import React, { useCallback, useEffect, useRef } from 'react'
import styled from 'styles'
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'
import { getFormattedCode } from 'utils/code'

export type IMonaco = typeof monaco

export type IMonacoEditor = monaco.editor.IStandaloneCodeEditor

const modifierKeys = ['Escape', 'Meta', 'Control', 'Shift', 'Option', 'Alt']

interface Props {
  value: string
  error: { line: number; column: number }
  fontSize: number
  monacoRef?: React.MutableRefObject<IMonaco>
  editorRef?: React.MutableRefObject<IMonacoEditor>
  readOnly?: boolean
  onMount?: (value: string, editor: IMonacoEditor) => void
  onUnmount?: (editor: IMonacoEditor) => void
  onChange?: (value: string, editor: IMonacoEditor) => void
  onSave?: (value: string, editor: IMonacoEditor) => void
  onError?: (error: Error, line: number, col: number) => void
  onKey?: () => void
}

export default function CodeEditor({
  editorRef,
  monacoRef,
  fontSize,
  value,
  error,
  readOnly,
  onChange,
  onSave,
  onKey,
}: Props): JSX.Element {
  const { theme } = useTheme()

  const rEditor = useRef<IMonacoEditor>(null)
  const rMonaco = useRef<IMonaco>(null)

  const handleBeforeMount = useCallback((monaco: Monaco) => {
    if (monacoRef) {
      monacoRef.current = monaco
    }

    rMonaco.current = monaco

    // Set the compiler options.

    monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
      allowJs: true,
      checkJs: true,
      strict: true,
      noLib: true,
      lib: ['es6'],
      target: monaco.languages.typescript.ScriptTarget.ES2016,
      allowNonTsExtensions: true,
    })

    // Sync the intellisense on load.

    monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true)

    // Run both semantic and syntax validation.

    monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
      noSemanticValidation: false,
      noSyntaxValidation: false,
    })

    // Add custom types

    monaco.languages.typescript.typescriptDefaults.addExtraLib(
      typesImport.content
    )

    // Add es5 library types

    monaco.languages.typescript.typescriptDefaults.addExtraLib(
      libImport.content
    )

    // Use prettier as a formatter

    monaco.languages.registerDocumentFormattingEditProvider('typescript', {
      async provideDocumentFormattingEdits(model) {
        try {
          const text = getFormattedCode(model.getValue())

          return [
            {
              range: model.getFullModelRange(),
              text,
            },
          ]
        } catch (e) {
          return [
            {
              range: model.getFullModelRange(),
              text: model.getValue(),
            },
          ]
        }
      },
    })
  }, [])

  const handleMount = useCallback((editor: IMonacoEditor) => {
    if (editorRef) {
      editorRef.current = editor
    }
    rEditor.current = editor

    editor.updateOptions({
      fontSize,
      fontFamily: "'Recursive Mono', monospace",
      wordBasedSuggestions: false,
      minimap: { enabled: false },
      lightbulb: {
        enabled: false,
      },
      readOnly,
    })
  }, [])

  const handleChange = useCallback((code: string | undefined) => {
    onChange(code, rEditor.current)
  }, [])

  const handleKeydown = useCallback(
    (e: React.KeyboardEvent<HTMLDivElement>) => {
      e.stopPropagation()
      !modifierKeys.includes(e.key) && onKey?.()
      const metaKey = navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey
      if (e.key === 's' && metaKey) {
        const editor = rEditor.current
        if (!editor) return
        editor
          .getAction('editor.action.formatDocument')
          .run()
          .then(() =>
            onSave(rEditor.current?.getModel().getValue(), rEditor.current)
          )

        e.preventDefault()
      }
      if (e.key === 'p' && metaKey) {
        e.preventDefault()
      }
      if (e.key === 'd' && metaKey) {
        e.preventDefault()
      }
    },
    []
  )

  const handleKeyUp = useCallback(
    (e: React.KeyboardEvent<HTMLDivElement>) => e.stopPropagation(),
    []
  )

  const rDecorations = useRef<any>([])

  useEffect(() => {
    const monaco = rMonaco.current
    if (!monaco) return
    const editor = rEditor.current
    if (!editor) return

    if (!error) {
      rDecorations.current = editor.deltaDecorations(rDecorations.current, [])
      return
    }

    if (!error.line) return

    rDecorations.current = editor.deltaDecorations(rDecorations.current, [
      {
        range: new monaco.Range(
          Number(error.line) - 1,
          0,
          Number(error.line) - 1,
          0
        ),
        options: {
          isWholeLine: true,
          className: 'editorLineError',
        },
      },
    ])
  }, [error])

  useEffect(() => {
    const monaco = rMonaco.current
    if (!monaco) return
    monaco.editor.setTheme(theme === 'dark' ? 'vs-dark' : 'light')
  }, [theme])

  useEffect(() => {
    const editor = rEditor.current
    if (!editor) return

    editor.updateOptions({
      fontSize,
    })
  }, [fontSize])

  return (
    <EditorContainer onKeyDown={handleKeydown} onKeyUp={handleKeyUp}>
      <Editor
        height="100%"
        language="typescript"
        value={value}
        theme={theme === 'dark' ? 'vs-dark' : 'light'}
        beforeMount={handleBeforeMount}
        onMount={handleMount}
        onChange={handleChange}
        defaultPath="index.ts"
      />
    </EditorContainer>
  )
}

const EditorContainer = styled('div', {
  height: '100%',
  pointerEvents: 'all',
  userSelect: 'all',

  '& > *': {
    userSelect: 'all',
    pointerEvents: 'all',
  },

  '.editorLineError': {
    backgroundColor: '$lineError',
  },
})
