/*
 * 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.mathematical.groups.impl;

import java.math.BigInteger;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.scytl.cryptolib.api.exceptions.CryptoLibException;
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.mathematical.groups.GroupElement;
import com.scytl.math.BigIntegers;

/**
 * Class which defines a Zp group element.
 * <P>
 * Instances of this class are immutable.
 */
@JsonRootName("zpGroupElement")
public final class ZpGroupElement extends AbstractJsonSerializable
        implements GroupElement<BigInteger> {

    private final BigInteger _value;

    private final BigInteger _p;

    private final BigInteger _q;

    /**
     * Creates {@code ZpSubgroupElement}. The specified value should be an
     * element of the group.
     *
     * @param value
     *            the value of the element. It must be between {@code [1..p-1]}
     * @param group
     *            the {@link ZpSubgroup} to which this element belongs.
     * @throws GeneralCryptoLibException
     *             if the parameters are not valid.
     */
    public ZpGroupElement(BigInteger value, ZpSubgroup group)
            throws GeneralCryptoLibException {
        this(value, getPFromGroup(group), getQFromGroup(group));
    }

    /**
     * Constructor which allows the creation of a {@code ZpSubgroupElement}
     * without the need of a ZpSubgroup. The received value should be an element
     * of the group defined by p and q. For the exact requirements that must be
     * fulfilled by a group member please see the description of the method
     * {@link ZpSubgroup#isGroupMember(ZpGroupElement)}
     * <P>
     * A group has an associated generator, and a generator is associated with a
     * group, therefore, the question of which to create first (the generator or
     * the group) is not obvious. The intended use of this constructor is to
     * allow the creation of a group element (which can serve as a generator),
     * without the need of first creating a group. This constructor can
     * therefore be called by the constructor of a mathematical group, in order
     * to create it's generator, without first having created a mathematical
     * group.
     *
     * @param value
     *            the value of the element. It must be between {@code [1..p-1]}.
     * @param p
     *            the ElGamal p parameter (modulus).
     * @param q
     *            the ElGamal q parameter (order).
     * @throws GeneralCryptoLibException
     *             if parameters are not valid.
     */
    @JsonCreator
    public ZpGroupElement(@JsonProperty("value") BigInteger value,
            @JsonProperty("p") BigInteger p,
            @JsonProperty("q") BigInteger q)
            throws GeneralCryptoLibException {
        validateInput(value, p, q);
        _value = value;
        _p = p;
        _q = q;
    }

    /**
     * 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 ZpGroupElement fromJson(String json)
            throws GeneralCryptoLibException {
        return fromJson(json, ZpGroupElement.class);
    }

    /**
     * In this implementation of {@link GroupElement}, the multiply operation
     * returns ZpSubgroupElement whose value is {@code (this * element) mod p}.
     *
     * @return {@code (this * element) mod p}.
     * @throws GeneralCryptoLibException
     *             if {@code element} is not valid.
     */
    @Override
    public ZpGroupElement multiply(final GroupElement<BigInteger> element)
            throws GeneralCryptoLibException {
        validateReceivedElementIsFromSameGroup(element);
        BigInteger multiplyResult =
            _value.multiply(element.getValue()).mod(_p);
        return new ZpGroupElement(multiplyResult, _p, _q);
    }

    /**
     * In this implementation of {@link GroupElement}, the exponentiate
     * operation returns ZpSubgroupElement whose value is
     * {@code (this<sup>exponent</sup>) mod p}.
     *
     * @return {@code (this<sup>exponent</sup>) mod p}.
     * @throws GeneralCryptoLibException
     *             if {@code exponent} is not valid.
     */
    @Override
    public ZpGroupElement exponentiate(final Exponent exponent)
            throws GeneralCryptoLibException {
        validateReceivedExponent(exponent);
        BigInteger valueExponentiated =
            BigIntegers.modPow(_value, exponent.getValue(), _p);
        return new ZpGroupElement(valueExponentiated, _p, _q);
    }

    /**
     * In this implementation of {@link GroupElement}, the invert operation
     * returns ZpSubgroupElement whose value is the inverse of this element mod
     * p.
     *
     * @return the inverse of this element mod p.
     */
    @Override
    public ZpGroupElement invert() {
        try {
            return new ZpGroupElement(_value.modInverse(_p), _p, _q);
        } catch (GeneralCryptoLibException e) {
            throw new CryptoLibException(e);
        }
    }

    /**
     * Retrieves the p parameter of the Zp subgroup to which the Zp group
     * element belongs.
     *
     * @return the p parameter of the Zp subgroup.
     */
    @JsonProperty("p")
    public BigInteger getP() {
        return _p;
    }

    /**
     * Retrieves the q parameter of the Zp subgroup to which the Zp group
     * element belongs.
     *
     * @return the q parameter of the Zp subgroup.
     */
    @JsonProperty("q")
    public BigInteger getQ() {
        return _q;
    }

    /**
     * Retrieves the value of the Zp group element.
     *
     * @return the value of the Zp group element.
     */
    @JsonProperty("value")
    public BigInteger getValue() {
        return _value;
    }

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

	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		builder.append("ZpGroupElement [_value=");
		builder.append(_value);
		builder.append(", _p=");
		builder.append(_p);
		builder.append(", _q=");
		builder.append(_q);
		builder.append("]");
		return builder.toString();
	}

    private static BigInteger getPFromGroup(final ZpSubgroup group)
            throws GeneralCryptoLibException {
        Validate.notNull(group, "Zp subgroup");
        return group.getP();
    }

    private static BigInteger getQFromGroup(final ZpSubgroup group)
            throws GeneralCryptoLibException {
        Validate.notNull(group, "Zp subgroup");
        return group.getQ();
    }

    private void validateReceivedElementIsFromSameGroup(
            final GroupElement<BigInteger> groupElement)
            throws GeneralCryptoLibException {

        Validate.notNull(groupElement, "Zp group element");

        ZpGroupElement ZpSubgroupElement;
        if (groupElement instanceof ZpGroupElement) {
            ZpSubgroupElement = (ZpGroupElement) groupElement;
        } else {
            throw new GeneralCryptoLibException(
                "This Zp group element is not a member of a Zp subgroup.");
        }

        if (!_q.equals(ZpSubgroupElement.getQ()) &&
            !_p.equals(ZpSubgroupElement.getP())) {
                throw new GeneralCryptoLibException(
                    "Operations can only be performed on group elements which are members of the same group");
        }
    }

    private void validateReceivedExponent(final Exponent exponent)
            throws GeneralCryptoLibException {

        Validate.notNull(exponent, "Exponent");
        Validate.notNull(exponent.getValue(), "Exponent value");

        if (!_q.equals(exponent.getQ())) {
            throw new GeneralCryptoLibException(
                new IllegalArgumentException(
                    "The exponent should be of the same Zp subgroup order as this Zp group element"));
        }
    }

    private void validateInput(final BigInteger value, final BigInteger p,
            final BigInteger q) throws GeneralCryptoLibException {

        Validate.notNull(value, "Zp group element value");
        Validate.notNull(p, "Zp subgroup p parameter");
        Validate.notNull(q, "Zp subgroup q parameter");
        BigInteger pMinusOne = p.subtract(BigInteger.ONE);
        Validate.inRange(q, BigInteger.ONE, pMinusOne,
            "Zp subgroup q parameter", "",
            "Zp subgroup p parameter minus 1");
        Validate.inRange(value, BigInteger.ONE, pMinusOne,
            "Zp group element value", "",
            "Zp subgroup p parameter minus 1");
    }
    
}
