import { Store, createStore, unwrap } from 'solid-js/store'
import { v4 as uuidv4 } from 'uuid'
import type { EditorState } from 'prosemirror-state'
import { undo, redo } from 'prosemirror-history'
import { selectAll, deleteSelection } from 'prosemirror-commands'
import * as Y from 'yjs'
import { undo as yUndo, redo as yRedo } from 'y-prosemirror'
import { WebrtcProvider } from 'y-webrtc'
import { uniqueNamesGenerator, adjectives, animals } from 'unique-names-generator'
import { debounce } from 'ts-debounce'
// import * as remote from '../prosemirror/remote'
import { createSchema, createExtensions, createEmptyText } from '../prosemirror/setup'
import { State, File, Config, ServiceError, newState } from '.'
// import { isTauri, mod } from '../env'
import { serialize, createMarkdownParser } from '../prosemirror/markdown'
import { isEmpty, isInitialized } from '../prosemirror/state'
import { Awareness } from 'y-protocols/awareness'
import { isServer } from 'solid-js/web'

const mod = 'Ctrl'
const isTauri = false
const isText = (x: any) => x && x.doc && x.selection
const isState = (x: any) => typeof x.lastModified !== 'string' && Array.isArray(x.files)
const isFile = (x: any): boolean => x && (x.text || x.path)

export const createCtrl = (initial: State): [Store<State>, any] => {
  const [store, setState] = createStore(initial)

  const discardText = async () => {
    const state = unwrap(store)
    const index = state.files.length - 1
    const file = index !== -1 ? state.files[index] : undefined

    let next: Partial<State>

    if (file) {
      next = await createTextFromFile(file)
    } else {
      const extensions = createExtensions({
        config: state.config ?? store.config,
        markdown: (state.markdown && store.markdown) as any,
        keymap
      })

      next = {
        text: createEmptyText(),
        extensions,
        lastModified: undefined,
        path: undefined,
        markdown: state.markdown
      }
    }

    const files = state.files.filter((f: File) => f !== file)

    setState({
      files,
      ...next,
      collab: file ? undefined : state.collab,
      error: undefined
    })
  }

  const addToFiles = (files: File[], prev: State) => {
    const text = prev.path ? undefined : (prev.text as EditorState).toJSON()

    return [
      ...files,
      {
        text,
        lastModified: prev.lastModified?.toISOString(),
        path: prev.path,
        markdown: prev.markdown
      }
    ]
  }

  const newFile = () => {
    if (isEmpty(store.text) && !store.path) {
      return
    }

    const state: State = unwrap(store)
    let files = state.files

    if (!state.error) {
      files = addToFiles(files, state)
    }

    const extensions: any[] = createExtensions({
      config: state.config ?? store.config,
      markdown: state.markdown,
      keymap
    })

    setState({
      text: createEmptyText(),
      extensions,
      files,
      lastModified: undefined,
      path: undefined,
      error: undefined,
      collab: undefined
    })
  }

  const discard = async () => {
    if (store.path) {
      await discardText()
    } else if (store.files?.length > 0 && isEmpty(store.text)) {
      await discardText()
    } else {
      selectAll(store.editorView.state, store.editorView.dispatch)
      deleteSelection(store.editorView.state, store.editorView.dispatch)
    }
  }

  // FIXME
  // eslint-disable-next-line unicorn/consistent-function-scoping
  const onQuit = () => {
    if (!isTauri) {
      console.debug('quit')
      // return
    }
    // remote.quit()
  }

  const onNew = () => {
    newFile()

    return true
  }

  const onDiscard = () => {
    discard()

    return true
  }

  const onFullscreen = () => {
    if (!isTauri) return

    ctrl.setFullscreen(!store.fullscreen)

    return true
  }

  const onToggleMarkdown = () => toggleMarkdown()

  const onUndo = () => {
    if (!isInitialized(store.text)) return

    const text = store.text as EditorState

    if (store.collab?.started) {
      yUndo(text)
    } else {
      undo(text, store.editorView.dispatch)
    }

    return true
  }

  const onRedo = () => {
    if (!isInitialized(store.text)) return

    const text = store.text as EditorState

    if (store.collab?.started) {
      yRedo(text)
    } else {
      redo(text, store.editorView.dispatch)
    }

    return true
  }

  const keymap = {
    [`${mod}-q`]: onQuit,
    [`${mod}-n`]: onNew,
    [`${mod}-w`]: onDiscard,
    'Cmd-Enter': onFullscreen,
    'Alt-Enter': onFullscreen,
    [`${mod}-z`]: onUndo,
    [`Shift-${mod}-z`]: onRedo,
    [`${mod}-y`]: onRedo,
    [`${mod}-m`]: onToggleMarkdown
  }

  const createTextFromFile = async (file: File) => {
    const state = unwrap(store)

    // if (file.path) file = await loadFile(state.config, file.path)

    const extensions = createExtensions({
      config: state.config,
      markdown: file.markdown,
      path: file.path,
      keymap
    })

    return {
      text: file.text,
      extensions,
      lastModified: file.lastModified ? new Date(file.lastModified) : undefined,
      path: file.path,
      markdown: file.markdown
    }
  }

  // FIXME
  // eslint-disable-next-line sonarjs/cognitive-complexity
  const fetchData = async (): Promise<State> => {
    let args = {} // await remote.getArgs().catch(() => undefined)
    const state: State = unwrap(store)

    if (!isTauri) {
      const room = window.location.pathname?.slice(1).trim()

      args = { room: room || undefined }
    }
    if (!isServer) {
      const { default: db } = await import('../db')
      const data: string = await db.get('state')
      let parsed: any

      if (data !== undefined) {
        try {
          parsed = JSON.parse(data)
        } catch {
          throw new ServiceError('invalid_state', data)
        }
      }

      if (!parsed) {
        return { ...state, args }
      }

      let text = state.text

      if (parsed.text) {
        if (!isText(parsed.text)) {
          throw new ServiceError('invalid_state', parsed.text)
        }

        text = parsed.text
      }

      const extensions = createExtensions({
        path: parsed.path,
        markdown: parsed.markdown,
        keymap,
        config: {} as Config
      })

      const nState = {
        ...parsed,
        text,
        extensions,
        // config,
        args
      }

      if (nState.lastModified) {
        nState.lastModified = new Date(nState.lastModified)
      }

      for (const file of parsed.files) {
        if (!isFile(file)) {
          throw new ServiceError('invalid_file', file)
        }
      }
      if (!isState(nState)) {
        throw new ServiceError('invalid_state', nState)
      }

      return nState
    } else {
      return
    }
  }

  const getTheme = (state: State) => ({ theme: state.config.theme })

  const clean = () => {
    setState({
      ...newState(),
      loading: 'initialized',
      files: [],
      fullscreen: store.fullscreen,
      lastModified: new Date(),
      error: undefined,
      text: undefined
    })
  }

  const init = async () => {
    let data = await fetchData()

    try {
      if (data.args?.room) {
        data = doStartCollab(data)
      } else if (data.args?.text) {
        data = await doOpenFile(data, { text: JSON.parse(data.args?.text) })
      } /* else if (data.args?.file) {
        const file = await loadFile(data.config, data.args?.file)

        data = await doOpenFile(data, file)
      } else if (data.path) {
        const file = await loadFile(data.config, data.path)

        data = await doOpenFile(data, file)
      } */ else if (!data.text) {
        const text = createEmptyText()
        const extensions = createExtensions({
          config: data.config,
          markdown: data.markdown,
          keymap
        })

        data = { ...data, text, extensions }
      }
    } catch (error: any) {
      data = { ...data, error: error.errorObject }
    }

    setState({
      ...data,
      config: { ...data.config, ...getTheme(data) },
      loading: 'initialized'
    })
  }
  /*
  const loadFile = async (config: Config, path: string): Promise<File> => {
    try {
      const fileContent = await remote.readFile(path)
      const lastModified = await remote.getFileLastModified(path)
      const schema = createSchema({
        config,
        markdown: false,
        path,
        keymap
      })

      const parser = createMarkdownParser(schema)
      const doc = parser.parse(fileContent).toJSON()
      const text = {
        doc,
        selection: {
          type: 'text',
          anchor: 1,
          head: 1
        }
      }

      return {
        text,
        lastModified: lastModified.toISOString(),
        path
      }
    } catch (e) {
      throw new ServiceError('file_permission_denied', { error: e })
    }
  }
  */
  const openFile = async (file: File) => {
    const state: State = unwrap(store)
    const update = await doOpenFile(state, file)

    setState(update)
  }

  const doOpenFile = async (state: State, file: File): Promise<State> => {
    const findIndexOfFile = (f: File) => {
      for (let i = 0; i < state.files.length; i++) {
        if (state.files[i] === f) return i

        if (f.path && state.files[i].path === f.path) return i
      }

      return -1
    }

    const index = findIndexOfFile(file)
    const item = index === -1 ? file : state.files[index]
    let files = state.files.filter((f) => f !== item)

    if (!isEmpty(state.text) && state.lastModified) {
      files = addToFiles(files, state)
    }

    file.lastModified = item.lastModified
    const next = await createTextFromFile(file)

    return {
      ...state,
      ...next,
      files,
      collab: undefined,
      error: undefined
    }
  }

  // eslint-disable-next-line solid/reactivity
  const saveState = debounce(async (state: State) => {
    const data: any = {
      lastModified: state.lastModified,
      files: state.files,
      config: state.config,
      path: state.path,
      markdown: state.markdown,
      collab: {
        room: state.collab?.room
      }
    }

    if (isInitialized(state.text)) {
      //if (state.path) {
      // const text = serialize(store.editorView.state)
      // await remote.writeFile(state.path, text)
      //}
      data.text = store.editorView.state.toJSON()
    } else if (state.text) {
      data.text = state.text
    }
    if (!isServer) {
      const { default: db } = await import('../db')
      db.set('state', JSON.stringify(data))
    }
  }, 200)

  const setFullscreen = (fullscreen: boolean) => {
    // remote.setFullscreen(fullscreen)
    setState({ fullscreen })
  }

  const startCollab = () => {
    const state: State = unwrap(store)
    const update = doStartCollab(state)

    setState(update)
  }

  const doStartCollab = (state: State): State => {
    const backup = state.args?.room && state.collab?.room !== state.args.room
    const room = state.args?.room ?? uuidv4()

    window.history.replaceState(null, '', `/${room}`)

    const ydoc = new Y.Doc()
    const type = ydoc.getXmlFragment('prosemirror')
    const webrtcOptions = {
      awareness: new Awareness(ydoc),
      filterBcConns: true,
      maxConns: 33,
      signaling: [
        // 'wss://signaling.discours.io',
        // 'wss://stun.l.google.com:19302',
        'wss://y-webrtc-signaling-eu.herokuapp.com',
        'wss://signaling.yjs.dev'
      ],
      peerOpts: {},
      password: ''
    }
    const provider = new WebrtcProvider(room, ydoc, webrtcOptions)
    const username = uniqueNamesGenerator({
      dictionaries: [adjectives, animals],
      style: 'capital',
      separator: ' ',
      length: 2
    })

    provider.awareness.setLocalStateField('user', {
      name: username
    })

    const extensions = createExtensions({
      config: state.config,
      markdown: state.markdown,
      path: state.path,
      keymap,
      y: { type, provider }
    })

    let nState = state

    if ((backup && !isEmpty(state.text)) || state.path) {
      let files = state.files

      if (!state.error) {
        files = addToFiles(files, state)
      }

      nState = {
        ...state,
        files,
        lastModified: undefined,
        path: undefined,
        error: undefined
      }
    }

    return {
      ...nState,
      extensions,
      collab: { started: true, room, y: { type, provider } }
    }
  }

  const stopCollab = (state: State) => {
    state.collab?.y?.provider.destroy()
    const extensions = createExtensions({
      config: state.config,
      markdown: state.markdown,
      path: state.path,
      keymap
    })

    setState({ collab: undefined, extensions })
    window.history.replaceState(null, '', '/')
  }

  const toggleMarkdown = () => {
    const state = unwrap(store)
    const editorState = store.text as EditorState
    const markdown = !state.markdown
    const selection = { type: 'text', anchor: 1, head: 1 }
    let doc: any

    if (markdown) {
      const lines = serialize(editorState).split('\n')
      const nodes = lines.map((text) => {
        return text ? { type: 'paragraph', content: [{ type: 'text', text }] } : { type: 'paragraph' }
      })

      doc = { type: 'doc', content: nodes }
    } else {
      const schema = createSchema({
        config: state.config,
        path: state.path,
        y: state.collab?.y,
        markdown,
        keymap
      })

      const parser = createMarkdownParser(schema)
      let textContent = ''

      editorState.doc.forEach((node) => {
        textContent += `${node.textContent}\n`
      })
      const text = parser.parse(textContent)
      doc = text?.toJSON()
    }

    const extensions = createExtensions({
      config: state.config,
      markdown,
      path: state.path,
      keymap,
      y: state.collab?.y
    })

    setState({
      text: { selection, doc },
      extensions,
      markdown
    })
  }

  const updateConfig = (config: Partial<Config>) => {
    const state = unwrap(store)
    const extensions = createExtensions({
      config: { ...state.config, ...config },
      markdown: state.markdown,
      path: state.path,
      keymap,
      y: state.collab?.y
    })

    setState({
      config: { ...state.config, ...config },
      extensions,
      lastModified: new Date()
    })
  }

  const updatePath = (path: string) => {
    setState({ path, lastModified: new Date() })
  }

  const updateTheme = () => {
    const { theme } = getTheme(unwrap(store))

    setState('config', { theme })
  }

  const ctrl = {
    clean,
    discard,
    getTheme,
    init,
    // loadFile,
    newFile,
    openFile,
    saveState,
    setFullscreen,
    setState,
    startCollab,
    stopCollab,
    toggleMarkdown,
    updateConfig,
    updatePath,
    updateTheme
  }

  return [store, ctrl]
}
