/*
 * 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.elgamal.bean;

import static com.scytl.cryptolib.commons.utils.validations.Validate.notNull;
import static com.scytl.cryptolib.commons.utils.validations.Validate.notNullOrEmptyAndNoNulls;
import static java.lang.Math.min;
import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableList;

import java.math.BigInteger;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
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.mathematical.groups.impl.Exponent;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;

/**
 * Encapsulates an ElGamal private key.
 * <P>
 * Instances of this class contain a list of the exponents corresponding to the
 * key and the Zp subgroup to which these exponents belong.
 */
@JsonRootName("privateKey")
public final class ElGamalPrivateKey extends AbstractJsonSerializable {

    private final Exponent[] _exponents;

    private final ZpSubgroup _zpSubgroup;

    /**
     * Constructs an {@link ElGamalPrivateKey} object, using the specified list
     * of exponents and the specified Zp subgroup.
     * <P>
     * Note: For performance reasons, a group membership check is not performed
     * for any exponent in the specified list. Therefore, this membership should
     * be ensured prior to specifying the list as input to this constructor.
     *
     * @param exponents
     *            the list of private key exponents.
     * @param zpSubgroup
     *            the Zp subgroup to which the exponents of this private key
     *            belong.
     * @throws GeneralCryptoLibException
     *             if the list of private key exponents is null, empty or
     *             contains one more null elements, or if the Zp subgroup is
     *             null.
     */
    public ElGamalPrivateKey(List<Exponent> exponents,
            ZpSubgroup zpSubgroup)
            throws GeneralCryptoLibException {
        notNullOrEmptyAndNoNulls(exponents,
            "List of ElGamal private key exponents");
        notNull(zpSubgroup, "Zp subgroup");
        _exponents = exponents.toArray(new Exponent[exponents.size()]);
        _zpSubgroup = zpSubgroup;
    }

    private ElGamalPrivateKey(Exponent[] _exponents,
            ZpSubgroup _zpSubgroup) {
        this._exponents = _exponents;
        this._zpSubgroup = _zpSubgroup;
    }

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

    /**
     * Deserializes the instance from a given byte array.
     * 
     * @param bytes
     *            the bytes
     * @return the instance
     * @throws GeneralCryptoLibException
     *             failed to deserialize the instance.
     */
    public static ElGamalPrivateKey fromBytes(byte[] bytes)
            throws GeneralCryptoLibException {
        notNull(bytes, "bytes");
        return Codec.decodePrivateKey(bytes);
    }

    /**
     * @deprecated
     * <p>
     * Combines given ElGamal private keys. See
     * {@link #combineWith(ElGamalPrivateKey)} for details.
     * <p>
     * This method is deprecated, please, use {@link #multiply(Collection)}
     * instead.
     * 
     * @param keys
     *            the keys
     * @return the combination
     * @throws GeneralCryptoLibException
     *             collection is empty or some key is null or uses a different
     *             group.
     */
    @Deprecated
    public static ElGamalPrivateKey combine(
            Collection<ElGamalPrivateKey> keys)
            throws GeneralCryptoLibException {
        Iterator<ElGamalPrivateKey> iterator = keys.iterator();
        if (!iterator.hasNext()) {
            throw new GeneralCryptoLibException("Keys are missing.");
        }
        ElGamalPrivateKey combination = iterator.next();
        while (iterator.hasNext()) {
            combination = combination.combineWith(iterator.next());
        }
        return combination;
    }

    /**
     * Multiplies given keys. See {@link #multiply(ElGamalPrivateKey)} for
     * details.
     * 
     * @param key1
     *            the first key
     * @param key2
     *            the second key
     * @param others
     *            the other keys
     * @return the "product" key
     * @throws GeneralCryptoLibException
     *             some of the keys is {@code null} or uses a different group.
     */
    public static ElGamalPrivateKey multiply(ElGamalPrivateKey key1,
            ElGamalPrivateKey key2, ElGamalPrivateKey... others)
            throws GeneralCryptoLibException {
        ElGamalPrivateKey product = key1.multiply(key2);
        for (ElGamalPrivateKey other : others) {
            product = product.multiply(other);
        }
        return product;
    }

    /**
     * Multiplies given keys. The specified collection must not be empty. See
     * {@link #multiply(ElGamalPrivateKey)} for details.
     * 
     * @param keys
     *            the keys
     * @return the "product" key
     * @throws GeneralCryptoLibException
     *             some of the keys is {@code null} or uses a different group.
     */
    public static ElGamalPrivateKey multiply(
            Collection<ElGamalPrivateKey> keys)
            throws GeneralCryptoLibException {
        Iterator<ElGamalPrivateKey> iterator = keys.iterator();
        if (!iterator.hasNext()) {
            throw new GeneralCryptoLibException("Keys are missing.");
        }
        ElGamalPrivateKey product = iterator.next();
        while (iterator.hasNext()) {
            product = product.multiply(iterator.next());
        }
        return product;
    }

    /**
     * Creates an instance from a given memento during JSON deserialization.
     * 
     * @param memento
     *            the memento
     * @return
     * @throws GeneralCryptoLibException
     *             failed to create the instance.
     */
    @JsonCreator
    static ElGamalPrivateKey fromMemento(Memento memento)
            throws GeneralCryptoLibException {
        notNullOrEmptyAndNoNulls(memento.exponents,
            "List of ElGamal private key exponents");
        notNull(memento.zpSubgroup, "Zp subgroup");
        BigInteger q = memento.zpSubgroup.getQ();
        Exponent[] exponents = new Exponent[memento.exponents.length];
        for (int i = 0; i < exponents.length; i++) {
            exponents[i] = new Exponent(q, memento.exponents[i]);
        }
        return new ElGamalPrivateKey(exponents, memento.zpSubgroup);
    }

    /**
     * Retrieves the list of private key exponents. The returned list is
     * read-only.
     * 
     * @return the list of exponents.
     */
    public List<Exponent> getKeys() {

        return unmodifiableList(asList(_exponents));
    }

    /**
     * Retrieves the Zp subgroup to which the private key exponents belong.
     * 
     * @return the Zp subgroup.
     */
    public ZpSubgroup getGroup() {
        return _zpSubgroup;
    }

    /**
     * @deprecated
     * <p>
     * Combines this key with a given one.
     * <p>
     * Combination of two ElGamal private keys is another ElGamal private keys
     * where
     * <ul>
     * <li>The number of exponents is the minimum of the number of exponents in
     * the original keys.
     * <li>Each exponent is a sum of the corresponding exponents in the original
     * keys.
     * <p>
     * This method is deprecated,please use
     * {@link #multiply(ElGamalPrivateKey)}.
     * 
     * @param other
     *            the other key
     * @return the combination
     * @throws GeneralCryptoLibException
     *             the other key is null or uses a different group.
     */
    @Deprecated
    public ElGamalPrivateKey combineWith(ElGamalPrivateKey other)
            throws GeneralCryptoLibException {
        return multiply(other);
    }
    
    /**
     * @deprecated
     * <p>
     * Combines this key with given ones. See
     * {@link #combineWith(ElGamalPrivateKey)} for details
     * <p>
     * This method is deprecated, please use {@link #multiply(Collection)}.
     * 
     * @param others
     *            the other keys
     * @return the combination
     * @throws GeneralCryptoLibException
     *             some key is null or uses a different group.
     */
    @Deprecated
    public ElGamalPrivateKey combineWith(
            Collection<ElGamalPrivateKey> others)
            throws GeneralCryptoLibException {
        ElGamalPrivateKey combination = this;
        for (ElGamalPrivateKey other : others) {
            combination = combination.combineWith(other);
        }
        return combination;
    }

    /**
     * @deprecated
     * <p>
     * Combines this key with given ones. See
     * {@link #combineWith(ElGamalPrivateKey)} for details.
     * <p>
     * This method is deprecated, please use
     * {@link #multiply(ElGamalPrivateKey, ElGamalPrivateKey, ElGamalPrivateKey...)}
     * 
     * @param first
     *            the first key to combine with
     * @param others
     *            the other keys to combine with
     * @return the combination
     * @throws GeneralCryptoLibException
     *             some key is null or uses a different group.
     */
    @Deprecated
    public ElGamalPrivateKey combineWith(ElGamalPrivateKey first,
            ElGamalPrivateKey... others)
            throws GeneralCryptoLibException {
        ElGamalPrivateKey combination = combineWith(first);
        for (ElGamalPrivateKey other : others) {
            combination = combination.combineWith(other);
        }
        return combination;
    }

    /**
     * <p>
     * "Multiplies" this key by a given one.
     * <p>
     * ElGamal private keys with the same number of exponents have a group
     * property, namely they are members of {@code Zq^n} group, and
     * multiplication is considered as a group operation.
     * <p>
     * The exponent number of the returned private key is the minimum of
     * exponent number of the keys being multiplied.
     * 
     * @param other
     *            the other
     * @return the "product" key
     * @throws GeneralCryptoLibException
     *             the other key is {@ode null} or uses a different group.
     */
    public ElGamalPrivateKey multiply(ElGamalPrivateKey other)
            throws GeneralCryptoLibException {
        notNull(other, "Other key");
        if (!_zpSubgroup.equals(other._zpSubgroup)) {
            throw new GeneralCryptoLibException(
                "The other key uses a different group.");
        }
        int length = min(_exponents.length, other._exponents.length);
        Exponent[] exponents = new Exponent[length];
        for (int i = 0; i < exponents.length; i++) {
            exponents[i] = _exponents[i].add(other._exponents[i]);
        }
        return new ElGamalPrivateKey(exponents, _zpSubgroup);
    }

    /**
     * <p>
     * "Inverts" this key.
     * <p>
     * ElGamal private keys with the same number of exponents have a group
     * property, namely they are members of {@code Zq^n} group, and inversion is
     * considered as a group operation.
     * 
     * @return the inverted key.
     */
    public ElGamalPrivateKey invert() {
        Exponent[] exponents = new Exponent[_exponents.length];
        for (int i = 0; i < exponents.length; i++) {
            exponents[i] = _exponents[i].negate();
        }
        return new ElGamalPrivateKey(exponents, _zpSubgroup);
    }

    /**
     * <p>
     * "Divides" this key by a given one.
     * <p>
     * ElGamal private keys with the same number of exponents have a group
     * property, namely they are members of {@code Zq^n} group, and division is
     * considered as a group operation.
     * <p>
     * The exponent number of the returned private key is the minimum of
     * exponent number of the keys being multiplied.
     * 
     * @param other
     *            the other
     * @return the "quotient" key
     * @throws GeneralCryptoLibException
     *             the other key is {@ode null} or uses a different group.
     */
    public ElGamalPrivateKey divide(ElGamalPrivateKey other)
            throws GeneralCryptoLibException {
        notNull(other, "Other key");
        return multiply(other.invert());
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + Arrays.hashCode(_exponents);
        result = prime * result
            + ((_zpSubgroup == null) ? 0 : _zpSubgroup.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;
        }
        ElGamalPrivateKey other = (ElGamalPrivateKey) obj;
        if (!Arrays.equals(_exponents, other._exponents)) {
            return false;
        }
        if (_zpSubgroup == null) {
            if (other._zpSubgroup != null) {
                return false;
            }
        } else if (!_zpSubgroup.equals(other._zpSubgroup)) {
            return false;
        }
        return true;
    }

    /**
     * Serializes the instance to a byte array.
     * 
     * @return the bytes.
     */
    public byte[] toBytes() {
        return Codec.encode(this);
    }

    /**
     * Returns a memento used during JSON serialization.
     * 
     * @return a memento.
     */
    @JsonValue
    Memento toMemento() {
        Memento memento = new Memento();
        memento.zpSubgroup = _zpSubgroup;
        memento.exponents = new BigInteger[_exponents.length];
        for (int i = 0; i < _exponents.length; i++) {
            memento.exponents[i] = _exponents[i].getValue();
        }
        return memento;
    }

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

        @JsonProperty("exponents")
        public BigInteger[] exponents;
    }
}
