constants = require('./constants')
streams = require('./streams')
Kefir = require('kefir')
R = require 'ramda'

class PingPongCommunicator
  makeHostStateStream = (sendingAndCheckingIsConnectProp, receivingIsConnectProp)->
    Kefir.combine([sendingAndCheckingIsConnectProp, receivingIsConnectProp], [],
      (sendingAndCheckingIsConnect, receivingIsConnect)->
        if(sendingAndCheckingIsConnect)
          return constants.hostState.CONNECTED
        else if(receivingIsConnect)
          return constants.hostState.PROBLEM_WITH_SENDING_OR_RECEIVING_FROM_OTHER_SIDE
        else
          return constants.hostState.DISCONNECTED
    ).throttle(@stateThrottle)

  makePongTimeStream = (sendingAndCheckingTimeProp, receivingTimeProp)->
    Kefir.combine([sendingAndCheckingTimeProp, receivingTimeProp], [],
      (sendingAndCheckingTime, receivingTime)->
        sendingAndCheckingTime - receivingTime
    ).throttle(@pongThrottle)

  makePingPongTimeStream = (pingTimeProp, pongTimeProp, allTimeProp)->
    Kefir.combine([pingTimeProp, pongTimeProp, allTimeProp], [],
      (pingTime, pongTime, allTime)->
        pingTime: pingTime, pongTime: pongTime, allTime: allTime
    ).throttle(@pingPongTimeThrottle)

  constructor: (duplex, ops) ->
    timeout = 1000
    payload = null
    @stateThrottle = 100
    @pongThrottle = 200
    @pingPongTimeThrottle = 700

    if ops
      timeout = if ops.timeout then ops.timeout else timeout
      payload = if ops.payload then ops.payload else payload
      @stateThrottle = if ops.stateThrottle then ops.stateThrottle else @stateThrottle
      @pongThrottle = if ops.pongThrottle then ops.pongThrottle else @pongThrottle
      @pingPongTimeThrottle = if ops.pingPongTimeThrottle then ops.pingPongTimeThrottle else @pingPongTimeThrottle

    @sendingAndCheckingObj = streams.sendingAndCheckingStream(duplex, timeout, payload)
    @receivingObj = streams.receivingStream(duplex, timeout, payload)

    pongTimeProp = makePongTimeStream(@sendingAndCheckingObj.lastPingAndPongSpeedProp, @receivingObj.lastPingSpeedProp)

    @stateStream = makeHostStateStream(
      @sendingAndCheckingObj.isConnectedProp, @receivingObj.isConnectedProp
    ).skipDuplicates().toProperty()

    @timeStream = makePingPongTimeStream(
      @receivingObj.lastPingSpeedProp,
      pongTimeProp,
      @sendingAndCheckingObj.lastPingAndPongSpeedProp
    ).toProperty()

  start: ()->
    if @sendingAndCheckingObj
      @sendingAndCheckingObj.start()
    if @receivingObj
      @receivingObj.start()

  kill: ()->
    if @sendingAndCheckingObj
      @sendingAndCheckingObj.kill()
    if @receivingObj
      @receivingObj.kill()

class PingPongNamedHost
  constructor: (@name, @partnerName) ->

  isAllowed: (whiteList)->
    isAllowed = R.contains(R.__, whiteList)
    #    console.log "whiteList is #{whiteList}"

    isAllowed(@name) && isAllowed(@partnerName)

class PingPongSingleHost extends PingPongNamedHost
  constructor: (name, partnerName) ->
    super(name, partnerName)
    @communicator = null

  spawn: (duplex)->
    ()->
      @communicator = new PingPongCommunicator(duplex)
      @stateStream = @communicator.stateStream
      @timeStream = @communicator.timeStream

      @communicator

  destroyHost: ()->
    if(@communicator)
      @communicator.kill()
      @communicator = null

class PingPongNestHost extends PingPongNamedHost
  constructor: (nameParam, partnerName) ->
    super(nameParam, partnerName)
    @hosts = {}
    @isNest = true
    @bus = Kefir.bus()
    @deleteHostStream = Kefir.emitter()
    @bus.plug(@deleteHostStream)

    @aggregateHostInfoProp = @bus.scan((stateAndTime, event) ->
      if(event.streamName == 'deleteHostStream')
        delete stateAndTime[event.hostName]
        return

      if(!stateAndTime[event.hostName])
        stateAndTime[event.hostName] = {}

      stateAndTimeInst = stateAndTime[event.hostName]

      if(event.streamName == 'stateStream')
#        console.log "event.value is #{event.value}"

        stateAndTimeInst['state'] = event.value

      if(event.streamName == 'timeStream')
#        console.log "event.value is #{event.value}"
        stateAndTimeInst['time'] = event.value

      stateAndTime[event.hostName] = stateAndTimeInst
      #      console.log "stateAndTime is #{JSON.stringify(stateAndTime)}"

      stateAndTime
    ,
      {}
    )

    @stateStream = @aggregateHostInfoProp.map((v)->
      R.compose(R.fromPairs,
      R.map((pair)->
        [name, val] = pair
        [name, val.state]
      ),
      R.toPairs)(v)
    ).skipDuplicates((a, b)->
      JSON.stringify(a) == JSON.stringify(b)
    )

    @timeStream = @aggregateHostInfoProp.map((v)->
      R.compose(R.fromPairs,
      R.map((pair)->
        [name, val] = pair
        [name, val.time]
      ),
      R.toPairs)(v)
    )

    @streamsMap = {}

  spawn: (duplex, instanceName)->
    @hosts[instanceName] = new PingPongSingleHost(
      "#{@nameParam}-#{instanceName}",
      "#{@partnerName}-#{instanceName}"
    ).spawn(duplex)()

    @streamsMap[instanceName] = {
      stateStream: @hosts[instanceName].stateStream.map((v)->
        hostName: instanceName,
        streamName: 'stateStream',
        value: v
      ),
      timeStream: @hosts[instanceName].timeStream.map((v)->
        hostName: instanceName,
        streamName: 'timeStream',
        value: v
      )
    }

    @bus.plug(@streamsMap[instanceName].stateStream)
    @bus.plug(@streamsMap[instanceName].timeStream)

    @hosts[instanceName]

  destroyHost: (hostName)->
    @deleteHostStream.emit({hostName: hostName, streamName: 'deleteHostStream'})
    @bus.unplug(@streamsMap[hostName].stateStream)
    @bus.unplug(@streamsMap[hostName].timeStream)
    delete @streamsMap[hostName]
    delete @hosts[hostName]

class PingPongPair
  getHostConstructor = (v)->
    (firstContext, secondContext)->
      if v['isNest'] then new PingPongNestHost(firstContext, secondContext) else new PingPongSingleHost(firstContext,
        secondContext)

  constructor: (firstContext, secondContext, opt) ->
    p = getHostConstructor(firstContext)(firstContext.name, secondContext.name)
    @pair = [p,
             getHostConstructor(secondContext)(secondContext.name, firstContext.name)]


  isAllowed: (whiteList)->
    isAllowed = R.contains(R.__, whiteList)
    isAllowed(@pair[0].name) && isAllowed(@pair[1].name)

class PingPongConfig
  initPingPongPair = (pairData) ->
    (new PingPongPair(pairData.pair[0], pairData.pair[1], timeout: pairData.timeout)).pair

  constructor: (data) ->
    @allList = data.allContextsList
    @pairs = []
    #    console.log "data is #{JSON.stringify(data)}"

    @blackList = R.difference(data.blackList, data.whiteList)
    @allowedList = R.difference(@allList, @blackList)

    @pairs = R.map(initPingPongPair)(data.pairs)
    return this

  getWhiteListedHosts: (context)->
    if(R.contains(@blackList)(context))
      return null
    getWhiteListedItems = R.filter((item) => item.isAllowed(@allowedList))
    getContextHosts = R.filter((item)-> item.name == context)
    R.compose(getContextHosts, getWhiteListedItems, R.flatten)(@pairs)

  getWhiteListedHostsMapByPartner: (context)->
    list = @getWhiteListedHosts(context)
    R.compose(R.fromPairs, R.map((item)->
      [item.partnerName, item]
    ))(list)

  getHostMap: (context)->
    availableHostsMap = @getWhiteListedHostsMapByPartner(context)

    (postStation)->
      if(!postStation)
        throw Error("post station is undefined")

      R.forEach((item)->
        if(!postStation[item])
          throw Error("NO #{context}-#{item} stream in this post station")

        duplex = postStation[item]
        host = availableHostsMap[item]

        host.spawn = R.curry(host.spawn)(duplex)
      )(R.keys(availableHostsMap))

      availableHostsMap

  spawnAll: (context, postStation)->
    map = @getHostMap(context)(postStation)
    R.forEach((item)->
      item.spawn().start()
    )(R.values(map))

module.exports =
  PingPongSingleHost: PingPongSingleHost
  PingPongNestHost: PingPongNestHost
  PingPongPair: PingPongPair
  PingPongConfig: PingPongConfig