/*
 * 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.ZpGroupElement;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;

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

    private final ZpGroupElement[] _elements;

    private final ZpSubgroup _zpSubgroup;

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

    private ElGamalPublicKey(ZpGroupElement[] _elements,
            ZpSubgroup _zpSubgroup) {
        this._elements = _elements;
        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 ElGamalPublicKey fromJson(String json)
            throws GeneralCryptoLibException {
        return fromJson(json, ElGamalPublicKey.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 ElGamalPublicKey fromBytes(byte[] bytes)
            throws GeneralCryptoLibException {
        notNull(bytes, "bytes");
        return Codec.decodePublicKey(bytes);
    }

    /**
     * @deprecated
     * <p>
     * Combines given ElGamal private keys. See
     * {@link #combineWith(ElGamalPublicKey)} 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 ElGamalPublicKey combine(
            Collection<ElGamalPublicKey> keys)
            throws GeneralCryptoLibException {
        Iterator<ElGamalPublicKey> iterator = keys.iterator();
        if (!iterator.hasNext()) {
            throw new GeneralCryptoLibException("Keys are missing.");
        }
        ElGamalPublicKey combination = iterator.next();
        while (iterator.hasNext()) {
            combination = combination.combineWith(iterator.next());
        }
        return combination;
    }

    /**
     * Multiplies given keys. See {@link #multiply(ElGamalPublicKey)} 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 ElGamalPublicKey multiply(ElGamalPublicKey key1,
            ElGamalPublicKey key2, ElGamalPublicKey... others)
            throws GeneralCryptoLibException {
        ElGamalPublicKey product = key1.multiply(key2);
        for (ElGamalPublicKey other : others) {
            product = product.multiply(other);
        }
        return product;
    }

    /**
     * Multiplies given keys. The specified collection must not be empty. See
     * {@link #multiply(ElGamalPublicKey)} 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 ElGamalPublicKey multiply(
            Collection<ElGamalPublicKey> keys)
            throws GeneralCryptoLibException {
        Iterator<ElGamalPublicKey> iterator = keys.iterator();
        if (!iterator.hasNext()) {
            throw new GeneralCryptoLibException("Keys are missing.");
        }
        ElGamalPublicKey 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 ElGamalPublicKey fromMemento(Memento memento)
            throws GeneralCryptoLibException {
        notNullOrEmptyAndNoNulls(memento.elements,
            "List of ElGamal public key elements");
        notNull(memento.zpSubgroup, "Zp subgroup");
        ZpGroupElement[] elements =
            new ZpGroupElement[memento.elements.length];
        for (int i = 0; i < elements.length; i++) {
            elements[i] = new ZpGroupElement(memento.elements[i],
                memento.zpSubgroup);
        }
        return new ElGamalPublicKey(elements, memento.zpSubgroup);
    }

    /**
     * Retrieves the list of public key Zp group elements. The returned list is
     * read-only.
     * 
     * @return the list of Zp group elements.
     */
    public List<ZpGroupElement> getKeys() {
        return unmodifiableList(asList(_elements));
    }

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

    /**
     * @deprecated
     * <p>
     * Combines this key with a given one.
     * <p>
     * Combination of two ElGamal public keys is another ElGamal public keys
     * where
     * <ul>
     * <li>The number of elements is the minimum of the number of elements in
     * the original keys.
     * <li>Each element is a product of the corresponding elements in the
     * original keys.
     * <p>
     * This method is deprecated, please, use
     * {@link #multiply(ElGamalPublicKey)} instead.
     * 
     * @param other
     *            the other key
     * @return the combination
     * @throws GeneralCryptoLibException
     *             the other key is null or uses a different group.
     */
    @Deprecated
    public ElGamalPublicKey combineWith(ElGamalPublicKey other)
            throws GeneralCryptoLibException {
        return multiply(other);
    }

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

    /**
     * @deprecated
     * <p>
     * Combines this key with given ones. See
     * {@link #combineWith(ElGamalPublicKey)} for details
     * <p>
     * This method is deprecated, please, use
     * {@link #multiply(ElGamalPublicKey, ElGamalPublicKey, ElGamalPublicKey...)}
     * instead.
     * 
     * @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 ElGamalPublicKey combineWith(ElGamalPublicKey first,
            ElGamalPublicKey... others)
            throws GeneralCryptoLibException {
        ElGamalPublicKey combination = combineWith(first);
        for (ElGamalPublicKey other : others) {
            combination = combination.combineWith(other);
        }
        return combination;
    }

    /**
     * <p>
     * "Multiplies" this key by a given one.
     * <p>
     * ElGamal public keys with the same number of elements have a group
     * property, namely they are members of {@code Zp^n} subgroup, and
     * multiplication is considered as a group operation.
     * <p>
     * The element number of the returned public key is the minimum of element
     * 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 ElGamalPublicKey multiply(ElGamalPublicKey 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(_elements.length, other._elements.length);
        ZpGroupElement[] elements = new ZpGroupElement[length];
        for (int i = 0; i < elements.length; i++) {
            elements[i] = _elements[i].multiply(other._elements[i]);
        }
        return new ElGamalPublicKey(elements, _zpSubgroup);
    }

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

    /**
     * <p>
     * "Divides" this key by a given one.
     * <p>
     * ElGamal public keys with the same number of elements have a group
     * property, namely they are members of {@code Zp^n} subgroup, and
     * division is considered as a group operation.
     * <p>
     * The element number of the returned public key is the minimum of element
     * 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 ElGamalPublicKey divide(ElGamalPublicKey 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(_elements);
        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;
        }
        ElGamalPublicKey other = (ElGamalPublicKey) obj;
        if (!Arrays.equals(_elements, other._elements)) {
            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.elements = new BigInteger[_elements.length];
        for (int i = 0; i < _elements.length; i++) {
            memento.elements[i] = _elements[i].getValue();
        }
        return memento;
    }

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

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