define(function () {

/**
 * Licensed under the standard MIT license:
 *
 * Copyright 2011 Joseph Gentle.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * See: https://github.com/share/ShareJS/blob/master/LICENSE
 */

/* This contains the textarea binding for ShareJS. This binding is really
 * simple, and a bit slow on big documents (Its O(N). However, it requires no
 * changes to the DOM and no heavy libraries like ace. It works for any kind of
 * text input field.
 *
 * You probably want to use this binding for small fields on forms and such.
 * For code editors or rich text editors or whatever, I recommend something
 * heavier.
 */


/* applyChange creates the edits to convert oldval -> newval.
 *
 * This function should be called every time the text element is changed.
 * Because changes are always localised, the diffing is quite easy. We simply
 * scan in from the start and scan in from the end to isolate the edited range,
 * then delete everything that was removed & add everything that was added.
 * This wouldn't work for complex changes, but this function should be called
 * on keystroke - so the edits will mostly just be single character changes.
 * Sometimes they'll paste text over other text, but even then the diff
 * generated by this algorithm is correct.
 *
 * This algorithm is O(N). I suspect you could speed it up somehow using regular expressions.
 */
var applyChange = function(ctx, oldval, newval) {
  // Strings are immutable and have reference equality. I think this test is O(1), so its worth doing.
  if (oldval === newval) { return; }

  var commonStart = 0;
  while (oldval.charAt(commonStart) === newval.charAt(commonStart)) {
    commonStart++;
  }

  var commonEnd = 0;
  while (oldval.charAt(oldval.length - 1 - commonEnd) === newval.charAt(newval.length - 1 - commonEnd) &&
      commonEnd + commonStart < oldval.length && commonEnd + commonStart < newval.length) {
    commonEnd++;
  }

  if (oldval.length !== commonStart + commonEnd) {
    ctx.remove(commonStart, oldval.length - commonStart - commonEnd);
  }
  if (newval.length !== commonStart + commonEnd) {
    ctx.insert(commonStart, newval.slice(commonStart, newval.length - commonEnd));
  }
};

/**
 * Fix issues with textarea content which is different per-browser.
 */
var cannonicalize = function (content) {

    return content.replace(/\r\n/g, '\n');
};

// Attach a textarea to a document's editing context.
//
// The context is optional, and will be created from the document if its not
// specified.
var attachTextarea = function(elem, ctx, cmElem) {

  // initial state will always fail the !== check in genop.
  var content = {};

  // Replace the content of the text area with newText, and transform the
  // current cursor by the specified function.
  var replaceText = function(newText, transformCursor, transformCursorCM) {
      var newCursor;
      var newSelection;

    if(cmElem) {
      // Fix cursor here?
      var cursorCM = cmElem.getCursor();
      var cursorCMStart = cmElem.getCursor('from');
      var cursorCMEnd = cmElem.getCursor('to');
      if(cursorCMStart !== cursorCMEnd) {
          newSelection = [transformCursorCM(elem.value, cursorCMStart), transformCursorCM(elem.value, cursorCMEnd)];
      }
      else {
          newCursor = transformCursorCM(elem.value, cursorCM);
      }
    }
    
    if (transformCursor && !cmElem) {
        newSelection = [transformCursor(elem.selectionStart), transformCursor(elem.selectionEnd)];
    }

    // Fixate the window's scroll while we set the element's value. Otherwise
    // the browser scrolls to the element.
    var scrollTop = elem.scrollTop;
    elem.value = newText;
    if(cmElem) {
      // Fix cursor here?
      cmElem.setValue(newText);
      if(newCursor) {
        cmElem.setCursor(newCursor);
      }
      else {
        cmElem.setSelection(newSelection[0], newSelection[1]);
      }
    }
    content = elem.value; // Not done on one line so the browser can do newline conversion.
    
    if(!cmElem) {
      if (elem.scrollTop !== scrollTop) { elem.scrollTop = scrollTop; }

      // Setting the selection moves the cursor. We'll just have to let your
      // cursor drift if the element isn't active, though usually users don't
      // care.
      if (newSelection && window.document.activeElement === elem) {
        elem.selectionStart = newSelection[0];
        elem.selectionEnd = newSelection[1];
      }
    }
  };

  //replaceText(ctx.get());


  // *** remote -> local changes

  ctx.onRemove(function(pos, length) {
    var transformCursor = function(cursor) {
      // If the cursor is inside the deleted region, we only want to move back to the start
      // of the region. Hence the Math.min.
      return pos < cursor ? cursor - Math.min(length, cursor - pos) : cursor;
    };
    var transformCursorCM = function(text, cursor) {
      var newCursor = cursor;
      var textLines = text.substr(0, pos).split("\n");
      var removedTextLineNumber = textLines.length-1;
      var removedTextColumnIndex = textLines[textLines.length-1].length;
      var removedLines = text.substr(pos, length).split("\n").length - 1;
      if(cursor.line > (removedTextLineNumber + removedLines)) {
        newCursor.line -= removedLines;
      }
      else if(removedLines > 0 && cursor.line === (removedTextLineNumber+removedLines)) {
        var lastLineCharsRemoved = text.substr(pos, length).split("\n")[removedLines].length;
        if(cursor.ch >= lastLineCharsRemoved) {
          newCursor.line = removedTextLineNumber;
          newCursor.ch = removedTextColumnIndex + cursor.ch - lastLineCharsRemoved;
        }
        else {
          newCursor.line -= removedLines;
          newCursor.ch = removedTextColumnIndex;
        }
      }
      else if(cursor.line === removedTextLineNumber && cursor.ch > removedTextLineNumber) {
        newCursor.ch -= Math.min(length, cursor.ch-removedTextLineNumber);
      }
      return newCursor;
    };
    replaceText(ctx.getUserDoc(), transformCursor, transformCursorCM);
  });

  ctx.onInsert(function(pos, text) {
    var transformCursor = function(cursor) {
      return pos < cursor ? cursor + text.length : cursor;
    };
    var transformCursorCM = function(oldtext, cursor) {
      var newCursor = cursor;
      var textLines = oldtext.substr(0, pos).split("\n");
      var addedTextLineNumber = textLines.length-1;
      var addedTextColumnIndex = textLines[textLines.length-1].length;
      var addedLines = text.split("\n").length - 1;
      if(cursor.line > addedTextLineNumber) {
        newCursor.line += addedLines;
      }
      else if(cursor.line === addedTextLineNumber && cursor.ch > addedTextColumnIndex) {
        newCursor.line += addedLines;
        if(addedLines > 0) {
          newCursor.ch = newCursor.ch - addedTextColumnIndex + text.split("\n")[addedLines].length;
        }
        else {
          newCursor.ch += text.split("\n")[addedLines].length;
        }
      }
      return newCursor;
    };
    replaceText(ctx.getUserDoc(), transformCursor, transformCursorCM);
  });


  // *** local -> remote changes

  // This function generates operations from the changed content in the textarea.
  var genOp = function() {
    // In a timeout so the browser has time to propogate the event's changes to the DOM.
    setTimeout(function() {
      var val = elem.value;
      if (val !== content) {
        applyChange(ctx, ctx.getUserDoc(), cannonicalize(val));
      }
    }, 0);
  };

  var eventNames = ['textInput', 'keydown', 'keyup', 'select', 'cut', 'paste'];
  for (var i = 0; i < eventNames.length; i++) {
    var e = eventNames[i];
    if (elem.addEventListener) {
      elem.addEventListener(e, genOp, false);
    } else {
      elem.attachEvent('on' + e, genOp);
    }
  }
  window.setTimeout(function() {
    if(cmElem) {
      var elem2 =  cmElem;
      elem2.on('change', function() {
        elem2.save();
        genOp();
      });
    }
    else {
    console.log('CM inexistant');
    }
  }, 500);


  ctx.detach = function() {
    for (var i = 0; i < eventNames.length; i++) {
      var e = eventNames[i];
      if (elem.removeEventListener) {
        elem.removeEventListener(e, genOp, false);
      } else {
        elem.detachEvent('on' + e, genOp);
      }
    }
  };

  return ctx;
};

return { attach: attachTextarea };
});
