import { nextTick, onBeforeUnmount, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'

import { DEFAULT_LOCALE } from 'src/i18n/languages'
import { useLocaleStore } from 'src/stores/localeStore'
import { useRuntimeTranslationStore } from 'src/stores/runtimeTranslationStore'

const textOriginals = new WeakMap()
const attrOriginals = new WeakMap()
const TRANSLATABLE_ATTRS = ['placeholder', 'title', 'aria-label']
const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA'])

function normalizeRuntimeText (value) {
  return String(value || '').trim().replace(/\s+/g, ' ')
}

function preserveOuterWhitespace (original, translated) {
  const text = String(original || '')
  const leading = text.match(/^\s*/)?.[0] || ''
  const trailing = text.match(/\s*$/)?.[0] || ''
  return `${leading}${translated}${trailing}`
}

function shouldSkipNode (node) {
  const parent = node?.parentElement
  if (!parent) return true
  if (SKIP_TAGS.has(parent.tagName)) return true
  if (parent.closest?.('[data-no-runtime-i18n="true"]')) return true
  return false
}

export function useRuntimeDomTranslations () {
  const route = useRoute()
  const localeStore = useLocaleStore()
  const runtimeTranslations = useRuntimeTranslationStore()
  let observer = null
  let applyTimer = null

  function translateTextNode (node) {
    if (shouldSkipNode(node)) return
    const current = String(node.nodeValue || '')
    if (!normalizeRuntimeText(current)) return

    if (!textOriginals.has(node)) {
      textOriginals.set(node, current)
    }
    const original = textOriginals.get(node)
    const key = normalizeRuntimeText(original)
    const translated = localeStore.locale === DEFAULT_LOCALE
      ? key
      : runtimeTranslations.byText[key]

    const nextValue = preserveOuterWhitespace(original, translated || key)
    if (node.nodeValue !== nextValue) {
      node.nodeValue = nextValue
    }
  }

  function translateElementAttrs (el) {
    if (!el || el.nodeType !== Node.ELEMENT_NODE) return
    if (el.closest?.('[data-no-runtime-i18n="true"]')) return
    let originals = attrOriginals.get(el)
    if (!originals) {
      originals = {}
      attrOriginals.set(el, originals)
    }

    for (const attr of TRANSLATABLE_ATTRS) {
      if (!el.hasAttribute(attr)) continue
      const current = el.getAttribute(attr)
      if (!normalizeRuntimeText(current)) continue
      if (!Object.prototype.hasOwnProperty.call(originals, attr)) {
        originals[attr] = current
      }
      const key = normalizeRuntimeText(originals[attr])
      const translated = localeStore.locale === DEFAULT_LOCALE
        ? key
        : runtimeTranslations.byText[key]
      const nextValue = translated || key
      if (el.getAttribute(attr) !== nextValue) {
        el.setAttribute(attr, nextValue)
      }
    }
  }

  function translateRoot (root = document.body) {
    if (typeof document === 'undefined' || !root) return

    const walker = document.createTreeWalker(
      root,
      NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
      {
        acceptNode (node) {
          if (node.nodeType === Node.TEXT_NODE) {
            return normalizeRuntimeText(node.nodeValue) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT
          }
          return NodeFilter.FILTER_ACCEPT
        }
      }
    )

    let node = walker.currentNode
    while (node) {
      if (node.nodeType === Node.TEXT_NODE) {
        translateTextNode(node)
      } else if (node.nodeType === Node.ELEMENT_NODE) {
        translateElementAttrs(node)
      }
      node = walker.nextNode()
    }
  }

  function scheduleApply () {
    if (applyTimer) clearTimeout(applyTimer)
    applyTimer = setTimeout(async () => {
      applyTimer = null
      await nextTick()
      translateRoot(document.body)
    }, 30)
  }

  onMounted(async () => {
    await runtimeTranslations.loadLocale(localeStore.locale)
    scheduleApply()
    observer = new MutationObserver(() => scheduleApply())
    observer.observe(document.body, {
      childList: true,
      subtree: true,
      characterData: true,
      attributes: true,
      attributeFilter: TRANSLATABLE_ATTRS
    })
  })

  watch(
    () => localeStore.locale,
    async (locale) => {
      await runtimeTranslations.loadLocale(locale)
      scheduleApply()
    }
  )

  watch(
    () => route.fullPath,
    () => scheduleApply()
  )

  watch(
    () => runtimeTranslations.version,
    () => scheduleApply()
  )

  onBeforeUnmount(() => {
    if (observer) observer.disconnect()
    if (applyTimer) clearTimeout(applyTimer)
  })
}
