basic_ops_uninitialized = require "./Basic"

module.exports = ()->
  basic_ops = basic_ops_uninitialized()
  ops = basic_ops.operations

  #
  # @nodoc
  # Manages map like objects. E.g. Json-Type and XML attributes.
  #
  class ops.MapManager extends ops.Operation

    #
    # @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
    #
    constructor: (custom_type, uid)->
      @_map = {}
      super custom_type, uid

    type: "MapManager"

    applyDelete: ()->
      for name,p of @_map
        p.applyDelete()
      super()

    cleanup: ()->
      super()

    map: (f)->
      for n,v of @_map
        f(n,v)
      undefined

    #
    # @see JsonOperations.val
    #
    val: (name, content)->
      if arguments.length > 1
        if content? and content._getModel?
          rep = content._getModel(@custom_types, @operations)
        else
          rep = content
        @retrieveSub(name).replace rep
        @getCustomType()
      else if name?
        prop = @_map[name]
        if prop? and not prop.isContentDeleted()
          res = prop.val()
          if res instanceof ops.Operation
            res.getCustomType()
          else
            res
        else
          undefined
      else
        result = {}
        for name,o of @_map
          if not o.isContentDeleted()
            result[name] = o.val()
        result

    delete: (name)->
      @_map[name]?.deleteContent()
      @

    retrieveSub: (property_name)->
      if not @_map[property_name]?
        event_properties =
          name: property_name
        event_this = @
        rm_uid =
          noOperation: true
          sub: property_name
          alt: @
        rm = new ops.ReplaceManager null, event_properties, event_this, rm_uid # this operation shall not be saved in the HB
        @_map[property_name] = rm
        rm.setParent @, property_name
        rm.execute()
      @_map[property_name]

  ops.MapManager.parse = (json)->
    {
      'uid' : uid
      'custom_type' : custom_type
    } = json
    new this(custom_type, uid)



  #
  # @nodoc
  # Manages a list of Insert-type operations.
  #
  class ops.ListManager extends ops.Operation

    #
    # A ListManager maintains a non-empty list that has a beginning and an end (both Delimiters!)
    # @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
    # @param {Delimiter} beginning Reference or Object.
    # @param {Delimiter} end Reference or Object.
    constructor: (custom_type, uid)->
      @beginning = new ops.Delimiter undefined, undefined
      @end =       new ops.Delimiter @beginning, undefined
      @beginning.next_cl = @end
      @beginning.execute()
      @end.execute()
      super custom_type, uid

    type: "ListManager"

    applyDelete: ()->
      o = @end
      while o?
        o.applyDelete()
        o = o.prev_cl
      super()

    cleanup: ()->
      super()

    toJson: (transform_to_value = false)->
      val = @val()
      for i, o in val
        if o instanceof ops.Object
          o.toJson(transform_to_value)
        else if o instanceof ops.ListManager
          o.toJson(transform_to_value)
        else if transform_to_value and o instanceof ops.Operation
          o.val()
        else
          o

    #
    # @private
    # @see Operation.execute
    #
    execute: ()->
      if @validateSavedOperations()
        @beginning.setParent @
        @end.setParent @
        super
      else
        false

    # Get the element previous to the delemiter at the end
    getLastOperation: ()->
      @end.prev_cl

    # similar to the above
    getFirstOperation: ()->
      @beginning.next_cl

    # Transforms the the list to an array
    # Doesn't return left-right delimiter.
    toArray: ()->
      o = @beginning.next_cl
      result = []
      while o isnt @end
        if not o.is_deleted
          result.push o.val()
        o = o.next_cl
      result

    map: (f)->
      o = @beginning.next_cl
      result = []
      while o isnt @end
        if not o.is_deleted
          result.push f(o)
        o = o.next_cl
      result

    fold: (init, f)->
      o = @beginning.next_cl
      while o isnt @end
        if not o.is_deleted
          init = f(init, o)
        o = o.next_cl
      init

    val: (pos)->
      if pos?
        o = @getOperationByPosition(pos+1)
        if not (o instanceof ops.Delimiter)
          o.val()
        else
          throw new Error "this position does not exist"
      else
        @toArray()


    #
    # Retrieves the x-th not deleted element.
    # e.g. "abc" : the 1th character is "a"
    # the 0th character is the left Delimiter
    #
    getOperationByPosition: (position)->
      o = @beginning
      while true
        # find the i-th op
        if o instanceof ops.Delimiter and o.prev_cl?
          # the user or you gave a position parameter that is to big
          # for the current array. Therefore we reach a Delimiter.
          # Then, we'll just return the last character.
          o = o.prev_cl
          while o.isDeleted() and o.prev_cl?
            o = o.prev_cl
          break
        if position <= 0 and not o.isDeleted()
          break

        o = o.next_cl
        if not o.isDeleted()
          position -= 1
      o

    push: (content)->
      @insertAfter @end.prev_cl, [content]

    insertAfter: (left, contents)->
      right = left.next_cl
      while right.isDeleted()
        right = right.next_cl # find the first character to the right, that is not deleted. In the case that position is 0, its the Delimiter.
      left = right.prev_cl

      # TODO: always expect an array as content. Then you can combine this with the other option (else)
      if contents instanceof ops.Operation
        (new ops.Insert null, content, undefined, left, right).execute()
      else
        for c in contents
          if c? and c._name? and c._getModel?
            c = c._getModel(@custom_types, @operations)
          tmp = (new ops.Insert null, c, undefined, left, right).execute()
          left = tmp
      @

    #
    # Inserts an array of content into this list.
    # @Note: This expects an array as content!
    #
    # @return {ListManager Type} This String object.
    #
    insert: (position, contents)->
      ith = @getOperationByPosition position
      # the (i-1)th character. e.g. "abc" the 1th character is "a"
      # the 0th character is the left Delimiter
      @insertAfter ith, contents

    #
    # Deletes a part of the word.
    #
    # @return {ListManager Type} This String object
    #
    delete: (position, length = 1)->
      o = @getOperationByPosition(position+1) # position 0 in this case is the deletion of the first character

      delete_ops = []
      for i in [0...length]
        if o instanceof ops.Delimiter
          break
        d = (new ops.Delete null, undefined, o).execute()
        o = o.next_cl
        while (not (o instanceof ops.Delimiter)) and o.isDeleted()
          o = o.next_cl
        delete_ops.push d._encode()
      @

  ops.ListManager.parse = (json)->
    {
      'uid' : uid
      'custom_type': custom_type
    } = json
    new this(custom_type, uid)

  #
  # @nodoc
  # Adds support for replace. The ReplaceManager manages Replaceable operations.
  # Each Replaceable holds a value that is now replaceable.
  #
  # The TextType-type has implemented support for replace
  # @see TextType
  #
  class ops.ReplaceManager extends ops.ListManager
    #
    # @param {Object} event_properties Decorates the event that is thrown by the RM
    # @param {Object} event_this The object on which the event shall be executed
    # @param {Operation} initial_content Initialize this with a Replaceable that holds the initial_content.
    # @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
    # @param {Delimiter} beginning Reference or Object.
    # @param {Delimiter} end Reference or Object.
    constructor: (custom_type, @event_properties, @event_this, uid, beginning, end)->
      if not @event_properties['object']?
        @event_properties['object'] = @event_this.getCustomType()
      super custom_type, uid, beginning, end

    type: "ReplaceManager"

    applyDelete: ()->
      o = @beginning
      while o?
        o.applyDelete()
        o = o.next_cl
      super()

    cleanup: ()->
      super()

    #
    # This doesn't throw the same events as the ListManager. Therefore, the
    # Replaceables also not throw the same events.
    # So, ReplaceManager and ListManager both implement
    # these functions that are called when an Insertion is executed (at the end).
    #
    #
    callEventDecorator: (events)->
      if not @isDeleted()
        for event in events
          for name,prop of @event_properties
            event[name] = prop
        @event_this.callEvent events
      undefined

    #
    # Replace the existing word with a new word.
    #
    # @param content {Operation} The new value of this ReplaceManager.
    # @param replaceable_uid {UID} Optional: Unique id of the Replaceable that is created
    #
    replace: (content, replaceable_uid)->
      o = @getLastOperation()
      relp = (new ops.Replaceable null, content, @, replaceable_uid, o, o.next_cl).execute()
      # TODO: delete repl (for debugging)
      undefined

    isContentDeleted: ()->
      @getLastOperation().isDeleted()

    deleteContent: ()->
      (new ops.Delete null, undefined, @getLastOperation().uid).execute()
      undefined

    #
    # Get the value of this
    # @return {String}
    #
    val: ()->
      o = @getLastOperation()
      #if o instanceof ops.Delimiter
        # throw new Error "Replace Manager doesn't contain anything."
      o.val?() # ? - for the case that (currently) the RM does not contain anything (then o is a Delimiter)

    #
    # Encode this operation in such a way that it can be parsed by remote peers.
    #
    _encode: (json = {})->
      json.beginning = @beginning.getUid()
      json.end = @end.getUid()
      super json

  #
  # @nodoc
  # The ReplaceManager manages Replaceables.
  # @see ReplaceManager
  #
  class ops.Replaceable extends ops.Insert

    #
    # @param {Operation} content The value that this Replaceable holds.
    # @param {ReplaceManager} parent Used to replace this Replaceable with another one.
    # @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
    #
    constructor: (custom_type, content, parent, uid, prev, next, origin)->
      @saveOperation 'parent', parent
      super custom_type, content, uid, prev, next, origin # Parent is already saved by Replaceable

    type: "Replaceable"

    #
    # This is called, when the Insert-type was successfully executed.
    # TODO: consider doing this in a more consistent manner. This could also be
    # done with execute. But currently, there are no specital Insert-ops for ListManager.
    #
    callOperationSpecificInsertEvents: ()->
      if @next_cl.type is "Delimiter" and @prev_cl.type isnt "Delimiter"
        # this replaces another Replaceable
        if not @is_deleted # When this is received from the HB, this could already be deleted!
          old_value = @prev_cl.val()
          @parent.callEventDecorator [
            type: "update"
            changedBy: @uid.creator
            oldValue: old_value
          ]
        @prev_cl.applyDelete()
      else if @next_cl.type isnt "Delimiter"
        # This won't be recognized by the user, because another
        # concurrent operation is set as the current value of the RM
        @applyDelete()
      else # prev _and_ next are Delimiters. This is the first created Replaceable in the RM
        @parent.callEventDecorator [
          type: "add"
          changedBy: @uid.creator
        ]
      undefined

    callOperationSpecificDeleteEvents: (o)->
      if @next_cl.type is "Delimiter"
        @parent.callEventDecorator [
          type: "delete"
          changedBy: o.uid.creator
          oldValue: @val()
        ]

    #
    # Encode this operation in such a way that it can be parsed by remote peers.
    #
    _encode: (json = {})->
      super json

  ops.Replaceable.parse = (json)->
    {
      'content' : content
      'parent' : parent
      'uid' : uid
      'prev': prev
      'next': next
      'origin' : origin
      'custom_type' : custom_type
    } = json
    if typeof content is "string"
      content = JSON.parse(content)
    new this(custom_type, content, parent, uid, prev, next, origin)


  basic_ops