// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the 'License');
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an 'AS IS' BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
(function(window, GrDiffGroup, GrDiffLine) {
  'use strict';

  function GrDiffBuilder(diff, comments, prefs, outputEl) {
    this._comments = comments;
    this._prefs = prefs;
    this._outputEl = outputEl;
    this._groups = [];

    this._commentLocations = this._getCommentLocations(comments);
    this._processContent(diff.content, this._groups, prefs.context);
  }

  GrDiffBuilder.LESS_THAN_CODE = '<'.charCodeAt(0);
  GrDiffBuilder.GREATER_THAN_CODE = '>'.charCodeAt(0);
  GrDiffBuilder.AMPERSAND_CODE = '&'.charCodeAt(0);
  GrDiffBuilder.SEMICOLON_CODE = ';'.charCodeAt(0);

  GrDiffBuilder.TAB_REGEX = /\t/g;

  GrDiffBuilder.LINE_FEED_HTML =
      '<span class="style-scope gr-new-diff br"></span>';

  GrDiffBuilder.GroupType = {
    ADDED: 'b',
    BOTH: 'ab',
    REMOVED: 'a',
  };

  GrDiffBuilder.Side = {
    LEFT: 'left',
    RIGHT: 'right',
  };

  GrDiffBuilder.prototype.emitDiff = function() {
    for (var i = 0; i < this._groups.length; i++) {
      this.emitGroup(this._groups[i]);
    }
  };

  GrDiffBuilder.prototype.emitGroup = function(group, opt_beforeSection) {
    throw Error('Subclasses must implement emitGroup');
  },

  GrDiffBuilder.prototype._processContent = function(content, groups, context) {
    var WHOLE_FILE = -1;
    context = content.length > 1 ? context : WHOLE_FILE;

    var lineNums = {
      left: 0,
      right: 0,
    };
    content = this._splitCommonGroupsWithComments(content, lineNums);
    for (var i = 0; i < content.length; i++) {
      var group = content[i];
      var lines = [];

      if (group[GrDiffBuilder.GroupType.BOTH] !== undefined) {
        var rows = group[GrDiffBuilder.GroupType.BOTH];
        this._appendCommonLines(rows, lines, lineNums);

        var hiddenRange = [context, rows.length - context];
        if (i === 0) {
          hiddenRange[0] = 0;
        } else if (i === content.length - 1) {
          hiddenRange[1] = rows.length;
        }

        if (context !== WHOLE_FILE && hiddenRange[1] - hiddenRange[0] > 0) {
          this._insertContextGroups(groups, lines, hiddenRange);
        } else {
          groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, lines));
        }
        continue;
      }

      if (group[GrDiffBuilder.GroupType.REMOVED] !== undefined) {
        this._appendRemovedLines(group[GrDiffBuilder.GroupType.REMOVED], lines,
            lineNums);
      }
      if (group[GrDiffBuilder.GroupType.ADDED] !== undefined) {
        this._appendAddedLines(group[GrDiffBuilder.GroupType.ADDED], lines,
            lineNums);
      }
      groups.push(new GrDiffGroup(GrDiffGroup.Type.DELTA, lines));
    }
  };

  GrDiffBuilder.prototype._getCommentLocations = function(comments) {
    var result = {
      left: {},
      right: {},
    };
    for (var side in comments) {
      if (side !== GrDiffBuilder.Side.LEFT &&
          side !== GrDiffBuilder.Side.RIGHT) {
        throw Error('Invalid side: ' + side);
      }
      comments[side].forEach(function(c) {
        result[side][c.line] = true;
      });
    }
    return result;
  };

  GrDiffBuilder.prototype._commentIsAtLineNum = function(side, lineNum) {
    return this._commentLocations[side][lineNum] === true;
  };

  // In order to show comments out of the bounds of the selected context,
  // treat them as separate chunks within the model so that the content (and
  // context surrounding it) renders correctly.
  GrDiffBuilder.prototype._splitCommonGroupsWithComments = function(content,
      lineNums) {
    var result = [];
    var leftLineNum = lineNums.left;
    var rightLineNum = lineNums.right;
    for (var i = 0; i < content.length; i++) {
      if (!content[i].ab) {
        result.push(content[i]);
        if (content[i].a) {
          leftLineNum += content[i].a.length;
        }
        if (content[i].b) {
          rightLineNum += content[i].b.length;
        }
        continue;
      }
      var chunk = content[i].ab;
      var currentChunk = {ab: []};
      for (var j = 0; j < chunk.length; j++) {
        leftLineNum++;
        rightLineNum++;
        if (this._commentIsAtLineNum(GrDiffBuilder.Side.LEFT, leftLineNum) ||
            this._commentIsAtLineNum(GrDiffBuilder.Side.RIGHT, rightLineNum)) {
          if (currentChunk.ab && currentChunk.ab.length > 0) {
            result.push(currentChunk);
            currentChunk = {ab: []};
          }
          result.push({ab: [chunk[j]]});
        } else {
          currentChunk.ab.push(chunk[j]);
        }
      }
      if (currentChunk.ab != null && currentChunk.ab.length > 0) {
        result.push(currentChunk);
      }
    }
    return result;
  };

  GrDiffBuilder.prototype._insertContextGroups = function(groups, lines,
      hiddenRange) {
    var linesBeforeCtx = lines.slice(0, hiddenRange[0]);
    var hiddenLines = lines.slice(hiddenRange[0], hiddenRange[1]);
    var linesAfterCtx = lines.slice(hiddenRange[1]);

    if (linesBeforeCtx.length > 0) {
      groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesBeforeCtx));
    }

    var ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL);
    ctxLine.contextLines = hiddenLines;
    groups.push(new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL,
        [ctxLine]));

    if (linesAfterCtx.length > 0) {
      groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesAfterCtx));
    }
  };

  GrDiffBuilder.prototype._appendCommonLines = function(rows, lines, lineNums) {
    for (var i = 0; i < rows.length; i++) {
      var line = new GrDiffLine(GrDiffLine.Type.BOTH);
      line.text = rows[i];
      line.beforeNumber = ++lineNums.left;
      line.afterNumber = ++lineNums.right;
      lines.push(line);
    }
  };

  GrDiffBuilder.prototype._appendRemovedLines = function(rows, lines,
      lineNums) {
    for (var i = 0; i < rows.length; i++) {
      var line = new GrDiffLine(GrDiffLine.Type.REMOVE);
      line.text = rows[i];
      line.beforeNumber = ++lineNums.left;
      lines.push(line);
    }
  };

  GrDiffBuilder.prototype._appendAddedLines = function(rows, lines, lineNums) {
    for (var i = 0; i < rows.length; i++) {
      var line = new GrDiffLine(GrDiffLine.Type.ADD);
      line.text = rows[i];
      line.afterNumber = ++lineNums.right;
      lines.push(line);
    }
  };

  GrDiffBuilder.prototype._createContextControl = function(section, line) {
    if (!line.contextLines.length) {
      return null;
    }
    var td = this._createElement('td');
    var button = this._createElement('gr-button', 'showContext');
    button.setAttribute('link', true);
    var commonLines = line.contextLines.length;
    var text = 'Show ' + commonLines + ' common line';
    if (commonLines > 1) {
      text += 's';
    }
    text += '...';
    button.textContent = text;
    button.addEventListener('tap', function(e) {
      e.detail = {
        group: new GrDiffGroup(GrDiffGroup.Type.BOTH, line.contextLines),
        section: section,
      };
      // Let it bubble up the DOM tree.
    });
    td.appendChild(button);
    return td;
  };

  GrDiffBuilder.prototype._getCommentsForLine = function(comments, line,
      opt_side) {
    var leftComments = comments[GrDiffBuilder.Side.LEFT].filter(
        function(c) { return c.line === line.beforeNumber; });
    var rightComments = comments[GrDiffBuilder.Side.RIGHT].filter(
        function(c) { return c.line === line.afterNumber; });

    var result;

    switch (opt_side) {
      case GrDiffBuilder.Side.LEFT:
        result = leftComments;
        break;
      case GrDiffBuilder.Side.RIGHT:
        result = rightComments;
        break;
      default:
        result = leftComments.concat(rightComments);
        break;
    }

    return result;
  };

  GrDiffBuilder.prototype._createCommentThread = function(line, opt_side) {
    var comments = this._getCommentsForLine(this._comments, line, opt_side);
    if (!comments || comments.length === 0) {
      return null;
    }
    var threadEl = document.createElement('gr-diff-comment-thread');
    threadEl.comments = comments;
    return threadEl;
  };

  GrDiffBuilder.prototype._createLineEl = function(line, number, type) {
    var td = this._createElement('td', 'lineNum');
    if (line.type === GrDiffLine.Type.BLANK) {
      return td;
    } else if (line.type === GrDiffLine.Type.CONTEXT_CONTROL) {
      td.setAttribute('data-value', '@@');
    } else if (line.type === GrDiffLine.Type.BOTH || line.type == type) {
      td.setAttribute('data-value', number);
    }
    return td;
  };

  GrDiffBuilder.prototype._createTextEl = function(line) {
    var td = this._createElement('td');
    if (line.type !== GrDiffLine.Type.BLANK) {
      td.classList.add('content');
    }
    td.classList.add(line.type);
    var text = line.text;
    var html = util.escapeHTML(text);
    if (text.length > this._prefs.line_length) {
      html = this._addNewlines(text, html);
    }
    html = this._addTabWrappers(html);

    // If the html is equivalent to the text then it didn't get highlighted
    // or escaped. Use textContent which is faster than innerHTML.
    if (html == text) {
      td.textContent = text;
    } else {
      td.innerHTML = html;
    }
    return td;
  };

  // Advance `index` by the appropriate number of characters that would
  // represent one source code character and return that index. For
  // example, for source code '<span>' the escaped html string is
  // '&lt;span&gt;'. Advancing from index 0 on the prior html string would
  // return 4, since &lt; maps to one source code character ('<').
  GrDiffBuilder.prototype._advanceChar = function(html, index) {
    // TODO(andybons): Unicode is all kinds of messed up in JS. Account for it.
    // https://mathiasbynens.be/notes/javascript-unicode

    // Tags don't count as characters
    while (index < html.length &&
           html.charCodeAt(index) == GrDiffBuilder.LESS_THAN_CODE) {
      while (index < html.length &&
             html.charCodeAt(index) != GrDiffBuilder.GREATER_THAN_CODE) {
        index++;
      }
      index++;  // skip the ">" itself
    }
    // An HTML entity (e.g., &lt;) counts as one character.
    if (index < html.length &&
        html.charCodeAt(index) == GrDiffBuilder.AMPERSAND_CODE) {
      while (index < html.length &&
             html.charCodeAt(index) != GrDiffBuilder.SEMICOLON_CODE) {
        index++;
      }
    }
    return index + 1;
  };

  GrDiffBuilder.prototype._addNewlines = function(text, html) {
    var htmlIndex = 0;
    var indices = [];
    var numChars = 0;
    for (var i = 0; i < text.length; i++) {
      if (numChars > 0 && numChars % this._prefs.line_length === 0) {
        indices.push(htmlIndex);
      }
      htmlIndex = this._advanceChar(html, htmlIndex);
      if (text[i] === '\t') {
        numChars += this._prefs.tab_size;
      } else {
        numChars++;
      }
    }
    var result = html;
    // Since the result string is being altered in place, start from the end
    // of the string so that the insertion indices are not affected as the
    // result string changes.
    for (var i = indices.length - 1; i >= 0; i--) {
      result = result.slice(0, indices[i]) + GrDiffBuilder.LINE_FEED_HTML +
          result.slice(indices[i]);
    }
    return result;
  };

  GrDiffBuilder.prototype._addTabWrappers = function(html) {
    var htmlStr = this._getTabWrapper(this._prefs.tab_size,
        this._prefs.show_tabs);
    return html.replace(GrDiffBuilder.TAB_REGEX, htmlStr);
  };

  GrDiffBuilder.prototype._getTabWrapper = function(tabSize, showTabs) {
    // Force this to be a number to prevent arbitrary injection.
    tabSize = +tabSize;
    if (isNaN(tabSize)) {
      throw Error('Invalid tab size from preferences.');
    }

    var str = '<span class="style-scope gr-new-diff tab ';
    if (showTabs) {
      str += 'withIndicator';
    }
    str += '" ';
    // TODO(andybons): CSS tab-size is not supported in IE.
    str += 'style="tab-size:' + tabSize + ';';
    str += 'style="-moz-tab-size:' + tabSize + ';';
    str += '">\t</span>';
    return str;
  };

  GrDiffBuilder.prototype._createElement = function(tagName, className) {
    var el = document.createElement(tagName);
    // When Shady DOM is being used, these classes are added to account for
    // Polymer's polyfill behavior. In order to guarantee sufficient
    // specificity within the CSS rules, these are added to every element.
    // Since the Polymer DOM utility functions (which would do this
    // automatically) are not being used for performance reasons, this is
    // done manually.
    el.classList.add('style-scope', 'gr-new-diff');
    if (!!className) {
      el.classList.add(className);
    }
    return el;
  };

  window.GrDiffBuilder = GrDiffBuilder;
})(window, GrDiffGroup, GrDiffLine);