import { EditorView } from '@codemirror/view'
import { asciiEscapeToHtml, assertNever, log, toElement } from '#utils/utils'
import { Signal } from '#utils/signal'
import { getContent } from '#editor/plugins/persistence'
import type { HtmlEscapedString } from 'hono/utils/html'
import { connectToNose, noseSignals } from '#editor/noseClient'
import type { Value } from 'reefvm'
import { Compartment } from '@codemirror/state'
import { lineNumbers } from '@codemirror/view'
import { shrimpSetup } from '#editor/plugins/shrimpSetup'

import '#editor/editor.css'

const lineNumbersCompartment = new Compartment()

connectToNose()

export const outputSignal = new Signal<Value | string>()
export const errorSignal = new Signal<string>()
export const multilineModeSignal = new Signal<boolean>()

export const Editor = () => {
  return (
    <>
      <div
        ref={(ref: Element) => {
          if (ref?.querySelector('.cm-editor')) return
          const view = new EditorView({
            parent: ref,
            doc: getContent(),
            extensions: shrimpSetup(lineNumbersCompartment),
          })

          multilineModeSignal.connect((isMultiline) => {
            view.dispatch({
              effects: lineNumbersCompartment.reconfigure(isMultiline ? lineNumbers() : []),
            })
          })

          requestAnimationFrame(() => view.focus())
        }}
      />
      <div id="status-bar">
        <div className="left"></div>
        <div className="right"></div>
      </div>
      <div id="output"></div>
      <div id="error"></div>
    </>
  )
}

noseSignals.connect((message) => {
  if (message.type === 'error') {
    log.error(`Nose error: ${message.data}`)
    errorSignal.emit(`Nose error: ${message.data}`)
  } else if (message.type === 'reef-output') {
    const x = outputSignal.emit(message.data)
  } else if (message.type === 'connected') {
    outputSignal.emit(`╞ Connected to Nose VM`)
  }
})

outputSignal.connect((value) => {
  const el = document.querySelector('#output')!
  el.innerHTML = ''
  el.innerHTML = asciiEscapeToHtml(valueToString(value))
})

errorSignal.connect((error) => {
  const el = document.querySelector('#output')!
  el.innerHTML = ''
  el.classList.add('error')
  el.innerHTML = asciiEscapeToHtml(error)
})

type StatusBarMessage = {
  side: 'left' | 'right'
  message: string | Promise<HtmlEscapedString>
  className: string
  order?: number
}
export const statusBarSignal = new Signal<StatusBarMessage>()
statusBarSignal.connect(async ({ side, message, className, order }) => {
  document.querySelector(`#status-bar .${className}`)?.remove()

  const sideEl = document.querySelector(`#status-bar .${side}`)!
  const messageEl = (
    <div data-order={order ?? 0} className={className}>
      {await message}
    </div>
  )

  // Now go through the nodes and put it in the right spot based on order. Higher number means further right
  const nodes = Array.from(sideEl.childNodes)
  const index = nodes.findIndex((node) => {
    if (!(node instanceof HTMLElement)) return false
    return Number(node.dataset.order) > (order ?? 0)
  })

  if (index === -1) {
    sideEl.appendChild(toElement(messageEl))
  } else {
    sideEl.insertBefore(toElement(messageEl), nodes[index]!)
  }
})

const valueToString = (value: Value | string): string => {
  if (typeof value === 'string') {
    return value
  }

  switch (value.type) {
    case 'null':
      return 'null'
    case 'boolean':
      return value.value ? 'true' : 'false'
    case 'number':
      return value.value.toString()
    case 'string':
      return value.value
    case 'array':
      return `${value.value.map(valueToString).join('\n')}`
    case 'dict': {
      const entries = Array.from(value.value.entries()).map(
        ([key, val]) => `"${key}": ${valueToString(val)}`
      )
      return `{${entries.join(', ')}}`
    }
    case 'regex':
      return `/${value.value.source}/`
    case 'function':
      return `<function>`
    case 'native':
      return `<function ${value.fn.name}>`
    default:
      assertNever(value)
      return `<unknown value type: ${(value as any).type}>`
  }
}
