Addr64 = (require '../log/Addr64.coffee').Addr64
ListNode = (require './ListNode.coffee').ListNode
EventEmitter = (require 'events').EventEmitter
RandomAccessByteIterable =
  (require '../log/RandomAccessByteIterable.coffee').RandomAccessByteIterable
DatabaseRoot = (require '../list_env/DatabaseRoot.coffee').DatabaseRoot
ArrayByteIterable =
  (require '../database/impl/iterate/ArrayByteIterable.coffee').
  ArrayByteIterable
LightOutputStream =
  (require '../database/impl/iterate/LightOutputStream.coffee').
  LightOutputStream
CompressedUnsignedLongByteIterable =
  (require '../log/iterate/CompressedUnsignedLongByteIterable.coffee').
  CompressedUnsignedLongByteIterable

# This class provides simple List, which can add elements, store itself in Log
# and restore from Log. It also can store changes (without overwriting all
# the Log).
class List extends EventEmitter
  # @private
  log: undefined
  # @private
  rootNode: undefined
  # @private
  dublicates: undefined
  # @private
  structureId: undefined
  # @private
  length: undefined

  @EMITTER = new EventEmitter()

  saveLengthToRoot: () ->
    @rootNode.key = ArrayByteIterable.create$Buffer new Buffer("L")
    l = CompressedUnsignedLongByteIterable.getCompressedSize$int @length
    stream = LightOutputStream.create$int l
    CompressedUnsignedLongByteIterable.
    fillBytes$int$LightOutputStream @length, stream
    @rootNode.value = stream.asArrayByteIterable()
    @rootNode.addrsAreInMemory = false

  # Constructor.
  #
  # @param log
  @create$Log$Boolean$int: (log, dublicates, structureId, o) ->
    if !o? then o = new List
    o.log = log
    o.dublicates = dublicates
    o.structureId = structureId
    o.rootNode = ListNode.create$Log log
    o.log.setMaxListeners 0
    o.setMaxListeners 0
    o.emptyList = false
    o.log.on 'getLastLoggableOfType', (loggable) =>
      if loggable?
        dr = DatabaseRoot.create$Addr64$int null, null
        onReadFromLoggable = () =>
          o.rootNode.ownAddr = dr.getRootAddress().copy()
          if o.rootNode.ownAddr.equals$Addr64(Addr64.create$int$int 0, 0)
            o.rootNode.ownAddr = o.rootNode.nullAddr
            o.length = 0
            o.saveLengthToRoot()
            List.EMITTER.emit 'create', o
          else
            o.rootNode.once 'getNode', (ok, newnode) =>
              o.rootNode = newnode
              CompressedUnsignedLongByteIterable.EMITTER.once 'getInt', (value) =>
                o.length = value
                List.EMITTER.emit 'create', o
              value = CompressedUnsignedLongByteIterable.
              getInt$ByteIterator$emit o.rootNode.value.source.iterator()
              if value?
                o.length = value
                List.EMITTER.emit 'create', o
            o.rootNode.getNode()
        dr.once 'readFromLoggable', () =>
          onReadFromLoggable()
        isValid = dr.readFromLoggable loggable
        if isValid?
          dr.removeAllListeners 'readFromLoggable'
          onReadFromLoggable()
      else
        o.rootNode.ownAddr = o.rootNode.nullAddr
        o.length = 0
        o.saveLengthToRoot()
        List.EMITTER.emit 'create', o
    o.log.getLastLoggableOfType$int$emit(DatabaseRoot.DATABASE_ROOT_TYPE)
    return undefined

  getLength: () ->
    return @length

  # Save all the changes in List to Log and write Log on disk.
  #
  # @return always emit 'flush' event.
  flush$emit: () ->
    @rootNode.flush$emit()
    @log.once 'flush', () =>
      @emit 'flush'
    @log.write$Loggable(DatabaseRoot.
    create$Addr64$int(@rootNode.ownAddr, @structureId).toLoggable())
    @log.flush$emit()

  # Save all the changes in List to Log and close Log.
  #
  # @return always emit 'close' event.
  close$emit: () ->
    @rootNode.flush$emit()
    @log.once 'close', () =>
      @emit 'close'
    @log.write$Loggable(DatabaseRoot.
    create$Addr64$int(@rootNode.ownAddr, @structureId).toLoggable())
    @log.close$emit()

  # Check element for key equal.
  #
  # @param node.
  # @param key.
  # @param event the name of the event to be emitted.
  # @return always emits the named event.
  check$ListNode$ByteIterable$String$emit: (node, key, event) ->
    onCompare = (res) =>
      if res == 0
        @emit event, node
      else
        oldNode = node
        node = node.next
        if node?
          node.once 'getNode', (ok, newnode) =>
            newnode.prev = oldNode
            if ok
              @check$ListNode$ByteIterable$String$emit newnode, key, event
            else
              @emit event, undefined
          node.getNode()
        else
          @emit event, undefined
    if node.isEmpty()
      @emit event, undefined
    else
      node.key.once 'compare', (res) =>
        onCompare res
      res = node.key.compareTo$ByteIterable$emit key
      if res?
        if node.key?
          node.key.removeAllListeners 'compare'
        onCompare res

  # @private
  # Find element by key.
  #
  # @param key.
  # @param event the name of the event to be emitted.
  # @return always emits the named event.
  find$ByteIterable$String$emit: (key, event) ->
    node = @rootNode
    node.once 'getNode', (ok, node) =>
      if ok
        if node.key?
          @check$ListNode$ByteIterable$String$emit node, key, event
        else
          @emit event, undefined
      else
        @emit event, undefined
    node.getNode()

  # Find element by key.
  #
  # @param key.
  # @return always emits the 'get' event.
  get$ByteIterable$emit: (key) ->
    @find$ByteIterable$String$emit key, 'get'

  # @private
  # Set new value to the targetNode. Use it instead of simply
  # ListNode::value = value! This function provides necessary changes in
  # List elements, do that it will be correctly saved to Log.
  #
  # @param targetNode.
  # @param value.
  setValue$ListNode$ByteIterable$emit: (targetNode, value) ->
    targetNode.value = value
    targetNode.valueAddr = targetNode.nullAddr
    targetNode.ownAddr = targetNode.nullAddr
    node = @rootNode
    while node != targetNode
      node.ownAddr = node.nullAddr
      node.nextAddr = node.nullAddr
      node = node.next

  # @private
  # Add node.
  #
  # @param key
  # @param value
  addNode$ByteIterable$ByteIterable: (key, value) ->
    newNode = ListNode.create$Log$ByteIterable$ByteIterable$int$ListNode @log,
            key, value, @structureId, @rootNode.next
    newNode.nextAddr = @rootNode.nextAddr
    newNode.prev = @rootNode
    @rootNode.next = newNode
    @rootNode.nextAddr = @rootNode.nullAddr
    @length += 1
    @saveLengthToRoot()

  # Add new element with specified value to List.
  #
  # @param key the key of the new element. If element already exists, it's value
  # is overwritten.
  # @param value.
  # @return always emits 'push' event.
  push$ByteIterable$ByteIterable$emit: (key, value) ->
    if @dublicates
      @addNode$ByteIterable$ByteIterable key, value
      @emit 'push'
    else
      @once 'getValue', (node) =>
        if !node?
          @addNode$ByteIterable$ByteIterable key, value
          @emit 'push'
        else
          @setValue$ListNode$ByteIterable$emit node, value
          @emit 'push'
      @find$ByteIterable$String$emit key, 'getValue'

  # Add new element with specified value to List.
  #
  # @param key the key of the new element.
  # @param value.
  # @return always emits 'add' event with true/false (see Store::add).
  add$ByteIterable$ByteIterable$emit: (key, value) ->
    @once 'getValue', (node) =>
      if !node?
        @addNode$ByteIterable$ByteIterable key, value
        @emit 'add', true
      else
        @emit 'add', false
    @find$ByteIterable$String$emit key, 'getValue'

  # Delete this exact node.
  #
  # @param node
  # @return previous node
  deleteNode$ListNode: (node) ->
    node.prev.nextAddr = node.nextAddr
    node.prev.next = node.next
    if node.next?
      node.next.prev = node.prev
    @setValue$ListNode$ByteIterable$emit node.prev, node.prev.value
    node = node.prev
    @length -= 1
    @saveLengthToRoot()
    return node

  # Delete all the nodes with given key.
  #
  # @param key.
  # @return always emits 'delete' event.
  delete$ByteIterable$emit: (key) ->
    @once 'getValueForDelete', (node) =>
      if !node?
        @emit 'delete', key
      else
        @deleteNode$ListNode node
        @delete$ByteIterable$emit key
    @find$ByteIterable$String$emit key, 'getValueForDelete'

  # Print list to console (debug only!).
  print: () ->
    node = @rootNode
    console.log "List in memory:"
    while node? && node.key?
      console.log "key =", node.key.getBytesUnsafe(),
              "prev_key =", (if node.prev? then node.prev.key.getBytesUnsafe()),
              "value =", node.value.getBytesUnsafe(),
              "nextAddr =", node.nextAddr.left, ":", node.nextAddr.right
      node = node.next
    console.log "End of dump"

exports.List = List
