/**
 * --------------------------------------------------------------------------
 * consent.js
 * Cookie Consent Banner.
 * --------------------------------------------------------------------------
 */

import EventHandler from 'bootstrap/js/src/dom/event-handler.js'
import SelectorEngine from 'bootstrap/js/src/dom/selector-engine.js'
import TemplateFactory from 'bootstrap/js/src/util/template-factory.js'
import EdGELBaseComponent from './edgel-base-component.js'
import { escapeHtml } from './helpers.js'

import tplBanner from './templates/uoe-consent-banner.js'
import tplErrors from './templates/uoe-consent-errors.js'
import tplLid from './templates/uoe-consent-lid.js'
import tplPreferences from './templates/uoe-consent-preferences.js'

/**
 * Constants
 */

const NAME = 'consent'
const DATA_KEY = 'uoe.consent'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const VERSION = 3

const CLASS_NAME_COOKIE_JAR = 'cookie-jar'
const CLASS_NAME_COOKIE_LID = 'cookie-lid'
const CLASS_NAME_SHOW = 'show'
const CLASS_NAME_COLLAPSING = 'cookie-collapsing'
const CLASS_NAME_COLLAPSE = 'cookie-collapse'
const TRANSITION_DURATION = 410

const COOKIE_DOMAIN = '.ed.ac.uk'
const MAIN_HEADING = 'Cookies on the University of Edinburgh website'
const PREFS_HEADING = 'Change my cookie settings for the University of Edinburgh website'
const NECESSARY_URL = 'https://www.ed.ac.uk/about/website/privacy/necessary-cookies'
const ADVERTS_URL = 'https://www.ed.ac.uk/about/website/privacy/advertising-cookies'
const COOKIE_NAME = `_uoeCookiePrefsV${VERSION}`
const COOKIE_EXPIRES_DAYS = 365
const COOKIE_EXPIRES = COOKIE_EXPIRES_DAYS * 24 * 60 * 60 * 1000
const DEFUNCT_COOKIES = [
  '_uoeCookiePrefs',
  '_uoeCookiePrefsV1',
  '_uoeCookiePrefsV2'
]

const SELECTOR_SAVE_BTN = 'button[data-cookie-save]'
const SELECTOR_CHOICES = 'input[data-cookie-choice]'
const SELECTOR_CHOICES_SELECTED = `${SELECTOR_CHOICES}:checked`

const TPL_SELECTOR_HEADING = 'h3.cookie-heading'
const TPL_SELECTOR_ERROR_LIST = 'ul'
const TPL_SELECTOR_NECESSARY_LINK = '.cookie-necessary-url'
const TPL_SELECTOR_ADVERTS_LINK = '.cookie-adverts-url'

const EVENT_CLICK = `click${EVENT_KEY}`
const EVENT_CHANGE = `change${EVENT_KEY}`
const EVENT_CHANGED = `changed${EVENT_KEY}`
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`

const Permissions = {
  necessary: {
    mandatory: true,
    isDefault: true
  },
  advertising: {
    mandatory: false,
    isDefault: true
  }
}

const Choices = {
  all: {
    permissions: ['advertising', 'necessary'],
    selected: false
  },
  necessaryOnly: {
    permissions: ['necessary'],
    selected: false
  }
}

const Default = {
  cookieDomain: COOKIE_DOMAIN,
  mainHeading: MAIN_HEADING,
  prefsHeading: PREFS_HEADING,
  necessaryUrl: NECESSARY_URL,
  advertsUrl: ADVERTS_URL,
  disabled: false
}

const DefaultType = {
  cookieDomain: 'string',
  mainHeading: 'string',
  prefsHeading: 'string',
  necessaryUrl: 'string',
  advertsUrl: 'string',
  disabled: 'boolean'
}

/**
 * Class definition
 */

class Consent extends EdGELBaseComponent {
  constructor(element, config) {
    super(element, config)

    this._loaded = false
    this._errors = this._errors || []

    if (this._config.disabled) {
      return
    }

    this._templateFactories = {}
    this._templateConfigs = {
      banner: {
        sanitize: false,
        html: false,
        template: tplBanner,
        content: {
          [TPL_SELECTOR_HEADING]: this._config.mainHeading
        }
      },
      errors: {
        sanitize: false,
        html: true,
        template: tplErrors,
        content: {
          [TPL_SELECTOR_ERROR_LIST]: this._renderErrors.bind(this)
        }
      },
      lid: {
        sanitize: false,
        html: false,
        template: tplLid,
        content: {}
      },
      preferences: {
        sanitize: false,
        html: true,
        template: tplPreferences,
        content: {
          [TPL_SELECTOR_HEADING]: escapeHtml(this._config.prefsHeading),
          [TPL_SELECTOR_NECESSARY_LINK]: this._renderNecessaryLink.bind(this),
          [TPL_SELECTOR_ADVERTS_LINK]: this._renderAdvertsLink.bind(this)
        }
      }
    }

    this._selections = {}
    this._choices = { ...Choices }

    for (const option of Object.getOwnPropertyNames(this._choices)) {
      this._choices[option].selected = false
      this._choices[option].merged = this._choices[option].permissions.sort().join(',')
    }

    // Add the cookie jar
    this._cookieJar = document.createElement('div')
    this._cookieJar.className = `${CLASS_NAME_COOKIE_JAR} ${CLASS_NAME_COLLAPSE}`
    this._element.insertBefore(this._cookieJar, this._element.firstChild)

    // Add the cookie lid
    this._cookieLid = document.createElement('div')
    this._cookieLid.className = `${CLASS_NAME_COOKIE_LID} ${CLASS_NAME_COLLAPSE}`
    this._cookieLid.append(this._getTemplateFactory('lid').toHtml())
    this._element.insertBefore(this._cookieLid, null)

    this._load()
    this._displayBanner(false)
    this._addEventListeners()
    this._notifyChoices()
  }

  // Getters
  static get Default() {
    return Default
  }

  static get DefaultType() {
    return DefaultType
  }

  static get NAME() {
    return NAME
  }

  // Public

  allowed(permission) {
    const prefs = this._getSelections()
    return prefs[permission]
  }

  allowedList() {
    const prefs = this._getSelections()
    const allowed = []

    for (const pref of Object.getOwnPropertyNames(prefs)) {
      if (prefs[pref]) {
        allowed.push(pref)
      }
    }

    return allowed.sort().join(',')
  }

  bannerRequired() {
    return !(this.hasSavedSettings())
  }

  hasSavedSettings() {
    return this._loaded
  }

  // Private
  _configAfterMerge(config) {
    if (config.cookieDomain.startsWith('.')) {
      config.cookieDomain = config.cookieDomain.slice(1)
    }

    this._errors = this._errors || []
    this._validateUrl(config.necessaryUrl, config.cookieDomain)
    this._validateUrl(config.advertsUrl, config.cookieDomain)

    return config
  }

  _validateUrl(testUrl, cookieDomain) {
    // Validate URL
    let url
    try {
      url = new URL(testUrl)
    } catch {
      this._setError(`Invalid URL provided in "${testUrl}"`)
      return
    }

    if (url.protocol !== 'https:') {
      this._setError(`Only https protocol accepted for "${testUrl}"`)
    }

    if (!url.hostname.endsWith(`.${cookieDomain}`)) {
      this._setError(`Mismatch between cookie domain and link hostname "${testUrl}"`)
    }
  }

  _notifyChoices() {
    const args = {
      permissions: { ...Permissions },
      allowedList: this.allowedList()
    }

    EventHandler.trigger(document, EVENT_CHANGED, args)
  }

  _addEventListeners() {
    EventHandler.on(this._cookieLid, EVENT_CLICK, event => {
      event.preventDefault()
      event.stopPropagation()

      this._editPrefs()
    })

    EventHandler.on(this._cookieJar, EVENT_CLICK, event => {
      if (event.target.matches('button[data-cookie-save]')) {
        event.preventDefault()
        event.stopPropagation()

        this._savePrefs()
        // Re-open the banner; it should close to the lid
        this._displayBanner()
      }

      if (event.target.matches('button[data-cookie-save-default]')) {
        event.preventDefault()
        event.stopPropagation()

        this._savePrefs(true)
        // Re-open the banner; it should close to the lid
        this._displayBanner()
      }

      if (event.target.matches('button[data-cookie-change]')) {
        event.preventDefault()
        event.stopPropagation()

        this._editPrefs()
      }

      if (event.target.matches('div.cookie-check')) {
        event.preventDefault()
        event.stopPropagation()

        // clicked on surround div, so click on the radio button
        const checkbox = SelectorEngine.findOne('input', event.target)
        checkbox.checked = true
      }
    })

    EventHandler.on(this._cookieJar, EVENT_CHANGE, SELECTOR_CHOICES, () => {
      SelectorEngine.findOne(SELECTOR_SAVE_BTN, this._cookieJar).disabled = ''
    })
  }

  _applyStyle(element, styles) {
    for (const prop of Object.getOwnPropertyNames(styles)) {
      element.style[prop] = styles[prop]
    }
  }

  _show(element, animate) {
    if (!animate) {
      element.classList.add(CLASS_NAME_SHOW)
    }

    if (!element.classList.contains(CLASS_NAME_SHOW)) {
      const { paddingTop, paddingBottom } = window.getComputedStyle(element)
      element.classList.remove(CLASS_NAME_COLLAPSE)
      element.classList.add(CLASS_NAME_COLLAPSING)

      // eslint-disable-next-line prefer-arrow-callback
      setTimeout(function (element) {
        element.classList.remove(CLASS_NAME_COLLAPSING)
        element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW)
        element.style.height = ''
        element.style.paddingTop = ''
        element.style.paddingBottom = ''
      }.bind(this, element), TRANSITION_DURATION)

      // eslint-disable-next-line prefer-arrow-callback
      setTimeout(function (element, paddingTop, paddingBottom) {
        element.style.height = `${element.scrollHeight}px`
        element.style.paddingTop = paddingTop
        element.style.paddingBottom = paddingBottom
      }.bind(this, element, paddingTop, paddingBottom), 10)
    }
  }

  _hide(element, animate) {
    if (!animate) {
      element.classList.remove(CLASS_NAME_SHOW)
    }

    if (element.classList.contains(CLASS_NAME_SHOW)) {
      element.style.height = `${element.scrollHeight}px`
      element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW)
      element.classList.add(CLASS_NAME_COLLAPSING)

      // eslint-disable-next-line prefer-arrow-callback
      setTimeout(function (element) {
        element.classList.remove(CLASS_NAME_COLLAPSING)
        element.classList.add(CLASS_NAME_COLLAPSE)
      }.bind(this, element), TRANSITION_DURATION)

      // eslint-disable-next-line prefer-arrow-callback
      setTimeout(function (element) {
        element.style.height = ''
      }.bind(this, element), 10)
    }
  }

  _openJar(animate) {
    if (!this._cookieJar || !this._cookieLid) {
      return
    }

    document.documentElement.scrollTop = 0 // <html>
    SelectorEngine.findOne('body').scrollTop = 0

    this._show(this._cookieJar, animate)
    this._hide(this._cookieLid, animate)
  }

  _closeJar(animate) {
    if (!this._cookieJar || !this._cookieLid) {
      return
    }

    this._show(this._cookieLid, animate)
    this._hide(this._cookieJar, animate)
  }

  _getSelections() {
    const values = {}

    for (const p of Object.getOwnPropertyNames(Permissions)) {
      values[p] = p in this._selections ? this._selections[p] : false
    }

    return values
  }

  _reset() {
    this._loaded = false
    this._selections = {}
  }

  _load() {
    this._reset()
    this._selections = this._readCookie(this._config.cookieDomain)
    this._loaded = true

    // If we are missing any mandatory values, we don't have a valid load.
    for (const perm of Object.getOwnPropertyNames(Permissions)) {
      if (Permissions[perm].mandatory && !(perm in this._selections)) {
        this._loaded = false
      }
    }
  }

  _readCookie(update = true) {
    const decodedCookies = decodeURIComponent(document.cookie).split(';')
    let json = ''

    for (let cookie of decodedCookies) {
      cookie = cookie.trim()
      if (cookie.startsWith(`${COOKIE_NAME}=`)) {
        json = cookie.slice(COOKIE_NAME.length + 1)
      }
    }

    if (json !== '') {
      let requiresUpdate = false
      let parsed = {}

      try {
        // If we have raw JSON string encode this since the value can cause
        // problems with other systems: I180531-0649
        if (json.startsWith('{')) {
          requiresUpdate = true
        } else {
          json = window.atob(json)
        }

        parsed = JSON.parse(json)
      } catch {}

      if (requiresUpdate && update) {
        this._writeCookie(parsed)
      }

      return parsed
    }

    return {}
  }

  _writeCookie() {
    if (!this._loaded) {
      return
    }

    const d = new Date()
    d.setTime(d.getTime() + COOKIE_EXPIRES)
    const expires = `expires=${d.toUTCString()}`

    const cValue = window.btoa(JSON.stringify(this._selections))
    document.cookie = `${COOKIE_NAME}=${cValue};${expires};domain=${this._config.cookieDomain};path=/`

    // Confirm that the write worked by doing straight comparison of btoa values
    const readBack = window.btoa(JSON.stringify(this._readCookie(false)))
    if (readBack !== cValue) {
      this._setError(`Cookie setting write failed. Check the cookie domain (${COOKIE_DOMAIN}).`)
      this._loaded = false
    }

    // Notify subscribers that a change has happened.
    this._notifyChoices()
  }

  _expireDefunct() {
    const d = new Date()
    d.setTime(d.getTime() - (60 * 60 * 1000)) // set expired date 1 hour ago
    const expires = `expires=${d.toUTCString()}`

    for (const defunct of DEFUNCT_COOKIES) {
      document.cookie = `${defunct}=;${expires};domain=${this._config.cookieDomain};path=/`
    }
  }

  _savePrefs(defaults = false) {
    this._reset()
    // Wipe all previously stored cookie settings.
    this._expireDefunct()

    for (const key of Object.getOwnPropertyNames(Permissions)) {
      this._selections[key] = Permissions[key].mandatory
    }

    if (defaults) {
      for (const key of Object.getOwnPropertyNames(Permissions)) {
        this._selections[key] = Boolean(this._selections[key]) || Permissions[key].isDefault
      }
    } else {
      const items = SelectorEngine.find(SELECTOR_CHOICES_SELECTED, this._cookieJar)
      for (const item of items) {
        for (const permission of item.value.split(',')) {
          this._selections[permission] = true
        }
      }
    }

    this._loaded = true
    this._writeCookie()
  }

  _editPrefs() {
    this._cookieJar.innerHTML = ''
    this._cookieJar.append(this._getTemplateFactory('preferences').toHtml())

    let oneOptionSelected = false

    for (const option of Object.getOwnPropertyNames(this._choices)) {
      this._choices[option].selected = false
    }

    this._load()
    if (this._loaded) {
      // Get a sorted comma-separated list of selected object keys
      const active = Object.entries(this._getSelections())
        .filter(a => {
          return a[1]
        })
        .map(a => {
          return a[0]
        })
        .sort()
        .join(',')

      for (const option of Object.getOwnPropertyNames(this._choices)) {
        if (this._choices[option].merged === active) {
          this._choices[option].selected = true
          const input = SelectorEngine.findOne(`input[value='${active}']`, this._cookieJar)
          if (input) {
            input.checked = true
            oneOptionSelected = true
          }
        }
      }
    }

    const button = SelectorEngine.findOne('button', this._cookieJar)
    button.disabled = !oneOptionSelected

    this._openJar(true)
  }

  _setError(error) {
    this._errors.push(error)
    // eslint-disable-next-line no-console
    console.error(error)
  }

  _displayBanner(animate = true) {
    // Reload preferences from _uoeCookiePrefsV* cookie, if present.
    this._load()

    if (this._errors.length) {
      this._cookieJar.innerHTML = ''
      this._cookieJar.append(this._getTemplateFactory('errors').toHtml())
      this._openJar(animate)
    } else if (this.bannerRequired()) {
      this._cookieJar.innerHTML = ''
      this._cookieJar.append(this._getTemplateFactory('banner').toHtml())
      this._openJar(animate)
    } else {
      this._closeJar(animate)
    }
  }

  _getTemplateFactory(tpl) {
    if (!this._templateFactories[tpl]) {
      this._templateFactories[tpl] = new TemplateFactory(this._templateConfigs[tpl])
    }

    return this._templateFactories[tpl]
  }

  _renderNecessaryLink() {
    const { necessaryUrl } = this._config

    if (necessaryUrl) {
      return `<a class="cookie-link" href="${encodeURI(necessaryUrl)}">Learn more about necessary cookies</a>`
    }
  }

  _renderAdvertsLink() {
    const { advertsUrl } = this._config

    if (advertsUrl) {
      return `<a class="cookie-link" href="${encodeURI(advertsUrl)}">Learn more about advertising cookies</a>`
    }
  }

  _renderErrors() {
    return this._errors.map(e => `<li>${escapeHtml(e)}</li>`)
  }

  // DEPRECATED: Allow extra departmental permissions to be added to the banner.
  addPermission() {
    // eslint-disable-next-line no-console
    console.warn('REMOVED: Cookie Consent addPermission can no longer be used to add extra departmental permissions')
  }
}

/**
 * Data API implementation
 * Add EdGEL cookie consent banner to page
 */

EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
  Consent.getOrCreateInstance(SelectorEngine.findOne('body'))
})

export default Consent
