/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
/**
 * @fileOverview
 * Everything about component event which includes event object, event listener,
 * event emitter and lifecycle hooks.
 */

/**
 * Event object definition. An event object has `type`, `timestamp` and
 * `detail` from which a component emit. The event object could be dispatched to
 * parents or broadcasted to children except `this.stop()` is called.
 * @param {string} type
 * @param {any}    detail
 */
function Evt (type, detail) {
  if (detail instanceof Evt) {
    return detail
  }

  this.timestamp = Date.now()
  this.detail = detail
  this.type = type

  let shouldStop = false

  /**
   * stop dispatch and broadcast
   */
  this.stop = function () {
    shouldStop = true
  }

  /**
   * check if it can't be dispatched or broadcasted
   */
  this.hasStopped = function () {
    return shouldStop
  }
}

/**
 * Emit an event but not broadcast down or dispatch up.
 * @param  {string} type
 * @param  {any}    detail
 */
export function $emit (type, detail) {
  const events = this._vmEvents
  const handlerList = events[type]
  if (handlerList) {
    const evt = new Evt(type, detail)
    handlerList.forEach((handler) => {
      handler.call(this, evt)
    })
  }
}

/**
 * Emit an event and dispatch it up.
 * @param  {string} type
 * @param  {any}    detail
 */
export function $dispatch (type, detail) {
  const evt = new Evt(type, detail)
  this.$emit(type, evt)

  if (!evt.hasStopped() && this._parent && this._parent.$dispatch) {
    this._parent.$dispatch(type, evt)
  }
}

/**
 * Emit an event and broadcast it down.
 * @param  {string} type
 * @param  {any}    detail
 */
export function $broadcast (type, detail) {
  const evt = new Evt(type, detail)
  this.$emit(type, evt)

  if (!evt.hasStopped() && this._childrenVms) {
    this._childrenVms.forEach((subVm) => {
      subVm.$broadcast(type, evt)
    })
  }
}

/**
 * Add event listener.
 * @param  {string}   type
 * @param  {function} handler
 */
export function $on (type, handler) {
  if (!type || typeof handler !== 'function') {
    return
  }
  const events = this._vmEvents
  const handlerList = events[type] || []
  handlerList.push(handler)
  events[type] = handlerList

  // fixed old version lifecycle design
  /* istanbul ignore if */
  if (type === 'hook:ready' && this._ready) {
    this.$emit('hook:ready')
  }
}

/**
 * Remove event listener.
 * @param  {string}   type
 * @param  {function} handler
 */
export function $off (type, handler) {
  if (!type) {
    return
  }
  const events = this._vmEvents
  if (!handler) {
    delete events[type]
    return
  }
  const handlerList = events[type]
  if (!handlerList) {
    return
  }
  handlerList.$remove(handler)
}

const LIFE_CYCLE_TYPES = ['init', 'created', 'ready', 'destroyed']

/**
 * Init events:
 * 1. listen `events` in component options & `externalEvents`.
 * 2. bind lifecycle hooks.
 * @param  {Vm}     vm
 * @param  {object} externalEvents
 */
export function initEvents (vm, externalEvents) {
  const options = vm._options || {}
  const events = options.events || {}
  for (const type1 in events) {
    vm.$on(type1, events[type1])
  }
  for (const type2 in externalEvents) {
    vm.$on(type2, externalEvents[type2])
  }
  LIFE_CYCLE_TYPES.forEach((type) => {
    vm.$on(`hook:${type}`, options[type])
  })
}

/**
 * Bind event related methods to ViewModel instance.
 * @param  {Vm} vm
 */
export function mixinEvents (vm) {
  vm.$emit = $emit
  vm.$dispatch = $dispatch
  vm.$broadcast = $broadcast
  vm.$on = $on
  vm.$off = $off
}
