package org.bouncycastle.pqc.legacy.math.ntru.polynomial;

import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;

import org.bouncycastle.pqc.legacy.math.ntru.euclid.BigIntEuclidean;
import org.bouncycastle.pqc.legacy.math.ntru.util.ArrayEncoder;
import org.bouncycastle.pqc.legacy.math.ntru.util.Util;
import org.bouncycastle.util.Arrays;

/**
 * A polynomial with <code>int</code> coefficients.<br>
 * Some methods (like <code>add</code>) change the polynomial, others (like <code>mult</code>) do
 * not but return the result as a new polynomial.
 */
public class IntegerPolynomial
    implements Polynomial
{
    private static final int NUM_EQUAL_RESULTANTS = 3;
    /**
     * Prime numbers &gt; 4500 for resultant computation. Starting them below ~4400 causes incorrect results occasionally.
     * Fortunately, 4500 is about the optimum number for performance.<br/>
     * This array contains enough prime numbers so primes never have to be computed on-line for any standard {@link org.bouncycastle.pqc.legacy.crypto.ntru.NTRUSigningParameters}.
     */
    private static final int[] PRIMES = new int[]{
        4507, 4513, 4517, 4519, 4523, 4547, 4549, 4561, 4567, 4583,
        4591, 4597, 4603, 4621, 4637, 4639, 4643, 4649, 4651, 4657,
        4663, 4673, 4679, 4691, 4703, 4721, 4723, 4729, 4733, 4751,
        4759, 4783, 4787, 4789, 4793, 4799, 4801, 4813, 4817, 4831,
        4861, 4871, 4877, 4889, 4903, 4909, 4919, 4931, 4933, 4937,
        4943, 4951, 4957, 4967, 4969, 4973, 4987, 4993, 4999, 5003,
        5009, 5011, 5021, 5023, 5039, 5051, 5059, 5077, 5081, 5087,
        5099, 5101, 5107, 5113, 5119, 5147, 5153, 5167, 5171, 5179,
        5189, 5197, 5209, 5227, 5231, 5233, 5237, 5261, 5273, 5279,
        5281, 5297, 5303, 5309, 5323, 5333, 5347, 5351, 5381, 5387,
        5393, 5399, 5407, 5413, 5417, 5419, 5431, 5437, 5441, 5443,
        5449, 5471, 5477, 5479, 5483, 5501, 5503, 5507, 5519, 5521,
        5527, 5531, 5557, 5563, 5569, 5573, 5581, 5591, 5623, 5639,
        5641, 5647, 5651, 5653, 5657, 5659, 5669, 5683, 5689, 5693,
        5701, 5711, 5717, 5737, 5741, 5743, 5749, 5779, 5783, 5791,
        5801, 5807, 5813, 5821, 5827, 5839, 5843, 5849, 5851, 5857,
        5861, 5867, 5869, 5879, 5881, 5897, 5903, 5923, 5927, 5939,
        5953, 5981, 5987, 6007, 6011, 6029, 6037, 6043, 6047, 6053,
        6067, 6073, 6079, 6089, 6091, 6101, 6113, 6121, 6131, 6133,
        6143, 6151, 6163, 6173, 6197, 6199, 6203, 6211, 6217, 6221,
        6229, 6247, 6257, 6263, 6269, 6271, 6277, 6287, 6299, 6301,
        6311, 6317, 6323, 6329, 6337, 6343, 6353, 6359, 6361, 6367,
        6373, 6379, 6389, 6397, 6421, 6427, 6449, 6451, 6469, 6473,
        6481, 6491, 6521, 6529, 6547, 6551, 6553, 6563, 6569, 6571,
        6577, 6581, 6599, 6607, 6619, 6637, 6653, 6659, 6661, 6673,
        6679, 6689, 6691, 6701, 6703, 6709, 6719, 6733, 6737, 6761,
        6763, 6779, 6781, 6791, 6793, 6803, 6823, 6827, 6829, 6833,
        6841, 6857, 6863, 6869, 6871, 6883, 6899, 6907, 6911, 6917,
        6947, 6949, 6959, 6961, 6967, 6971, 6977, 6983, 6991, 6997,
        7001, 7013, 7019, 7027, 7039, 7043, 7057, 7069, 7079, 7103,
        7109, 7121, 7127, 7129, 7151, 7159, 7177, 7187, 7193, 7207,
        7211, 7213, 7219, 7229, 7237, 7243, 7247, 7253, 7283, 7297,
        7307, 7309, 7321, 7331, 7333, 7349, 7351, 7369, 7393, 7411,
        7417, 7433, 7451, 7457, 7459, 7477, 7481, 7487, 7489, 7499,
        7507, 7517, 7523, 7529, 7537, 7541, 7547, 7549, 7559, 7561,
        7573, 7577, 7583, 7589, 7591, 7603, 7607, 7621, 7639, 7643,
        7649, 7669, 7673, 7681, 7687, 7691, 7699, 7703, 7717, 7723,
        7727, 7741, 7753, 7757, 7759, 7789, 7793, 7817, 7823, 7829,
        7841, 7853, 7867, 7873, 7877, 7879, 7883, 7901, 7907, 7919,
        7927, 7933, 7937, 7949, 7951, 7963, 7993, 8009, 8011, 8017,
        8039, 8053, 8059, 8069, 8081, 8087, 8089, 8093, 8101, 8111,
        8117, 8123, 8147, 8161, 8167, 8171, 8179, 8191, 8209, 8219,
        8221, 8231, 8233, 8237, 8243, 8263, 8269, 8273, 8287, 8291,
        8293, 8297, 8311, 8317, 8329, 8353, 8363, 8369, 8377, 8387,
        8389, 8419, 8423, 8429, 8431, 8443, 8447, 8461, 8467, 8501,
        8513, 8521, 8527, 8537, 8539, 8543, 8563, 8573, 8581, 8597,
        8599, 8609, 8623, 8627, 8629, 8641, 8647, 8663, 8669, 8677,
        8681, 8689, 8693, 8699, 8707, 8713, 8719, 8731, 8737, 8741,
        8747, 8753, 8761, 8779, 8783, 8803, 8807, 8819, 8821, 8831,
        8837, 8839, 8849, 8861, 8863, 8867, 8887, 8893, 8923, 8929,
        8933, 8941, 8951, 8963, 8969, 8971, 8999, 9001, 9007, 9011,
        9013, 9029, 9041, 9043, 9049, 9059, 9067, 9091, 9103, 9109,
        9127, 9133, 9137, 9151, 9157, 9161, 9173, 9181, 9187, 9199,
        9203, 9209, 9221, 9227, 9239, 9241, 9257, 9277, 9281, 9283,
        9293, 9311, 9319, 9323, 9337, 9341, 9343, 9349, 9371, 9377,
        9391, 9397, 9403, 9413, 9419, 9421, 9431, 9433, 9437, 9439,
        9461, 9463, 9467, 9473, 9479, 9491, 9497, 9511, 9521, 9533,
        9539, 9547, 9551, 9587, 9601, 9613, 9619, 9623, 9629, 9631,
        9643, 9649, 9661, 9677, 9679, 9689, 9697, 9719, 9721, 9733,
        9739, 9743, 9749, 9767, 9769, 9781, 9787, 9791, 9803, 9811,
        9817, 9829, 9833, 9839, 9851, 9857, 9859, 9871, 9883, 9887,
        9901, 9907, 9923, 9929, 9931, 9941, 9949, 9967, 9973};
    private static final List BIGINT_PRIMES;

    static
    {
        BIGINT_PRIMES = new ArrayList();
        for (int i = 0; i != PRIMES.length; i++)
        {
            BIGINT_PRIMES.add(BigInteger.valueOf(PRIMES[i]));
        }
    }

    public int[] coeffs;

    /**
     * Constructs a new polynomial with <code>N</code> coefficients initialized to 0.
     *
     * @param N the number of coefficients
     */
    public IntegerPolynomial(int N)
    {
        coeffs = new int[N];
    }

    /**
     * Constructs a new polynomial with a given set of coefficients.
     *
     * @param coeffs the coefficients
     */
    public IntegerPolynomial(int[] coeffs)
    {
        this.coeffs = coeffs;
    }

    /**
     * Constructs a <code>IntegerPolynomial</code> from a <code>BigIntPolynomial</code>. The two polynomials are independent of each other.
     *
     * @param p the original polynomial
     */
    public IntegerPolynomial(BigIntPolynomial p)
    {
        coeffs = new int[p.coeffs.length];
        for (int i = 0; i < p.coeffs.length; i++)
        {
            coeffs[i] = p.coeffs[i].intValue();
        }
    }

    /**
     * Decodes a byte array to a polynomial with <code>N</code> ternary coefficients<br>
     * Ignores any excess bytes.
     *
     * @param data an encoded ternary polynomial
     * @param N    number of coefficients
     * @return the decoded polynomial
     */
    public static IntegerPolynomial fromBinary3Sves(byte[] data, int N)
    {
        return new IntegerPolynomial(ArrayEncoder.decodeMod3Sves(data, N));
    }

    /**
     * Converts a byte array produced by {@link #toBinary3Tight()} to a polynomial.
     *
     * @param b a byte array
     * @param N number of coefficients
     * @return the decoded polynomial
     */
    public static IntegerPolynomial fromBinary3Tight(byte[] b, int N)
    {
        return new IntegerPolynomial(ArrayEncoder.decodeMod3Tight(b, N));
    }

    /**
     * Reads data produced by {@link #toBinary3Tight()} from an input stream and converts it to a polynomial.
     *
     * @param is an input stream
     * @param N  number of coefficients
     * @return the decoded polynomial
     */
    public static IntegerPolynomial fromBinary3Tight(InputStream is, int N)
        throws IOException
    {
        return new IntegerPolynomial(ArrayEncoder.decodeMod3Tight(is, N));
    }

    /**
     * Returns a polynomial with N coefficients between <code>0</code> and <code>q-1</code>.<br>
     * <code>q</code> must be a power of 2.<br>
     * Ignores any excess bytes.
     *
     * @param data an encoded ternary polynomial
     * @param N    number of coefficients
     * @param q
     * @return the decoded polynomial
     */
    public static IntegerPolynomial fromBinary(byte[] data, int N, int q)
    {
        return new IntegerPolynomial(ArrayEncoder.decodeModQ(data, N, q));
    }

    /**
     * Returns a polynomial with N coefficients between <code>0</code> and <code>q-1</code>.<br>
     * <code>q</code> must be a power of 2.<br>
     * Ignores any excess bytes.
     *
     * @param is an encoded ternary polynomial
     * @param N  number of coefficients
     * @param q
     * @return the decoded polynomial
     */
    public static IntegerPolynomial fromBinary(InputStream is, int N, int q)
        throws IOException
    {
        return new IntegerPolynomial(ArrayEncoder.decodeModQ(is, N, q));
    }

    /**
     * Encodes a polynomial with ternary coefficients to binary.
     * <code>coeffs[2*i]</code> and <code>coeffs[2*i+1]</code> must not both equal -1 for any integer <code>i</code>,
     * so this method is only safe to use with polynomials produced by <code>fromBinary3Sves()</code>.
     *
     * @return the encoded polynomial
     */
    public byte[] toBinary3Sves()
    {
        return ArrayEncoder.encodeMod3Sves(coeffs);
    }

    /**
     * Converts a polynomial with ternary coefficients to binary.
     *
     * @return the encoded polynomial
     */
    public byte[] toBinary3Tight()
    {
        BigInteger sum = Constants.BIGINT_ZERO;
        for (int i = coeffs.length - 1; i >= 0; i--)
        {
            sum = sum.multiply(BigInteger.valueOf(3));
            sum = sum.add(BigInteger.valueOf(coeffs[i] + 1));
        }

        int size = (BigInteger.valueOf(3).pow(coeffs.length).bitLength() + 7) / 8;
        byte[] arr = sum.toByteArray();

        if (arr.length < size)
        {
            // pad with leading zeros so arr.length==size
            byte[] arr2 = new byte[size];
            System.arraycopy(arr, 0, arr2, size - arr.length, arr.length);
            return arr2;
        }

        if (arr.length > size)
        // drop sign bit
        {
            arr = Arrays.copyOfRange(arr, 1, arr.length);
        }
        return arr;
    }

    /**
     * Encodes a polynomial whose coefficients are between 0 and q, to binary. q must be a power of 2.
     *
     * @param q
     * @return the encoded polynomial
     */
    public byte[] toBinary(int q)
    {
        return ArrayEncoder.encodeModQ(coeffs, q);
    }

    /**
     * Multiplies the polynomial with another, taking the values mod modulus and the indices mod N
     */
    public IntegerPolynomial mult(IntegerPolynomial poly2, int modulus)
    {
        IntegerPolynomial c = mult(poly2);
        c.mod(modulus);
        return c;
    }

    /**
     * Multiplies the polynomial with another, taking the indices mod N
     */
    public IntegerPolynomial mult(IntegerPolynomial poly2)
    {
        int N = coeffs.length;
        if (poly2.coeffs.length != N)
        {
            throw new IllegalArgumentException("Number of coefficients must be the same");
        }

        IntegerPolynomial c = multRecursive(poly2);

        if (c.coeffs.length > N)
        {
            for (int k = N; k < c.coeffs.length; k++)
            {
                c.coeffs[k - N] += c.coeffs[k];
            }
            c.coeffs = Arrays.copyOf(c.coeffs, N);
        }
        return c;
    }

    public BigIntPolynomial mult(BigIntPolynomial poly2)
    {
        return new BigIntPolynomial(this).mult(poly2);
    }

    /**
     * Karazuba multiplication
     */
    private IntegerPolynomial multRecursive(IntegerPolynomial poly2)
    {
        int[] a = coeffs;
        int[] b = poly2.coeffs;

        int n = poly2.coeffs.length;
        if (n <= 32)
        {
            int cn = 2 * n - 1;
            IntegerPolynomial c = new IntegerPolynomial(new int[cn]);
            for (int k = 0; k < cn; k++)
            {
                for (int i = Math.max(0, k - n + 1); i <= Math.min(k, n - 1); i++)
                {
                    c.coeffs[k] += b[i] * a[k - i];
                }
            }
            return c;
        }
        else
        {
            int n1 = n / 2;

            IntegerPolynomial a1 = new IntegerPolynomial(Arrays.copyOf(a, n1));
            IntegerPolynomial a2 = new IntegerPolynomial(Arrays.copyOfRange(a, n1, n));
            IntegerPolynomial b1 = new IntegerPolynomial(Arrays.copyOf(b, n1));
            IntegerPolynomial b2 = new IntegerPolynomial(Arrays.copyOfRange(b, n1, n));

            IntegerPolynomial A = (IntegerPolynomial)a1.clone();
            A.add(a2);
            IntegerPolynomial B = (IntegerPolynomial)b1.clone();
            B.add(b2);

            IntegerPolynomial c1 = a1.multRecursive(b1);
            IntegerPolynomial c2 = a2.multRecursive(b2);
            IntegerPolynomial c3 = A.multRecursive(B);
            c3.sub(c1);
            c3.sub(c2);

            IntegerPolynomial c = new IntegerPolynomial(2 * n - 1);
            for (int i = 0; i < c1.coeffs.length; i++)
            {
                c.coeffs[i] = c1.coeffs[i];
            }
            for (int i = 0; i < c3.coeffs.length; i++)
            {
                c.coeffs[n1 + i] += c3.coeffs[i];
            }
            for (int i = 0; i < c2.coeffs.length; i++)
            {
                c.coeffs[2 * n1 + i] += c2.coeffs[i];
            }
            return c;
        }
    }

    /**
     * Computes the inverse mod <code>q; q</code> must be a power of 2.<br>
     * Returns <code>null</code> if the polynomial is not invertible.
     *
     * @param q the modulus
     * @return a new polynomial
     */
    public IntegerPolynomial invertFq(int q)
    {
        int N = coeffs.length;
        int k = 0;
        IntegerPolynomial b = new IntegerPolynomial(N + 1);
        b.coeffs[0] = 1;
        IntegerPolynomial c = new IntegerPolynomial(N + 1);
        IntegerPolynomial f = new IntegerPolynomial(N + 1);
        f.coeffs = Arrays.copyOf(coeffs, N + 1);
        f.modPositive(2);
        // set g(x) = x^N − 1
        IntegerPolynomial g = new IntegerPolynomial(N + 1);
        g.coeffs[0] = 1;
        g.coeffs[N] = 1;
        while (true)
        {
            while (f.coeffs[0] == 0)
            {
                for (int i = 1; i <= N; i++)
                {
                    f.coeffs[i - 1] = f.coeffs[i];   // f(x) = f(x) / x
                    c.coeffs[N + 1 - i] = c.coeffs[N - i];   // c(x) = c(x) * x
                }
                f.coeffs[N] = 0;
                c.coeffs[0] = 0;
                k++;
                if (f.equalsZero())
                {
                    return null;   // not invertible
                }
            }
            if (f.equalsOne())
            {
                break;
            }
            if (f.degree() < g.degree())
            {
                // exchange f and g
                IntegerPolynomial temp = f;
                f = g;
                g = temp;
                // exchange b and c
                temp = b;
                b = c;
                c = temp;
            }
            f.add(g, 2);
            b.add(c, 2);
        }

        if (b.coeffs[N] != 0)
        {
            return null;
        }
        // Fq(x) = x^(N-k) * b(x)
        IntegerPolynomial Fq = new IntegerPolynomial(N);
        int j = 0;
        k %= N;
        for (int i = N - 1; i >= 0; i--)
        {
            j = i - k;
            if (j < 0)
            {
                j += N;
            }
            Fq.coeffs[j] = b.coeffs[i];
        }

        return mod2ToModq(Fq, q);
    }

    /**
     * Computes the inverse mod q from the inverse mod 2
     *
     * @param Fq
     * @param q
     * @return The inverse of this polynomial mod q
     */
    private IntegerPolynomial mod2ToModq(IntegerPolynomial Fq, int q)
    {
        if (Util.is64BitJVM() && q == 2048)
        {
            LongPolynomial2 thisLong = new LongPolynomial2(this);
            LongPolynomial2 FqLong = new LongPolynomial2(Fq);
            int v = 2;
            while (v < q)
            {
                v *= 2;
                LongPolynomial2 temp = (LongPolynomial2)FqLong.clone();
                temp.mult2And(v - 1);
                FqLong = thisLong.mult(FqLong).mult(FqLong);
                temp.subAnd(FqLong, v - 1);
                FqLong = temp;
            }
            return FqLong.toIntegerPolynomial();
        }
        else
        {
            int v = 2;
            while (v < q)
            {
                v *= 2;
                IntegerPolynomial temp = new IntegerPolynomial(Arrays.copyOf(Fq.coeffs, Fq.coeffs.length));
                temp.mult2(v);
                Fq = mult(Fq, v).mult(Fq, v);
                temp.sub(Fq, v);
                Fq = temp;
            }
            return Fq;
        }
    }

    /**
     * Computes the inverse mod 3.
     * Returns <code>null</code> if the polynomial is not invertible.
     *
     * @return a new polynomial
     */
    public IntegerPolynomial invertF3()
    {
        int N = coeffs.length;
        int k = 0;
        IntegerPolynomial b = new IntegerPolynomial(N + 1);
        b.coeffs[0] = 1;
        IntegerPolynomial c = new IntegerPolynomial(N + 1);
        IntegerPolynomial f = new IntegerPolynomial(N + 1);
        f.coeffs = Arrays.copyOf(coeffs, N + 1);
        f.modPositive(3);
        // set g(x) = x^N − 1
        IntegerPolynomial g = new IntegerPolynomial(N + 1);
        g.coeffs[0] = -1;
        g.coeffs[N] = 1;
        while (true)
        {
            while (f.coeffs[0] == 0)
            {
                for (int i = 1; i <= N; i++)
                {
                    f.coeffs[i - 1] = f.coeffs[i];   // f(x) = f(x) / x
                    c.coeffs[N + 1 - i] = c.coeffs[N - i];   // c(x) = c(x) * x
                }
                f.coeffs[N] = 0;
                c.coeffs[0] = 0;
                k++;
                if (f.equalsZero())
                {
                    return null;   // not invertible
                }
            }
            if (f.equalsAbsOne())
            {
                break;
            }
            if (f.degree() < g.degree())
            {
                // exchange f and g
                IntegerPolynomial temp = f;
                f = g;
                g = temp;
                // exchange b and c
                temp = b;
                b = c;
                c = temp;
            }
            if (f.coeffs[0] == g.coeffs[0])
            {
                f.sub(g, 3);
                b.sub(c, 3);
            }
            else
            {
                f.add(g, 3);
                b.add(c, 3);
            }
        }

        if (b.coeffs[N] != 0)
        {
            return null;
        }
        // Fp(x) = [+-] x^(N-k) * b(x)
        IntegerPolynomial Fp = new IntegerPolynomial(N);
        int j = 0;
        k %= N;
        for (int i = N - 1; i >= 0; i--)
        {
            j = i - k;
            if (j < 0)
            {
                j += N;
            }
            Fp.coeffs[j] = f.coeffs[0] * b.coeffs[i];
        }

        Fp.ensurePositive(3);
        return Fp;
    }

    /**
     * Resultant of this polynomial with <code>x^n-1</code> using a probabilistic algorithm.
     * <p>
     * Unlike EESS, this implementation does not compute all resultants modulo primes
     * such that their product exceeds the maximum possible resultant, but rather stops
     * when <code>NUM_EQUAL_RESULTANTS</code> consecutive modular resultants are equal.<br>
     * This means the return value may be incorrect. Experiments show this happens in
     * about 1 out of 100 cases when <code>N=439</code> and <code>NUM_EQUAL_RESULTANTS=2</code>,
     * so the likelyhood of leaving the loop too early is <code>(1/100)^(NUM_EQUAL_RESULTANTS-1)</code>.
     * <p>
     * Because of the above, callers must verify the output and try a different polynomial if necessary.
     *
     * @return <code>(rho, res)</code> satisfying <code>res = rho*this + t*(x^n-1)</code> for some integer <code>t</code>.
     */
    public Resultant resultant()
    {
        int N = coeffs.length;

        // Compute resultants modulo prime numbers. Continue until NUM_EQUAL_RESULTANTS consecutive modular resultants are equal.
        LinkedList<ModularResultant> modResultants = new LinkedList<ModularResultant>();
        BigInteger pProd = Constants.BIGINT_ONE;
        BigInteger res = Constants.BIGINT_ONE;
        int numEqual = 1;   // number of consecutive modular resultants equal to each other

        PrimeGenerator primes = new PrimeGenerator();

        while (true)
        {
            BigInteger prime = primes.nextPrime();
            ModularResultant crr = resultant(prime.intValue());
            modResultants.add(crr);

            BigInteger temp = pProd.multiply(prime);
            BigIntEuclidean er = BigIntEuclidean.calculate(prime, pProd);
            BigInteger resPrev = res;
            res = res.multiply(er.x.multiply(prime));
            BigInteger res2 = crr.res.multiply(er.y.multiply(pProd));
            res = res.add(res2).mod(temp);
            pProd = temp;

            BigInteger pProd2 = pProd.divide(BigInteger.valueOf(2));
            BigInteger pProd2n = pProd2.negate();
            if (res.compareTo(pProd2) > 0)
            {
                res = res.subtract(pProd);
            }
            else if (res.compareTo(pProd2n) < 0)
            {
                res = res.add(pProd);
            }

            if (res.equals(resPrev))
            {
                numEqual++;
                if (numEqual >= NUM_EQUAL_RESULTANTS)
                {
                    break;
                }
            }
            else
            {
                numEqual = 1;
            }
        }

        // Combine modular rho's to obtain the final rho.
        // For efficiency, first combine all pairs of small resultants to bigger resultants,
        // then combine pairs of those, etc. until only one is left.
        while (modResultants.size() > 1)
        {
            ModularResultant modRes1 = modResultants.removeFirst();
            ModularResultant modRes2 = modResultants.removeFirst();
            ModularResultant modRes3 = ModularResultant.combineRho(modRes1, modRes2);
            modResultants.addLast(modRes3);
        }
        BigIntPolynomial rhoP = modResultants.getFirst().rho;

        BigInteger pProd2 = pProd.divide(BigInteger.valueOf(2));
        BigInteger pProd2n = pProd2.negate();
        if (res.compareTo(pProd2) > 0)
        {
            res = res.subtract(pProd);
        }
        if (res.compareTo(pProd2n) < 0)
        {
            res = res.add(pProd);
        }

        for (int i = 0; i < N; i++)
        {
            BigInteger c = rhoP.coeffs[i];
            if (c.compareTo(pProd2) > 0)
            {
                rhoP.coeffs[i] = c.subtract(pProd);
            }
            if (c.compareTo(pProd2n) < 0)
            {
                rhoP.coeffs[i] = c.add(pProd);
            }
        }

        return new Resultant(rhoP, res);
    }

    /**
     * Multithreaded version of {@link #resultant()}.
     *
     * @return <code>(rho, res)</code> satisfying <code>res = rho*this + t*(x^n-1)</code> for some integer <code>t</code>.
     */
    public Resultant resultantMultiThread()
    {
        int N = coeffs.length;

        // upper bound for resultant(f, g) = ||f, 2||^deg(g) * ||g, 2||^deg(f) = squaresum(f)^(N/2) * 2^(deg(f)/2) because g(x)=x^N-1
        // see https://jondalon.mathematik.uni-osnabrueck.de/staff/phpages/brunsw/CompAlg.pdf chapter 3
        BigInteger max = squareSum().pow((N + 1) / 2);
        max = max.multiply(BigInteger.valueOf(2).pow((degree() + 1) / 2));
        BigInteger max2 = max.multiply(BigInteger.valueOf(2));

        // compute resultants modulo prime numbers
        BigInteger prime = BigInteger.valueOf(10000);
        BigInteger pProd = Constants.BIGINT_ONE;
        LinkedBlockingQueue<Future<ModularResultant>> resultantTasks = new LinkedBlockingQueue<Future<ModularResultant>>();
        Iterator<BigInteger> primes = BIGINT_PRIMES.iterator();
        ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
        while (pProd.compareTo(max2) < 0)
        {
            if (primes.hasNext())
            {
                prime = primes.next();
            }
            else
            {
                prime = prime.nextProbablePrime();
            }
            Future<ModularResultant> task = executor.submit(new ModResultantTask(prime.intValue()));
            resultantTasks.add(task);
            pProd = pProd.multiply(prime);
        }

        // Combine modular resultants to obtain the resultant.
        // For efficiency, first combine all pairs of small resultants to bigger resultants,
        // then combine pairs of those, etc. until only one is left.
        ModularResultant overallResultant = null;
        while (!resultantTasks.isEmpty())
        {
            try
            {
                Future<ModularResultant> modRes1 = resultantTasks.take();
                Future<ModularResultant> modRes2 = resultantTasks.poll();
                if (modRes2 == null)
                {
                    // modRes1 is the only one left
                    overallResultant = modRes1.get();
                    break;
                }
                Future<ModularResultant> newTask = executor.submit(new CombineTask(modRes1.get(), modRes2.get()));
                resultantTasks.add(newTask);
            }
            catch (Exception e)
            {
                throw new IllegalStateException(e.toString());
            }
        }
        executor.shutdown();
        BigInteger res = overallResultant.res;
        BigIntPolynomial rhoP = overallResultant.rho;

        BigInteger pProd2 = pProd.divide(BigInteger.valueOf(2));
        BigInteger pProd2n = pProd2.negate();

        if (res.compareTo(pProd2) > 0)
        {
            res = res.subtract(pProd);
        }
        if (res.compareTo(pProd2n) < 0)
        {
            res = res.add(pProd);
        }

        for (int i = 0; i < N; i++)
        {
            BigInteger c = rhoP.coeffs[i];
            if (c.compareTo(pProd2) > 0)
            {
                rhoP.coeffs[i] = c.subtract(pProd);
            }
            if (c.compareTo(pProd2n) < 0)
            {
                rhoP.coeffs[i] = c.add(pProd);
            }
        }

        return new Resultant(rhoP, res);
    }

    /**
     * Resultant of this polynomial with <code>x^n-1 mod p</code>.
     *
     * @return <code>(rho, res)</code> satisfying <code>res = rho*this + t*(x^n-1) mod p</code> for some integer <code>t</code>.
     */
    public ModularResultant resultant(int p)
    {
        // Add a coefficient as the following operations involve polynomials of degree deg(f)+1
        int[] fcoeffs = Arrays.copyOf(coeffs, coeffs.length + 1);
        IntegerPolynomial f = new IntegerPolynomial(fcoeffs);
        int N = fcoeffs.length;

        IntegerPolynomial a = new IntegerPolynomial(N);
        a.coeffs[0] = -1;
        a.coeffs[N - 1] = 1;
        IntegerPolynomial b = new IntegerPolynomial(f.coeffs);
        IntegerPolynomial v1 = new IntegerPolynomial(N);
        IntegerPolynomial v2 = new IntegerPolynomial(N);
        v2.coeffs[0] = 1;
        int da = N - 1;
        int db = b.degree();
        int ta = da;
        int c = 0;
        int r = 1;
        while (db > 0)
        {
            c = Util.invert(b.coeffs[db], p);
            c = (c * a.coeffs[da]) % p;
            a.multShiftSub(b, c, da - db, p);
            v1.multShiftSub(v2, c, da - db, p);

            da = a.degree();
            if (da < db)
            {
                r *= Util.pow(b.coeffs[db], ta - da, p);
                r %= p;
                if (ta % 2 == 1 && db % 2 == 1)
                {
                    r = (-r) % p;
                }
                IntegerPolynomial temp = a;
                a = b;
                b = temp;
                int tempdeg = da;
                da = db;
                temp = v1;
                v1 = v2;
                v2 = temp;
                ta = db;
                db = tempdeg;
            }
        }
        r *= Util.pow(b.coeffs[0], da, p);
        r %= p;
        c = Util.invert(b.coeffs[0], p);
        v2.mult(c);
        v2.mod(p);
        v2.mult(r);
        v2.mod(p);

        // drop the highest coefficient so #coeffs matches the original input
        v2.coeffs = Arrays.copyOf(v2.coeffs, v2.coeffs.length - 1);
        return new ModularResultant(new BigIntPolynomial(v2), BigInteger.valueOf(r), BigInteger.valueOf(p));
    }

    /**
     * Computes <code>this-b*c*(x^k) mod p</code> and stores the result in this polynomial.<br/>
     * See steps 4a,4b in EESS algorithm 2.2.7.1.
     *
     * @param b
     * @param c
     * @param k
     * @param p
     */
    private void multShiftSub(IntegerPolynomial b, int c, int k, int p)
    {
        int N = coeffs.length;
        for (int i = k; i < N; i++)
        {
            coeffs[i] = (coeffs[i] - b.coeffs[i - k] * c) % p;
        }
    }

    /**
     * Adds the squares of all coefficients.
     *
     * @return the sum of squares
     */
    private BigInteger squareSum()
    {
        BigInteger sum = Constants.BIGINT_ZERO;
        for (int i = 0; i < coeffs.length; i++)
        {
            sum = sum.add(BigInteger.valueOf(coeffs[i] * coeffs[i]));
        }
        return sum;
    }

    /**
     * Returns the degree of the polynomial
     *
     * @return the degree
     */
    int degree()
    {
        int degree = coeffs.length - 1;
        while (degree > 0 && coeffs[degree] == 0)
        {
            degree--;
        }
        return degree;
    }

    /**
     * Adds another polynomial which can have a different number of coefficients,
     * and takes the coefficient values mod <code>modulus</code>.
     *
     * @param b another polynomial
     */
    public void add(IntegerPolynomial b, int modulus)
    {
        add(b);
        mod(modulus);
    }

    /**
     * Adds another polynomial which can have a different number of coefficients.
     *
     * @param b another polynomial
     */
    public void add(IntegerPolynomial b)
    {
        if (b.coeffs.length > coeffs.length)
        {
            coeffs = Arrays.copyOf(coeffs, b.coeffs.length);
        }
        for (int i = 0; i < b.coeffs.length; i++)
        {
            coeffs[i] += b.coeffs[i];
        }
    }

    /**
     * Subtracts another polynomial which can have a different number of coefficients,
     * and takes the coefficient values mod <code>modulus</code>.
     *
     * @param b another polynomial
     */
    public void sub(IntegerPolynomial b, int modulus)
    {
        sub(b);
        mod(modulus);
    }

    /**
     * Subtracts another polynomial which can have a different number of coefficients.
     *
     * @param b another polynomial
     */
    public void sub(IntegerPolynomial b)
    {
        if (b.coeffs.length > coeffs.length)
        {
            coeffs = Arrays.copyOf(coeffs, b.coeffs.length);
        }
        for (int i = 0; i < b.coeffs.length; i++)
        {
            coeffs[i] -= b.coeffs[i];
        }
    }

    /**
     * Subtracts a <code>int</code> from each coefficient. Does not return a new polynomial but modifies this polynomial.
     *
     * @param b
     */
    void sub(int b)
    {
        for (int i = 0; i < coeffs.length; i++)
        {
            coeffs[i] -= b;
        }
    }

    /**
     * Multiplies each coefficient by a <code>int</code>. Does not return a new polynomial but modifies this polynomial.
     *
     * @param factor
     */
    public void mult(int factor)
    {
        for (int i = 0; i < coeffs.length; i++)
        {
            coeffs[i] *= factor;
        }
    }

    /**
     * Multiplies each coefficient by a 2 and applies a modulus. Does not return a new polynomial but modifies this polynomial.
     *
     * @param modulus a modulus
     */
    private void mult2(int modulus)
    {
        for (int i = 0; i < coeffs.length; i++)
        {
            coeffs[i] *= 2;
            coeffs[i] %= modulus;
        }
    }

    /**
     * Multiplies each coefficient by a 2 and applies a modulus. Does not return a new polynomial but modifies this polynomial.
     *
     * @param modulus a modulus
     */
    public void mult3(int modulus)
    {
        for (int i = 0; i < coeffs.length; i++)
        {
            coeffs[i] *= 3;
            coeffs[i] %= modulus;
        }
    }

    /**
     * Divides each coefficient by <code>k</code> and rounds to the nearest integer. Does not return a new polynomial but modifies this polynomial.
     *
     * @param k the divisor
     */
    public void div(int k)
    {
        int k2 = (k + 1) / 2;
        for (int i = 0; i < coeffs.length; i++)
        {
            coeffs[i] += coeffs[i] > 0 ? k2 : -k2;
            coeffs[i] /= k;
        }
    }

    /**
     * Takes each coefficient modulo 3 such that all coefficients are ternary.
     */
    public void mod3()
    {
        for (int i = 0; i < coeffs.length; i++)
        {
            coeffs[i] %= 3;
            if (coeffs[i] > 1)
            {
                coeffs[i] -= 3;
            }
            if (coeffs[i] < -1)
            {
                coeffs[i] += 3;
            }
        }
    }

    /**
     * Ensures all coefficients are between 0 and <code>modulus-1</code>
     *
     * @param modulus a modulus
     */
    public void modPositive(int modulus)
    {
        mod(modulus);
        ensurePositive(modulus);
    }

    /**
     * Reduces all coefficients to the interval [-modulus/2, modulus/2)
     */
    void modCenter(int modulus)
    {
        mod(modulus);
        for (int j = 0; j < coeffs.length; j++)
        {
            while (coeffs[j] < modulus / 2)
            {
                coeffs[j] += modulus;
            }
            while (coeffs[j] >= modulus / 2)
            {
                coeffs[j] -= modulus;
            }
        }
    }

    /**
     * Takes each coefficient modulo <code>modulus</code>.
     */
    public void mod(int modulus)
    {
        for (int i = 0; i < coeffs.length; i++)
        {
            coeffs[i] %= modulus;
        }
    }

    /**
     * Adds <code>modulus</code> until all coefficients are above 0.
     *
     * @param modulus a modulus
     */
    public void ensurePositive(int modulus)
    {
        for (int i = 0; i < coeffs.length; i++)
        {
            while (coeffs[i] < 0)
            {
                coeffs[i] += modulus;
            }
        }
    }

    /**
     * Computes the centered euclidean norm of the polynomial.
     *
     * @param q a modulus
     * @return the centered norm
     */
    public long centeredNormSq(int q)
    {
        int N = coeffs.length;
        IntegerPolynomial p = (IntegerPolynomial)clone();
        p.shiftGap(q);

        long sum = 0;
        long sqSum = 0;
        for (int i = 0; i != p.coeffs.length; i++)
        {
            int c = p.coeffs[i];
            sum += c;
            sqSum += c * c;
        }

        long centeredNormSq = sqSum - sum * sum / N;
        return centeredNormSq;
    }

    /**
     * Shifts all coefficients so the largest gap is centered around <code>-q/2</code>.
     *
     * @param q a modulus
     */
    void shiftGap(int q)
    {
        modCenter(q);

        int[] sorted = Arrays.clone(coeffs);

        sort(sorted);

        int maxrange = 0;
        int maxrangeStart = 0;
        for (int i = 0; i < sorted.length - 1; i++)
        {
            int range = sorted[i + 1] - sorted[i];
            if (range > maxrange)
            {
                maxrange = range;
                maxrangeStart = sorted[i];
            }
        }

        int pmin = sorted[0];
        int pmax = sorted[sorted.length - 1];

        int j = q - pmax + pmin;
        int shift;
        if (j > maxrange)
        {
            shift = (pmax + pmin) / 2;
        }
        else
        {
            shift = maxrangeStart + maxrange / 2 + q / 2;
        }

        sub(shift);
    }

    private void sort(int[] ints)
    {
        boolean swap = true;

        while (swap)
        {
            swap = false;
            for (int i = 0; i != ints.length - 1; i++)
            {
                if (ints[i] > ints[i+1])
                {
                    int tmp = ints[i];
                    ints[i] = ints[i+1];
                    ints[i+1] = tmp;
                    swap = true;
                }
            }
        }
    }

    /**
     * Shifts the values of all coefficients to the interval <code>[-q/2, q/2]</code>.
     *
     * @param q a modulus
     */
    public void center0(int q)
    {
        for (int i = 0; i < coeffs.length; i++)
        {
            while (coeffs[i] < -q / 2)
            {
                coeffs[i] += q;
            }
            while (coeffs[i] > q / 2)
            {
                coeffs[i] -= q;
            }
        }
    }

    /**
     * Returns the sum of all coefficients, i.e. evaluates the polynomial at 0.
     *
     * @return the sum of all coefficients
     */
    public int sumCoeffs()
    {
        int sum = 0;
        for (int i = 0; i < coeffs.length; i++)
        {
            sum += coeffs[i];
        }
        return sum;
    }

    /**
     * Tests if <code>p(x) = 0</code>.
     *
     * @return true iff all coefficients are zeros
     */
    private boolean equalsZero()
    {
        for (int i = 0; i < coeffs.length; i++)
        {
            if (coeffs[i] != 0)
            {
                return false;
            }
        }
        return true;
    }

    /**
     * Tests if <code>p(x) = 1</code>.
     *
     * @return true iff all coefficients are equal to zero, except for the lowest coefficient which must equal 1
     */
    public boolean equalsOne()
    {
        for (int i = 1; i < coeffs.length; i++)
        {
            if (coeffs[i] != 0)
            {
                return false;
            }
        }
        return coeffs[0] == 1;
    }

    /**
     * Tests if <code>|p(x)| = 1</code>.
     *
     * @return true iff all coefficients are equal to zero, except for the lowest coefficient which must equal 1 or -1
     */
    private boolean equalsAbsOne()
    {
        for (int i = 1; i < coeffs.length; i++)
        {
            if (coeffs[i] != 0)
            {
                return false;
            }
        }
        return Math.abs(coeffs[0]) == 1;
    }

    /**
     * Counts the number of coefficients equal to an integer
     *
     * @param value an integer
     * @return the number of coefficients equal to <code>value</code>
     */
    public int count(int value)
    {
        int count = 0;
        for (int i = 0; i != coeffs.length; i++)
        {
            if (coeffs[i] == value)
            {
                count++;
            }
        }
        return count;
    }

    /**
     * Multiplication by <code>X</code> in <code>Z[X]/Z[X^n-1]</code>.
     */
    public void rotate1()
    {
        int clast = coeffs[coeffs.length - 1];
        for (int i = coeffs.length - 1; i > 0; i--)
        {
            coeffs[i] = coeffs[i - 1];
        }
        coeffs[0] = clast;
    }

    public void clear()
    {
        for (int i = 0; i < coeffs.length; i++)
        {
            coeffs[i] = 0;
        }
    }

    public IntegerPolynomial toIntegerPolynomial()
    {
        return (IntegerPolynomial)clone();
    }

    public Object clone()
    {
        return new IntegerPolynomial(coeffs.clone());
    }

    public boolean equals(Object obj)
    {
        if (obj instanceof IntegerPolynomial)
        {
            return Arrays.areEqual(coeffs, ((IntegerPolynomial)obj).coeffs);
        }
        else
        {
            return false;
        }
    }

    /**
     * Calls {@link IntegerPolynomial#resultant(int)
     */
    private class ModResultantTask
        implements Callable<ModularResultant>
    {
        private int modulus;

        private ModResultantTask(int modulus)
        {
            this.modulus = modulus;
        }

        public ModularResultant call()
        {
            return resultant(modulus);
        }
    }

    /**
     * Calls {@link ModularResultant#combineRho(ModularResultant, ModularResultant)
     */
    private static class CombineTask
        implements Callable<ModularResultant>
    {
        private ModularResultant modRes1;
        private ModularResultant modRes2;

        private CombineTask(ModularResultant modRes1, ModularResultant modRes2)
        {
            this.modRes1 = modRes1;
            this.modRes2 = modRes2;
        }

        public ModularResultant call()
        {
            return ModularResultant.combineRho(modRes1, modRes2);
        }
    }

    private static class PrimeGenerator
    {
        private int index = 0;
        private BigInteger prime;

        public BigInteger nextPrime()
        {
            if (index < BIGINT_PRIMES.size())
            {
                prime = (BigInteger)BIGINT_PRIMES.get(index++);
            }
            else
            {
                prime = prime.nextProbablePrime();
            }

            return prime;
        }
    }
}
