/* eslint-disable no-unused-expressions */
/* eslint-disable no-param-reassign */
/* eslint-disable consistent-return */
/* eslint-disable no-multi-assign */
/* eslint-disable no-multi-spaces */
/* eslint-disable class-methods-use-this */
/* eslint-disable prefer-rest-params */
/* eslint-disable import/prefer-default-export */
import Vue from 'vue'
import { getNestedValue } from 'vuetify/lib/util/helpers' // Types
import { Service } from '../service' // Utilities
import * as ThemeUtils from './utils'

export class Theme extends Service {
  constructor (preset) {
    super()
    this.disabled = false
    this.isDark = null
    this.unwatch = null
    this.vueMeta = null
    const {
      dark,
      disable,
      options,
      themes
    } = preset[Theme.property]
    this.dark = Boolean(dark)
    this.defaults = this.themes = themes
    this.options = options

    if (disable) {
      this.disabled = true
      return
    }

    this.themes = {
      dark: this.fillVariant(themes.dark, true),
      light: this.fillVariant(themes.light, false)
    }
  } // When setting css, check for element and apply new values

  /* eslint-disable-next-line accessor-pairs */

  set css (val) {
    if (this.vueMeta) {
      if (this.isVueMeta23) {
        this.applyVueMeta23()
      }

      return
    }

    this.checkOrCreateStyleElement() && (this.styleEl.innerHTML = val)
  }

  set dark (val) {
    const oldDark = this.isDark
    this.isDark = val // Only apply theme after dark
    // has already been set before

    oldDark != null && this.applyTheme()
  }

  get dark () {
    return Boolean(this.isDark)
  } // Apply current theme default
  // only called on client side

  applyTheme () {
    if (this.disabled) return this.clearCss()
    this.css = this.generatedStyles
  }

  clearCss () {
    this.css = ''
  } // Initialize theme for SSR and SPA
  // Attach to ssrContext head or
  // apply new theme to document

  init (root, ssrContext) {
    if (this.disabled) return
    /* istanbul ignore else */
    if (root.$meta) {
      this.initVueMeta(root)
    } else if (ssrContext) {
      this.initSSR(ssrContext)
    }

    this.initTheme(root)
  } // Allows for you to set target theme

  setTheme (theme, value) {
    this.themes[theme] = Object.assign(this.themes[theme], value)
    this.applyTheme()
  } // Reset theme defaults

  resetThemes () {
    this.themes.light = { ...this.defaults.light }
    this.themes.dark = { ...this.defaults.dark }
    this.applyTheme()
  } // Check for existence of style element

  checkOrCreateStyleElement () {
    this.styleEl = document.getElementById('shipnity-theme-stylesheet')
    /* istanbul ignore next */

    if (this.styleEl) return true
    this.genStyleElement() // If doesn't have it, create it

    return Boolean(this.styleEl)
  }

  fillVariant (theme = {}, dark) {
    const defaultTheme = this.themes[dark ? 'dark' : 'light']
    return { ...defaultTheme, ...theme }
  } // Generate the style element
  // if applicable

  genStyleElement () {
    /* istanbul ignore if */
    if (typeof document === 'undefined') return
    /* istanbul ignore next */

    this.styleEl = document.createElement('style')
    this.styleEl.type = 'text/css'
    this.styleEl.id = 'shipnity-theme-stylesheet'

    if (this.options.cspNonce) {
      this.styleEl.setAttribute('nonce', this.options.cspNonce)
    }

    document.head.appendChild(this.styleEl)
  }

  initVueMeta (root) {
    this.vueMeta = root.$meta()

    if (this.isVueMeta23) {
      // vue-meta needs to apply after mounted()
      root.$nextTick(() => {
        this.applyVueMeta23()
      })
      return
    }

    const metaKeyName = typeof this.vueMeta.getOptions === 'function' ? this.vueMeta.getOptions().keyName : 'metaInfo'
    const metaInfo = root.$options[metaKeyName] || {}

    root.$options[metaKeyName] = () => {
      metaInfo.style = metaInfo.style || []
      const shipnityStylesheet = metaInfo.style.find((s) => s.id === 'shipnity-theme-stylesheet')

      if (!shipnityStylesheet) {
        metaInfo.style.push({
          cssText: this.generatedStyles,
          type: 'text/css',
          id: 'shipnity-theme-stylesheet',
          nonce: (this.options || {}).cspNonce
        })
      } else {
        shipnityStylesheet.cssText = this.generatedStyles
      }

      return metaInfo
    }
  }

  applyVueMeta23 () {
    const {
      set
    } = this.vueMeta.addApp('shipnity')
    set({
      style: [{
        cssText: this.generatedStyles,
        type: 'text/css',
        id: 'shipnity-theme-stylesheet',
        nonce: this.options.cspNonce
      }]
    })
  }

  initSSR (ssrContext) {
    // SSR
    const nonce = this.options.cspNonce ? ` nonce="${this.options.cspNonce}"` : ''
    ssrContext.head = ssrContext.head || ''
    ssrContext.head += `<style type="text/css" id="shipnity-theme-stylesheet"${nonce}>${this.generatedStyles}</style>`
  }

  initTheme (root) {
    // Only watch for reactivity on client side
    if (typeof document === 'undefined') return // If we get here somehow, ensure
    // existing instance is removed

    if (this.unwatch) {
      this.unwatch()
      this.unwatch = null
    } // TODO: Update to use RFC if merged
    // https://github.com/vuejs/rfcs/blob/advanced-reactivity-api/active-rfcs/0000-advanced-reactivity-api.md

    root.$once('hook:created', () => {
      const obs = Vue.observable({
        themes: this.themes
      })
      this.unwatch = root.$watch(() => obs.themes, () => this.applyTheme(), {
        deep: true
      })
    })
    this.applyTheme()
  }

  get currentTheme () {
    const target = this.dark ? 'dark' : 'light'
    return this.themes[target]
  }

  get generatedStyles () {
    const theme = this.parsedTheme
    /* istanbul ignore next */

    const options = this.options || {}
    let css

    if (options.themeCache != null) {
      css = options.themeCache.get(theme)
      /* istanbul ignore if */

      if (css != null) return css
    }

    css = ThemeUtils.genStyles(theme, options.customProperties)

    if (options.minifyTheme != null) {
      css = options.minifyTheme(css)
    }

    if (options.themeCache != null) {
      options.themeCache.set(theme, css)
    }

    return css
  }

  get parsedTheme () {
    return ThemeUtils.parse(this.currentTheme || {}, undefined, getNestedValue(this.options, ['variations'], true))
  } // Is using v2.3 of vue-meta
  // https://github.com/nuxt/vue-meta/releases/tag/v2.3.0

  get isVueMeta23 () {
    return typeof this.vueMeta.addApp === 'function'
  }
}
Theme.property = 'theme'
