import type { Element, EventMutLevel, Node, backend as ShadowBackend } from '@flexui/core'

export interface ReplayHandler {
  createElement?(
    ownerShadowRoot: ShadowBackend.ShadowRootContext,
    elem: Node,
  ): ShadowBackend.Element
  afterReplayElement?(
    ownerShadowRoot: ShadowBackend.ShadowRootContext,
    elem: Node,
    be: ShadowBackend.Element,
  ): void
}

export function replayShadowBackend(
  // eslint-disable-next-line @typescript-eslint/consistent-type-imports
  flexui: typeof import('@flexui/core'),
  context: ShadowBackend.Context,
  element: Node,
  handlers: ReplayHandler,
) {
  const createChildren = (elem: Element, ownerShadowRoot: ShadowBackend.ShadowRootContext) =>
    elem.childNodes.map((child) => recv(child, ownerShadowRoot))

  const insertChildren = (be: ShadowBackend.Element, children: ShadowBackend.Element[]): void => {
    if (children.length >= 5) {
      const frag = context.createFragment()

      children.forEach((child) => {
        frag.appendChild(child)
      })

      be.spliceAppend(frag)
      frag.release()
    } else {
      children.forEach((child) => {
        be.appendChild(child)
      })
    }
  }

  const setAttributes = (elem: Element, be: ShadowBackend.Element): void => {
    if (elem.id) be.setId(elem.id)
    if (elem.classList) {
      const classNames = elem.classList.getClassNames()
      if (classNames) classNames.split(' ').forEach((className) => be.addClass(className))
    }
    elem.getNodeStyleSegments().forEach((style, index) => {
      if (style) be.setStyle(style, index)
    })
    elem.attributes.forEach(({ name, value }) => {
      be.setAttribute(name, value)
    })
    const dataset = elem.dataset
    Object.keys(dataset).forEach((key) => {
      be.setDataset(key, dataset[key])
    })
    const listeners = elem.getListeners()
    Object.keys(listeners).forEach((name) => {
      let mutLevel: EventMutLevel | undefined
      let captureMutLevel: EventMutLevel | undefined
      listeners[name]!.forEach(({ mutated, final, capture }) => {
        // eslint-disable-next-line no-nested-ternary
        const currentMutLevel = final
          ? flexui.EventMutLevel.Final
          : mutated
          ? flexui.EventMutLevel.Mut
          : flexui.EventMutLevel.None
        if (capture) {
          captureMutLevel =
            captureMutLevel !== undefined && currentMutLevel < captureMutLevel
              ? captureMutLevel
              : currentMutLevel
        } else {
          mutLevel =
            mutLevel !== undefined && currentMutLevel < mutLevel ? mutLevel : currentMutLevel
        }
      })
      if (mutLevel !== undefined) be.setListenerStats(name, false, mutLevel)
      if (captureMutLevel !== undefined) be.setListenerStats(name, true, captureMutLevel)
    })
    const slotName = elem.slot
    if (slotName) be.setSlot(slotName)
  }

  const setSlotName = (node: Node, be: ShadowBackend.Element): void => {
    if (flexui.Element.isElement(node)) {
      const slotName = flexui.Element.getSlotName(node)
      if (slotName !== undefined) be.setSlotName(slotName)
    }
    const slotElement = flexui.Element.getSlotElement(node)
    if (slotElement) be.setSlotElement(backendElementMap.get(slotElement)!)
  }

  const backendElementMap = new Map<Node, ShadowBackend.Element>()

  const recv = (
    elem: Node,
    ownerShadowRoot: ShadowBackend.ShadowRootContext,
  ): ShadowBackend.Element => {
    let be: ShadowBackend.Element
    if (flexui.TextNode.isTextNode(elem)) {
      be =
        handlers.createElement?.(ownerShadowRoot, elem) ||
        ownerShadowRoot.createTextNode(elem.textContent)
      setSlotName(elem, be)
    } else if (flexui.NativeNode.isNativeNode(elem)) {
      be =
        handlers.createElement?.(ownerShadowRoot, elem) ||
        ownerShadowRoot.createElement(elem.is, elem.stylingName)
      be.__wxElement = elem
      be.associateValue(elem)
      setAttributes(elem, be)
      const listeners = elem.getModelBindingListeners()
      Object.keys(listeners).forEach((name) => {
        be.setModelBindingStat(name, listeners[name]!)
      })
      insertChildren(be, createChildren(elem, ownerShadowRoot))
      setSlotName(elem, be)
    } else if (flexui.ShadowRoot.isShadowRoot(elem)) {
      be = ownerShadowRoot
      be.__wxElement = elem
      be.associateValue(elem)
      setAttributes(elem, be)
      insertChildren(be, createChildren(elem, ownerShadowRoot))
    } else if (flexui.VirtualNode.isVirtualNode(elem)) {
      be =
        handlers.createElement?.(ownerShadowRoot, elem) ||
        ownerShadowRoot.createVirtualNode(elem.is)
      be.__wxElement = elem
      be.associateValue(elem)
      if (elem.isInheritSlots()) {
        be.setInheritSlots()
      }
      setAttributes(elem, be)
      insertChildren(be, createChildren(elem, ownerShadowRoot))
      setSlotName(elem, be)
    } else if (flexui.Component.isComponent(elem)) {
      const options = elem.getComponentOptions()
      be =
        handlers.createElement?.(ownerShadowRoot, elem) ||
        (() => {
          const [styleScope, extraStyleScope] = elem.getStyleScopes()
          return ownerShadowRoot.createComponent(
            elem.tagName,
            elem.isExternal(),
            elem.isVirtual(),
            styleScope ?? flexui.StyleScopeManager.globalScope(),
            extraStyleScope,
            Object.keys(elem.getExternalClasses()),
            elem.getShadowRoot()?.getSlotMode() ?? null,
            options.writeIdToDOM,
          )
        })()
      be.__wxElement = elem
      be.associateValue(elem)
      setAttributes(elem, be)
      const externalClasses = elem.getExternalClasses()
      if (externalClasses) {
        Object.entries(externalClasses).forEach(([key, value]) => {
          if (value) be.setClassAlias(key, value)
        })
      }
      const shadowRoot = elem.getShadowRoot()
      if (shadowRoot) {
        const shadowRootBe = be.getShadowRoot()
        if (shadowRootBe) {
          backendElementMap.set(shadowRoot, recv(shadowRoot, shadowRootBe))
        }
      }
      insertChildren(be, createChildren(elem, ownerShadowRoot))
      setSlotName(elem, be)
    } else {
      throw new Error(`Unknown elem type ${elem.constructor.name}`)
    }
    handlers.afterReplayElement?.(ownerShadowRoot, elem, be)
    backendElementMap.set(elem, be)
    return be
  }

  const result = recv(element, context.getRootNode())
  backendElementMap.clear()
  return result
}