structured_types_uninitialized = require "./StructuredTypes"

module.exports = (HB)->
  structured_types = structured_types_uninitialized HB
  types = structured_types.types
  parser = structured_types.parser

  #
  # @nodoc
  # Extends the basic Insert type to an operation that holds a text value
  #
  class types.TextInsert extends types.Insert
    #
    # @param {String} content The content of this Insert-type Operation. Usually you restrict the length of content to size 1
    # @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
    #
    constructor: (content, uid, prev, next, origin, parent)->
      if content?.creator
        @saveOperation 'content', content
      else
        @content = content
      super uid, prev, next, origin, parent

    type: "TextInsert"

    #
    # Retrieve the effective length of the $content of this operation.
    #
    getLength: ()->
      if @isDeleted()
        0
      else
        @content.length

    applyDelete: ()->
      super # no braces indeed!
      if @content instanceof types.Operation
        @content.applyDelete()
      @content = null

    execute: ()->
      if not @validateSavedOperations()
        return false
      else
        if @content instanceof types.Operation
          @content.insert_parent = @
        super()

    #
    # The result will be concatenated with the results from the other insert operations
    # in order to retrieve the content of the engine.
    # @see HistoryBuffer.toExecutedArray
    #
    val: (current_position)->
      if @isDeleted() or not @content?
        ""
      else
        @content

    #
    # Convert all relevant information of this operation to the json-format.
    # This result can be send to other clients.
    #
    _encode: ()->
      json =
        {
          'type': @type
          'uid' : @getUid()
          'prev': @prev_cl.getUid()
          'next': @next_cl.getUid()
          'origin': @origin.getUid()
          'parent': @parent.getUid()
        }

      if @content?.getUid?
        json['content'] = @content.getUid()
      else
        json['content'] = @content
      json

  types.TextInsert.parse = (json)->
    {
      'content' : content
      'uid' : uid
      'prev': prev
      'next': next
      'origin' : origin
      'parent' : parent
    } = json
    new types.TextInsert content, uid, prev, next, origin, parent


  class types.Array extends types.ListManager

    type: "Array"

    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 types.Object
          o.toJson(transform_to_value)
        else if o instanceof types.Array
          o.toJson(transform_to_value)
        else if transform_to_value and o instanceof types.Operation
          o.val()
        else
          o

    val: (pos)->
      if pos?
        o = @getOperationByPosition(pos+1)
        if not (o instanceof types.Delimiter)
          o.val()
        else
          throw new Error "this position does not exist"
      else
        o = @beginning.next_cl
        result = []
        while o isnt @end
          result.push o.val()
          o = o.next_cl
        result

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

    insertAfter: (left, content, options)->
      createContent = (content, options)->
        if content? and content.constructor?
          type = types[content.constructor.name]
          if type? and type.create?
            type.create content, options
          else
            throw new Error "The #{content.constructor.name}-type is not (yet) supported in Yatta."
        else
          content

      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

      if content instanceof types.Operation
        (new types.TextInsert content, undefined, left, right).execute()
      else
        for c in content
          tmp = (new types.TextInsert createContent(c, options), undefined, left, right).execute()
          left = tmp
      @

    #
    # Inserts a string into the word.
    #
    # @return {Array Type} This String object.
    #
    insert: (position, content, options)->
      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, [content], options

    #
    # Deletes a part of the word.
    #
    # @return {Array Type} This String object
    #
    delete: (position, length)->
      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 types.Delimiter
          break
        d = (new types.Delete undefined, o).execute()
        o = o.next_cl
        while not (o instanceof types.Delimiter) and o.isDeleted()
          o = o.next_cl
        delete_ops.push d._encode()
      @

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

  types.Array.parse = (json)->
    {
      'uid' : uid
    } = json
    new this(uid)

  types.Array.create = (content, mutable)->
    if (mutable is "mutable")
      list = new types.Array().execute()
      ith = list.getOperationByPosition 0
      list.insertAfter ith, content
      list
    else if (not mutable?) or (mutable is "immutable")
      content
    else
      throw new Error "Specify either \"mutable\" or \"immutable\"!!"

  #
  # Handles a String-like data structures with support for insert/delete at a word-position.
  # @note Currently, only Text is supported!
  #
  class types.String extends types.Array

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

    #
    # Identifies this class.
    # Use it to check whether this is a word-type or something else.
    #
    # @example
    #   var x = yatta.val('unknown')
    #   if (x.type === "String") {
    #     console.log JSON.stringify(x.toJson())
    #   }
    #
    type: "String"

    #
    # Get the String-representation of this word.
    # @return {String} The String-representation of this object.
    #
    val: ()->
      c = for o in @toArray()
        if o.val?
          o.val()
        else
          ""
      c.join('')

    #
    # Same as String.val
    # @see String.val
    #
    toString: ()->
      @val()

    #
    # Inserts a string into the word.
    #
    # @return {Array Type} This String object.
    #
    insert: (position, content, options)->
      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, content, options

    #
    # Bind this String to a textfield or input field.
    #
    # @example
    #   var textbox = document.getElementById("textfield");
    #   yatta.bind(textbox);
    #
    bind: (textfield, dom_root)->
      dom_root ?= window;
      # don't duplicate!
      for t in @textfields
        if t is textfield
          return
      creator_token = false;

      word = @
      textfield.value = @val()
      @textfields.push textfield

      if textfield.selectionStart? and textfield.setSelectionRange?
        createRange = (fix)->
          left = textfield.selectionStart
          right = textfield.selectionEnd
          if fix?
            left = fix left
            right = fix right
          {
            left: left
            right: right
          }

        writeRange = (range)->
          writeContent word.val()
          textfield.setSelectionRange range.left, range.right

        writeContent = (content)->
          textfield.value = content
      else
        createRange = (fix)->
          s = dom_root.getSelection()
          clength = textfield.textContent.length
          left = Math.min s.anchorOffset, clength
          right = Math.min s.focusOffset, clength
          if fix?
            left = fix left
            right = fix right
          {
            left: left
            right: right
            isReal: true
          }

        writeRange = (range)->
          writeContent word.val()
          textnode = textfield.childNodes[0]
          if range.isReal and textnode?
            if range.left < 0
              range.left = 0
            range.right = Math.max range.left, range.right
            if range.right > textnode.length
              range.right = textnode.length
            range.left = Math.min range.left, range.right
            r = new Range()
            r.setStart(textnode, range.left)
            r.setEnd(textnode, range.right)
            s = window.getSelection()
            s.removeAllRanges()
            s.addRange(r)
        writeContent = (content)->
          append = ""
          if content[content.length - 1] is " "
            content = content.slice(0,content.length-1)
            append = '&nbsp;'
          textfield.textContent = content
          textfield.innerHTML += append

      writeContent this.val()

      @observe (events)->
        for event in events
          if not creator_token
            if event.type is "insert"
              o_pos = event.position
              fix = (cursor)->
                if cursor <= o_pos
                  cursor
                else
                  cursor += 1
                  cursor
              r = createRange fix
              writeRange r

            else if event.type is "delete"
              o_pos = event.position
              fix = (cursor)->
                if cursor < o_pos
                  cursor
                else
                  cursor -= 1
                  cursor
              r = createRange fix
              writeRange r

      # consume all text-insert changes.
      textfield.onkeypress = (event)->
        creator_token = true
        if word.is_deleted
          # if word is deleted, do not do anything ever again
          textfield.onkeypress = null
          return true
        char = null
        if event.key?
          if event.charCode is 32
            char = " "
          else if event.keyCode is 13
            char = '\n'
          else
            char = event.key
        else
          char = window.String.fromCharCode event.keyCode
        if char.length > 0
          r = createRange()
          pos = Math.min r.left, r.right
          diff = Math.abs(r.right - r.left)
          word.delete pos, diff
          word.insert pos, char
          r.left = pos + char.length
          r.right = r.left
          writeRange r
          event.preventDefault()
        else
          event.preventDefault()
        creator_token = false

      textfield.onpaste = (event)->
        if word.is_deleted
          # if word is deleted, do not do anything ever again
          textfield.onpaste = null
          return true
        event.preventDefault()
      textfield.oncut = (event)->
        if word.is_deleted
          # if word is deleted, do not do anything ever again
          textfield.oncut = null
          return true
        event.preventDefault()

      #
      # consume deletes. Note that
      #   chrome: won't consume deletions on keypress event.
      #   keyCode is deprecated. BUT: I don't see another way.
      #     since event.key is not implemented in the current version of chrome.
      #     Every browser supports keyCode. Let's stick with it for now..
      #
      textfield.onkeydown = (event)->
        creator_token = true
        if word.is_deleted
          # if word is deleted, do not do anything ever again
          textfield.onkeydown = null
          return true
        r = createRange()
        pos = Math.min r.left, r.right
        diff = Math.abs(r.left - r.right)
        if event.keyCode? and event.keyCode is 8 # Backspace
          if diff > 0
            word.delete pos, diff
            r.left = pos
            r.right = pos
            writeRange r
          else
            if event.ctrlKey? and event.ctrlKey
              if textfield.value?
                val = textfield.value
              else
                val = textfield.textContent
              new_pos = pos
              del_length = 0
              if pos > 0
                new_pos--
                del_length++
              while new_pos > 0 and val[new_pos] isnt " " and val[new_pos] isnt '\n'
                new_pos--
                del_length++
              word.delete new_pos, (pos-new_pos)
              r.left = new_pos
              r.right = new_pos
              writeRange r
            else
              if pos > 0
                word.delete (pos-1), 1
                r.left = pos-1
                r.right = pos-1
                writeRange r
          event.preventDefault()
        else if event.keyCode? and event.keyCode is 46 # Delete
          if diff > 0
            word.delete pos, diff
            r.left = pos
            r.right = pos
            writeRange r
          else
            word.delete pos, 1
            r.left = pos
            r.right = pos
            writeRange r
        creator_token = false
        true

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

  types.String.parse = (json)->
    {
      'uid' : uid
    } = json
    new this(uid)

  types.String.create = (content, mutable)->
    if (mutable is "mutable")
      word = new types.String().execute()
      word.insert 0, content
      word
    else if (not mutable?) or (mutable is "immutable")
      content
    else
      throw new Error "Specify either \"mutable\" or \"immutable\"!!"


  structured_types


