import {
  loadImageAsync,
  ObjectKeys,
  noop
} from './util'

let imageCache = {}

// el: {
//     state,
//     src,
//     error,
//     loading
// }

export default class ReactiveListener {
  constructor ({ el, src, error, loading, bindType, $parent, options, elRenderer }) {
    this.el = el
    this.src = src
    this.error = error
    this.loading = loading
    this.bindType = bindType
    this.attempt = 0

    this.naturalHeight = 0
    this.naturalWidth = 0

    this.options = options

    this.rect = null

    this.$parent = $parent
    this.elRenderer = elRenderer

    this.performanceData = {
      init: Date.now(),
      loadStart: 0,
      loadEnd: 0
    }

    this.filter()
    this.initState()
    this.render('loading', false)
  }

  /*
   * init listener state
   * @return
   */
  initState () {
    this.el.dataset.src = this.src
    this.state = {
      error: false,
      loaded: false,
      rendered: false
    }
  }

  /*
   * record performance
   * @return
   */
  record (event) {
    this.performanceData[event] = Date.now()
  }

  /*
   * update image listener data
   * @param  {String} image uri
   * @param  {String} loading image uri
   * @param  {String} error image uri
   * @return
   */
  update ({ src, loading, error }) {
    const oldSrc = this.src
    this.src = src
    this.loading = loading
    this.error = error
    this.filter()
    if (oldSrc !== this.src) {
      this.attempt = 0
      this.initState()
    }
  }

  /*
   * get el node rect
   * @return
   */
  getRect () {
    this.rect = this.el.getBoundingClientRect()
  }

  /*
   *  check el is in view
   * @return {Boolean} el is in view
   */
  checkInView () {
    this.getRect()
    return (this.rect.top < window.innerHeight * this.options.preLoad && this.rect.bottom > this.options.preLoadTop) &&
            (this.rect.left < window.innerWidth * this.options.preLoad && this.rect.right > 0)
  }

  /*
   * listener filter
   */
  filter () {
    ObjectKeys(this.options.filter).map(key => {
      this.options.filter[key](this, this.options)
    })
  }

  /*
   * render loading first
   * @params cb:Function
   * @return
   */
  renderLoading (cb) {
    loadImageAsync({
      src: this.loading
    }, data => {
      this.render('loading', false)
      cb()
    }, () => {
      // handler `loading image` load failed
      cb()
      if (!this.options.silent) console.warn(`VueLazyload log: load failed with loading image(${this.loading})`)
    })
  }

  /*
   * try load image and  render it
   * @return
   */
  load (onFinish = noop) {
    if ((this.attempt > this.options.attempt - 1) && this.state.error) {
      if (!this.options.silent) console.log(`VueLazyload log: ${this.src} tried too more than ${this.options.attempt} times`)
      onFinish()
      return
    }

    if (this.state.loaded || imageCache[this.src]) {
      this.state.loaded = true
      onFinish()
      return this.render('loaded', true)
    }

    this.renderLoading(() => {
      this.attempt++

      this.record('loadStart')

      loadImageAsync({
        src: this.src
      }, data => {
        this.naturalHeight = data.naturalHeight
        this.naturalWidth = data.naturalWidth
        this.state.loaded = true
        this.state.error = false
        this.record('loadEnd')
        this.render('loaded', false)
        imageCache[this.src] = 1
        onFinish()
      }, err => {
        !this.options.silent && console.error(err)
        this.state.error = true
        this.state.loaded = false
        this.render('error', false)
      })
    })
  }

  /*
   * render image
   * @param  {String} state to render // ['loading', 'src', 'error']
   * @param  {String} is form cache
   * @return
   */
  render (state, cache) {
    this.elRenderer(this, state, cache)
  }

  /*
   * output performance data
   * @return {Object} performance data
   */
  performance () {
    let state = 'loading'
    let time = 0

    if (this.state.loaded) {
      state = 'loaded'
      time = (this.performanceData.loadEnd - this.performanceData.loadStart) / 1000
    }

    if (this.state.error) state = 'error'

    return {
      src: this.src,
      state,
      time
    }
  }

  /*
   * destroy
   * @return
   */
  destroy () {
    this.el = null
    this.src = null
    this.error = null
    this.loading = null
    this.bindType = null
    this.attempt = 0
  }
}
