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, content, content_operations)->
      @_map = {}
      super custom_type, uid, content, content_operations

    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
      'content' : content
      'content_operations' : content_operations
    } = json
    new this(custom_type, uid, content, content_operations)



  #
  # @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, content, content_operations)->
      @beginning = new ops.Delimiter undefined, undefined
      @end =       new ops.Delimiter @beginning, undefined
      @beginning.next_cl = @end
      @beginning.execute()
      @end.execute()
      super custom_type, uid, content, content_operations

    type: "ListManager"


    applyDelete: ()->
      o = @beginning
      while o?
        o.applyDelete()
        o = o.next_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()

    ref: (pos)->
      if pos?
        o = @getOperationByPosition(pos+1)
        if not (o instanceof ops.Delimiter)
          o
        else
          null
          # throw new Error "this position does not exist"
      else
        throw new Error "you must specify a position parameter"

    #
    # 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, null, undefined, 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, null, undefined, 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()
      @


    callOperationSpecificInsertEvents: (op)->
      getContentType = (content)->
        if content instanceof ops.Operation
          content.getCustomType()
        else
          content
      @callEvent [
        type: "insert"
        reference: op
        position: op.getPosition()
        object: @getCustomType()
        changedBy: op.uid.creator
        value: getContentType op.val()
      ]

    callOperationSpecificDeleteEvents: (op, del_op)->
      @callEvent [
        type: "delete"
        reference: op
        position: op.getPosition()
        object: @getCustomType() # TODO: You can combine getPosition + getParent in a more efficient manner! (only left Delimiter will hold @parent)
        length: 1
        changedBy: del_op.uid.creator
        oldValue: op.val()
      ]

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

  class ops.Composition extends ops.ListManager

    constructor: (custom_type, @_composition_value, composition_value_operations, uid, tmp_composition_ref)->
      # we can't use @seveOperation 'composition_ref', tmp_composition_ref here,
      # because then there is a "loop" (insertion refers to parent, refers to insertion..)
      # This is why we have to check in @callOperationSpecificInsertEvents until we find it
      super custom_type, uid
      if tmp_composition_ref?
        @tmp_composition_ref = tmp_composition_ref
      else
        @composition_ref = @end.prev_cl
      if composition_value_operations?
        @composition_value_operations = {}
        for n,o of composition_value_operations
          @saveOperation n, o, '_composition_value'

    type: "Composition"

    #
    # @private
    # @see Operation.execute
    #
    execute: ()->
      if @validateSavedOperations()
        @getCustomType()._setCompositionValue @_composition_value
        delete @_composition_value
        super
      else
        false

    #
    # This is called, when the Insert-operation was successfully executed.
    #
    callOperationSpecificInsertEvents: (op)->
      if @tmp_composition_ref?
        if op.uid.creator is @tmp_composition_ref.creator and op.uid.op_number is @tmp_composition_ref.op_number
          @composition_ref = op
          delete @tmp_composition_ref
          op = op.next_cl
          if op is @end
            return
        else
          return

      o = @end.prev_cl
      while o isnt op
        @getCustomType()._unapply o.undo_delta
        o = o.prev_cl
      while o isnt @end
        o.undo_delta = @getCustomType()._apply o.val()
        o = o.next_cl
      @composition_ref = @end.prev_cl

      @callEvent [
        type: "update"
        changedBy: op.uid.creator
        newValue: @val()
      ]

    callOperationSpecificDeleteEvents: (op, del_op)->
      return

    #
    # Create a new Delta
    # - inserts new Content at the end of the list
    # - updates the composition_value
    # - updates the composition_ref
    #
    # @param delta The delta that is applied to the composition_value
    #
    applyDelta: (delta, operations)->
      (new ops.Insert null, delta, operations, @, null, @end.prev_cl, @end).execute()
      undefined

    #
    # Encode this operation in such a way that it can be parsed by remote peers.
    #
    _encode: (json = {})->
      custom = @getCustomType()._getCompositionValue()
      json.composition_value = custom.composition_value
      if custom.composition_value_operations?
        json.composition_value_operations = {}
        for n,o of custom.composition_value_operations
          json.composition_value_operations[n] = o.getUid()
      if @composition_ref?
        json.composition_ref = @composition_ref.getUid()
      else
        json.composition_ref = @tmp_composition_ref
      super json

  ops.Composition.parse = (json)->
    {
      'uid' : uid
      'custom_type': custom_type
      'composition_value' : composition_value
      'composition_value_operations' : composition_value_operations
      'composition_ref' : composition_ref
    } = json
    new this(custom_type, composition_value, composition_value_operations, uid, composition_ref)


  #
  # @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)->
      if not @event_properties['object']?
        @event_properties['object'] = @event_this.getCustomType()
      super custom_type, uid

    type: "ReplaceManager"

    #
    # 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

    #
    # 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: (op)->
      if op.next_cl.type is "Delimiter" and op.prev_cl.type isnt "Delimiter"
        # this replaces another Replaceable
        if not op.is_deleted # When this is received from the HB, this could already be deleted!
          old_value = op.prev_cl.val()
          @callEventDecorator [
            type: "update"
            changedBy: op.uid.creator
            oldValue: old_value
          ]
        op.prev_cl.applyDelete()
      else if op.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
        op.applyDelete()
      else # prev _and_ next are Delimiters. This is the first created Replaceable in the RM
        @callEventDecorator [
          type: "add"
          changedBy: op.uid.creator
        ]
      undefined

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


    #
    # 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.Insert null, content, null, @, replaceable_uid, o, o.next_cl).execute()
      # TODO: delete repl (for debugging)
      undefined

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

    deleteContent: ()->
      last_op = @getLastOperation()
      if (not last_op.isDeleted()) and last_op.type isnt "Delimiter"
        (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)



  basic_ops
