/*
 * 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.products.ov.config.multishare;

import java.math.BigInteger;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;

import com.scytl.math.LagrangePolynomial;
import com.scytl.products.ov.config.shares.exception.ConfigSharesException;
import com.scytl.shares.Share;
import com.scytl.shares.ShareException;

/**
 * This a container class that allows a list of shares to be group together. This container also acts as a share itself
 * (it can be written to, and read from a smartcard using the infrastructure that reads and writes Share objects).
 * <P>
 * Note: this class is based on the Share class from the Shares library.
 * <P>
 * This container was created to handle the situation where multiple secrets must be split and stored (typically on
 * smartcards). The approach that is followed is to perform a split on each of the secrets, and then group one share
 * from each of the sets of outputs (of the split operation) into containers (these containers could be considered
 * 'super-shares', as they contain within them multiple shares).
 */
public final class MultipleSharesContainer extends Share {

    // note: this field has the same meaning as the field with the same name in its parent class. Therefore, it does not
    // refer to the number of shares in this particular container, which can be obtained from the size of the list of
    // shares field in this class.
    private final transient int numberOfParts;

    private final transient int threshold;

    private final transient List<byte[]> shares;

    private static final int MSC_INT_BYTE_LENGTH = 4;

    /**
     * Initialize from a list of shares.
     * 
     * @param shares
     *            the shares to set.
     * @throws ShareException
     */
    public MultipleSharesContainer(final int numberOfParts, final int threshold, List<byte[]> shares)
            throws ShareException {

        super(0, 0, null, null);

        this.numberOfParts = numberOfParts;
        this.threshold = threshold;

        basicInputValidations(shares);
        this.shares = shares;
    }

    /**
     * Initialize from a serialized object.
     * 
     * @param serialized
     *            the serialised object.
     * @throws ShareException
     *             if there is an exception while initializing from the serialized object.
     */
    public MultipleSharesContainer(byte[] serialized) throws ShareException {

        super(0, 0, null, null);

        basicInputValidations(serialized);

        shares = new ArrayList<byte[]>();

        ByteBuffer bb = ByteBuffer.wrap(serialized);
        try {

            numberOfParts = bb.getInt();
            threshold = bb.getInt();

            int numShares = bb.getInt();
            for (int i = 0; i < numShares; i++) {
                byte[] shareBytes = new byte[bb.getInt()];
                bb.get(shareBytes);
                shares.add(shareBytes);
            }

        } catch (BufferUnderflowException bue) {
            throw new ShareException("The byte array is shorter than expected.", bue);
        } finally {
            // clear the entry parameter, and the used byte buffer.
            Arrays.fill(serialized, (byte) 0x00);
            bb.clear();
            bb.put(serialized);
        }
    }

    /**
     * @return the list of shares.
     */
    public List<byte[]> getShares() {
        return shares;
    }

    /**
     * Obtain a serialized representation of the data. The format is of the form:
     * <p>
     * numOfSharesThatTheSecretsWereSplitInto|threshold|numOfSharesInThisContainer|numBytesInShare1|bytesOfShare1|...|numBytesInShareN|bytesOfShareN
     */
    @Override
    public byte[] serialize() {

        ByteBuffer bb = ByteBuffer.allocate(calculateLength());
        bb.putInt(numberOfParts);
        bb.putInt(threshold);
        bb.putInt(shares.size());
        for (int i = 0; i < shares.size(); i++) {
            bb.putInt(shares.get(i).length);
            bb.put(shares.get(i));
        }
        return bb.array();
    }

    /**
     * Check if the given {@link MultipleSharesContainer} is compatible with this.
     * <P>
     * Note: the check that is performed here is very rudimentary, all that is checked is that the number of shares in
     * this container is equal to the number of shares in the other container.
     *
     * @param other
     *            the MultipleSharesContainer to check compatibility with.
     * @return true if the MultipleSharesContainers are compatible, false otherwise.
     * @throws ShareException
     */
    @Override
    public boolean isCompatible(final Share other) {

        if (other == null) {
            return false;
        } else if (!(other instanceof MultipleSharesContainer)) {
            return false;
        }
        MultipleSharesContainer o = (MultipleSharesContainer) other;

        if (shares.size() != o.getShares().size()) {
            return false;
        } else if ((numberOfParts != o.getNumberOfParts()) || (this.threshold != o.getThreshold())) {
            return false;
        }

        return true;
    }

    /**
     * Perform basic health checks on the share. Checks if this container contains at least 1 share.
     *
     * @throws ShareException
     *             if the check does not pass.
     */
    @Override
    public void check() throws ShareException {

        if (shares.isEmpty()) {
            throw new ShareException("Container does not contain any shares.");
        }
    }

    /**
     * Get the number of parts of this set of shares.
     * 
     * @return Returns the numberOfParts.
     */
    @Override
    public int getNumberOfParts() {
        return numberOfParts;
    }

    /**
     * Get the threshold of this set of shares.
     * 
     * @return Returns the threshold.
     */
    @Override
    public int getThreshold() {
        return threshold;
    }

    /**
     * Get the modulus of the shares in this container (all the shares should have the same modulus).
     * 
     * @return the modulus.
     */
    @Override
    public BigInteger getModulus() {

        byte[] share1AsByteArray = this.shares.get(0).clone();

        try {
            return new Share(share1AsByteArray).getModulus();
        } catch (ShareException e) {
			throw new ConfigSharesException(
					"Error trying to obtain the modulus of the shares in this container.", e);
        }
    }

    /**
     * Override of method in parent class, however this method will throw an exception if called.
     */
    @Override
    public LagrangePolynomial.Point getPoint() {

        throw new UnsupportedOperationException(
            "There is no single point associated with a MultipleSharesContainer (because it can include multiple shares, which all have their own points).");
    }

    /**
     * Clean up {@link Share} secret information from memory. Implementations MUST call this method in order to remove
     * any sensitive value from memory, once done with this object. Essentially once the secret is split and saved into
     * shares and once the secret is recovered from shares
     */
    @Override
    public void destroy() {

        for (int i = 0; i < shares.size(); i++) {
            Arrays.fill(shares.get(i), (byte) 0x00);
        }
    }

    private void basicInputValidations(List<byte[]> shares) throws ShareException {

        if (shares == null || shares.isEmpty()) {
            throw new ShareException("The receive list of shares was not an initialized non-empty list");
        }
    }

    private void basicInputValidations(byte[] serialized) throws ShareException {

        if (serialized == null || serialized.length == 0) {
            throw new ShareException("The receive serialized object was not an initialized non-empty array.");
        }
    }

    private int calculateLength() {

        int total = MSC_INT_BYTE_LENGTH + MSC_INT_BYTE_LENGTH + MSC_INT_BYTE_LENGTH;

        for (int i = 0; i < shares.size(); i++) {
            total += MSC_INT_BYTE_LENGTH;
            total += shares.get(i).length;
        }
        return total;
    }
    
    @Override
    public boolean equals(final Object other) {
        return EqualsBuilder.reflectionEquals(this, other, true);
    }

    @Override
    public int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this, true);
    }
}
