'use strict'
var Obv = require('obv')

var STATES = {
  queried: 1,
  checking: 2,
  checked: 3,

  //I realized that this state is maybe not necessary
  //since we actually track this on a per peer basis.
  //requesting: 4,

  responded: 5, //we have received at least one response

  processing: 6,
  processed: 7 //now we can broadcast this
}

function each (obj, fn) {
  for(var k in obj)
    fn(obj[k], k, obj)
}

function isEmpty (o) {
  for(var k in o) return false
  return true
}

module.exports = function (opts) {
  //opts has {check, process, increment, isQuery, isResponse}
  var state = {}
  var localCbs = {}

  var initialWeight = opts.initialWeight || -1
  var increment = opts.increment || function (n) { return Number(n) - 1 }
  var isRequest = opts.isRequest || function (value) { return typeof value === 'number' && value < 0 }
  var isResponse = opts.isResponse || function (value) { return !isRequest(value) }
  var isQuery = opts.isQuery || function () { return true }
  var process = opts.process || function (k, v, cb) { cb (null, v) }
  var compare = opts.compare || function (a, b) {
    return b - a
  }
  var maximum = opts.maximum || -3
  var timeout = opts.timeout || 30e3
  var nextTimeout = 0

  var obv = Obv()
  obv.set(state)
  function next (fn) {
    if(!fn) obv.set(state)
    else obv.once(fn, false)
  }

  function ErrorTimeout() {
    var err = new Error('gossip-query: request timed out')
    err.name = 'request_timeout'
    return err
  }

  function setTimestamp() {
    return Date.now()
  }

  function callback (k, value, err) {
    if (localCbs[k]) {
      var cbs = localCbs[k]
      delete localCbs[k]
      while (cbs.length) cbs.shift()(err, value)
    }
  }

  obv(function () {
    //XXX: I think there could be some bugs in this because
    //obv.set is triggered from inside the loop (if check/process)
    //callback sync, which can happen.

    each(state, function (item, k) {
      //check the local store when new queries are added
      if(item.state === STATES.queried) {
        item.state = STATES.checking
        item.ts = setTimestamp()
        opts.check(k, function (_, value) {
          //igore error
          if(value && !item.value) {
            item.state = STATES.processed
            //UPDATE VALUE
            callback(k, item.value = value)
          }
          else
            item.state = STATES.checked

          item.ts = setTimestamp()

          obv.set(state)
        })
      }

      //process items received
      if(item.value != null && item.state === STATES.responded) {
        item.state = STATES.processing
        item.ts = setTimestamp()
        process(k, item.value, function (err, value) {
          if(err) {
            //stay in processing state
            return
          }
          item.state = STATES.processed
          item.ts = setTimestamp()
          //this is the only place that localCbs is called,
          //except for in query(key, cb) if key is already ready.
          if(value)
            //UPDATE VALUE
            callback(k, item.value = value)

          obv.set(state)
        })
      }
    })
  })

  function initial (weight) {
    return {
      state: STATES.queried,
      weight: weight,
      value: null,
      requestedBy: {},
      requestedFrom: {},
      respondedTo: {},
      ts: setTimestamp()
    }
  }

  return {
    state: state,
    createStream: function (peerId) {
      return {
        source: function (end, cb) {
          if(end) {
            for(var k in state) {
              delete state[k].respondedTo[peerId]
              delete state[k].requestedFrom[peerId]
            }
            return
          }
          //read the next pieces of data from the state object.
          ;(function read () {
            var data = {}
            for(var k in state) {
              if(
                //this item has been received & processed, respond to any other peers
                //idea: make respondedTo be a counter, for queries with multiple answers.
                state[k].state === STATES.processed &&
                state[k].requestedBy[peerId] &&
                //responded to != state[k].value.length
                !state[k].respondedTo[peerId]
              ) {
                // change bool to integer, and send data since that index
                state[k].respondedTo[peerId] = true
                data[k] = state[k].value
              }
              else if(
                state[k].state === STATES.checked &&
                !state[k].requestedFrom[peerId]
              ) {
                state[k].requestedFrom[peerId] = true
                data[k] = state[k].weight //the number of hops, etc
              }
            }
            //next(read) calls read again when something changes in the state.
            if(isEmpty(data)) next(read)
            else cb(null, data)
          })()
        },
        sink: function (read) {
          read(null, function more (end, data) {
            if(end) {
              for(var k in state) {
                delete state[k].requestedBy[peerId]
              }
              //Q: how does the source decide to end?
              //A: the network connection aborts the stream.
              return
            }
            //process this message and possibly update the state.
            var update = false
            for(var k in data) {
              var value = data[k]
               //ignore invalid requests
              if(!isQuery(k) || !(isRequest(value) || isResponse(value, k)))
                ;
              else if(isRequest(data[k])) {
                //if we already have seen this query:
                if(compare(data[k], maximum) < 0) {
                  if(!state[k]) {
                    update = true
                    state[k] = initial(increment(data[k]))
                    state[k].requestedBy[peerId] = true
                  }
                  else if (compare(data[k], state[k].weight) < 0){
                    update = true
                    state[k].requestedBy[peerId] = true
                    //update the weight if this peer is closer to us
                    state[k].weight = increment(data[k])
                  }
                }
              }
              else if(isResponse(data[k])) {
                if(state[k].state == STATES.checked) {
                  //what states can it be in here?
                  //what if we are currently processing something and a new response arrives?
                  state[k].state = STATES.responded
                  state[k].value = data[k]
                  update = true
                }
              }
            }
            if(update) next()
            read(null, more)
          })
        }
      }
    },

    query: function (k, cb) {
      var update
      //add to state object and update
      if(state[k]) {
        if(state[k].state == STATES.processed) cb(null, state[k].value)
        else (localCbs[k] = localCbs[k] || []).push(cb)
      }
      else {
        update = true
        state[k] = initial(initialWeight)
        localCbs[k] = [cb]
      }
     if(update) next()
    },
    checkTimeout: function () {
      var ts = Date.now()
      for(var k in state)
        if(state[k].ts + timeout < ts) {
          callback(k, null, ErrorTimeout(k))
          delete state[k]
        }
    },
    progress: function () {
      var prog = {start: 0, current: 0, target: 0}

      for(var k in state) {
        if(Date.now() - state[k].ts < timeout) {
          prog.current += state[k].state
          prog.target += STATES.processed
        }
      }
      return prog
    }
  }
}



