json_types_uninitialized = require "./JsonTypes"

# some dom implementations may call another dom.method that simulates the behavior of another.
# For example xml.insertChild(dom) , wich inserts an element at the end, and xml.insertAfter(dom,null) wich does the same
# But yatta's proxy may be called only once!
proxy_token = false
dont_proxy = (f)->
  proxy_token = true
  try
    f()
  catch e
    proxy_token = false
    throw new Error e
  proxy_token = false

_proxy = (f_name, f)->
  old_f = @[f_name]
  if old_f?
    @[f_name] = ()->
      if not proxy_token and not @_yatta?.isDeleted()
        that = this
        args = arguments
        dont_proxy ()->
          f.apply that, args
          old_f.apply that, args
      else
        old_f.apply this, arguments
  #else
  #  @[f_name] = f
Element?.prototype._proxy = _proxy


module.exports = (HB)->
  json_types = json_types_uninitialized HB
  types = json_types.types
  parser = json_types.parser

  #
  # Manages XML types
  # Not supported:
  # * Attribute nodes
  # * Real replace of child elements (to much overhead). Currently, the new element is inserted after the 'replaced' element, and then it is deleted.
  # * Namespaces (*NS)
  # * Browser specific methods (webkit-* operations)
  class XmlType extends types.Insert

    constructor: (uid, @tagname, attributes, elements, @xml)->
      ### In case you make this instanceof Insert again
      if prev? and (not next?) and prev.type?
        # adjust what you actually mean. you want to insert after prev, then
        # next is not defined. but we only insert after non-deleted elements.
        # This is also handled in TextInsert.
        while prev.isDeleted()
          prev = prev.prev_cl
        next = prev.next_cl
      ###

      super(uid)


      if @xml?._yatta?
        d = new types.Delete undefined, @xml._yatta
        HB.addOperation(d).execute()
        @xml._yatta = null

      if attributes? and elements?
        @saveOperation 'attributes', attributes
        @saveOperation 'elements', elements
      else if (not attributes?) and (not elements?)
        @attributes = new types.JsonType()
        @attributes.setMutableDefault 'immutable'
        HB.addOperation(@attributes).execute()
        @elements = new types.WordType()
        @elements.parent = @
        HB.addOperation(@elements).execute()
      else
        throw new Error "Either define attribute and elements both, or none of them"

      if @xml?
        @tagname = @xml.tagName
        for i in [0...@xml.attributes.length]
          attr = xml.attributes[i]
          @attributes.val(attr.name, attr.value)
        for n in @xml.childNodes
          if n.nodeType is n.TEXT_NODE
            word = new TextNodeType(undefined, n)
            HB.addOperation(word).execute()
            @elements.push word
          else if n.nodeType is n.ELEMENT_NODE
            element = new XmlType undefined, undefined, undefined, undefined, n
            HB.addOperation(element).execute()
            @elements.push element
          else
            throw new Error "I don't know Node-type #{n.nodeType}!!"
        @setXmlProxy()
      undefined

    #
    # Identifies this class.
    # Use it in order to check whether this is an xml-type or something else.
    #
    type: "XmlType"

    applyDelete: (op)->
      if @insert_parent? and not @insert_parent.isDeleted()
        @insert_parent.applyDelete op
      else
        @attributes.applyDelete()
        @elements.applyDelete()
        super

    cleanup: ()->
      super()

    setXmlProxy: ()->
      @xml._yatta = @
      that = @

      @elements.on 'insert', (event, op)->
        if op.creator isnt HB.getUserId() and this is that.elements
          newNode = op.content.val()
          right = op.next_cl
          while right? and right.isDeleted()
            right = right.next_cl
          rightNode = null
          if right.type isnt 'Delimiter'
            rightNode = right.val().val()
          dont_proxy ()->
            that.xml.insertBefore newNode, rightNode
      @elements.on 'delete', (event, op)->
        del_op = op.deleted_by[0]
        if del_op? and del_op.creator isnt HB.getUserId() and this is that.elements
          deleted = op.content.val()
          dont_proxy ()->
            that.xml.removeChild deleted

      @attributes.on ['addProperty', 'change'], (event, property_name, op)->
        if op.creator isnt HB.getUserId() and this is that.attributes
          dont_proxy ()->
            newval = op.val().val()
            if newval?
              that.xml.setAttribute(property_name, op.val().val())
            else
              that.xml.removeAttribute(property_name)








      ## Here are all methods that proxy the behavior of the xml

      # you want to find a specific child element. Since they are carried by an Insert-Type, you want to find that Insert-Operation.
      # @param child {DomElement} Dom element.
      # @return {InsertType} This carries the XmlType that represents the DomElement (child). false if i couldn't find it.
      #
      findNode = (child)->
        if not child?
          throw new Error "you must specify a parameter!"
        child = child._yatta
        elem = that.elements.beginning.next_cl
        while elem.type isnt 'Delimiter' and elem.content isnt child
          elem = elem.next_cl
        if elem.type is 'Delimiter'
          false
        else
          elem

      insertBefore = (insertedNode_s, adjacentNode)->
        next = null
        if adjacentNode?
          next = findNode adjacentNode
        prev = null
        if next
          prev = next.prev_cl
        else
          prev = @_yatta.elements.end.prev_cl
          while prev.isDeleted()
            prev = prev.prev_cl
        inserted_nodes = null
        if insertedNode_s.nodeType is insertedNode_s.DOCUMENT_FRAGMENT_NODE
          child = insertedNode_s.lastChild
          while child?
            element = new XmlType undefined, undefined, undefined, undefined, child
            HB.addOperation(element).execute()
            that.elements.insertAfter prev, element
            child = child.previousSibling
        else
          element = new XmlType undefined, undefined, undefined, undefined, insertedNode_s
          HB.addOperation(element).execute()
          that.elements.insertAfter prev, element

      @xml._proxy 'insertBefore', insertBefore
      @xml._proxy 'appendChild', insertBefore
      @xml._proxy 'removeAttribute', (name)->
        that.attributes.val(name, undefined)
      @xml._proxy 'setAttribute', (name, value)->
        that.attributes.val name, value

      renewClassList = (newclass)->
        dont_do_it = false
        if newclass?
          for elem in this
            if newclass is elem
              dont_do_it = true
        value = Array.prototype.join.call this, " "
        if newclass? and not dont_do_it
          value += " "+newclass
        that.attributes.val('class', value )
      _proxy.call @xml.classList, 'add', renewClassList
      _proxy.call @xml.classList, 'remove', renewClassList
      @xml.__defineSetter__ 'className', (val)->
        @setAttribute('class', val)
      @xml.__defineGetter__ 'className', ()->
        that.attributes.val('class')
      @xml.__defineSetter__ 'textContent', (val)->
        # remove all nodes
        elem = that.xml.firstChild
        while elem?
          remove = elem
          elem = elem.nextSibling
          that.xml.removeChild remove

        # insert word content
        if val isnt ""
          text_node = document.createTextNode val
          that.xml.appendChild text_node

      removeChild = (node)->
        elem = findNode node
        if not elem
          throw new Error "You are only allowed to delete existing (direct) child elements!"
        d = new types.Delete undefined, elem
        HB.addOperation(d).execute()
        node._yatta = null
      @xml._proxy 'removeChild', removeChild
      @xml._proxy 'replaceChild', (insertedNode, replacedNode)->
        insertBefore.call this, insertedNode, replacedNode
        removeChild.call this, replacedNode



    val: (enforce = false)->
      if document?
        if (not @xml?) or enforce
          @xml = document.createElement @tagname

          attr = @attributes.val()
          for attr_name, value of attr
            if value?
              a = document.createAttribute attr_name
              a.value = value
              @xml.setAttributeNode a

          e = @elements.beginning.next_cl
          while e.type isnt "Delimiter"
            n = e.content
            if not e.isDeleted() and e.content? # TODO: how can this happen?  Probably because listeners
              if n.type is "XmlType"
                @xml.appendChild n.val(enforce)
              else if n.type is "TextNodeType"
                text_node = n.val()
                @xml.appendChild text_node
              else
                throw new Error "Internal structure cannot be transformed to dom"
            e = e.next_cl
        @setXmlProxy()
        @xml


    execute: ()->
      super()
    ###
      if not @validateSavedOperations()
        return false
      else

        return true
    ###

    #
    # Get the parent of this JsonType.
    # @return {XmlType}
    #
    getParent: ()->
      @parent

    #
    # @private
    #
    # Convert all relevant information of this operation to the json-format.
    # This result can be send to other clients.
    #
    _encode: ()->
      json =
        {
          'type' : @type
          'attributes' : @attributes.getUid()
          'elements' : @elements.getUid()
          'tagname' : @tagname
          'uid' : @getUid()
        }
      json

  parser['XmlType'] = (json)->
    {
      'uid' : uid
      'attributes' : attributes
      'elements' : elements
      'tagname' : tagname
    } = json

    new XmlType uid, tagname, attributes, elements, undefined

#
  # @nodoc
  # Defines an object that is cannot be changed. You can use this to set an immutable string, or a number.
  #
  class TextNodeType extends types.ImmutableObject

    #
    # @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
    # @param {Object} content
    #
    constructor: (uid, content)->
      if content._yatta?
        d = new types.Delete undefined, content._yatta
        HB.addOperation(d).execute()
        content._yatta = null
      content._yatta = @
      super uid, content

    applyDelete: (op)->
      if @insert_parent? and not @insert_parent.isDeleted()
        @insert_parent.applyDelete op
      else
        super


    type: "TextNodeType"

    #
    # Encode this operation in such a way that it can be parsed by remote peers.
    #
    _encode: ()->
      json = {
        'type': @type
        'uid' : @getUid()
        'content' : @content.textContent
      }
      json

  parser['TextNodeType'] = (json)->
    {
      'uid' : uid
      'content' : content
    } = json
    textnode = document.createTextNode content
    new TextNodeType uid, textnode

  types['XmlType'] = XmlType

  json_types