// Constants
const TRANSITION_EVENTS = ['transitionend','webkitTransitionEnd','oTransitionEnd']
const TRANSITION_PROPERTIES = ['transition', 'MozTransition', 'webkitTransition', 'WebkitTransition', 'OTransition']
const INLINE_STYLE = `
.layout-fixed .layout-1 .layout-sidenav,
.layout-fixed-offcanvas .layout-1 .layout-sidenav {
  top: {navbarHeight}px !important;
}
.layout-container {
  padding-top: {navbarHeight}px !important;
}
.layout-content {
  padding-bottom: {footerHeight}px !important;
}`

// Guard
function requiredParam(name) {
  throw new Error(`Parameter required${name ? ': `' + name + '`' : ''}`)
}

const layoutHelpers = {
  // Root container
  CONTAINER: typeof window !== 'undefined' ? document.documentElement : null,

  // Large screens breakpoint
  LAYOUT_BREAKPOINT: 992,

  // Resize delay in milliseconds
  RESIZE_DELAY: 200,

  // Internal variables
  _curStyle: null,
  _styleEl: null,
  _resizeTimeout: null,
  _resizeCallback: null,
  _transitionCallback: null,
  _transitionCallbackTimeout: null,
  _listeners: [],
  _initialized: false,
  _autoUpdate: false,
  _lastWindowHeight: 0,


  // *******************************************************************************
  // * Utilities

  // ---
  // Add class
  _addClass(cls, el = this.CONTAINER) {
    cls.split(' ').forEach(c => el.classList.add(c))
  },

  // ---
  // Remove class
  _removeClass(cls, el = this.CONTAINER) {
    cls.split(' ').forEach(c => el.classList.remove(c))
  },

  // ---
  // Has class
  _hasClass(cls, el = this.CONTAINER) {
    let result = false

    cls.split(' ').forEach(c => {
      if (el.classList.contains(c)) result = true
    })

    return result
  },

  // ---
  // Check for transition support
  _supportsTransitionEnd() {
    if (window.QUnit) return false

    const el = document.body || document.documentElement

    if (!el) return false

    let result = false
    TRANSITION_PROPERTIES.forEach(evnt => {
      if (typeof el.style[evnt] !== 'undefined') result = true
    })

    return result
  },

  // ---
  // Get animation duration of element
  _getAnimationDuration(el) {
    let duration = window.getComputedStyle(el).transitionDuration

    return parseFloat(duration) * (duration.indexOf('ms') !== -1 ? 1 : 1000)
  },

  // ---
  // Trigger window event
  _triggerWindowEvent(name) {
    if (typeof window === 'undefined') return

    if (document.createEvent) {
      let event

      if (typeof(Event) === 'function') {
        event = new Event(name)
      } else {
        event = document.createEvent('Event')
        event.initEvent(name, false, true)
      }

      window.dispatchEvent(event)
    } else {
      window.fireEvent(`on${name}`, document.createEventObject())
    }
  },

  // ---
  // Trigger event
  _triggerEvent(name) {
    this._triggerWindowEvent(`layout${name}`)

    this._listeners
      .filter(listener => listener.event === name)
      .forEach(listener => listener.callback.call(null))
  },

  // ---
  // Update style
  _updateInlineStyle(navbarHeight = 0, footerHeight = 0) {
    if (!this._styleEl) {
      this._styleEl = document.createElement('style')
      this._styleEl.type = 'text/css'
      document.head.appendChild(this._styleEl)
    }

    const newStyle = INLINE_STYLE
      .replace(/\{navbarHeight\}/ig, navbarHeight)
      .replace(/\{footerHeight\}/ig, footerHeight)

    if (this._curStyle !== newStyle) {
      this._curStyle = newStyle
      this._styleEl.textContent = newStyle
    }
  },

  // ---
  // Remove style
  _removeInlineStyle() {
    if (this._styleEl) document.head.removeChild(this._styleEl)
    this._styleEl = null
    this._curStyle = null
  },

  // ---
  // Redraw layout sidenav (Safari bugfix)
  _redrawLayoutSidenav() {
    const layoutSidenav = this.getLayoutSidenav()

    if (layoutSidenav && layoutSidenav.querySelector('.sidenav')) {
      const inner = layoutSidenav.querySelector('.sidenav-inner')
      const scrollTop = inner.scrollTop
      const pageScrollTop = document.documentElement.scrollTop

      layoutSidenav.style.display = 'none'
      layoutSidenav.offsetHeight
      layoutSidenav.style.display = ''
      inner.scrollTop = scrollTop
      document.documentElement.scrollTop = pageScrollTop

      return true
    }

    return false
  },

  // ---
  // Calculate current navbar height
  _getNavbarHeight() {
    const layoutNavbar = this.getLayoutNavbar()

    if (!layoutNavbar) return 0
    if (!this.isSmallScreen()) return layoutNavbar.getBoundingClientRect().height

    // Needs some logic to get navbar height on small screens

    const clonedEl = layoutNavbar.cloneNode(true)
    clonedEl.id = null
    clonedEl.style.visibility = 'hidden'
    clonedEl.style.position = 'absolute'

    Array.prototype.slice.call(clonedEl.querySelectorAll('.collapse.show'))
      .forEach(el => this._removeClass('show', el))

    layoutNavbar.parentNode.insertBefore(clonedEl, layoutNavbar)

    const navbarHeight = clonedEl.getBoundingClientRect().height

    clonedEl.parentNode.removeChild(clonedEl)

    return navbarHeight
  },

  // ---
  // Get current footer height
  _getFooterHeight() {
    const layoutFooter = this.getLayoutFooter()

    if (!layoutFooter) return 0

    return layoutFooter.getBoundingClientRect().height
  },

  // ---
  // Add layout sivenav toggle animationEnd event
  _bindLayoutAnimationEndEvent(modifier, cb) {
    const sidenav = this.getSidenav()
    const duration = sidenav ? this._getAnimationDuration(sidenav) + 50 : 0

    if (!duration) {
      modifier.call(this)
      cb.call(this)
      return
    }

    this._transitionCallback = e => {
      if (e.target !== sidenav) return
      this._unbindLayoutAnimationEndEvent()
      cb.call(this)
    }

    TRANSITION_EVENTS.forEach(e => {
      sidenav.addEventListener(e, this._transitionCallback, false)
    })

    modifier.call(this)

    this._transitionCallbackTimeout = setTimeout(() => {
      this._transitionCallback.call(this, { target: sidenav })
    }, duration)
  },

  // ---
  // Remove layout sivenav toggle animationEnd event
  _unbindLayoutAnimationEndEvent() {
    const sidenav = this.getSidenav()

    if (this._transitionCallbackTimeout) {
      clearTimeout(this._transitionCallbackTimeout)
      this._transitionCallbackTimeout = null
    }

    if (sidenav && this._transitionCallback) {
      TRANSITION_EVENTS.forEach(e => {
        sidenav.removeEventListener(e, this._transitionCallback, false)
      })
    }

    if (this._transitionCallback) {
      this._transitionCallback = null
    }
  },

  // ---
  // Bind delayed window resize event
  _bindWindowResizeEvent() {
    this._unbindWindowResizeEvent()

    const cb = () => {
      if (this._resizeTimeout) {
        clearTimeout(this._resizeTimeout)
        this._resizeTimeout = null
      }
      this._triggerEvent('resize')
    }

    this._resizeCallback = () => {
      if (this._resizeTimeout) clearTimeout(this._resizeTimeout)
      this._resizeTimeout = setTimeout(cb, this.RESIZE_DELAY)
    }

    window.addEventListener('resize', this._resizeCallback, false)
  },

  // ---
  // Unbind delayed window resize event
  _unbindWindowResizeEvent() {
    if (this._resizeTimeout) {
      clearTimeout(this._resizeTimeout)
      this._resizeTimeout = null
    }

    if (this._resizeCallback) {
      window.removeEventListener('resize', this._resizeCallback, false)
      this._resizeCallback = null
    }
  },

  // ---
  // Set sidenav hover state
  _setSidenavHoverState(hovered) {
    this[hovered ? '_addClass' : '_removeClass']('layout-sidenav-hover')
  },

  // ---
  // Toggle collapsed
  _setCollapsed(collapsed) {
    if (this.isSmallScreen()) {
      if (collapsed) {
        this._removeClass('layout-expanded')
      } else {
        setTimeout(() => {
          this._addClass('layout-expanded')
        }, this._redrawLayoutSidenav() ? 5 : 0)
      }
    } else {
      this[collapsed ? '_addClass' : '_removeClass']('layout-collapsed')
    }
  },

  _findParent(el, cls) {
    if (el && el.tagName.toUpperCase() === 'BODY') return null
    el = el.parentNode
    while (el && el.tagName.toUpperCase() !== 'BODY' && !el.classList.contains(cls)) {
      el = el.parentNode
    }
    el = el && el.tagName.toUpperCase() !== 'BODY' ? el : null
    return el
  },

  _bindSidenavMouseEvents() {
    if (this._sidenavMouseEnter && this._sidenavMouseLeave && this._windowTouchStart) return

    const layoutSidenav = this.getLayoutSidenav()
    if (!layoutSidenav) return this._unbindSidenavMouseEvents()

    if (!this._sidenavMouseEnter) {
      this._sidenavMouseEnter = () => {
        if (this.isSmallScreen() || !this._hasClass('layout-collapsed') ||
            this.isOffcanvas() || this._hasClass('layout-transitioning')) {
          return this._setSidenavHoverState(false)
        }

        this._setSidenavHoverState(true)
      }
      layoutSidenav.addEventListener('mouseenter', this._sidenavMouseEnter, false)
      layoutSidenav.addEventListener('touchstart', this._sidenavMouseEnter, false)
    }

    if (!this._sidenavMouseLeave) {
      this._sidenavMouseLeave = () => {
        this._setSidenavHoverState(false)
      }
      layoutSidenav.addEventListener('mouseleave', this._sidenavMouseLeave, false)
    }

    if (!this._windowTouchStart) {
      this._windowTouchStart = e => {
        if (!e || !e.target || !this._findParent(e.target, '.layout-sidenav')) {
          this._setSidenavHoverState(false)
        }
      }
      window.addEventListener('touchstart', this._windowTouchStart, true)
    }
  },

  _unbindSidenavMouseEvents() {
    if (!this._sidenavMouseEnter && !this._sidenavMouseLeave && !this._windowTouchStart) return

    const layoutSidenav = this.getLayoutSidenav()

    if (this._sidenavMouseEnter) {
      if (layoutSidenav) {
        layoutSidenav.removeEventListener('mouseenter', this._sidenavMouseEnter, false)
        layoutSidenav.removeEventListener('touchstart', this._sidenavMouseEnter, false)
      }
      this._sidenavMouseEnter = null
    }

    if (this._sidenavMouseLeave) {
      if (layoutSidenav) {
        layoutSidenav.removeEventListener('mouseleave', this._sidenavMouseLeave, false)
      }
      this._sidenavMouseLeave = null
    }

    if (this._windowTouchStart) {
      if (layoutSidenav) {
        window.addEventListener('touchstart', this._windowTouchStart, true)
      }
      this._windowTouchStart = null
    }

    this._setSidenavHoverState(false)
  },


  // *******************************************************************************
  // * Getters

  getLayoutSidenav() {
    return document.querySelector('.layout-sidenav')
  },

  getSidenav() {
    const layoutSidenav = this.getLayoutSidenav()

    if (!layoutSidenav) return null

    return !this._hasClass('sidenav', layoutSidenav) ?
      layoutSidenav.querySelector('.sidenav') :
      layoutSidenav
  },

  getLayoutNavbar() {
    return document.querySelector('.layout-navbar')
  },

  getLayoutFooter() {
    return document.querySelector('.layout-footer')
  },

  getLayoutContainer() {
    return document.querySelector('.layout-container')
  },


  // *******************************************************************************
  // * Tests

  isMobileDevice() {
    return (typeof window.orientation !== "undefined") || (navigator.userAgent.indexOf('IEMobile') !== -1)
  },

  isSmallScreen() {
    return (
      window.innerWidth ||
      document.documentElement.clientWidth ||
      document.body.clientWidth
    ) < this.LAYOUT_BREAKPOINT
  },

  isLayout1() {
    return !!document.querySelector('.layout-wrapper.layout-1')
  },

  isCollapsed() {
    if (this.isSmallScreen()) {
      return !this._hasClass('layout-expanded')
    } else {
      return this._hasClass('layout-collapsed')
    }
  },

  isFixed() {
    return this._hasClass('layout-fixed layout-fixed-offcanvas')
  },

  isOffcanvas() {
    return this._hasClass('layout-offcanvas layout-fixed-offcanvas')
  },

  isNavbarFixed() {
    return this._hasClass('layout-navbar-fixed') ||
           (!this.isSmallScreen() && this.isFixed() && this.isLayout1())
  },

  isFooterFixed() {
    return this._hasClass('layout-footer-fixed')
  },

  isReversed() {
    return this._hasClass('layout-reversed')
  },


  // *******************************************************************************
  // * Methods

  // ---
  // Collapse / expand layout
  setCollapsed(collapsed = requiredParam('collapsed'), animate = true) {
    const layoutSidenav = this.getLayoutSidenav()

    if (!layoutSidenav) return

    this._unbindLayoutAnimationEndEvent()

    if (animate && this._supportsTransitionEnd()) {
      this._addClass('layout-transitioning')
      if (collapsed) this._setSidenavHoverState(false)

      this._bindLayoutAnimationEndEvent(() => {
        // Collapse / Expand
        this._setCollapsed(collapsed)
      }, () => {
        this._removeClass('layout-transitioning')
        this._triggerWindowEvent('resize')
        this._triggerEvent('toggle')
        this._setSidenavHoverState(false)
      })
    } else {
      this._addClass('layout-no-transition')
      if (collapsed) this._setSidenavHoverState(false)

      // Collapse / Expand
      this._setCollapsed(collapsed)

      setTimeout(() => {
        this._removeClass('layout-no-transition')
        this._triggerWindowEvent('resize')
        this._triggerEvent('toggle')
        this._setSidenavHoverState(false)
      }, 1)
    }
  },

  // ---
  // Toggle layout
  toggleCollapsed(animate = true) {
    this.setCollapsed(!this.isCollapsed(), animate)
  },

  // ---
  // Set layout positioning
  setPosition(fixed = requiredParam('fixed'), offcanvas = requiredParam('offcanvas')) {
    this._removeClass('layout-offcanvas layout-fixed layout-fixed-offcanvas')

    if (!fixed && offcanvas) {
      this._addClass('layout-offcanvas')
    } else if (fixed && !offcanvas) {
      this._addClass('layout-fixed')
      this._redrawLayoutSidenav()
    } else if (fixed && offcanvas) {
      this._addClass('layout-fixed-offcanvas')
      this._redrawLayoutSidenav()
    }

    this.update()
  },

  setNavbarFixed(fixed = requiredParam('fixed')) {
    this[fixed ? '_addClass' : '_removeClass']('layout-navbar-fixed')
    this.update()
  },

  setFooterFixed(fixed = requiredParam('fixed')) {
    this[fixed ? '_addClass' : '_removeClass']('layout-footer-fixed')
    this.update()
  },

  setReversed(reversed = requiredParam('reversed')) {
    this[reversed ? '_addClass' : '_removeClass']('layout-reversed')
  },


  // *******************************************************************************
  // * Update

  update() {
    if (
      (this.getLayoutNavbar() && (
        (!this.isSmallScreen() && this.isLayout1() && this.isFixed()) || this.isNavbarFixed()
      )) || (this.getLayoutFooter() && this.isFooterFixed())
    ) {
      this._updateInlineStyle(this._getNavbarHeight(), this._getFooterHeight())
    }

    this._bindSidenavMouseEvents()
  },

  setAutoUpdate(enable = requiredParam('enable')) {
    if (enable && !this._autoUpdate) {
      this.on('resize.layoutHelpers:autoUpdate', () => this.update())
      this._autoUpdate = true
    } else if (!enable && this._autoUpdate) {
      this.off('resize.layoutHelpers:autoUpdate')
      this._autoUpdate = false
    }
  },


  // *******************************************************************************
  // * Events

  on(event = requiredParam('event'), callback = requiredParam('callback')) {
    let [_event, ...namespace] = event.split('.')
    namespace = namespace.join('.') || null

    this._listeners.push({ event: _event, namespace, callback })
  },

  off(event = requiredParam('event')) {
    let [_event, ...namespace] = event.split('.')
    namespace = namespace.join('.') || null

    this._listeners
      .filter(listener => listener.event === _event && listener.namespace === namespace)
      .forEach(listener => this._listeners.splice(this._listeners.indexOf(listener), 1))
  },


  // *******************************************************************************
  // * Life cycle

  init() {
    if (this._initialized) return
    this._initialized = true

    // Initialize `style` element
    this._updateInlineStyle(0)

    // Bind window resize event
    this._bindWindowResizeEvent()

    // Bind init event
    this.off('init._layoutHelpers')
    this.on('init._layoutHelpers', () => {
      this.off('resize._layoutHelpers:redrawSidenav')
      this.on('resize._layoutHelpers:redrawSidenav', () => {
        this.isSmallScreen() && !this.isCollapsed() && this._redrawLayoutSidenav()
      })

      // Force repaint in IE 10
      if (typeof document.documentMode === 'number' && document.documentMode < 11) {
        this.off('resize._layoutHelpers:ie10RepaintBody')
        this.on('resize._layoutHelpers:ie10RepaintBody', () => {
          if (this.isFixed()) return
          const scrollTop = document.documentElement.scrollTop
          document.body.style.display = 'none'
          document.body.offsetHeight
          document.body.style.display = 'block'
          document.documentElement.scrollTop = scrollTop
        })
      }
    })

    this._triggerEvent('init')
  },

  destroy() {
    if (!this._initialized) return
    this._initialized = false

    this._removeClass('layout-transitioning')
    this._removeInlineStyle()
    this._unbindLayoutAnimationEndEvent()
    this._unbindWindowResizeEvent()
    this._unbindSidenavMouseEvents()
    this.setAutoUpdate(false)

    this.off('init._layoutHelpers')

    // Remove all listeners except `init`
    this._listeners
      .filter(listener => listener.event !== 'init')
      .forEach(listener => this._listeners.splice(this._listeners.indexOf(listener), 1))
  }
}


// *******************************************************************************
// * Initialization

if (typeof window !== 'undefined') {
  layoutHelpers.init()

  if (layoutHelpers.isMobileDevice() && window.chrome) {
    document.documentElement.classList.add('layout-sidenav-100vh')
  }

  // Update layout after page load
  if (document.readyState === 'complete') layoutHelpers.update()
  else document.addEventListener('DOMContentLoaded', function onContentLoaded() {
    layoutHelpers.update()
    document.removeEventListener('DOMContentLoaded', onContentLoaded)
  })
}

// ---
export { layoutHelpers }
