import { Box, BoxProps, HStack } from '@chakra-ui/react'
import { EditorState, EditorView, basicSetup } from '@codemirror/basic-setup'
import { json, jsonParseLinter } from '@codemirror/lang-json'
import { css } from '@codemirror/lang-css'
import { javascript } from '@codemirror/lang-javascript'
import { html } from '@codemirror/lang-html'
import { useEffect, useRef, useState } from 'react'
import { useDebouncedCallback } from 'use-debounce'
import { linter } from '@codemirror/lint'
import { VariablesButton } from './buttons/VariablesButton'
import { Variable } from 'models'

const linterExtension = linter(jsonParseLinter())

type Props = {
  value: string
  lang?: 'css' | 'json' | 'js' | 'html'
  isReadOnly?: boolean
  debounceTimeout?: number
  withVariableButton?: boolean
  onChange?: (value: string) => void
}
export const CodeEditor = ({
  value,
  lang,
  onChange,
  withVariableButton = true,
  isReadOnly = false,
  debounceTimeout = 1000,
  ...props
}: Props & Omit<BoxProps, 'onChange'>) => {
  const editorContainer = useRef<HTMLDivElement | null>(null)
  const editorView = useRef<EditorView | null>(null)
  const [, setPlainTextValue] = useState(value)
  const [carretPosition, setCarretPosition] = useState<number>(0)
  const isVariableButtonDisplayed = withVariableButton && !isReadOnly

  const debounced = useDebouncedCallback(
    (value) => {
      setPlainTextValue(value)
      onChange && onChange(value)
    },
    process.env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout
  )

  useEffect(
    () => () => {
      debounced.flush()
    },
    [debounced]
  )

  useEffect(() => {
    if (!editorView.current || !isReadOnly) return
    editorView.current.dispatch({
      changes: {
        from: 0,
        to: editorView.current.state.doc.length,
        insert: value,
      },
    })
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [value])

  useEffect(() => {
    const editor = initEditor(value)
    return () => {
      editor?.destroy()
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  const initEditor = (value: string) => {
    if (!editorContainer.current) return
    const updateListenerExtension = EditorView.updateListener.of((update) => {
      if (update.docChanged && onChange)
        debounced(update.state.doc.toJSON().join('\n'))
    })
    const extensions = [
      updateListenerExtension,
      basicSetup,
      EditorState.readOnly.of(isReadOnly),
    ]
    if (lang === 'json') {
      extensions.push(json())
      extensions.push(linterExtension)
    }
    if (lang === 'css') extensions.push(css())
    if (lang === 'js') extensions.push(javascript())
    if (lang === 'html') extensions.push(html())
    extensions.push(
      EditorView.theme({
        '&': { maxHeight: '500px' },
        '.cm-gutter,.cm-content': { minHeight: isReadOnly ? '0' : '250px' },
        '.cm-scroller': { overflow: 'auto' },
      })
    )
    const editor = new EditorView({
      state: EditorState.create({
        extensions,
      }),
      parent: editorContainer.current,
    })
    editor.dispatch({
      changes: { from: 0, insert: value },
    })
    editorView.current = editor
    return editor
  }

  const handleVariableSelected = (variable?: Pick<Variable, 'id' | 'name'>) => {
    editorView.current?.focus()
    const insert = `{{${variable?.name}}}`
    editorView.current?.dispatch({
      changes: {
        from: carretPosition,
        insert,
      },
      selection: { anchor: carretPosition + insert.length },
    })
  }

  const handleKeyUp = () => {
    if (!editorContainer.current) return
    setCarretPosition(editorView.current?.state.selection.main.from ?? 0)
  }

  return (
    <HStack align="flex-end" spacing={0}>
      <Box
        w={isVariableButtonDisplayed ? 'calc(100% - 32px)' : '100%'}
        ref={editorContainer}
        data-testid="code-editor"
        {...props}
        onKeyUp={handleKeyUp}
      />
      {isVariableButtonDisplayed && (
        <VariablesButton onSelectVariable={handleVariableSelected} size="sm" />
      )}
    </HStack>
  )
}
