// 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.
//
////////////////////////////////////////////////////////////////////////////////

goog.module('tink.KeysetHandle');

const Aead = goog.require('tink.Aead');
const InvalidArgumentsException = goog.require('tink.exception.InvalidArgumentsException');
const KeyManager = goog.require('tink.KeyManager');
const KeysetReader = goog.require('tink.KeysetReader');
const KeysetWriter = goog.require('tink.KeysetWriter');
const PbKeyMaterialType = goog.require('proto.google.crypto.tink.KeyData.KeyMaterialType');
const PbKeyStatusType = goog.require('proto.google.crypto.tink.KeyStatusType');
const PbKeyTemplate = goog.require('proto.google.crypto.tink.KeyTemplate');
const PbKeyset = goog.require('proto.google.crypto.tink.Keyset');
const PrimitiveSet = goog.require('tink.PrimitiveSet');
const Random = goog.require('tink.subtle.Random');
const Registry = goog.require('tink.Registry');
const SecurityException = goog.require('tink.exception.SecurityException');
const Util = goog.require('tink.Util');

/**
 * Keyset handle provide abstracted access to Keysets, to limit the exposure of
 * actual protocol buffers that hold sensitive key material.
 *
 * @final
 */
class KeysetHandle {
  /**
   * @param {!PbKeyset} keyset
   */
  constructor(keyset) {
    Util.validateKeyset(keyset);

    /** @const @private {!PbKeyset} */
    this.keyset_ = keyset;
  }

  /**
   * Creates a KeysetHandle from an encrypted keyset obtained via reader, using
   * masterKeyAead to decrypt the keyset.
   *
   * @param {!KeysetReader} reader
   * @param {!Aead} masterKeyAead
   *
   * @return {!Promise<!KeysetHandle>}
   */
  static async read(reader, masterKeyAead) {
    // TODO implement
    throw new SecurityException('KeysetHandle -- read: Not implemented yet.');
  }

  /**
   * Creates a KeysetHandle from a keyset, obtained via reader, which
   * must contain no secret key material.
   *
   * This can be used to load public keysets or envelope encryption keysets.
   * Users that need to load cleartext keysets can use CleartextKeysetHandle.
   *
   * @param {!KeysetReader} reader
   * @return {!KeysetHandle}
   */
  static readNoSecret(reader) {
    if (reader === null) {
      throw new SecurityException('Reader has to be non-null.');
    }
    const keyset = reader.read();
    const keyList = keyset.getKeyList();
    for (let key of keyList) {
      switch (key.getKeyData().getKeyMaterialType()) {
        case PbKeyMaterialType.ASYMMETRIC_PUBLIC:  // fall through
        case PbKeyMaterialType.REMOTE:
          continue;
      }
      throw new SecurityException('Keyset contains secret key material.');
    }
    return new KeysetHandle(keyset);
  }

  /**
   * Returns a new KeysetHandle that contains a single new key generated
   * according to keyTemplate.
   *
   * @param {!PbKeyTemplate} keyTemplate
   *
   * @return {!Promise<!KeysetHandle>}
   */
  static async generateNew(keyTemplate) {
    // TODO(thaidn): move this to a key manager.
    const keyset = await KeysetHandle.generateNewKeyset_(keyTemplate);
    return new KeysetHandle(keyset);
  }

  /**
   * Generates a new Keyset that contains a single new key generated
   * according to keyTemplate.
   *
   * @param {!PbKeyTemplate} keyTemplate
   * @private
   * @return {!Promise<!PbKeyset>}
   */
  static async generateNewKeyset_(keyTemplate) {
    const key = new PbKeyset.Key()
                    .setStatus(PbKeyStatusType.ENABLED)
                    .setOutputPrefixType(keyTemplate.getOutputPrefixType());
    const keyId = KeysetHandle.generateNewKeyId_();
    key.setKeyId(keyId);
    const keyData = await Registry.newKeyData(keyTemplate);
    key.setKeyData(keyData);
    const keyset = new PbKeyset();
    keyset.addKey(key);
    keyset.setPrimaryKeyId(keyId);
    return keyset;
  }

  /**
   * Generates a new random key ID.
   *
   * @private
   * @return {number} The key ID.
   */
  static generateNewKeyId_() {
    const bytes = Random.randBytes(4);
    let value = 0;
    for (let i = 0; i < bytes.length; i++) {
      value += (bytes[i] & 0xFF) << (i * 8);
    }
    // Make sure the key ID is a positive integer smaller than 2^32.
    return Math.abs(value) % 2 ** 32;
  };


  /**
   * Returns a primitive that uses key material from this keyset handle. If
   * opt_customKeyManager is defined then the provided key manager is used to
   * instantiate primitives. Otherwise key manager from Registry is used.
   *
   * @template P
   *
   * @param {!Object} primitiveType
   * @param {?KeyManager.KeyManager<P>=} opt_customKeyManager
   *
   * @return {!Promise<!P>}
   */
  async getPrimitive(primitiveType, opt_customKeyManager) {
    if (!primitiveType) {
      throw new InvalidArgumentsException('primitive type must be non-null');
    }
    const primitiveSet =
        await this.getPrimitiveSet_(primitiveType, opt_customKeyManager);
    return Registry.wrap(primitiveSet);
  }

  /**
   * Creates a set of primitives corresponding to the keys with status Enabled
   * in the given keysetHandle, assuming all the correspoding key managers are
   * present (keys with status different from Enabled are skipped). If provided
   * uses customKeyManager instead of registered key managers for keys supported
   * by the customKeyManager.
   *
   * @template P
   * @private
   *
   * @param {!Object} primitiveType
   * @param {?KeyManager.KeyManager<P>=} opt_customKeyManager
   *
   * @return {!Promise.<!PrimitiveSet.PrimitiveSet<P>>}
   */
  async getPrimitiveSet_(primitiveType, opt_customKeyManager) {
    const primitiveSet = new PrimitiveSet.PrimitiveSet(primitiveType);
    const keys = this.keyset_.getKeyList();
    const keysLength = keys.length;
    for (let i = 0; i < keysLength; i++) {
      const key = keys[i];
      if (key.getStatus() === PbKeyStatusType.ENABLED) {
        const keyData = key.getKeyData();
        if (!keyData) {
          throw new SecurityException('Key data has to be non null.');
        }
        let primitive;
        if (opt_customKeyManager &&
            opt_customKeyManager.getKeyType() === keyData.getTypeUrl()) {
          primitive =
              await opt_customKeyManager.getPrimitive(primitiveType, keyData);
        } else {
          primitive = await Registry.getPrimitive(primitiveType, keyData);
        }
        const entry = primitiveSet.addPrimitive(primitive, key);
        if (key.getKeyId() === this.keyset_.getPrimaryKeyId()) {
          primitiveSet.setPrimary(entry);
        }
      }
    }
    return primitiveSet;
  }


  /**
   * Encrypts the underlying keyset with the provided masterKeyAead wnd writes
   * the resulting encryptedKeyset to the given writer which must be non-null.
   *
   * @param {!KeysetWriter} writer
   * @param {!Aead} masterKeyAead
   *
   */
  async write(writer, masterKeyAead) {
    // TODO implement
    throw new SecurityException('KeysetHandle -- write: Not implemented yet.');
  }

  /**
   * Returns the keyset held by this KeysetHandle.
   *
   * @package
   * @return {!PbKeyset}
   */
  getKeyset() {
    return this.keyset_;
  }
}

goog.exportSymbol('tink.KeysetHandle', KeysetHandle);
exports = KeysetHandle;