text: /* global diff_match_patch */
  var TEXT_DIFF = 2;
  var DEFAULT_MIN_LENGTH = 60;
  var cachedDiffPatch = null;
  
  var getDiffMatchPatch = function() {
    /*jshint camelcase: false */
  
    if (!cachedDiffPatch) {
      var instance;
      if (typeof diff_match_patch !== 'undefined') {
        // already loaded, probably a browser
        instance = typeof diff_match_patch === 'function' ?
          new diff_match_patch() : new diff_match_patch.diff_match_patch();
      } else if (typeof require === 'function') {
        try {
          var dmpModuleName = 'diff_match_patch_uncompressed';
          var dmp = require('../../public/external/' + dmpModuleName);
          instance = new dmp.diff_match_patch();
        } catch (err) {
          instance = null;
        }
      }
      if (!instance) {
        var error = new Error('text diff_match_patch library not found');
        error.diff_match_patch_not_found = true;
        throw error;
      }
      cachedDiffPatch = {
        diff: function(txt1, txt2) {
          return instance.patch_toText(instance.patch_make(txt1, txt2));
        },
        patch: function(txt1, patch) {
          var results = instance.patch_apply(instance.patch_fromText(patch), txt1);
          for (var i = 0; i < results[1].length; i++) {
            if (!results[1][i]) {
              var error = new Error('text patch failed');
              error.textPatchFailed = true;
            }
          }
          return results[0];
        }
      };
    }
    return cachedDiffPatch;
  };
  
  var diffFilter = function textsDiffFilter(context) {
    if (context.leftType !== 'string') {
      return;
    }
    var minLength = (context.options && context.options.textDiff &&
      context.options.textDiff.minLength) || DEFAULT_MIN_LENGTH;
    if (context.left.length < minLength ||
      context.right.length < minLength) {
      context.setResult([context.left, context.right]).exit();
      return;
    }
    // large text, use a text-diff algorithm
    var diff = getDiffMatchPatch().diff;
    context.setResult([diff(context.left, context.right), 0, TEXT_DIFF]).exit();
  };
  diffFilter.filterName = 'texts';
  
  var patchFilter = function textsPatchFilter(context) {
    if (context.nested) {
      return;
    }
    if (context.delta[2] !== TEXT_DIFF) {
      return;
    }
  
    // text-diff, use a text-patch algorithm
    var patch = getDiffMatchPatch().patch;
    context.setResult(patch(context.left, context.delta[0])).exit();
  };
  patchFilter.filterName = 'texts';
  
  var textDeltaReverse = function(delta) {
    var i, l, lines, line, lineTmp, header = null,
      headerRegex = /^@@ +\-(\d+),(\d+) +\+(\d+),(\d+) +@@$/,
      lineHeader, lineAdd, lineRemove;
    lines = delta.split('\n');
    for (i = 0, l = lines.length; i < l; i++) {
      line = lines[i];
      var lineStart = line.slice(0, 1);
      if (lineStart === '@') {
        header = headerRegex.exec(line);
        lineHeader = i;
        lineAdd = null;
        lineRemove = null;
  
        // fix header
        lines[lineHeader] = '@@ -' + header[3] + ',' + header[4] + ' +' + header[1] + ',' + header[2] + ' @@';
      } else if (lineStart === '+') {
        lineAdd = i;
        lines[i] = '-' + lines[i].slice(1);
        if (lines[i - 1].slice(0, 1) === '+') {
          // swap lines to keep default order (-+)
          lineTmp = lines[i];
          lines[i] = lines[i - 1];
          lines[i - 1] = lineTmp;
        }
      } else if (lineStart === '-') {
        lineRemove = i;
        lines[i] = '+' + lines[i].slice(1);
      }
    }
    return lines.join('\n');
  };
  
  var reverseFilter = function textsReverseFilter(context) {
    if (context.nested) {
      return;
    }
    if (context.delta[2] !== TEXT_DIFF) {
      return;
    }
  
    // text-diff, use a text-diff algorithm
    context.setResult([textDeltaReverse(context.delta[0]), 0, TEXT_DIFF]).exit();
  };
  reverseFilter.filterName = 'texts';
  
  exports.diffFilter = diffFilter;
  exports.patchFilter = patchFilter;
  exports.reverseFilter = reverseFilter;
  