/* global VERSION */

import 'noty.scss'
import Promise from 'es6-promise'
import * as Utils from 'utils'
import * as API from 'api'
import {NotyButton} from 'button'
import {Push} from 'push'

export default class Noty {
  /**
   * @param {object} options
   * @return {Noty}
   */
  constructor (options = {}) {
    this.options = Utils.deepExtend({}, API.Defaults, options)
    this.id = this.options.id || Utils.generateID('bar')
    this.closeTimer = -1
    this.barDom = null
    this.layoutDom = null
    this.progressDom = null
    this.showing = false
    this.shown = false
    this.closed = false
    this.closing = false
    this.killable = this.options.timeout || this.options.closeWith.length > 0
    this.hasSound = this.options.sounds.sources.length > 0
    this.soundPlayed = false
    this.listeners = {
      beforeShow: [],
      onShow: [],
      afterShow: [],
      onClose: [],
      afterClose: [],
      onClick: [],
      onHover: [],
      onTemplate: []
    }
    this.promises = {
      show: null,
      close: null
    }
    this.on('beforeShow', this.options.callbacks.beforeShow)
    this.on('onShow', this.options.callbacks.onShow)
    this.on('afterShow', this.options.callbacks.afterShow)
    this.on('onClose', this.options.callbacks.onClose)
    this.on('afterClose', this.options.callbacks.afterClose)
    this.on('onClick', this.options.callbacks.onClick)
    this.on('onHover', this.options.callbacks.onHover)
    this.on('onTemplate', this.options.callbacks.onTemplate)

    return this
  }

  /**
   * @param {string} eventName
   * @param {function} cb
   * @return {Noty}
   */
  on (eventName, cb = () => {}) {
    if (typeof cb === 'function' && this.listeners.hasOwnProperty(eventName)) {
      this.listeners[eventName].push(cb)
    }

    return this
  }

  /**
   * @return {Noty}
   */
  show () {
    if (this.options.killer === true) {
      Noty.closeAll()
    } else if (typeof this.options.killer === 'string') {
      Noty.closeAll(this.options.killer)
    }

    let queueCounts = API.getQueueCounts(this.options.queue)

    if (
      queueCounts.current >= queueCounts.maxVisible ||
      (API.PageHidden && this.options.visibilityControl)
    ) {
      API.addToQueue(this)

      if (
        API.PageHidden &&
        this.hasSound &&
        Utils.inArray('docHidden', this.options.sounds.conditions)
      ) {
        Utils.createAudioElements(this)
      }

      if (
        API.PageHidden &&
        Utils.inArray('docHidden', this.options.titleCount.conditions)
      ) {
        API.docTitle.increment()
      }

      return this
    }

    API.Store[this.id] = this

    API.fire(this, 'beforeShow')

    this.showing = true

    if (this.closing) {
      this.showing = false
      return this
    }

    API.build(this)
    API.handleModal(this)

    if (this.options.force) {
      this.layoutDom.insertBefore(this.barDom, this.layoutDom.firstChild)
    } else {
      this.layoutDom.appendChild(this.barDom)
    }

    if (
      this.hasSound &&
      !this.soundPlayed &&
      Utils.inArray('docVisible', this.options.sounds.conditions)
    ) {
      Utils.createAudioElements(this)
    }

    if (Utils.inArray('docVisible', this.options.titleCount.conditions)) {
      API.docTitle.increment()
    }

    this.shown = true
    this.closed = false

    // bind button events if any
    if (API.hasButtons(this)) {
      Object.keys(this.options.buttons).forEach(key => {
        const btn = this.barDom.querySelector(
          `#${this.options.buttons[key].id}`
        )
        Utils.addListener(btn, 'click', e => {
          Utils.stopPropagation(e)
          this.options.buttons[key].cb()
        })
      })
    }

    this.progressDom = this.barDom.querySelector('.noty_progressbar')

    if (Utils.inArray('click', this.options.closeWith)) {
      Utils.addClass(this.barDom, 'noty_close_with_click')
      Utils.addListener(
        this.barDom,
        'click',
        e => {
          Utils.stopPropagation(e)
          API.fire(this, 'onClick')
          this.close()
        },
        false
      )
    }

    Utils.addListener(
      this.barDom,
      'mouseenter',
      () => {
        API.fire(this, 'onHover')
      },
      false
    )

    if (this.options.timeout) Utils.addClass(this.barDom, 'noty_has_timeout')
    if (this.options.progressBar) {
      Utils.addClass(this.barDom, 'noty_has_progressbar')
    }

    if (Utils.inArray('button', this.options.closeWith)) {
      Utils.addClass(this.barDom, 'noty_close_with_button')

      const closeButton = document.createElement('div')
      Utils.addClass(closeButton, 'noty_close_button')
      closeButton.innerHTML = '×'
      this.barDom.appendChild(closeButton)

      Utils.addListener(
        closeButton,
        'click',
        e => {
          Utils.stopPropagation(e)
          this.close()
        },
        false
      )
    }

    API.fire(this, 'onShow')

    if (this.options.animation.open === null) {
      this.promises.show = new Promise(resolve => {
        resolve()
      })
    } else if (typeof this.options.animation.open === 'function') {
      this.promises.show = new Promise(this.options.animation.open.bind(this))
    } else {
      Utils.addClass(this.barDom, this.options.animation.open)
      this.promises.show = new Promise(resolve => {
        Utils.addListener(this.barDom, Utils.animationEndEvents, () => {
          Utils.removeClass(this.barDom, this.options.animation.open)
          resolve()
        })
      })
    }

    this.promises.show.then(() => {
      const _t = this
      setTimeout(
        () => {
          API.openFlow(_t)
        },
        100
      )
    })

    return this
  }

  /**
   * @return {Noty}
   */
  stop () {
    API.dequeueClose(this)
    return this
  }

  /**
   * @return {Noty}
   */
  resume () {
    API.queueClose(this)
    return this
  }

  /**
   * @param {int|boolean} ms
   * @return {Noty}
   */
  setTimeout (ms) {
    this.stop()
    this.options.timeout = ms

    if (this.barDom) {
      if (this.options.timeout) {
        Utils.addClass(this.barDom, 'noty_has_timeout')
      } else {
        Utils.removeClass(this.barDom, 'noty_has_timeout')
      }

      const _t = this
      setTimeout(
        function () {
          // ugly fix for progressbar display bug
          _t.resume()
        },
        100
      )
    }

    return this
  }

  /**
   * @param {string} html
   * @param {boolean} optionsOverride
   * @return {Noty}
   */
  setText (html, optionsOverride = false) {
    if (this.barDom) {
      this.barDom.querySelector('.noty_body').innerHTML = html
    }

    if (optionsOverride) this.options.text = html

    return this
  }

  /**
   * @param {string} type
   * @param {boolean} optionsOverride
   * @return {Noty}
   */
  setType (type, optionsOverride = false) {
    if (this.barDom) {
      let classList = Utils.classList(this.barDom).split(' ')

      classList.forEach(c => {
        if (c.substring(0, 11) === 'noty_type__') {
          Utils.removeClass(this.barDom, c)
        }
      })

      Utils.addClass(this.barDom, `noty_type__${type}`)
    }

    if (optionsOverride) this.options.type = type

    return this
  }

  /**
   * @param {string} theme
   * @param {boolean} optionsOverride
   * @return {Noty}
   */
  setTheme (theme, optionsOverride = false) {
    if (this.barDom) {
      let classList = Utils.classList(this.barDom).split(' ')

      classList.forEach(c => {
        if (c.substring(0, 12) === 'noty_theme__') {
          Utils.removeClass(this.barDom, c)
        }
      })

      Utils.addClass(this.barDom, `noty_theme__${theme}`)
    }

    if (optionsOverride) this.options.theme = theme

    return this
  }

  /**
   * @return {Noty}
   */
  close () {
    if (this.closed) return this

    if (!this.shown) {
      // it's in the queue
      API.removeFromQueue(this)
      return this
    }

    API.fire(this, 'onClose')

    this.closing = true

    if (this.options.animation.close === null) {
      this.promises.close = new Promise(resolve => {
        resolve()
      })
    } else if (typeof this.options.animation.close === 'function') {
      this.promises.close = new Promise(
        this.options.animation.close.bind(this)
      )
    } else {
      Utils.addClass(this.barDom, this.options.animation.close)
      this.promises.close = new Promise(resolve => {
        Utils.addListener(this.barDom, Utils.animationEndEvents, () => {
          if (this.options.force) {
            Utils.remove(this.barDom)
          } else {
            API.ghostFix(this)
          }
          resolve()
        })
      })
    }

    this.promises.close.then(() => {
      API.closeFlow(this)
      API.handleModalClose(this)
    })

    this.closed = true

    return this
  }

  // API functions

  /**
   * @param {boolean|string} queueName
   * @return {Noty}
   */
  static closeAll (queueName = false) {
    Object.keys(API.Store).forEach(id => {
      if (queueName) {
        if (
          API.Store[id].options.queue === queueName && API.Store[id].killable
        ) {
          API.Store[id].close()
        }
      } else if (API.Store[id].killable) {
        API.Store[id].close()
      }
    })
    return this
  }

  /**
   * @param {Object} obj
   * @return {Noty}
   */
  static overrideDefaults (obj) {
    API.Defaults = Utils.deepExtend({}, API.Defaults, obj)
    return this
  }

  /**
   * @param {int} amount
   * @param {string} queueName
   * @return {Noty}
   */
  static setMaxVisible (amount = API.DefaultMaxVisible, queueName = 'global') {
    if (!API.Queues.hasOwnProperty(queueName)) {
      API.Queues[queueName] = {maxVisible: amount, queue: []}
    }

    API.Queues[queueName].maxVisible = amount
    return this
  }

  /**
   * @param {string} innerHtml
   * @param {String} classes
   * @param {Function} cb
   * @param {Object} attributes
   * @return {NotyButton}
   */
  static button (innerHtml, classes = null, cb, attributes = {}) {
    return new NotyButton(innerHtml, classes, cb, attributes)
  }

  /**
   * @return {string}
   */
  static version () {
    return VERSION
  }

  /**
   * @param {String} workerPath
   * @return {Push}
   */
  static Push (workerPath) {
    return new Push(workerPath)
  }
}

// Document visibility change controller
Utils.visibilityChangeFlow()
