structured_types_uninitialized = require "./StructuredTypes"

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

  #
  # @nodoc
  # At the moment TextDelete type equals the Delete type in BasicTypes.
  # @see BasicTypes.Delete
  #
  class TextDelete extends types.Delete
  parser["TextDelete"] = parser["Delete"]

  #
  # @nodoc
  # Extends the basic Insert type to an operation that holds a text value
  #
  class 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)->
      if content?.creator?
        @saveOperation 'content', content
      else
        @content = content
      if not (prev? and next?)
        throw new Error "You must define prev, and next for TextInsert-types!"
      super uid, prev, next, origin

    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': "TextInsert"
          'uid' : @getUid()
          'prev': @prev_cl.getUid()
          'next': @next_cl.getUid()
        }
      if @content?.getUid?
        json['content'] = @content.getUid()
      else
        json['content'] = @content
      if @origin isnt @prev_cl
        json["origin"] = @origin.getUid()
      json

  parser["TextInsert"] = (json)->
    {
      'content' : content
      'uid' : uid
      'prev': prev
      'next': next
      'origin' : origin
    } = json
    new TextInsert content, uid, prev, next, origin

  #
  # Handles a WordType-like data structures with support for insertText/deleteText at a word-position.
  # @note Currently, only Text is supported!
  #
  class WordType extends types.ListManager

    #
    # @private
    # @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
    #
    constructor: (uid, beginning, end, prev, next, origin)->
      super uid, beginning, end, prev, next, origin

    #
    # 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 === "WordType") {
    #     console.log JSON.stringify(x.toJson())
    #   }
    #
    type: "WordType"

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

    cleanup: ()->
      super()

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

    insertAfter: (left, content)->
      while left.isDeleted()
        left = left.prev_cl # find the first character to the left, that is not deleted. Case position is 0, its the Delimiter.
      right = left.next_cl
      if content.type?
        op = new TextInsert content, undefined, left, right
        HB.addOperation(op).execute()
      else
        for c in content
          op = new TextInsert c, undefined, left, right
          HB.addOperation(op).execute()
          left = op
      @
    #
    # Inserts a string into the word.
    #
    # @return {WordType} This WordType object.
    #
    insertText: (position, content)->
      # TODO: getOperationByPosition should return "(i-2)th" character
      ith = @getOperationByPosition position # the (i-1)th character. e.g. "abc" a is the 0th character
      left = ith.prev_cl # left is the non-deleted charather to the left of ith
      @insertAfter left, content

    #
    # Deletes a part of the word.
    #
    # @return {WordType} This WordType object
    #
    deleteText: (position, length)->
      o = @getOperationByPosition position

      delete_ops = []
      for i in [0...length]
        if o instanceof types.Delimiter
          break
        d = HB.addOperation(new TextDelete 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()
      @

    #
    # Replace the content of this word with another one. Concurrent replacements are not merged!
    # Only one of the replacements will be used.
    #
    # @return {WordType} Returns the new WordType object.
    #
    replaceText: (text)->
      # Can only be used if the ReplaceManager was set!
      # @see WordType.setReplaceManager
      if @replace_manager?
        word = HB.addOperation(new WordType undefined).execute()
        word.insertText 0, text
        @replace_manager.replace(word)
        word
      else
        throw new Error "This type is currently not maintained by a ReplaceManager!"

    #
    # 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 WordType.val
    # @see WordType.val
    #
    toString: ()->
      @val()

    #
    # @private
    # In most cases you would embed a WordType in a Replaceable, wich is handled by the ReplaceManager in order
    # to provide replace functionality.
    #
    setReplaceManager: (op)->
      @saveOperation 'replace_manager', op
      @validateSavedOperations()
      @on 'insert', (event, ins)=>
        @replace_manager?.forwardEvent @, 'change', ins
      @on 'delete', (event, ins, del)=>
        @replace_manager?.forwardEvent @, 'change', del
    #
    # Bind this WordType to a textfield or input field.
    #
    # @example
    #   var textbox = document.getElementById("textfield");
    #   yatta.bind(textbox);
    #
    bind: (textfield)->
      word = @
      textfield.value = @val()

      @on "insert", (event, op)->
        o_pos = op.getPosition()
        fix = (cursor)->
          if cursor <= o_pos
            cursor
          else
            cursor += 1
            cursor
        left = fix textfield.selectionStart
        right = fix textfield.selectionEnd

        textfield.value = word.val()
        textfield.setSelectionRange left, right


      @on "delete", (event, op)->
        o_pos = op.getPosition()
        fix = (cursor)->
          if cursor < o_pos
            cursor
          else
            cursor -= 1
            cursor
        left = fix textfield.selectionStart
        right = fix textfield.selectionEnd

        textfield.value = word.val()
        textfield.setSelectionRange left, right

      # consume all text-insert changes.
      textfield.onkeypress = (event)->
        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 = String.fromCharCode event.keyCode
        if char.length > 0
          pos = Math.min textfield.selectionStart, textfield.selectionEnd
          diff = Math.abs(textfield.selectionEnd - textfield.selectionStart)
          word.deleteText (pos), diff
          word.insertText pos, char
          new_pos = pos + char.length
          textfield.setSelectionRange new_pos, new_pos
          event.preventDefault()
        else
          event.preventDefault()

      textfield.onpaste = (event)->
        event.preventDefault()
      textfield.oncut = (event)->
        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)->
        pos = Math.min textfield.selectionStart, textfield.selectionEnd
        diff = Math.abs(textfield.selectionEnd - textfield.selectionStart)
        if event.keyCode? and event.keyCode is 8 # Backspace
          if diff > 0
            word.deleteText pos, diff
            textfield.setSelectionRange pos, pos
          else
            if event.ctrlKey? and event.ctrlKey
              val = textfield.value
              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.deleteText new_pos, (pos-new_pos)
              textfield.setSelectionRange new_pos, new_pos
            else
              word.deleteText (pos-1), 1
          event.preventDefault()
        else if event.keyCode? and event.keyCode is 46 # Delete
          if diff > 0
            word.deleteText pos, diff
            textfield.setSelectionRange pos, pos
          else
            word.deleteText pos, 1
            textfield.setSelectionRange pos, pos
          event.preventDefault()



    #
    # @private
    # Encode this operation in such a way that it can be parsed by remote peers.
    #
    _encode: ()->
      json = {
        'type': "WordType"
        'uid' : @getUid()
        'beginning' : @beginning.getUid()
        'end' : @end.getUid()
      }
      if @prev_cl?
        json['prev'] = @prev_cl.getUid()
      if @next_cl?
        json['next'] = @next_cl.getUid()
      if @origin? # and @origin isnt @prev_cl
        json["origin"] = @origin().getUid()
      json

  parser['WordType'] = (json)->
    {
      'uid' : uid
      'beginning' : beginning
      'end' : end
      'prev': prev
      'next': next
      'origin' : origin
    } = json
    new WordType uid, beginning, end, prev, next, origin

  types['TextInsert'] = TextInsert
  types['TextDelete'] = TextDelete
  types['WordType'] = WordType
  structured_types


