/**
 * @license
 * Copyright (C) 2017 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) {
  'use strict';

  // Prevent redefinition.
  if (window.GrReviewerUpdatesParser) { return; }

  function GrReviewerUpdatesParser(change) {
    // TODO (viktard): Polyfill Object.assign for IE.
    this.result = Object.assign({}, change);
    this._lastState = {};
  }

  GrReviewerUpdatesParser.parse = function(change) {
    if (!change ||
        !change.messages ||
        !change.reviewer_updates ||
        !change.reviewer_updates.length) {
      return change;
    }
    const parser = new GrReviewerUpdatesParser(change);
    parser._filterRemovedMessages();
    parser._groupUpdates();
    parser._formatUpdates();
    parser._advanceUpdates();
    return parser.result;
  };

  GrReviewerUpdatesParser.MESSAGE_REVIEWERS_THRESHOLD_MILLIS = 500;
  GrReviewerUpdatesParser.REVIEWER_UPDATE_THRESHOLD_MILLIS = 6000;

  GrReviewerUpdatesParser.prototype.result = null;
  GrReviewerUpdatesParser.prototype._batch = null;
  GrReviewerUpdatesParser.prototype._updateItems = null;
  GrReviewerUpdatesParser.prototype._lastState = null;

  /**
   * Removes messages that describe removed reviewers, since reviewer_updates
   * are used.
   */
  GrReviewerUpdatesParser.prototype._filterRemovedMessages = function() {
    this.result.messages = this.result.messages.filter(message => {
      return message.tag !== 'autogenerated:gerrit:deleteReviewer';
    });
  };

  /**
   * Is a part of _groupUpdates(). Creates a new batch of updates.
   * @param {Object} update instance of ReviewerUpdateInfo
   */
  GrReviewerUpdatesParser.prototype._startBatch = function(update) {
    this._updateItems = [];
    return {
      author: update.updated_by,
      date: update.updated,
      type: 'REVIEWER_UPDATE',
    };
  };

  /**
   * Is a part of _groupUpdates(). Validates current batch:
   * - filters out updates that don't change reviewer state.
   * - updates current reviewer state.
   * @param {Object} update instance of ReviewerUpdateInfo
   */
  GrReviewerUpdatesParser.prototype._completeBatch = function(update) {
    const items = [];
    for (const accountId in this._updateItems) {
      if (!this._updateItems.hasOwnProperty(accountId)) continue;
      const updateItem = this._updateItems[accountId];
      if (this._lastState[accountId] !== updateItem.state) {
        this._lastState[accountId] = updateItem.state;
        items.push(updateItem);
      }
    }
    if (items.length) {
      this._batch.updates = items;
    }
  };

  /**
   * Groups reviewer updates. Sequential updates are grouped if:
   * - They were performed within short timeframe (6 seconds)
   * - Made by the same person
   * - Non-change updates are discarded within a group
   * - Groups with no-change updates are discarded (eg CC -> CC)
   */
  GrReviewerUpdatesParser.prototype._groupUpdates = function() {
    const updates = this.result.reviewer_updates;
    const newUpdates = updates.reduce((newUpdates, update) => {
      if (!this._batch) {
        this._batch = this._startBatch(update);
      }
      const updateDate = util.parseDate(update.updated).getTime();
      const batchUpdateDate = util.parseDate(this._batch.date).getTime();
      const reviewerId = update.reviewer._account_id.toString();
      if (updateDate - batchUpdateDate >
          GrReviewerUpdatesParser.REVIEWER_UPDATE_THRESHOLD_MILLIS ||
          update.updated_by._account_id !== this._batch.author._account_id) {
        // Next sequential update should form new group.
        this._completeBatch();
        if (this._batch.updates && this._batch.updates.length) {
          newUpdates.push(this._batch);
        }
        this._batch = this._startBatch(update);
      }
      this._updateItems[reviewerId] = {
        reviewer: update.reviewer,
        state: update.state,
      };
      if (this._lastState[reviewerId]) {
        this._updateItems[reviewerId].prev_state = this._lastState[reviewerId];
      }
      return newUpdates;
    }, []);
    this._completeBatch();
    if (this._batch.updates && this._batch.updates.length) {
      newUpdates.push(this._batch);
    }
    this.result.reviewer_updates = newUpdates;
  };

  /**
   * Generates update message for reviewer state change.
   * @param {string} prev previous reviewer state.
   * @param {string} state current reviewer state.
   * @return {string}
   */
  GrReviewerUpdatesParser.prototype._getUpdateMessage = function(prev, state) {
    if (prev === 'REMOVED' || !prev) {
      return 'added to ' + state + ': ';
    } else if (state === 'REMOVED') {
      if (prev) {
        return 'removed from ' + prev + ': ';
      } else {
        return 'removed : ';
      }
    } else {
      return 'moved from ' + prev + ' to ' + state + ': ';
    }
  };

  /**
   * Groups updates for same category (eg CC->CC) into a hash arrays of
   * reviewers.
   * @param {!Array<!Object>} updates Array of ReviewerUpdateItemInfo.
   * @return {!Object} Hash of arrays of AccountInfo, message as key.
   */
  GrReviewerUpdatesParser.prototype._groupUpdatesByMessage = function(updates) {
    return updates.reduce((result, item) => {
      const message = this._getUpdateMessage(item.prev_state, item.state);
      if (!result[message]) {
        result[message] = [];
      }
      result[message].push(item.reviewer);
      return result;
    }, {});
  };

  /**
   * Generates text messages for grouped reviewer updates.
   * Formats reviewer updates to a (not yet implemented) EventInfo instance.
   * @see https://gerrit-review.googlesource.com/c/94490/
   */
  GrReviewerUpdatesParser.prototype._formatUpdates = function() {
    for (const update of this.result.reviewer_updates) {
      const grouppedReviewers = this._groupUpdatesByMessage(update.updates);
      const newUpdates = [];
      for (const message in grouppedReviewers) {
        if (grouppedReviewers.hasOwnProperty(message)) {
          newUpdates.push({
            message,
            reviewers: grouppedReviewers[message],
          });
        }
      }
      update.updates = newUpdates;
    }
  };

  /**
   * Moves reviewer updates that are within short time frame of change messages
   * back in time so they would come before change messages.
   * TODO(viktard): Remove when server-side serves reviewer updates like so.
   */
  GrReviewerUpdatesParser.prototype._advanceUpdates = function() {
    const updates = this.result.reviewer_updates;
    const messages = this.result.messages;
    messages.forEach((message, index) => {
      const messageDate = util.parseDate(message.date).getTime();
      const nextMessageDate = index === messages.length - 1 ? null :
          util.parseDate(messages[index + 1].date).getTime();
      for (const update of updates) {
        const date = util.parseDate(update.date).getTime();
        if (date >= messageDate
            && (!nextMessageDate || date < nextMessageDate)) {
          const timestamp = util.parseDate(update.date).getTime() -
              GrReviewerUpdatesParser.MESSAGE_REVIEWERS_THRESHOLD_MILLIS;
          update.date = new Date(timestamp)
            .toISOString().replace('T', ' ').replace('Z', '000000');
        }
        if (nextMessageDate && date > nextMessageDate) {
          break;
        }
      }
    });
  };

  window.GrReviewerUpdatesParser = GrReviewerUpdatesParser;
})(window);