text: var Changeset = require('./Changeset');
  var ChangesetUtils = require('./ChangesetUtils');
  var _ = require('./underscore');
  
  var lineMarkerAttribute = 'lmkr';
  
  // If one of these attributes are set to the first character of a
  // line it is considered as a line attribute marker i.e. attributes
  // set on this marker are applied to the whole line.
  // The list attribute is only maintained for compatibility reasons
  var lineAttributes = [lineMarkerAttribute,'list'];
  
  /*
    The Attribute manager builds changesets based on a document
    representation for setting and removing range or line-based attributes.
  
    @param rep the document representation to be used
    @param applyChangesetCallback this callback will be called
      once a changeset has been built.
  
  
    A document representation contains
    - an array `alines` containing 1 attributes string for each line
    - an Attribute pool `apool`
    - a SkipList `lines` containing the text lines of the document.
  */
  
  var AttributeManager = function(rep, applyChangesetCallback)
  {
    this.rep = rep;
    this.applyChangesetCallback = applyChangesetCallback;
    this.author = '';
  
    // If the first char in a line has one of the following attributes
    // it will be considered as a line marker
  };
  
  AttributeManager.lineAttributes = lineAttributes;
  
  AttributeManager.prototype = _(AttributeManager.prototype).extend({
  
    applyChangeset: function(changeset){
      if(!this.applyChangesetCallback) return changeset;
  
      var cs = changeset.toString();
      if (!Changeset.isIdentity(cs))
      {
        this.applyChangesetCallback(cs);
      }
  
      return changeset;
    },
  
    /*
      Sets attributes on a range
      @param start [row, col] tuple pointing to the start of the range
      @param end [row, col] tuple pointing to the end of the range
      @param attribs: an array of attributes
    */
    setAttributesOnRange: function(start, end, attribs)
    {
      // instead of applying the attributes to the whole range at once, we need to apply them
      // line by line, to be able to disregard the "*" used as line marker. For more details,
      // see https://github.com/ether/etherpad-lite/issues/2772
      var allChangesets;
      for(var row = start[0]; row <= end[0]; row++) {
        var rowRange = this._findRowRange(row, start, end);
        var startCol = rowRange[0];
        var endCol   = rowRange[1];
  
        var rowChangeset = this._setAttributesOnRangeByLine(row, startCol, endCol, attribs);
  
        // compose changesets of all rows into a single changeset, as the range might not be continuous
        // due to the presence of line markers on the rows
        if (allChangesets) {
          allChangesets = Changeset.compose(allChangesets.toString(), rowChangeset.toString(), this.rep.apool);
        } else {
          allChangesets = rowChangeset;
        }
      }
  
      return this.applyChangeset(allChangesets);
    },
  
    _findRowRange: function(row, start, end)
    {
      var startCol, endCol;
  
      var startLineOffset = this.rep.lines.offsetOfIndex(row);
      var endLineOffset   = this.rep.lines.offsetOfIndex(row+1);
      var lineLength      = endLineOffset - startLineOffset;
  
      // find column where range on this row starts
      if (row === start[0]) { // are we on the first row of range?
        startCol = start[1];
      } else {
        startCol = this.lineHasMarker(row) ? 1 : 0; // remove "*" used as line marker
      }
  
      // find column where range on this row ends
      if (row === end[0]) { // are we on the last row of range?
        endCol = end[1]; // if so, get the end of range, not end of row
      } else {
        endCol = lineLength - 1; // remove "\n"
      }
  
      return [startCol, endCol];
    },
  
    /*
      Sets attributes on a range, by line
      @param row the row where range is
      @param startCol column where range starts
      @param endCol column where range ends
      @param attribs: an array of attributes
    */
    _setAttributesOnRangeByLine: function(row, startCol, endCol, attribs)
    {
      var builder = Changeset.builder(this.rep.lines.totalWidth());
      ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [row, startCol]);
      ChangesetUtils.buildKeepRange(this.rep, builder, [row, startCol], [row, endCol], attribs, this.rep.apool);
      return builder;
    },
  
    /*
      Returns if the line already has a line marker
      @param lineNum: the number of the line
    */
    lineHasMarker: function(lineNum){
      var that = this;
  
      return _.find(lineAttributes, function(attribute){
        return that.getAttributeOnLine(lineNum, attribute) != '';
      }) !== undefined;
    },
  
    /*
      Gets a specified attribute on a line
      @param lineNum: the number of the line to set the attribute for
      @param attributeKey: the name of the attribute to get, e.g. list
    */
    getAttributeOnLine: function(lineNum, attributeName){
      // get  `attributeName` attribute of first char of line
      var aline = this.rep.alines[lineNum];
      if (aline)
      {
        var opIter = Changeset.opIterator(aline);
        if (opIter.hasNext())
        {
          return Changeset.opAttributeValue(opIter.next(), attributeName, this.rep.apool) || '';
        }
      }
      return '';
    },
  
    /*
      Gets all attributes on a line
      @param lineNum: the number of the line to get the attribute for
    */
    getAttributesOnLine: function(lineNum){
      // get attributes of first char of line
      var aline = this.rep.alines[lineNum];
      var attributes = []
      if (aline)
      {
        var opIter = Changeset.opIterator(aline)
          , op
        if (opIter.hasNext())
        {
          op = opIter.next()
          if(!op.attribs) return []
  
          Changeset.eachAttribNumber(op.attribs, function(n) {
            attributes.push([this.rep.apool.getAttribKey(n), this.rep.apool.getAttribValue(n)])
          }.bind(this))
          return attributes;
        }
      }
      return [];
    },
  
   /*
      Gets a given attribute on a selection
      @param attributeName
      @param prevChar
      returns true or false if an attribute is visible in range
    */
    getAttributeOnSelection: function(attributeName, prevChar){
      var rep = this.rep;
      if (!(rep.selStart && rep.selEnd)) return
      // If we're looking for the caret attribute not the selection
      // has the user already got a selection or is this purely a caret location?
      var isNotSelection = (rep.selStart[0] == rep.selEnd[0] && rep.selEnd[1] === rep.selStart[1]);
      if(isNotSelection){
        if(prevChar){
          // If it's not the start of the line
          if(rep.selStart[1] !== 0){
            rep.selStart[1]--;
          }
        }
      }
  
      var withIt = Changeset.makeAttribsString('+', [
        [attributeName, 'true']
      ], rep.apool);
      var withItRegex = new RegExp(withIt.replace(/\*/g, '\\*') + "(\\*|$)");
      function hasIt(attribs)
      {
        return withItRegex.test(attribs);
      }
  
      return rangeHasAttrib(rep.selStart, rep.selEnd)
  
      function rangeHasAttrib(selStart, selEnd) {
        // if range is collapsed -> no attribs in range
        if(selStart[1] == selEnd[1] && selStart[0] == selEnd[0]) return false
  
        if(selStart[0] != selEnd[0]) { // -> More than one line selected
          var hasAttrib = true
  
          // from selStart to the end of the first line
          hasAttrib = hasAttrib && rangeHasAttrib(selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length])
  
          // for all lines in between
          for(var n=selStart[0]+1; n < selEnd[0]; n++) {
            hasAttrib = hasAttrib && rangeHasAttrib([n, 0], [n, rep.lines.atIndex(n).text.length])
          }
  
          // for the last, potentially partial, line
          hasAttrib = hasAttrib && rangeHasAttrib([selEnd[0], 0], [selEnd[0], selEnd[1]])
  
          return hasAttrib
        }
  
        // Logic tells us we now have a range on a single line
  
        var lineNum = selStart[0]
          , start = selStart[1]
          , end = selEnd[1]
          , hasAttrib = true
  
        // Iterate over attribs on this line
  
        var opIter = Changeset.opIterator(rep.alines[lineNum])
          , indexIntoLine = 0
  
        while (opIter.hasNext()) {
          var op = opIter.next();
          var opStartInLine = indexIntoLine;
          var opEndInLine = opStartInLine + op.chars;
          if (!hasIt(op.attribs)) {
            // does op overlap selection?
            if (!(opEndInLine <= start || opStartInLine >= end)) {
              hasAttrib = false; // since it's overlapping but hasn't got the attrib -> range hasn't got it
              break;
            }
          }
          indexIntoLine = opEndInLine;
        }
  
        return hasAttrib
      }
    },
  
    /*
      Gets all attributes at a position containing line number and column
      @param lineNumber starting with zero
      @param column starting with zero
      returns a list of attributes in the format
      [ ["key","value"], ["key","value"], ...  ]
    */
    getAttributesOnPosition: function(lineNumber, column){
      // get all attributes of the line
      var aline = this.rep.alines[lineNumber];
  
      if (!aline) {
          return [];
      }
      // iterate through all operations of a line
      var opIter = Changeset.opIterator(aline);
  
      // we need to sum up how much characters each operations take until the wanted position
      var currentPointer = 0;
      var attributes = [];
      var currentOperation;
  
      while (opIter.hasNext()) {
        currentOperation = opIter.next();
        currentPointer = currentPointer + currentOperation.chars;
  
        if (currentPointer > column) {
          // we got the operation of the wanted position, now collect all its attributes
          Changeset.eachAttribNumber(currentOperation.attribs, function (n) {
            attributes.push([
              this.rep.apool.getAttribKey(n),
              this.rep.apool.getAttribValue(n)
            ]);
          }.bind(this));
  
          // skip the loop
          return attributes;
        }
      }
      return attributes;
  
    },
  
    /*
      Gets all attributes at caret position
      if the user selected a range, the start of the selection is taken
      returns a list of attributes in the format
      [ ["key","value"], ["key","value"], ...  ]
    */
    getAttributesOnCaret: function(){
      return this.getAttributesOnPosition(this.rep.selStart[0], this.rep.selStart[1]);
    },
  
    /*
      Sets a specified attribute on a line
      @param lineNum: the number of the line to set the attribute for
      @param attributeKey: the name of the attribute to set, e.g. list
      @param attributeValue: an optional parameter to pass to the attribute (e.g. indention level)
  
    */
    setAttributeOnLine: function(lineNum, attributeName, attributeValue){
      var loc = [0,0];
      var builder = Changeset.builder(this.rep.lines.totalWidth());
      var hasMarker = this.lineHasMarker(lineNum);
  
      ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
  
      if(hasMarker){
        ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [
          [attributeName, attributeValue]
        ], this.rep.apool);
      }else{
          // add a line marker
          builder.insert('*', [
            ['author', this.author],
            ['insertorder', 'first'],
            [lineMarkerAttribute, '1'],
            [attributeName, attributeValue]
          ], this.rep.apool);
      }
  
      return this.applyChangeset(builder);
    },
  
   /**
     * Removes a specified attribute on a line
     *  @param lineNum the number of the affected line
     *  @param attributeName the name of the attribute to remove, e.g. list
     *  @param attributeValue if given only attributes with equal value will be removed
     */
   removeAttributeOnLine: function(lineNum, attributeName, attributeValue){
     var builder = Changeset.builder(this.rep.lines.totalWidth());
     var hasMarker = this.lineHasMarker(lineNum);
     var found = false;
  
     var attribs = _(this.getAttributesOnLine(lineNum)).map(function (attrib) {
       if (attrib[0] === attributeName && (!attributeValue || attrib[0] === attributeValue)){
         found = true;
         return [attributeName, ''];
       }else if (attrib[0] === 'author'){
         // update last author to make changes to line attributes on this line
         return [attributeName, this.author];
       }
       return attrib;
     });
  
     if (!found) {
       return;
     }
  
     ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]);
  
     var countAttribsWithMarker = _.chain(attribs).filter(function(a){return !!a[1];})
       .map(function(a){return a[0];}).difference(['author', 'lmkr', 'insertorder', 'start']).size().value();
  
     //if we have marker and any of attributes don't need to have marker. we need delete it
     if(hasMarker && !countAttribsWithMarker){
       ChangesetUtils.buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]);
     }else{
       ChangesetUtils.buildKeepRange(this.rep, builder, [lineNum, 0], [lineNum, 1], attribs, this.rep.apool);
     }
  
     return this.applyChangeset(builder);
   },
  
     /*
       Toggles a line attribute for the specified line number
       If a line attribute with the specified name exists with any value it will be removed
       Otherwise it will be set to the given value
       @param lineNum: the number of the line to toggle the attribute for
       @param attributeKey: the name of the attribute to toggle, e.g. list
       @param attributeValue: the value to pass to the attribute (e.g. indention level)
    */
    toggleAttributeOnLine: function(lineNum, attributeName, attributeValue) {
      return this.getAttributeOnLine(lineNum, attributeName) ?
        this.removeAttributeOnLine(lineNum, attributeName) :
        this.setAttributeOnLine(lineNum, attributeName, attributeValue);
  
    }
  });
  
  module.exports = AttributeManager;
  