/*
 * 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.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
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.MathematicalGroup;
import com.scytl.math.BigIntegers;

/**
 * Given p and Zp representing 'Integers (mod p)' group, a Zp subgroup is a
 * group where the elements are a subset of elements from Zp.
 * <P>
 * The order of the subgroup is q, that is the number of elements. Both Zp and
 * the ZpSubgroup are finite cyclic groups, it means that all elements can be
 * generated exponentiating a special group element called generator.
 * <P>
 * When the p and q are related with the restriction p = 2q + 1 the subgroup is
 * also defined as 'Quadratic Residue'.
 * <P>
 * Instances of this class are immutable.
 */
@JsonRootName("zpSubgroup")
@JsonIgnoreProperties({"generator", "identity" })
public final class ZpSubgroup extends AbstractJsonSerializable
        implements MathematicalGroup<ZpGroupElement> {

    private final BigInteger _p;

    private final BigInteger _q;

    private final BigInteger _g;

    private final ZpGroupElement _generator;

    private final ZpGroupElement _identity;

    /**
     * Constructs a {@code ZpSubgroup} using provided parameters.
     * <P>
     * Note: generator must be a group element different from one. For the exact
     * requirements that must be fulfilled by a group member please see the
     * description of the method
     * {@link ZpSubgroup#isGroupMember(ZpGroupElement)}
     *
     * @param g
     *            The generator value of the subgroup. This value must pertain
     *            to a group element different from one.
     * @param p
     *            The modulus.
     * @param q
     *            The order of the subgroup.
     * @throws GeneralCryptoLibException
     *             if {@code 0<q<p} restriction is not accomplished.
     */
    @JsonCreator
    public ZpSubgroup(@JsonProperty("g") BigInteger g,
            @JsonProperty("p") BigInteger p,
            @JsonProperty("q") BigInteger q)
            throws GeneralCryptoLibException {
        validateInput(p, q, g);
        _p = p;
        _q = q;
        _g = g;
        _generator = new ZpGroupElement(_g, _p, _q);
        _identity = new ZpGroupElement(BigInteger.ONE, this);
    }

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

    /**
     * Checks if an {@code element} is a member of this group. For this
     * implementation of {@link MathematicalGroup}, a given element is a member
     * of this group if:
     * <ul>
     * <li>The given element has an integer value between {@code 1} and
     * {@code p-1}: {@code (0 < element < p)}</li>
     * <li>{@code (this<sup>q</sup> mod p) = 1}</li>
     * </ul>
     */
    @Override
    public boolean isGroupMember(final ZpGroupElement element) {

        // The check that the element has an integer value between 1 and p-1 is
        // done in the element constructor, so it is not necessary to do it here
        // again

        if (element == null) {
            return false;
        }

        if (!element.getP().equals(_p)) {
            return false;
        }

        BigInteger modPow = BigIntegers.modPow(element.getValue(), _q, _p);
        return BigInteger.ONE.equals(modPow);
    }

    @Override
    @JsonProperty("p")
    public BigInteger getP() {
        return _p;
    }

    @Override
    @JsonProperty("q")
    public BigInteger getQ() {
        return _q;
    }

    @Override
    @JsonProperty("g")
    public BigInteger getG() {
        return _g;
    }

    @Override
    public ZpGroupElement getGenerator() {
        return _generator;
    }

    @Override
    public ZpGroupElement getIdentity() {
        return _identity;
    }

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

    private void validateInput(final BigInteger p, final BigInteger q,
            final BigInteger g) throws GeneralCryptoLibException {
        Validate.notNull(p, "Zp subgroup p parameter");
        Validate.notNull(q, "Zp subgroup q parameter");
        Validate.notNull(g, "Zp subgroup generator");
        BigInteger pMinusOne = p.subtract(BigInteger.ONE);
        Validate.inRange(q, BigInteger.ONE, pMinusOne,
            "Zp subgroup q parameter", "",
            "Zp subgroup p parameter minus 1");
        Validate.inRange(g, BigInteger.valueOf(2), pMinusOne,
            "Zp subgroup generator", "",
            "Zp subgroup p parameter minus 1");
    }
}
