uuid = require('node-uuid')
$q = require('node-promise')
lru = require('lru')

debug = process.env['DEBUG']

opts =
  max: 1000
  maxAgeInMilliseconds: 1000 * 60 * 60 * 24 * 4 # 4 days timeout of objects no matter what

class spinredis

  constructor: (dbUrl) ->
    console.log 'spinclient +++++++++ constructor called +++++++++++'
    @subscribers = []
    @objsubscribers = []
    @objectsSubscribedTo = []

    @outstandingMessages = []
    @modelcache = []

    @seenMessages = []
    @sessionId = null
    @objects = new lru(opts)

    @savedMessagesInCaseOfRetries = new lru({max:1000, maxAgeInMilliseconds: 5000})

    if debug then console.log 'redis-spincycle dbUrl = ' + dbUrl
    rhost = dbUrl or process.env['REDIS_PORT_6379_TCP_ADDR'] or '127.0.0.1'
    rport = process.env['REDIS_PORT_6379_TCP_PORT'] or '6379'

    @sendredis = require('redis').createClient(rport, rhost)
    @listenredis = require('redis').createClient(rport, rhost)

    @listenredis.on 'error', (err) ->
      console.log 'spinredis listen ERROR: ' + err

    @listenredis.on 'end', (err) ->
      console.log 'spinredis listen end event: ' + err

    @sendredis.on 'error', (err) ->
      console.log 'spinredis send ERROR: ' + err

    @sendredis.on 'end', (err) ->
      console.log 'spinredis send end event: ' + err


    @subscribers['OBJECT_UPDATE'] = [(obj) =>
      console.log 'spinredis +++++++++ obj update message router got obj '+obj.id+' of type '+obj.type
      console.dir(obj);
      #console.dir(@objsubscribers)
      objsubs = @objsubscribers[obj.id] or []
      for k,v of objsubs
        #console.log 'updating subscriber to @objects updates on id '+k
        if not @objects.get(obj.id)
          @objects.set(obj.id, obj)
        else
          o = @objects.get(obj.id)
          obj = o
          for prop, val of obj
            o[prop] = val
        v obj
    ]

    @setup()

  failed: (msg)->
    console.log 'spinclient message failed!! ' + msg

  setSessionId: (id) ->
    if(id)
      console.log '++++++++++++++++++++++++++++++++++++++ spinclient setting session id to ' + id
      @sessionId = id

  dumpOutstanding: ()->
    console.log '-------------------------------- ' + @outstandingMessages.length + ' outstanding messages ---------------------------------'
    @outstandingMessages.forEach (os)->
      console.log os.messageId + ' -> ' + os.target + ' - ' + os.d
    console.log '-----------------------------------------------------------------------------------------'

  emit: (message) =>
    message.channelID = 'spinchannel_' + @channelID
    if debug then console.log 'redisclient emitting message..'
    if debug then console.dir message
    @savedMessagesInCaseOfRetries.set(message.messageId, message)
    @sendredis.publish('spinchannel', JSON.stringify(message))

  setup: () =>
    @channelID = uuid.v4()
    @listenredis.subscribe('spinchannel_' + @channelID)

    @listenredis.on 'message', (channel, replystr) =>
      if debug then console.log 'spinredis on message got ' + replystr
      reply = JSON.parse(replystr)
      status = reply.status
      message = reply.payload
      info = reply.info

      if message and message.error and message.error == 'ERRCHILLMAN'
        console.log 'got ERRCHILLMAN from spinycle service, preparing to retry sending message...'
        oldmsg = @savedMessagesInCaseOfRetries[reply.messageId]
        setTimeout(
          ()=>
            console.log 'resending message '+oldmsg.messageId+' due to target endpoint not open yet'
            @emit(oldmsg)
          ,250
        )

      else if not @hasSeenThisMessage reply.messageId
        @savedMessagesInCaseOfRetries.remove(reply.messageId)
        if reply.messageId and reply.messageId isnt 'undefined' then @seenMessages.push(reply.messageId)
        if @seenMessages.length > 10 then @seenMessages.shift()
        if debug then console.log 'redis-spincycle got reply messageId ' + reply.messageId + ' status ' + status + ', info ' + info + ' data ' + message + ' outstandingMessages = ' + @outstandingMessages.length
        if debug then @dumpOutstanding()
        #console.dir reply
        index = -1
        if reply.messageId
          i = 0
          while i < @outstandingMessages.length
            index = i
            detail = @outstandingMessages[i]
            if detail and not detail.delivered and detail.messageId == reply.messageId
              if reply.status == 'FAILURE' or reply.status == 'NOT_ALLOWED'
                console.log 'spinclient message FAILURE'
                console.dir reply
                detail.d.reject reply
                break
              else
                #console.log 'delivering message '+message+' reply to '+detail.target+' to '+reply.messageId
                detail.d.resolve(message)
                break
              detail.delivered = true
            i++
          if index > -1
            @outstandingMessages.splice index, 1
        else
          subs = @subscribers[info]
          if subs
            subs.forEach (listener) ->
              listener message
          else
            if debug then console.log 'no subscribers for message ' + message
            if debug then console.dir reply
      else
        if debug then console.log '-- skipped resent message ' + reply.messageId

  hasSeenThisMessage: (messageId) =>
    @seenMessages.some (mid) -> messageId == mid

  registerListener: (detail) =>
    #console.log 'spinclient::registerListener called for ' + detail.message
    subs = @subscribers[detail.message] or []
    subs.push detail.callback
    @subscribers[detail.message] = subs

  registerObjectSubscriber: (detail) =>
    d = $q.defer()
    sid = uuid.v4()
    localsubs = @objectsSubscribedTo[detail.id]
    #console.log 'register@objectsSubscriber localsubs is'
    #console.dir localsubs
    if not localsubs
      localsubs = []
      console.log 'spinredis no local subs, so get the original server-side subscription for id ' + detail.id
      # actually set up subscription, once for each @objects
      @_registerObjectSubscriber({
        id: detail.id, type: detail.type, cb: (updatedobj) =>
          #console.log '-- register@objectsSubscriber getting obj update callback for ' + detail.id
          lsubs = @objectsSubscribedTo[detail.id]
          #console.dir(lsubs)
          for k,v of lsubs
            if (v.cb)
              #console.log '--*****--*****-- calling back @objects update to local sid --****--*****-- ' + k
              v.cb updatedobj
      }).then( (remotesid) =>
          localsubs['remotesid'] = remotesid
          localsubs[sid] = detail
          #console.log '-- adding local callback listener to @objects updates for ' + detail.id + ' local sid = ' + sid + ' remotesid = ' + remotesid
          @objectsSubscribedTo[detail.id] = localsubs
          d.resolve(sid)
        ,(rejection)=>
          console.log 'spinredis registerObjectSubscriber rejection: '+rejection
          console.dir rejection
      )
    else
      localsubs[sid] = detail
    return d.promise

  _registerObjectSubscriber: (detail) =>
    d = $q.defer()
    #console.log 'spinredis message-router registering subscriber for @objects ' + detail.id + ' type ' + detail.type
    subs = @objsubscribers[detail.id] or []

    @emitMessage({target: 'registerForUpdatesOn', obj: {id: detail.id, type: detail.type}}).then(
      (reply)=>
        console.log 'spinredis server subscription id for id ' + detail.id + ' is ' + reply
        subs[reply] = detail.cb
        @objsubscribers[detail.id] = subs
        d.resolve(reply)
    , (reply)=>
      @failed(reply)
    )
    return d.promise

  deRegisterObjectsSubscriber: (sid, o) =>
    localsubs = @objectsSubscribedTo[o.id] or []
    if localsubs[sid]
      console.log 'deregistering local updates for @objects ' + o.id
      delete localsubs[sid]
      count = 0
      for k,v in localsubs
        count++
      if count == 1 # only remotesid property left
        @_deRegisterObjectsSubscriber('remotesid', o)

  _deRegisterObjectsSubscriber: (sid, o) =>
    subs = @objsubscribers[o.id] or []
    if subs and subs[sid]
      delete subs[sid]
      @objsubscribers[o.id] = subs
      @emitMessage({target: 'deRegisterForUpdatesOn', id: o.id, type: o.type, listenerid: sid}).then (reply)->
        console.log 'deregistering server updates for @objects ' + o.id

  emitMessage: (detail) =>
    if debug then console.log 'emitMessage called'
    if debug then console.dir detail
    d = $q.defer()
    detail.messageId = uuid.v4()
    detail.sessionId = detail.sessionId or @sessionId
    detail.d = d
    @outstandingMessages.push detail
    if debug then console.log 'saving outstanding reply to messageId ' + detail.messageId + ' and @sessionId ' + detail.sessionId
    @emit detail

    return d.promise

# ------------------------------------------------------------------------------------------------------------------

  getModelFor: (type) =>
    d = $q.defer()
    if @modelcache[type]
      d.resolve(@modelcache[type])
    else
      @emitMessage({target: 'getModelFor', modelname: type}).then((model)->
          @modelcache[type] = model
          d.resolve(model)
        ,(rejection)=>
          console.log 'spinredis getModelFor rejection: '+rejection
          console.dir rejection
      )
    return d.promise

  listTargets: () =>
    d = $q.defer()
    @emitMessage({target: 'listcommands'}).then((targets)->
        d.resolve(targets)
      ,(rejection)->
        console.log 'spinredis listTargets rejection: '+rejection
        console.dir rejection
    )
    return d.promise

  flattenModel: (model) =>
    rv = {}
    for k,v of model
      if angular.isArray(v)
        rv[k] = v.map (e) -> e.id
      else
        rv[k] = v
    return rv


module.exports = spinredis
