// 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() {
  'use strict';

  var WHOLE_FILE = -1;

  var DiffSide = {
    LEFT: 'left',
    RIGHT: 'right',
  };

  var DiffGroupType = {
    ADDED: 'b',
    BOTH: 'ab',
    REMOVED: 'a',
  };

  var DiffHighlights = {
    ADDED: 'edit_b',
    REMOVED: 'edit_a',
  };

  Polymer({
    is: 'gr-diff-processor',

    properties: {

      /**
       * The amount of context around collapsed groups.
       */
      context: Number,

      /**
       * The array of groups output by the processor.
       */
      groups: {
        type: Array,
        notify: true,
      },

      /**
       * Locations that should not be collapsed, including the locations of
       * comments.
       */
      keyLocations: {
        type: Object,
        value: function() { return {left: {}, right: {}}; },
      },

      _content: Object,
    },

    process: function(content) {
      return new Promise(function(resolve) {
        var groups = [];
        this._processContent(content, groups, this.context);
        this.groups = groups;
        resolve(groups);
      }.bind(this));
    },

    _processContent: function(content, groups, context) {
      this._appendFileComments(groups);

      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[DiffGroupType.BOTH] !== undefined) {
          var rows = group[DiffGroupType.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[DiffGroupType.REMOVED] !== undefined) {
          var highlights = undefined;
          if (group[DiffHighlights.REMOVED] !== undefined) {
            highlights = this._normalizeIntralineHighlights(
                group[DiffGroupType.REMOVED],
                group[DiffHighlights.REMOVED]);
          }
          this._appendRemovedLines(group[DiffGroupType.REMOVED], lines,
              lineNums, highlights);
        }

        if (group[DiffGroupType.ADDED] !== undefined) {
          var highlights = undefined;
          if (group[DiffHighlights.ADDED] !== undefined) {
            highlights = this._normalizeIntralineHighlights(
              group[DiffGroupType.ADDED],
              group[DiffHighlights.ADDED]);
          }
          this._appendAddedLines(group[DiffGroupType.ADDED], lines,
              lineNums, highlights);
        }
        groups.push(new GrDiffGroup(GrDiffGroup.Type.DELTA, lines));
      }
    },

    _appendFileComments: function(groups) {
      var line = new GrDiffLine(GrDiffLine.Type.BOTH);
      line.beforeNumber = GrDiffLine.FILE;
      line.afterNumber = GrDiffLine.FILE;
      groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, [line]));
    },

    /**
     * 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.
     */
    _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.keyLocations[DiffSide.LEFT][leftLineNum] ||
              this.keyLocations[DiffSide.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]);
          }
        }
        // != instead of !== because we want to cover both undefined and null.
        if (currentChunk.ab != null && currentChunk.ab.length > 0) {
          result.push(currentChunk);
        }
      }
      return result;
    },

    _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);
      }
    },

    _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.contextGroup =
          new GrDiffGroup(GrDiffGroup.Type.BOTH, hiddenLines);
      groups.push(new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL,
          [ctxLine]));

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

    /**
     * The `highlights` array consists of a list of <skip length, mark length>
     * pairs, where the skip length is the number of characters between the
     * end of the previous edit and the start of this edit, and the mark
     * length is the number of edited characters following the skip. The start
     * of the edits is from the beginning of the related diff content lines.
     *
     * Note that the implied newline character at the end of each line is
     * included in the length calculation, and thus it is possible for the
     * edits to span newlines.
     *
     * A line highlight object consists of three fields:
     * - contentIndex: The index of the diffChunk `content` field (the line
     *   being referred to).
     * - startIndex: Where the highlight should begin.
     * - endIndex: (optional) Where the highlight should end. If omitted, the
     *   highlight is meant to be a continuation onto the next line.
     */
    _normalizeIntralineHighlights: function(content, highlights) {
      var contentIndex = 0;
      var idx = 0;
      var normalized = [];
      for (var i = 0; i < highlights.length; i++) {
        var line = content[contentIndex] + '\n';
        var hl = highlights[i];
        var j = 0;
        while (j < hl[0]) {
          if (idx === line.length) {
            idx = 0;
            line = content[++contentIndex] + '\n';
            continue;
          }
          idx++;
          j++;
        }
        var lineHighlight = {
          contentIndex: contentIndex,
          startIndex: idx,
        };

        j = 0;
        while (line && j < hl[1]) {
          if (idx === line.length) {
            idx = 0;
            line = content[++contentIndex] + '\n';
            normalized.push(lineHighlight);
            lineHighlight = {
              contentIndex: contentIndex,
              startIndex: idx,
            };
            continue;
          }
          idx++;
          j++;
        }
        lineHighlight.endIndex = idx;
        normalized.push(lineHighlight);
      }
      return normalized;
    },

    _appendRemovedLines: function(rows, lines, lineNums, opt_highlights) {
      for (var i = 0; i < rows.length; i++) {
        var line = new GrDiffLine(GrDiffLine.Type.REMOVE);
        line.text = rows[i];
        line.beforeNumber = ++lineNums.left;
        if (opt_highlights) {
          line.highlights = opt_highlights.filter(function(hl) {
            return hl.contentIndex === i;
          });
        }
        lines.push(line);
      }
    },

    _appendAddedLines: function(rows, lines, lineNums, opt_highlights) {
      for (var i = 0; i < rows.length; i++) {
        var line = new GrDiffLine(GrDiffLine.Type.ADD);
        line.text = rows[i];
        line.afterNumber = ++lineNums.right;
        if (opt_highlights) {
          line.highlights = opt_highlights.filter(function(hl) {
            return hl.contentIndex === i;
          });
        }
        lines.push(line);
      }
    },
  });
})();