package org.bouncycastle.pqc.legacy.math.linearalgebra;


import java.security.SecureRandom;
import java.util.Random;
import java.util.Vector;


/**
 * This class implements the abstract class <tt>GF2nField</tt> for ONB
 * representation. It computes the fieldpolynomial, multiplication matrix and
 * one of its roots mONBRoot, (see for example <a
 * href=https://www2.certicom.com/ecc/intro.htm>Certicoms Whitepapers</a>).
 * GF2nField is used by GF2nONBElement which implements the elements of this
 * field.
 *
 * @see GF2nField
 * @see GF2nONBElement
 */
public class GF2nONBField
    extends GF2nField
{

    // ///////////////////////////////////////////////////////////////////
    // Hashtable for irreducible normal polynomials //
    // ///////////////////////////////////////////////////////////////////

    // i*5 + 0 i*5 + 1 i*5 + 2 i*5 + 3 i*5 + 4
    /*
     * private static int[][] mNB = {{0, 0, 0}, {0, 0, 0}, {1, 0, 0}, {1, 0, 0},
     * {1, 0, 0}, // i = 0 {2, 0, 0}, {1, 0, 0}, {1, 0, 0}, {4, 3, 1}, {1, 0,
     * 0}, // i = 1 {3, 0, 0}, {2, 0, 0}, {3, 0, 0}, {4, 3, 1}, {5, 0, 0}, // i =
     * 2 {1, 0, 0}, {5, 3, 1}, {3, 0, 0}, {3, 0, 0}, {5, 2, 1}, // i = 3 {3, 0,
     * 0}, {2, 0, 0}, {1, 0, 0}, {5, 0, 0}, {4, 3, 1}, // i = 4 {3, 0, 0}, {4,
     * 3, 1}, {5, 2, 1}, {1, 0, 0}, {2, 0, 0}, // i = 5 {1, 0, 0}, {3, 0, 0},
     * {7, 3, 2}, {10, 0, 0}, {7, 0, 0}, // i = 6 {2, 0, 0}, {9, 0, 0}, {6, 4,
     * 1}, {6, 5, 1}, {4, 0, 0}, // i = 7 {5, 4, 3}, {3, 0, 0}, {7, 0, 0}, {6,
     * 4, 3}, {5, 0, 0}, // i = 8 {4, 3, 1}, {1, 0, 0}, {5, 0, 0}, {5, 3, 2},
     * {9, 0, 0}, // i = 9 {4, 3, 2}, {6, 3, 1}, {3, 0, 0}, {6, 2, 1}, {9, 0,
     * 0}, // i = 10 {7, 0, 0}, {7, 4, 2}, {4, 0, 0}, {19, 0, 0}, {7, 4, 2}, //
     * i = 11 {1, 0, 0}, {5, 2, 1}, {29, 0, 0}, {1, 0, 0}, {4, 3, 1}, // i = 12
     * {18, 0, 0}, {3, 0, 0}, {5, 2, 1}, {9, 0, 0}, {6, 5, 2}, // i = 13 {5, 3,
     * 1}, {6, 0, 0}, {10, 9, 3}, {25, 0, 0}, {35, 0, 0}, // i = 14 {6, 3, 1},
     * {21, 0, 0}, {6, 5, 2}, {6, 5, 3}, {9, 0, 0}, // i = 15 {9, 4, 2}, {4, 0,
     * 0}, {8, 3, 1}, {7, 4, 2}, {5, 0, 0}, // i = 16 {8, 2, 1}, {21, 0, 0},
     * {13, 0, 0}, {7, 6, 2}, {38, 0, 0}, // i = 17 {27, 0, 0}, {8, 5, 1}, {21,
     * 0, 0}, {2, 0, 0}, {21, 0, 0}, // i = 18 {11, 0, 0}, {10, 9, 6}, {6, 0,
     * 0}, {11, 0, 0}, {6, 3, 1}, // i = 19 {15, 0, 0}, {7, 6, 1}, {29, 0, 0},
     * {9, 0, 0}, {4, 3, 1}, // i = 20 {4, 0, 0}, {15, 0, 0}, {9, 7, 4}, {17, 0,
     * 0}, {5, 4, 2}, // i = 21 {33, 0, 0}, {10, 0, 0}, {5, 4, 3}, {9, 0, 0},
     * {5, 3, 2}, // i = 22 {8, 7, 5}, {4, 2, 1}, {5, 2, 1}, {33, 0, 0}, {8, 0,
     * 0}, // i = 23 {4, 3, 1}, {18, 0, 0}, {6, 2, 1}, {2, 0, 0}, {19, 0, 0}, //
     * i = 24 {7, 6, 5}, {21, 0, 0}, {1, 0, 0}, {7, 2, 1}, {5, 0, 0}, // i = 25
     * {3, 0, 0}, {8, 3, 2}, {17, 0, 0}, {9, 8, 2}, {57, 0, 0}, // i = 26 {11,
     * 0, 0}, {5, 3, 2}, {21, 0, 0}, {8, 7, 1}, {8, 5, 3}, // i = 27 {15, 0, 0},
     * {10, 4, 1}, {21, 0, 0}, {5, 3, 2}, {7, 4, 2}, // i = 28 {52, 0, 0}, {71,
     * 0, 0}, {14, 0, 0}, {27, 0, 0}, {10, 9, 7}, // i = 29 {53, 0, 0}, {3, 0,
     * 0}, {6, 3, 2}, {1, 0, 0}, {15, 0, 0}, // i = 30 {62, 0, 0}, {9, 0, 0},
     * {6, 5, 2}, {8, 6, 5}, {31, 0, 0}, // i = 31 {5, 3, 2}, {18, 0, 0 }, {27,
     * 0, 0}, {7, 6, 3}, {10, 8, 7}, // i = 32 {9, 8, 3}, {37, 0, 0}, {6, 0, 0},
     * {15, 3, 2}, {34, 0, 0}, // i = 33 {11, 0, 0}, {6, 5, 2}, {1, 0, 0}, {8,
     * 5, 2}, {13, 0, 0}, // i = 34 {6, 0, 0}, {11, 3, 2}, {8, 0, 0}, {31, 0,
     * 0}, {4, 2, 1}, // i = 35 {3, 0, 0}, {7, 6, 1}, {81, 0, 0}, {56, 0, 0},
     * {9, 8, 7}, // i = 36 {24, 0, 0}, {11, 0, 0}, {7, 6, 5}, {6, 5, 2}, {6, 5,
     * 2}, // i = 37 {8, 7, 6}, {9, 0, 0}, {7, 2, 1}, {15, 0, 0}, {87, 0, 0}, //
     * i = 38 {8, 3, 2}, {3, 0, 0}, {9, 4, 2}, {9, 0, 0}, {34, 0, 0}, // i = 39
     * {5, 3, 2}, {14, 0, 0}, {55, 0, 0}, {8, 7, 1}, {27, 0, 0}, // i = 40 {9,
     * 5, 2}, {10, 9, 5}, {43, 0, 0}, {8, 6, 2}, {6, 0, 0}, // i = 41 {7, 0, 0},
     * {11, 10, 8}, {105, 0, 0}, {6, 5, 2}, {73, 0, 0}}; // i = 42
     */
    // /////////////////////////////////////////////////////////////////////
    // member variables
    // /////////////////////////////////////////////////////////////////////
    private static final int MAXLONG = 64;

    /**
     * holds the length of the array-representation of degree mDegree.
     */
    private int mLength;

    /**
     * holds the number of relevant bits in mONBPol[mLength-1].
     */
    private int mBit;

    /**
     * holds the type of mONB
     */
    private int mType;

    /**
     * holds the multiplication matrix
     */
    int[][] mMult;

    // /////////////////////////////////////////////////////////////////////
    // constructors
    // /////////////////////////////////////////////////////////////////////

    /**
     * constructs an instance of the finite field with 2<sup>deg</sup>
     * elements and characteristic 2.
     *
     * @param deg    -the extention degree of this field
     * @param random - a source of randomness for generating polynomials on the field.
     */
    public GF2nONBField(int deg, SecureRandom random)
        throws RuntimeException
    {
        super(random);

        if (deg < 3)
        {
            throw new IllegalArgumentException("k must be at least 3");
        }

        mDegree = deg;
        mLength = mDegree / MAXLONG;
        mBit = mDegree & (MAXLONG - 1);
        if (mBit == 0)
        {
            mBit = MAXLONG;
        }
        else
        {
            mLength++;
        }

        computeType();

        // only ONB-implementations for type 1 and type 2
        //
        if (mType < 3)
        {
            mMult = new int[mDegree][2];
            for (int i = 0; i < mDegree; i++)
            {
                mMult[i][0] = -1;
                mMult[i][1] = -1;
            }
            computeMultMatrix();
        }
        else
        {
            throw new RuntimeException("\nThe type of this field is "
                + mType);
        }
        computeFieldPolynomial();
        fields = new Vector();
        matrices = new Vector();
    }

    // /////////////////////////////////////////////////////////////////////
    // access
    // /////////////////////////////////////////////////////////////////////

    int getONBLength()
    {
        return mLength;
    }

    int getONBBit()
    {
        return mBit;
    }

    // /////////////////////////////////////////////////////////////////////
    // arithmetic
    // /////////////////////////////////////////////////////////////////////

    /**
     * Computes a random root of the given polynomial.
     *
     * @param polynomial a polynomial
     * @return a random root of the polynomial
     * @see "P1363 A.5.6, p103f"
     */
    protected GF2nElement getRandomRoot(GF2Polynomial polynomial)
    {
        // We are in B1!!!
        GF2nPolynomial c;
        GF2nPolynomial ut;
        GF2nElement u;
        GF2nPolynomial h;
        int hDegree;
        // 1. Set g(t) <- f(t)
        GF2nPolynomial g = new GF2nPolynomial(polynomial, this);
        int gDegree = g.getDegree();
        int i;

        // 2. while deg(g) > 1
        while (gDegree > 1)
        {
            do
            {
                // 2.1 choose random u (element of) GF(2^m)
                u = new GF2nONBElement(this, random);
                ut = new GF2nPolynomial(2, GF2nONBElement.ZERO(this));
                // 2.2 Set c(t) <- ut
                ut.set(1, u);
                c = new GF2nPolynomial(ut);
                // 2.3 For i from 1 to m-1 do
                for (i = 1; i <= mDegree - 1; i++)
                {
                    // 2.3.1 c(t) <- (c(t)^2 + ut) mod g(t)
                    c = c.multiplyAndReduce(c, g);
                    c = c.add(ut);
                }
                // 2.4 set h(t) <- GCD(c(t), g(t))
                h = c.gcd(g);
                // 2.5 if h(t) is constant or deg(g) = deg(h) then go to
                // step 2.1
                hDegree = h.getDegree();
                gDegree = g.getDegree();
            }
            while ((hDegree == 0) || (hDegree == gDegree));
            // 2.6 If 2deg(h) > deg(g) then set g(t) <- g(t)/h(t) ...
            if ((hDegree << 1) > gDegree)
            {
                g = g.quotient(h);
            }
            else
            {
                // ... else g(t) <- h(t)
                g = new GF2nPolynomial(h);
            }
            gDegree = g.getDegree();
        }
        // 3. Output g(0)
        return g.at(0);

    }

    /**
     * Computes the change-of-basis matrix for basis conversion according to
     * 1363. The result is stored in the lists fields and matrices.
     *
     * @param B1 the GF2nField to convert to
     * @see "P1363 A.7.3, p111ff"
     */
    protected void computeCOBMatrix(GF2nField B1)
    {
        // we are in B0 here!
        if (mDegree != B1.mDegree)
        {
            throw new IllegalArgumentException(
                "GF2nField.computeCOBMatrix: B1 has a "
                    + "different degree and thus cannot be coverted to!");
        }
        int i, j;
        GF2nElement[] gamma;
        GF2nElement u;
        GF2Polynomial[] COBMatrix = new GF2Polynomial[mDegree];
        for (i = 0; i < mDegree; i++)
        {
            COBMatrix[i] = new GF2Polynomial(mDegree);
        }

        // find Random Root
        do
        {
            // u is in representation according to B1
            u = B1.getRandomRoot(fieldPolynomial);
        }
        while (u.isZero());

        gamma = new GF2nPolynomialElement[mDegree];
        // build gamma matrix by squaring
        gamma[0] = (GF2nElement)u.clone();
        for (i = 1; i < mDegree; i++)
        {
            gamma[i] = gamma[i - 1].square();
        }
        // convert horizontal gamma matrix by vertical Bitstrings
        for (i = 0; i < mDegree; i++)
        {
            for (j = 0; j < mDegree; j++)
            {
                if (gamma[i].testBit(j))
                {
                    COBMatrix[mDegree - j - 1].setBit(mDegree - i - 1);
                }
            }
        }

        fields.addElement(B1);
        matrices.addElement(COBMatrix);
        B1.fields.addElement(this);
        B1.matrices.addElement(invertMatrix(COBMatrix));
    }

    /**
     * Computes the field polynomial for a ONB according to IEEE 1363 A.7.2
     * (p110f).
     *
     * @see "P1363 A.7.2, p110f"
     */
    protected void computeFieldPolynomial()
    {
        if (mType == 1)
        {
            fieldPolynomial = new GF2Polynomial(mDegree + 1, "ALL");
        }
        else if (mType == 2)
        {
            // 1. q = 1
            GF2Polynomial q = new GF2Polynomial(mDegree + 1, "ONE");
            // 2. p = t+1
            GF2Polynomial p = new GF2Polynomial(mDegree + 1, "X");
            p.addToThis(q);
            GF2Polynomial r;
            int i;
            // 3. for i = 1 to (m-1) do
            for (i = 1; i < mDegree; i++)
            {
                // r <- q
                r = q;
                // q <- p
                q = p;
                // p = tq+r
                p = q.shiftLeft();
                p.addToThis(r);
            }
            fieldPolynomial = p;
        }
    }

    /**
     * Compute the inverse of a matrix <tt>a</tt>.
     *
     * @param a the matrix
     * @return <tt>a<sup>-1</sup></tt>
     */
    int[][] invMatrix(int[][] a)
    {

        int[][] A = new int[mDegree][mDegree];
        A = a;
        int[][] inv = new int[mDegree][mDegree];

        for (int i = 0; i < mDegree; i++)
        {
            inv[i][i] = 1;
        }

        for (int i = 0; i < mDegree; i++)
        {
            for (int j = i; j < mDegree; j++)
            {
                A[mDegree - 1 - i][j] = A[i][i];
            }
        }
        return null;
    }

    private void computeType()
        throws RuntimeException
    {
        if ((mDegree & 7) == 0)
        {
            throw new RuntimeException(
                "The extension degree is divisible by 8!");
        }
        // checking for the type
        int s = 0;
        int k = 0;
        mType = 1;
        for (int d = 0; d != 1; mType++)
        {
            s = mType * mDegree + 1;
            if (IntegerFunctions.isPrime(s))
            {
                k = IntegerFunctions.order(2, s);
                d = IntegerFunctions.gcd(mType * mDegree / k, mDegree);
            }
        }
        mType--;
        if (mType == 1)
        {
            s = (mDegree << 1) + 1;
            if (IntegerFunctions.isPrime(s))
            {
                k = IntegerFunctions.order(2, s);
                int d = IntegerFunctions.gcd((mDegree << 1) / k, mDegree);
                if (d == 1)
                {
                    mType++;
                }
            }
        }
    }

    private void computeMultMatrix()
    {

        if ((mType & 7) != 0)
        {
            int p = mType * mDegree + 1;

            // compute sequence F[1] ... F[p-1] via A.3.7. of 1363.
            // F[0] will not be filled!
            //
            int[] F = new int[p];

            int u;
            if (mType == 1)
            {
                u = 1;
            }
            else if (mType == 2)
            {
                u = p - 1;
            }
            else
            {
                u = elementOfOrder(mType, p);
            }

            int w = 1;
            int n;
            for (int j = 0; j < mType; j++)
            {
                n = w;

                for (int i = 0; i < mDegree; i++)
                {
                    F[n] = i;
                    n = (n << 1) % p;
                    if (n < 0)
                    {
                        n += p;
                    }
                }
                w = u * w % p;
                if (w < 0)
                {
                    w += p;
                }
            }

            // building the matrix (mDegree * 2)
            //
            if (mType == 1)
            {
                for (int k = 1; k < p - 1; k++)
                {
                    if (mMult[F[k + 1]][0] == -1)
                    {
                        mMult[F[k + 1]][0] = F[p - k];
                    }
                    else
                    {
                        mMult[F[k + 1]][1] = F[p - k];
                    }
                }

                int m_2 = mDegree >> 1;
                for (int k = 1; k <= m_2; k++)
                {

                    if (mMult[k - 1][0] == -1)
                    {
                        mMult[k - 1][0] = m_2 + k - 1;
                    }
                    else
                    {
                        mMult[k - 1][1] = m_2 + k - 1;
                    }

                    if (mMult[m_2 + k - 1][0] == -1)
                    {
                        mMult[m_2 + k - 1][0] = k - 1;
                    }
                    else
                    {
                        mMult[m_2 + k - 1][1] = k - 1;
                    }
                }
            }
            else if (mType == 2)
            {
                for (int k = 1; k < p - 1; k++)
                {
                    if (mMult[F[k + 1]][0] == -1)
                    {
                        mMult[F[k + 1]][0] = F[p - k];
                    }
                    else
                    {
                        mMult[F[k + 1]][1] = F[p - k];
                    }
                }
            }
            else
            {
                throw new RuntimeException("only type 1 or type 2 implemented");
            }
        }
        else
        {
            throw new RuntimeException("bisher nur fuer Gausssche Normalbasen"
                + " implementiert");
        }
    }

    private int elementOfOrder(int k, int p)
    {
        Random random = new Random();
        int m = 0;
        while (m == 0)
        {
            m = random.nextInt();
            m %= p - 1;
            if (m < 0)
            {
                m += p - 1;
            }
        }

        int l = IntegerFunctions.order(m, p);

        while (l % k != 0 || l == 0)
        {
            while (m == 0)
            {
                m = random.nextInt();
                m %= p - 1;
                if (m < 0)
                {
                    m += p - 1;
                }
            }
            l = IntegerFunctions.order(m, p);
        }
        int r = m;

        l = k / l;

        for (int i = 2; i <= l; i++)
        {
            r *= m;
        }

        return r;
    }

}
