/**
 * Utility functions for EdGEL custom elements.
 */
import { adoptStyles, supportsAdoptingStyleSheets, unsafeCSS } from 'lit'

function ceStore() {
  globalThis.EdGEL ||= {}
  globalThis.EdGEL.CustomElement ||= {
    backlog: []
  }

  // Allow CSS href to be set if EdGEL not used elsewhere on page.
  globalThis.EdGEL.CustomElement.css ||= globalThis.EdGELCSS

  return globalThis.EdGEL.CustomElement
}

export function getId(base) {
  const store = ceStore()

  store.idCounter ||= 0
  base ||= 'edgel'

  return `${base}-${store.idCounter++}`
}

function clearBacklog() {
  const store = ceStore()

  if (store.stylesheet) {
    while (store.backlog.length) {
      const ce = store.backlog.shift()
      globalThis.customElements.define(...ce)
    }
  }
}

function buildSheet(cssRules, baseUrl) {
  let cssText = ''

  for (const ruleText of cssRules) {
    cssText += baseUrl ?
      ruleText.replace(/url\(['"]?([^'"]+)['"]?\)/g, (_, url) => {
        const absolute = URL.parse(url, baseUrl)
        return `url("${absolute ? absolute.href : url}")`
      }) :
      ruleText
  }

  return cssText ? unsafeCSS(cssText) : null
}

// We're using Lit's adoptStyles function (for adding styles to an element's renderRoot) to add
// a stylesheet to the document. If the browser does not support adopting stylesheets, target
// document.head for the <style> tag location instead of using adoptedStyleSheets on document.
function documentAddSheet(sheet) {
  const renderRoot = supportsAdoptingStyleSheets ? document : document.head
  adoptStyles(renderRoot, [sheet])
}

function storeSheet(sheet, baseUrl) {
  // We need to apply body and :root differently to work in ShadowDOM.  The body selector becomes :host,
  // but note this context has a much higher priority in the cascades than the rest of the stylesheet
  // and so cannot be used for the default CSS variable variables, so we have to leave the :root selector
  // applied to the Document.
  const cssShadow = []
  const cssLight = []
  for (const rule of sheet.cssRules) {
    const cssLine = rule.cssText.replace(/^\s*body/gm, ':host')

    if (cssLine.includes(':root') || cssLine.includes('@font-face')) {
      cssLight.push(cssLine)
    } else {
      cssShadow.push(cssLine)
    }
  }

  // Drop the colour styling in :host context since it takes priority in cascades.
  cssShadow.push(':host { background: unset; color: unset; }')

  const store = ceStore()
  store.stylesheet = buildSheet(cssShadow)

  // NB We do not use store.rootStylesheet if the author linked the stylesheet
  // on the page; we leave just leave the original sheet there.
  if (cssLight) {
    store.rootStylesheet = buildSheet(cssLight, baseUrl)
  }
}

// Clone stylesheet and remove from main document
function cssLoaded(event) {
  const store = ceStore()
  if (event.target.sheet) {
    storeSheet(event.target.sheet, event.target.href)
    if (store.rootStylesheet) {
      documentAddSheet(store.rootStylesheet)
    }

    clearBacklog()
  } else {
    // eslint-disable-next-line no-console
    console.error(`Could not parse configured external CSS resource at ${store.css}`)
  }

  event.target.remove()
}

function cssFailed(event) {
  const store = ceStore()
  // eslint-disable-next-line no-console
  console.error(`Could not load configured external CSS resource at ${store.css}`)
  event.target.remove()
}

// Find the EdGEL CSS loaded to the page and construct a new stylesheet based on it since
// only constructed stylesheets can be adopted in our components.
function findEdgelCss(timestamp) {
  const store = ceStore()
  if (!store.stylesheet) {
    for (const sheet of globalThis.document.styleSheets) {
      const found = Object.keys(sheet.cssRules).some(k => sheet.cssRules[k].cssText.includes('--edgel-body-bg:'))

      if (found) {
        storeSheet(sheet)
      }
    }
  }

  if (store.stylesheet) {
    clearBacklog()
  } else if (timestamp) {
    // eslint-disable-next-line no-console
    console.error('Could not locate EdGEL CSS on page when trying to define custom elements. ' +
      'Either include it in the page or specify the file location in globalThis.EdGELCSS')
  }
}

// If we haven't found any EdGEL CSS loaded on the page, add a <style> tag to the page to do that.
function loadCSS() {
  const store = ceStore()

  if (store.stylesheet === undefined && store.css) {
    const linkElement = document.createElement('link')
    linkElement.setAttribute('rel', 'stylesheet')
    linkElement.setAttribute('type', 'text/css')
    linkElement.setAttribute('href', store.css)
    linkElement.addEventListener('load', cssLoaded, { once: true })
    linkElement.addEventListener('error', cssFailed, { once: true })
    document.querySelector('head').append(linkElement)

    // Avoid re-entry adding a second link tag
    store.stylesheet = null
  }
}

export function edgelCSS() {
  const store = ceStore()
  return store.stylesheet
}

// Pause defining a custom element until EdGEL CSS has been loaded to the page
// since all elements adopt the same constructed stylesheet.
export function customElement(tag, Element) {
  if (typeof globalThis !== 'undefined' && !globalThis.customElements.get(tag)) {
    const store = ceStore()
    let missingCSS = false

    if (!store.stylesheet) {
      findEdgelCss()

      if (!store.stylesheet) {
        missingCSS = true
      }
    }

    if (missingCSS) {
      if (!store.backlog.length) {
        // Add style for hiding FOUC since there will be a delay while the EdGEL stylesheet is loaded
        const sheet = buildSheet([':not(:defined) { display: none; }'])
        documentAddSheet(sheet)

        if (store.css) {
          loadCSS()
        } else {
          globalThis.requestAnimationFrame(findEdgelCss)
        }
      }

      store.backlog.push([tag, Element])
    } else {
      globalThis.customElements.define(tag, Element)
    }
  }
}
