// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview A multiple gnubby signer wraps the process of opening a number
 * of gnubbies, signing each challenge in an array of challenges until a
 * success condition is satisfied, and yielding each succeeding gnubby.
 */
'use strict';

/**
 * Creates a new sign handler with an array of gnubby indexes.
 * @param {!GnubbyFactory} factory Used to create and open the gnubbies.
 * @param {Array.<llGnubbyDeviceId>} gnubbyIndexes Which gnubbies to open.
 * @param {boolean} forEnroll Whether this signer is signing for an attempted
 *     enroll operation.
 * @param {function(boolean, (number|undefined))} completedCb Called when this
 *     signer completes sign attempts, i.e. no further results should be
 *     expected.
 * @param {function(number, MultipleSignerResult)} gnubbyFoundCb Called with
 *     each gnubby/challenge that yields a successful result.
 * @param {Countdown=} opt_timer An advisory timer, beyond whose expiration the
 *     signer will not attempt any new operations, assuming the caller is no
 *     longer interested in the outcome.
 * @param {string=} opt_logMsgUrl A URL to post log messages to.
 * @constructor
 */
function MultipleGnubbySigner(factory, gnubbyIndexes, forEnroll, completedCb,
    gnubbyFoundCb, opt_timer, opt_logMsgUrl) {
  /** @private {!GnubbyFactory} */
  this.factory_ = factory;
  /** @private {Array.<llGnubbyDeviceId>} */
  this.gnubbyIndexes_ = gnubbyIndexes;
  /** @private {boolean} */
  this.forEnroll_ = forEnroll;
  /** @private {function(boolean, (number|undefined))} */
  this.completedCb_ = completedCb;
  /** @private {function(number, MultipleSignerResult)} */
  this.gnubbyFoundCb_ = gnubbyFoundCb;
  /** @private {Countdown|undefined} */
  this.timer_ = opt_timer;
  /** @private {string|undefined} */
  this.logMsgUrl_ = opt_logMsgUrl;

  /** @private {Array.<SignHelperChallenge>} */
  this.challenges_ = [];
  /** @private {boolean} */
  this.challengesFinal_ = false;

  // Create a signer for each gnubby.
  /** @private {boolean} */
  this.anySucceeded_ = false;
  /** @private {number} */
  this.numComplete_ = 0;
  /** @private {Array.<SingleGnubbySigner>} */
  this.signers_ = [];
  /** @private {Array.<boolean>} */
  this.stillGoing_ = [];
  /** @private {Array.<number>} */
  this.errorStatus_ = [];
  for (var i = 0; i < gnubbyIndexes.length; i++) {
    this.addGnubby(gnubbyIndexes[i]);
  }
}

/**
 * Attempts to open this signer's gnubbies, if they're not already open.
 * (This is implicitly done by addChallenges.)
 */
MultipleGnubbySigner.prototype.open = function() {
  for (var i = 0; i < this.signers_.length; i++) {
    this.signers_[i].open();
  }
};

/**
 * Closes this signer's gnubbies, if any are open.
 */
MultipleGnubbySigner.prototype.close = function() {
  for (var i = 0; i < this.signers_.length; i++) {
    this.signers_[i].close();
  }
};

/**
 * Adds challenges to the set of challenges being tried by this signer.
 * The challenges are an array of challenge objects, where each challenge
 * object's values are base64-encoded.
 * If the signer is currently idle, begins signing the new challenges.
 *
 * @param {Array} challenges Encoded challenges
 * @param {boolean} finalChallenges True iff there are no more challenges to add
 * @return {boolean} whether the challenges were successfully added.
 */
MultipleGnubbySigner.prototype.addEncodedChallenges =
    function(challenges, finalChallenges) {
  var decodedChallenges = [];
  if (challenges) {
    for (var i = 0; i < challenges.length; i++) {
      var decodedChallenge = {};
      var challenge = challenges[i];
      decodedChallenge['challengeHash'] =
          B64_decode(challenge['challengeHash']);
      decodedChallenge['appIdHash'] = B64_decode(challenge['appIdHash']);
      decodedChallenge['keyHandle'] = B64_decode(challenge['keyHandle']);
      if (challenge['version']) {
        decodedChallenge['version'] = challenge['version'];
      }
      decodedChallenges.push(decodedChallenge);
    }
  }
  return this.addChallenges(decodedChallenges, finalChallenges);
};

/**
 * Adds challenges to the set of challenges being tried by this signer.
 * If the signer is currently idle, begins signing the new challenges.
 *
 * @param {Array.<SignHelperChallenge>} challenges Challenges to add
 * @param {boolean} finalChallenges True iff there are no more challnges to add
 * @return {boolean} whether the challenges were successfully added.
 */
MultipleGnubbySigner.prototype.addChallenges =
    function(challenges, finalChallenges) {
  if (this.challengesFinal_) {
    // Can't add new challenges once they're finalized.
    return false;
  }

  if (challenges) {
    for (var i = 0; i < challenges.length; i++) {
      this.challenges_.push(challenges[i]);
    }
  }
  this.challengesFinal_ = finalChallenges;

  for (var i = 0; i < this.signers_.length; i++) {
    this.stillGoing_[i] =
        this.signers_[i].addChallenges(challenges, finalChallenges);
    this.errorStatus_[i] = 0;
  }
  return true;
};

/**
 * Adds a new gnubby to this signer's list of gnubbies. (Only possible while
 * this signer is still signing: without this restriction, the morePossible
 * indication in the callbacks could become violated.) If this signer has
 * challenges to sign, begins signing on the new gnubby with them.
 * @param {llGnubbyDeviceId} gnubbyIndex The index of the gnubby to add.
 * @return {boolean} Whether the gnubby was added successfully.
 */
MultipleGnubbySigner.prototype.addGnubby = function(gnubbyIndex) {
  if (this.numComplete_ && this.numComplete_ == this.signers_.length)
    return false;

  var index = this.signers_.length;
  this.signers_.push(
      new SingleGnubbySigner(
          this.factory_,
          gnubbyIndex,
          this.forEnroll_,
          this.signFailedCallback_.bind(this, index),
          this.signSucceededCallback_.bind(this, index),
          this.timer_ ? this.timer_.clone() : null,
          this.logMsgUrl_));
  this.stillGoing_.push(false);

  if (this.challenges_.length) {
    this.stillGoing_[index] =
        this.signers_[index].addChallenges(this.challenges_,
            this.challengesFinal_);
  }
  return true;
};

/**
 * Called by a SingleGnubbySigner upon failure, i.e. unsuccessful completion of
 * all its sign operations.
 * @param {number} index the index of the gnubby whose result this is
 * @param {number} code the result code of the sign operation
 * @private
 */
MultipleGnubbySigner.prototype.signFailedCallback_ = function(index, code) {
  console.log(
      UTIL_fmt('failure. gnubby ' + index + ' got code ' + code.toString(16)));
  if (!this.stillGoing_[index]) {
    console.log(UTIL_fmt('gnubby ' + index + ' no longer running!'));
    // Shouldn't ever happen? Disregard.
    return;
  }
  this.stillGoing_[index] = false;
  this.errorStatus_[index] = code;
  this.numComplete_++;
  var morePossible = this.numComplete_ < this.signers_.length;
  if (!morePossible)
    this.notifyComplete_();
};

/**
 * Called by a SingleGnubbySigner upon success.
 * @param {number} index the index of the gnubby whose result this is
 * @param {usbGnubby} gnubby the underlying gnubby that succeded.
 * @param {number} code the result code of the sign operation
 * @param {SingleSignerResult=} signResult Result object
 * @private
 */
MultipleGnubbySigner.prototype.signSucceededCallback_ =
    function(index, gnubby, code, signResult) {
  console.log(UTIL_fmt('success! gnubby ' + index + ' got code ' +
      code.toString(16)));
  if (!this.stillGoing_[index]) {
    console.log(UTIL_fmt('gnubby ' + index + ' no longer running!'));
    // Shouldn't ever happen? Disregard.
    return;
  }
  this.anySucceeded_ = true;
  this.stillGoing_[index] = false;
  this.notifySuccess_(code, gnubby, index, signResult);
  this.numComplete_++;
  var morePossible = this.numComplete_ < this.signers_.length;
  if (!morePossible)
    this.notifyComplete_();
};

/**
 * @private
 */
MultipleGnubbySigner.prototype.notifyComplete_ = function() {
  // See if any of the signers failed with a strange error. If so, report a
  // single error to the caller, partly as a diagnostic aid and partly to
  // distinguish real failures from wrong data.
  var funnyBusiness;
  for (var i = 0; i < this.errorStatus_.length; i++) {
    if (this.errorStatus_[i] &&
        this.errorStatus_[i] != DeviceStatusCodes.WRONG_DATA_STATUS &&
        this.errorStatus_[i] != DeviceStatusCodes.WAIT_TOUCH_STATUS) {
      funnyBusiness = this.errorStatus_[i];
      break;
    }
  }
  if (funnyBusiness) {
    console.warn(UTIL_fmt('all done (success: ' + this.anySucceeded_ + ', ' +
        'funny error = ' + funnyBusiness + ')'));
  } else {
    console.log(UTIL_fmt('all done (success: ' + this.anySucceeded_ + ')'));
  }
  this.completedCb_(this.anySucceeded_, funnyBusiness);
};

/**
 * @param {number} code Success status code
 * @param {usbGnubby} gnubby The gnubby that succeeded
 * @param {number} gnubbyIndex The gnubby's index
 * @param {SingleSignerResult=} singleSignerResult Result object
 * @private
 */
MultipleGnubbySigner.prototype.notifySuccess_ =
    function(code, gnubby, gnubbyIndex, singleSignerResult) {
  console.log(UTIL_fmt('success (' + code.toString(16) + ')'));
  var signResult = {
    'gnubby': gnubby,
    'gnubbyIndex': gnubbyIndex
  };
  if (singleSignerResult && singleSignerResult['challenge'])
    signResult['challenge'] = singleSignerResult['challenge'];
  if (singleSignerResult && singleSignerResult['info'])
    signResult['info'] = singleSignerResult['info'];
  this.gnubbyFoundCb_(code, signResult);
};