{EventEmitter} = require 'events'

User = require './user'
BrainSegment = require './brain-segment'
Q = require 'q'
_ = require 'lodash'

class Brain extends EventEmitter
  # Represents somewhat persistent storage for the robot. Extend this.
  #
  # Returns a new Brain with no external storage.
  constructor: (@robot) ->
    @data =
      users: {}
      _private: {}

    @autoSave = true
    @ready = Q @

  # Take a dump
  #
  # Returns promise for object
  dump: ->
    Q(@data)

  # Public: get the length of the list stored at `key`
  #
  # Returns int
  llen: (key) ->
    Q(@data._private[@key key].length)

  # Public: set the list value at the specified index
  #
  # Returns promise
  lset: (key, index, value) ->
    @data._private[@key key][index] = @serialize value
    Q @

  # Public: insert a value into the list before or after the pivot element.
  #
  # Returns promise
  linsert: (key, placement, pivot, value) ->
    key = @key key
    if @data._private[key] isnt undefined
      index = _.find @data._private[key], pivot

      if index
        @data._private[key].splice 0, placement is 'AFTER' ? index + 1 : index, @serialize(value)

    Q @

  # Public: push a new value onto the left-side of the list
  #
  # Returns promise
  lpush: (key, value) ->
    key = @key key
    if @data._private[key] is undefined
      @data._private[key] = []

    @data._private[key].unshift(@serialize value)
    Q @

  # Public: push a new value onto the right-side of the list
  #
  # Returns promise
  rpush: (key, value) ->
    key = @key key
    if @data._private[key] is undefined
      @data._private[key] = []

    @data._private[key].push(@serialize value)
    Q @

  # Public: pop a value off of the left-side of the list
  #
  # Returns promise for list item
  lpop: (key) ->
    Q(@deserialize(@data._private[@key key].shift()))

  # Public: pop a value off of the right-side of the list
  #
  # Returns promise for list item
  rpop: (key) ->
    Q(@deserialize(@data._private[@key key].pop()))

  # Public: get a list item by index
  #
  # Returns promise for list item
  lindex: (key, index) ->
    Q(@deserialize(@data._private[@key key][index]))

  # Public: get an entire list
  #
  # Returns promise for array
  lgetall: (key) ->
    @lrange(key, 0, -1)

  # Public: get a slice of the list
  #
  # Returns promise for array
  lrange: (key, start, end) ->
    key = @key key

    if end < 0
      end = @data._private[key].length + end

    Q(_.map(@data._private[key].slice(start, end + 1), @deserialize.bind(@)))

  # Public: remove values from a list
  #
  # Returns promise
  lrem: (key, value) ->
    key = @key key
    @data._private[key] = _.without @data._private[key], value
    Q()

  # Public: Add a member to the set specified by `key`
  #
  # Returns promise
  sadd: (key, value) ->
    key = @key key
    if @data._private[key] is undefined
      @data._private[key] = []

    #TODO behavior with objects may not be what's expected. maybe make JSON.stringify the default serializer?
    if not _.contains @data._private[key], value
      @data._private[key].push(@serialize value)

    Q @

  # Public: Test whether the member is in the set
  #
  # Returns promise for boolean
  sismember: (key, value) ->
    Q(_.contains @data._private[key], value)

  # Public: Remove a member from the set
  #
  # Returns promise
  srem: (key, value) ->
    key = @key key
    index = _.findIndex @data._private[key], value
    if index
      @data._private[key].splice(1, index)

    Q @

  # Public: Get the size of the set
  #
  # Returns promise for int
  scard: (key) ->
    Q @data._private[@key key].length

  # Public: Get and remove a random member from the set
  #
  # Returns promise for a set member
  spop: (key) ->
    key = @key key
    index = _.random 0, @data._private[key].length - 1
    item = @data._private[key][index]

    @data._private[key].splice(1, index)

    Q item

  # Public: Get a random member from the set
  #
  # Returns promise for a set member
  srandmember: (key) ->
    key = @key key
    Q @data._private[key][_.random 0, @data._private[key].length - 1]

  # Public: Get all the members of the set
  #
  # Returns promise for array
  smembers: (key) ->
    Q @data._private[@key key]

  # Public: get all the keys, optionally restricted to keys prefixed with `searchKey`
  #
  # Returns promise for array
  keys: (searchKey = '') ->
    searchKey = @key searchKey
    Q(_.map(_.filter(_.keys(@data._private), (key) -> key.indexOf searchKey is 0), @unkey.bind(@)))

  # Public: transform a key from internal brain key, to user-facing key
  #
  # Returns string
  unkey: (key) ->
    key

  # Public: transform the key for internal use
  # overridden by brain-segment
  #
  # Returns string.
  key: (key) ->
    key

  # Public: get the key for the users
  #
  # Returns string.
  usersKey: () ->
    'users'

  # Public: Store key-value pair under the private namespace and extend
  # existing.
  #
  # Returns promise
  set: (key, value) ->
    if value is undefined
      _.each key, (v, k) =>
        @set k, v
    else
      @data._private[@key key] = @serialize value

    Q @

  # Public: Get value by key from the private namespace in @data
  # or return null if not found.
  #
  # Returns promise
  get: (key) ->
    Q(@deserialize(@data._private[@key key] ? null))

  # Public: Check whether the given key has been set
  #
  # Return promise for boolean
  exists: (key) ->
    Q(@data._private[@key key] isnt undefined)

  # Public: increment the value by num atomically
  #
  # Returns promise
  incrby: (key, num) ->
    key = @key key
    @data._private[key] = (@data._private[key] or 0) + num
    Q @data._private[key]

  # Public: Get all the keys for the given hash table name
  #
  # Returns promise for array.
  hkeys: (table) ->
    Q(_.keys(@data._private[@key table] or {}))

  # Public: Get all the values for the given hash table name
  #
  # Returns promise for array.
  hvals: (table) ->
    Q(_.mapValues(@data._private[@key table] or {}, @deserialize.bind(@)))

  # Public: get the size of the hash table.
  #
  # Returns promise for int
  hlen: (table) ->
    Q(_.size(@data._private[@key table]))

  # Public: Set a value in the specified hash table
  #
  # Returns promise for the value.
  hset: (table, key, value) ->
    table = @key table
    @data._private[table] = @data._private[table] or {}
    @data._private[table][key] = @serialize value
    Q @

  # Public: Get a value from the specified hash table.
  #
  # Returns: promise for the value.
  hget: (table, key) ->
    Q(@deserialize @data._private[@key table][key])

  # Public: Delete a field from a hash table
  #
  # Returns promise
  hdel: (table, key) ->
    delete @data._private[@key table][key]
    Q @

  # Public: Get the whole hash table as an object.
  #
  # Returns: promise for object.
  hgetall: (table) ->
    Q(_.clone(_.mapValues(@data._private[@key table], @deserialize.bind @)))

  # Public: increment the hash value by num atomically
  #
  # Returns promise
  hincrby: (table, key, num) ->
    table = @key table
    @data._private[table] = @data._private[table] or {}
    @data._private[table][key] = (@data._private[table][key] or 0) + num
    Q @data._private[table][key]

  # Public: Remove value by key from the private namespace in @data
  # if it exists
  #
  # Returns promise
  remove: (key) ->
    delete @data._private[@key key]
    Q @
  # alias for remove
  del: (key) ->
    @remove key

  # Public: nothin to close
  #
  # Returns promise
  close: ->
    Q @

  # Public: Merge keys against the in memory representation.
  #
  # Returns promise
  #
  # Caveats: Deeply nested structures don't merge well.
  mergeData: (data) ->
    @set data

  # Public: Perform any necessary pre-set serialization on a value
  #
  # Returns serialized value
  serialize: (value) ->
    value

  # Public: Perform any necessary post-get deserialization on a value
  #
  # Returns deserialized value
  deserialize: (value) ->
    value

  # Public: Get an Array of User objects stored in the brain.
  #
  # Returns promise for an Array of User objects.
  users: ->
    Q(@data.users)

  # Public: Add a user to the data-store
  #
  # Returns promise for user
  addUser: (user) ->
    @data.users[user.id] = user
    Q(user)

  # Public: Get or create a User object given a unique identifier.
  #
  # Returns promise for a User instance of the specified user.
  userForId: (id, options) ->
    user = @data.users[id]

    if !user or (options and options.room and (user.room isnt options.room))
      return @addUser(new User(id, options))

    Q(user)

  # Public: Get a User object given a name.
  #
  # Returns promise for a User instance for the user with the specified name.
  userForName: (name) ->
    result = null
    lowerName = name.toLowerCase()
    for k of (@data.users or { })
      userName = @data.users[k]['name']
      if userName? and userName.toString().toLowerCase() is lowerName
        result = @data.users[k]
    Q(result)

  # Public: Get all users whose names match fuzzyName. Currently, match
  # means 'starts with', but this could be extended to match initials,
  # nicknames, etc.
  #
  # Returns promise an Array of User instances matching the fuzzy name.
  usersForRawFuzzyName: (fuzzyName) ->
    lowerFuzzyName = fuzzyName.toLowerCase()
    Q(user) for key, user of (@data.users or {}) when (
      user.name.toLowerCase().lastIndexOf(lowerFuzzyName, 0) is 0
    )

  # Public: If fuzzyName is an exact match for a user, returns an array with
  # just that user. Otherwise, returns an array of all users for which
  # fuzzyName is a raw fuzzy match (see usersForRawFuzzyName).
  #
  # Returns promise an Array of User instances matching the fuzzy name.
  usersForFuzzyName: (fuzzyName) ->
    matchedUsers = @usersForRawFuzzyName(fuzzyName)
    lowerFuzzyName = fuzzyName.toLowerCase()
    for user in matchedUsers
      return [user] if user.name.toLowerCase() is lowerFuzzyName

    Q(matchedUsers)

  # Public: Return a brain segment bound to the given key-prefix.
  #
  # Returns BrainSegment
  segment: (segment) ->
    new BrainSegment @, segment

module.exports = Brain
