/**
 * --------------------------------------------------------------------------
 * display.js
 * Provide animations for display screens
 * --------------------------------------------------------------------------
 */

import {
  defineJQueryPlugin
} from 'bootstrap/js/src/util/index.js'
import Modal from 'bootstrap/js/src/modal.js'
import Manipulator from 'bootstrap/js/src/dom/manipulator.js'
import SelectorEngine from 'bootstrap/js/src/dom/selector-engine.js'
import EventHandler from 'bootstrap/js/src/dom/event-handler.js'
import EdGELBaseComponent from './edgel-base-component.js'
import {
  delay,
  debounce,
  DAYS,
  MONTHS
} from './helpers.js'

/**
 * Constants
 */

const NAME = 'display'
const DATA_KEY = 'uoe.display'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`

const SELECTOR_DISPLAY_SCREEN = '.card-display'
const SELECTOR_CARD_BODY = '.card-body'
const SELECTOR_CLOCK = '.display-clock'
const SELECTOR_LAST_REFRESHED = '[data-provide="uoe-refreshed"]'
const SELECTOR_PAGE_SCROLLER = '[data-provide="uoe-display"]'
const SELECTOR_GRID_BLOCK = '.display-schedule-room'
const SELECTOR_MULTIROOM_BLOCK = '.display-page'
const SELECTOR_ROOM_BLOCK = '.display-booking'
const SELECTOR_SCHEDULE = '.display-schedule'

const BLOCK_SELECTORS = [SELECTOR_GRID_BLOCK, SELECTOR_MULTIROOM_BLOCK, SELECTOR_ROOM_BLOCK]

const Default = {
  animate: true,
  pageHoldSeconds: 15,
  pageReloadSeconds: 181,
  browserTime: true,
  screensaverSrc: null,
  screensaverSeconds: 0
}

const DefaultType = {
  animate: 'boolean',
  pageHoldSeconds: 'number',
  pageReloadSeconds: 'number',
  browserTime: 'boolean',
  screensaverSrc: '(string|null)',
  screensaverSeconds: 'number'
}

/**
 * Class definition
 */

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

    this._page = 0
    this._lastPage = 0
    this._pages = []
    this._scrollerID = null
    this._screensaverID = null
    this._screensaverScheduleID = null
    this._reloadID = null
    this._newContent = ''

    const screensaver = document.createElement('div')
    screensaver.className = 'modal hero-bg'
    screensaver.dataset.bsBackdrop = 'static'
    screensaver.dataset.bsKeyboard = 'false'
    screensaver.setAttribute('tabindex', '-1')
    screensaver.innerHTML = '<div class="modal-dialog"></div>'
    document.body.append(screensaver)
    this._screensaver = Modal.getOrCreateInstance(screensaver)

    this._setRefreshed()
    this._init()
    this._setReload()

    this._observer = new ResizeObserver(debounce(this._resetScroller, this))
    this._observer.observe(this._element)
  }

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

  static get DefaultType() {
    return DefaultType
  }

  static get NAME() {
    return NAME
  }

  _init() {
    this._paged = SelectorEngine.findOne(SELECTOR_PAGE_SCROLLER, this._element)
    this._clock = SelectorEngine.findOne(SELECTOR_CLOCK, this._element)
    this._schedule = SelectorEngine.findOne(SELECTOR_SCHEDULE, this._element)
    this._reloadTimeout = this._config.pageReloadSeconds

    if (this._clock && !this._config.browserTime) {
      // If we are getting time from server then we have to refresh frequently and don't have time for scrolling.
      this._config.animate = false
      this._reloadTimeout = this._config.pageHoldSeconds + this._config.screensaverSeconds
    }

    this._refreshClock()
    this._addScheduleTimes()
    this._resetScroller()

    const refreshed = SelectorEngine.findOne(SELECTOR_LAST_REFRESHED, this._element)
    if (refreshed && this._lastRefreshed && this._config.browserTime) {
      refreshed.textContent = this._lastRefreshed
    }
  }

  _showScreensaver() {
    if (!this._config.screensaverSrc || !this._config.screensaverSeconds) {
      return
    }

    this._screensaver._element.style.backgroundImage = `url(${this._config.screensaverSrc})`

    const { animate } = this._config
    this._screensaverID ||= setTimeout(() => {
      this._screensaverID = null
      this._screensaver.hide()
      if (animate && this._pages.length > 1) {
        this.run()
      } else {
        this._resetScroller()
      }
    }, this._config.screensaverSeconds * 1000)

    this.pause()
    this._screensaver.show()
  }

  _setReload() {
    if (this._reloadTimeout) {
      this._reloadID ||= setTimeout(() => {
        this._reloadPage()
          .catch(error => {
            // eslint-disable-next-line no-console
            console.error(error.toString())
          })
          .finally(() => {
            this._reloadID = null
            this._setReload()
          })
      }, this._reloadTimeout * 1000)
    }
  }

  _refreshClock() {
    if (!this._config.browserTime || !this._clock) {
      return
    }

    const d = new Date()
    const time = `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`
    const date = `${DAYS[d.getDay()]} ${d.getDate().toString().padStart(2, '0')} ${MONTHS[d.getMonth()]}`
    this._clock.innerHTML = `<div>${time}</div><div class="display-small">${date}</div>`

    this._clockID ||= setInterval(this._refreshClock.bind(this), 7000)
  }

  _addScheduleTimes() {
    if (!this._schedule) {
      return
    }

    const start = Number(getComputedStyle(this._schedule).getPropertyValue('--edgel-schedule-start-hour')) || 8
    const end = Number(getComputedStyle(this._schedule).getPropertyValue('--edgel-schedule-end-hour')) || 18
    const timesEl = document.createElement('div')
    timesEl.className = 'display-schedule-times'
    timesEl.setAttribute('aria-hidden', 'true')

    for (let h = start; h < end; h++) {
      const hourEl = document.createElement('div')
      hourEl.className = 'display-schedule-times-item'
      hourEl.style.setProperty('--edgel-schedule-hour', h)
      hourEl.textContent = `${h}:00`.padStart(5, '0')
      timesEl.append(hourEl)
    }

    const beforeEl = SelectorEngine.findOne(SELECTOR_CARD_BODY, this._element)
    if (this._paged && beforeEl) {
      const wrapper = document.createElement('div')
      wrapper.className = 'card-body flex-shrink-0 pb-0'
      wrapper.append(timesEl)
      beforeEl.classList.add('pt-0')
      beforeEl.before(wrapper)
    } else {
      this._schedule.before(timesEl)
    }
  }

  async _scrollNext() {
    if (this._config.animate && this._pages.length > 1) {
      this._page = (this._page + 1) % this._pages.length

      if (this._page === 0 && this._lastPage !== 0) {
        this._paged.classList.add('fade')
        this._paged.classList.remove('show')
        await delay(0.15)
        if (this._newContent) {
          this._refreshPage()
        }

        this._showScreensaver()

        this._paged.scrollTop = this._pages[0]
        this._paged.classList.add('show')
      } else {
        this._paged.scrollTop = this._pages[this._page]
      }

      this._lastPage = this.page
    }
  }

  _scheduleScroll() {
    this._scrollerID ||= setTimeout(() => {
      this._scrollNext()
      this._scrollerID = null
      this._scheduleScroll()
    }, this._config.pageHoldSeconds * 1000)
  }

  _cancelScroll() {
    clearTimeout(this._scrollerID)
    this._scrollerID = null
  }

  _cancelScreensaver() {
    clearTimeout(this._screensaverScheduleID)
    this._screensaverScheduleID = null
  }

  _resetScroller() {
    this._getPages()
    if (this._config.animate && this._pages.length > 1) {
      this._scheduleScroll()
      this._cancelScreensaver()
    } else {
      this._cancelScroll()
      this._scheduleScreensaver()
    }
  }

  // If there is no scrolling, run the screensaver on a timer.
  _scheduleScreensaver() {
    // If the screensaver is currently displayed, there's nothing to do
    if (this._screensaverID) {
      return
    }

    this._screensaverScheduleID ||= setTimeout(() => {
      this._screensaverScheduleID = null
      this._showScreensaver()
    }, this._config.pageHoldSeconds * 1000)
  }

  _getPages() {
    if (!this._paged) {
      this._pages = []
      return
    }

    const pageList = []
    const containerRect = this._paged.getBoundingClientRect()
    const visibleHeight = containerRect.height
    let pageTop = 0
    let offset = 0
    let items = []
    for (const selector of BLOCK_SELECTORS) {
      items = SelectorEngine.find(selector, this._paged)
      if (items.length) {
        break
      }
    }

    for (const [, item] of items.entries()) {
      const rect = item.getBoundingClientRect()
      if (pageList.length === 0) {
        offset = this._paged.scrollTop + rect.top
        pageList.push(pageTop)
      } else if (this._paged.scrollTop + rect.bottom > pageTop + offset + visibleHeight) {
        // The item has fallen off the bottom of the page, so add it as the top of the next page.
        pageTop = this._paged.scrollTop + rect.top - offset
        pageList.push(pageTop)
      }
    }

    this._pages = pageList
  }

  async _reloadPage() {
    const response = await fetch(globalThis.location.href)
    if (!response.ok) {
      throw new Error(`Response status: ${response.status}`)
    }

    const html = await response.text()
    const doc = new DOMParser().parseFromString(html, 'text/html')
    const card = doc.querySelector(SELECTOR_DISPLAY_SCREEN)
    if (!card || !(this._newContent = card.innerHTML)) {
      throw new Error('Invalid page content received')
    }

    const newConfig = Manipulator.getDataAttributes(card)
    this._config = this._getConfig(newConfig)

    this._setRefreshed()

    if (!this._scrollerID) {
      this._refreshPage()
    }
  }

  _refreshPage() {
    if (this._newContent) {
      this._element.innerHTML = this._newContent
      this._newContent = ''
    }

    this._init()
    this._setReload()
  }

  _setRefreshed() {
    const d = new Date()
    const time = `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`
    const date = `${d.getDate().toString().padStart(2, '0')}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getFullYear()}`
    this._lastRefreshed = `Last refreshed: ${date} ${time}`
  }

  dispose() {
    clearTimeout(this._reloadID)
    clearTimeout(this._scrollerID)
    clearTimeout(this._screensaverID)
    clearTimeout(this._screensaverScheduleID)
    clearInterval(this._clockID)
    this._observer.disconnect()
    super.dispose()
  }

  // Public
  pause() {
    this._config.animate = false
    this._resetScroller()
  }

  run() {
    this._config.animate = true
    this._resetScroller()
  }

  // Static
  static jQueryInterface(config) {
    return this.each(function () {
      const data = Display.getOrCreateInstance(this)

      if (typeof config === 'string') {
        if (data[config] === undefined) {
          throw new TypeError(`No method named "${config}"`)
        }

        data[config]()
      }
    })
  }
}

/**
 * Data API implementation
 */
EventHandler.on(globalThis, EVENT_LOAD_DATA_API, () => {
  const displays = SelectorEngine.find(SELECTOR_DISPLAY_SCREEN)
  for (const display of displays) {
    Display.getOrCreateInstance(display)
  }
})

/**
 * jQuery
 */

defineJQueryPlugin(Display)

export default Display
