/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
package com.scytl.cryptolib.digital.envelope.factory;

import static com.scytl.cryptolib.commons.utils.validations.Validate.notNullOrBlank;
import static com.scytl.cryptolib.commons.utils.validations.Validate.notNullOrEmptyAndNoNulls;

import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Base64.Decoder;
import java.util.Base64.Encoder;
import java.util.List;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.annotation.JsonValue;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.commons.utils.serialization.AbstractJsonSerializable;
import com.scytl.cryptolib.commons.utils.validations.Validate;
import com.scytl.cryptolib.digital.envelope.bean.SecretKeyEncryption;
import com.scytl.cryptolib.digital.envelope.cryptoapi.CryptoAPIDigitalEnvelope;
import com.scytl.cryptolib.primitives.primes.utils.PemUtils;

/**
 * Implementation of the interface {@link CryptoAPIDigitalEnvelope}.
 * <p>
 * Instances of this class are immutable.
 */
@JsonRootName("digitalEnvelope")
public final class CryptoDigitalEnvelope extends AbstractJsonSerializable
        implements CryptoAPIDigitalEnvelope {
    private final byte[] _encryptedData;

    private final byte[] _mac;

    private final List<SecretKeyEncryption> _secretKeyEncryptions;

    /**
     * Constructor used by the digital envelope generator
     * {@link CryptoDigitalEnvelopeGenerator} to generate an instance of this
     * class.
     * 
     * @param encryptedData
     *            the symmetrically encrypted data.
     * @param mac
     *            the MAC of the symmetrically encrypted data.
     * @param secretKeyEncryptions
     *            the list of asymmetric encryptions of the secret key
     *            concatenation, each encryption having been created using a
     *            different one of the public keys provided as input to the
     *            digital envelope generator.
     * @throws GeneralCryptoLibException
     *             if there are public key to PEM conversion fails.
     */
    CryptoDigitalEnvelope(byte[] encryptedData, byte[] mac,
            List<SecretKeyEncryption> secretKeyEncryptions)
            throws GeneralCryptoLibException {
        _encryptedData = encryptedData.clone();
        _mac = mac.clone();
        _secretKeyEncryptions = new ArrayList<>(secretKeyEncryptions);
    }

    /**
     * Deserializes the instance from a string in JSON format.
     * 
     * @param json
     *            the JSON
     * @return instance
     * @throws GeneralCryptoLibException
     *             failed to deserialize the instance.
     */
    public static CryptoDigitalEnvelope fromJson(String json)
            throws GeneralCryptoLibException {
        return fromJson(json, CryptoDigitalEnvelope.class);
    }

    /**
     * Creates an instance from a given memento during JSON deserialization.
     * 
     * @param memento
     *            the memento
     * @return
     * @throws GeneralCryptoLibException
     *             failed to create the instance.
     */
    @JsonCreator
    static CryptoDigitalEnvelope fromMemento(Memento memento)
            throws GeneralCryptoLibException {
        notNullOrBlank(memento.encryptedDataBase64,
            "Base64 encoding of symmetrically encrypted data");
        notNullOrBlank(memento.macBase64,
            "Base64 encoding of HMAC of symmetrically encrypted data");
        notNullOrEmptyAndNoNulls(memento.encryptedSecretKeyConcatsBase64,
            "List of Base64 encoded asymmetric encryptions of secret key concatenation");
        notNullOrEmptyAndNoNulls(memento.publicKeysPem,
            "List of public keys used for asymmetric encryptions of secret key concatenation");
        Validate.equals(memento.encryptedSecretKeyConcatsBase64.size(),
            memento.publicKeysPem.size(),
            "Size of the list of Base64 encoded asymmetric encryptions",
            "size of the list of public keys");

        Decoder decoder = Base64.getDecoder();
        byte[] encryptedData = decoder.decode(memento.encryptedDataBase64);
        byte[] mac = decoder.decode(memento.macBase64);
        List<SecretKeyEncryption> secretKeyEncryptions =
            new ArrayList<>(memento.publicKeysPem.size());
        for (int i = 0; i < memento.publicKeysPem.size(); i++) {
            byte[] encryptedSecretKeyConcat = decoder.decode(
                memento.encryptedSecretKeyConcatsBase64.get(i));
            PublicKey publicKey =
                PemUtils.publicKeyFromPem(memento.publicKeysPem.get(i));
            SecretKeyEncryption secretKeyInfo = new SecretKeyEncryption(
                encryptedSecretKeyConcat, publicKey);
            secretKeyEncryptions.add(secretKeyInfo);
        }
        return new CryptoDigitalEnvelope(encryptedData, mac,
            secretKeyEncryptions);
    }

    @Override
    public byte[] getEncryptedData() {
        return _encryptedData.clone();
    }

    @Override
    public byte[] getMac() {
        return _mac.clone();
    }

    @Override
    public List<SecretKeyEncryption> getSecretKeyEncryptions() {
        return new ArrayList<>(_secretKeyEncryptions);
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + Arrays.hashCode(_encryptedData);
        result = prime * result + Arrays.hashCode(_mac);
        result = prime * result + ((_secretKeyEncryptions == null) ? 0
                : _secretKeyEncryptions.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        CryptoDigitalEnvelope other = (CryptoDigitalEnvelope) obj;
        if (!Arrays.equals(_encryptedData, other._encryptedData)) {
            return false;
        }
        if (!Arrays.equals(_mac, other._mac)) {
            return false;
        }
        if (_secretKeyEncryptions == null) {
            if (other._secretKeyEncryptions != null) {
                return false;
            }
        } else if (!_secretKeyEncryptions
            .equals(other._secretKeyEncryptions)) {
            return false;
        }
        return true;
    }

    /**
     * Returns a memento used during JSON serialization.
     * 
     * @return a memento.
     * @throws GeneralCryptoLibException
     *             failed to create memento.
     */
    @JsonValue
    Memento toMemento() throws GeneralCryptoLibException {
        Encoder encoder = Base64.getEncoder();
        Memento memento = new Memento();
        memento.encryptedDataBase64 =
            encoder.encodeToString(_encryptedData);
        memento.macBase64 = encoder.encodeToString(_mac);
        memento.encryptedSecretKeyConcatsBase64 =
            new ArrayList<>(_secretKeyEncryptions.size());
        memento.publicKeysPem =
            new ArrayList<String>(_secretKeyEncryptions.size());
        for (SecretKeyEncryption secretKeyEncryption : _secretKeyEncryptions) {
            byte[] encryptedSecretKeyConcatenation =
                secretKeyEncryption.getEncryptedSecretKeyConcatenation();
            memento.encryptedSecretKeyConcatsBase64.add(encoder
                .encodeToString(encryptedSecretKeyConcatenation));
            PublicKey publicKey = secretKeyEncryption.getPublicKey();
            String publicKeyPem = PemUtils.publicKeyToPem(publicKey)
                .replace("\n", "").replace("\r", "");
            memento.publicKeysPem.add(publicKeyPem);
        }
        return memento;
    }

    /**
     * Memento for JSON serialization.
     */
    static class Memento {
        @JsonProperty("encryptedDataBase64")
        public String encryptedDataBase64;

        @JsonProperty("macBase64")
        public String macBase64;

        @JsonProperty("encryptedSecretKeyConcatsBase64")
        public List<String> encryptedSecretKeyConcatsBase64;

        @JsonProperty("publicKeysPem")
        public List<String> publicKeysPem;
    }
}
