"""
Module of Supersingular Points

The module of divisors on the modular curve `X_0(N)` over `F_p` supported at supersingular points.

AUTHORS:

- William Stein

- David Kohel

- Iftikhar Burhanuddin

EXAMPLES::

    sage: x = SupersingularModule(389)
    sage: m = x.T(2).matrix()
    sage: a = m.change_ring(GF(97))
    sage: D = a.decomposition()
    sage: D[:3]
    [
    (Vector space of degree 33 and dimension 1 over Finite Field of size 97
    Basis matrix:
    [ 0  0  0  1 96 96  1  0 95  1  1  1  1 95  2 96  0  0 96  0 96  0 96  2 96 96  0  1  0  2  1 95  0], True),
    (Vector space of degree 33 and dimension 1 over Finite Field of size 97
    Basis matrix:
    [ 0  1 96 16 75 22 81  0  0 17 17 80 80  0  0 74 40  1 16 57 23 96 81  0 74 23  0 24  0  0 73  0  0], True),
    (Vector space of degree 33 and dimension 1 over Finite Field of size 97
    Basis matrix:
    [ 0  1 96 90 90  7  7  0  0 91  6  6 91  0  0 91  0 13  7  0  6 84 90  0  6 91  0 90  0  0  7  0  0], True)
    ]
    sage: len(D)
    9

We compute a Hecke operator on a space of huge dimension!::

    sage: X = SupersingularModule(next_prime(10000))
    sage: t = X.T(2).matrix()            # long time (21s on sage.math, 2011)
    sage: t.nrows()                      # long time
    835

TESTS::

    sage: X = SupersingularModule(389)
    sage: T = X.T(2).matrix().change_ring(QQ)
    sage: d = T.decomposition()
    sage: len(d)
    6
    sage: [a[0].dimension() for a in d]
    [1, 1, 2, 3, 6, 20]
    sage: loads(dumps(X)) == X
    True
    sage: loads(dumps(d)) == d
    True
"""

#*****************************************************************************
#       Copyright (C) 2004, 2006 William Stein <wstein@gmail.com>
#       Copyright (C) 2006 David Kohel <kohel@maths.usyd.edu.au>
#       Copyright (C) 2006 Iftikhar Burhanuddin <burhanud@usc.edu>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#                  http://www.gnu.org/licenses/
#*****************************************************************************


import math

import sage.modular.hecke.all as hecke
import sage.rings.all as rings
from sage.arith.all import kronecker, next_prime
from sage.matrix.matrix_space import MatrixSpace
from sage.modular.arithgroup.all import Gamma0
from sage.libs.pari.all import pari
from sage.misc.misc import verbose
from sage.structure.richcmp import richcmp_method, richcmp

ZZy = rings.PolynomialRing(rings.ZZ, 'y')


def Phi2_quad(J3, ssJ1, ssJ2):
    r"""
    This function returns a certain quadratic polynomial over a finite
    field in indeterminate J3.

    The roots of the polynomial along with ssJ1 are the
    neighboring/2-isogenous supersingular j-invariants of ssJ2.

    INPUT:

    - ``J3`` -- indeterminate of a univariate polynomial ring defined over a finite
      field with p^2 elements where p is a prime number

    - ``ssJ2``, ``ssJ2`` -- supersingular j-invariants over the finite field

    OUTPUT:

    - polynomial -- defined over the finite field

    EXAMPLES:

    The following code snippet produces a factor of the modular polynomial
    `\Phi_{2}(x,j_{in})`, where `j_{in}` is a supersingular j-invariant
    defined over the finite field with `37^2` elements::

        sage: F = GF(37^2, 'a')
        sage: X = PolynomialRing(F, 'x').gen()
        sage: j_in = supersingular_j(F)
        sage: poly = sage.modular.ssmod.ssmod.Phi_polys(2,X,j_in)
        sage: poly.roots()
        [(8, 1), (27*a + 23, 1), (10*a + 20, 1)]
        sage: sage.modular.ssmod.ssmod.Phi2_quad(X, F(8), j_in)
        x^2 + 31*x + 31

    .. note::

        Given a root (j1,j2) to the polynomial `Phi_2(J1,J2)`, the pairs
        (j2,j3) not equal to (j2,j1) which solve `Phi_2(j2,j3)` are roots of
        the quadratic equation:

        J3^2 + (-j2^2 + 1488*j2 + (j1 - 162000))*J3 + (-j1 + 1488)*j2^2 +
        (1488*j1 + 40773375)*j2 + j1^2 - 162000*j1 + 8748000000

        This will be of use to extend the 2-isogeny graph, once the initial
        three roots are determined for `Phi_2(J1,J2)`.

    AUTHORS:

    - David Kohel -- kohel@maths.usyd.edu.au

    - Iftikhar Burhanuddin -- burhanud@usc.edu
    """
    ssJ1_pow2 = ssJ1**2
    ssJ2_pow2 = ssJ2**2

    return J3.parent()([(-ssJ1 + 1488)*ssJ2_pow2+ (1488*ssJ1 +
    40773375)*ssJ2 + ssJ1_pow2 - 162000*ssJ1 + 8748000000,
    -ssJ2_pow2 + 1488*ssJ2 + (ssJ1 - 162000),
    1])



def Phi_polys(L, x, j):
    r"""
    This function returns a certain polynomial of degree `L+1` in the
    indeterminate x over a finite field.

    The roots of the **modular** polynomial `\Phi(L, x, j)` are the
    `L`-isogenous supersingular j-invariants of j.

    INPUT:

    - ``L`` -- integer

    - ``x`` -- indeterminate of a univariate polynomial ring defined over a
      finite field with p^2 elements, where p is a prime number

    - ``j`` -- supersingular j-invariant over the finite field

    OUTPUT:

    - polynomial -- defined over the finite field

    EXAMPLES:

    The following code snippet produces the modular polynomial
    `\Phi_{L}(x,j_{in})`, where `j_{in}` is a supersingular j-invariant
    defined over the finite field with `7^2` elements::

        sage: F = GF(7^2, 'a')
        sage: X = PolynomialRing(F, 'x').gen()
        sage: j_in = supersingular_j(F)
        sage: sage.modular.ssmod.ssmod.Phi_polys(2,X,j_in)
        x^3 + 3*x^2 + 3*x + 1
        sage: sage.modular.ssmod.ssmod.Phi_polys(3,X,j_in)
        x^4 + 4*x^3 + 6*x^2 + 4*x + 1
        sage: sage.modular.ssmod.ssmod.Phi_polys(5,X,j_in)
        x^6 + 6*x^5 + x^4 + 6*x^3 + x^2 + 6*x + 1
        sage: sage.modular.ssmod.ssmod.Phi_polys(7,X,j_in)
        x^8 + x^7 + x + 1
        sage: sage.modular.ssmod.ssmod.Phi_polys(11,X,j_in)
        x^12 + 5*x^11 + 3*x^10 + 3*x^9 + 5*x^8 + x^7 + x^5 + 5*x^4 + 3*x^3 + 3*x^2 + 5*x + 1
        sage: sage.modular.ssmod.ssmod.Phi_polys(13,X,j_in)
        x^14 + 2*x^7 + 1
    """
    r = 0
    for pol in pari.polmodular(L).Vec():
        r = r*x + ZZy(pol)(j)
    return r


def dimension_supersingular_module(prime, level=1):
    r"""
    This function returns the dimension of the Supersingular module, which is
    equal to the dimension of the space of modular forms of weight `2`
    and conductor equal to prime times level.

    INPUT:

    - ``prime`` -- integer, prime

    - ``level`` -- integer, positive

    OUTPUT:
       dimension -- integer, nonnegative

    EXAMPLES:

    The code below computes the dimensions of Supersingular modules
    with level=1 and prime = 7, 15073 and 83401::

        sage: dimension_supersingular_module(7)
        1

        sage: dimension_supersingular_module(15073)
        1256

        sage: dimension_supersingular_module(83401)
        6950

    NOTES:
    The case of level > 1 has not been implemented yet.

    AUTHORS:

    - David Kohel -- kohel@maths.usyd.edu.au

    - Iftikhar Burhanuddin - burhanud@usc.edu
    """
    if not(rings.Integer(prime).is_prime()):
        raise ValueError("%s is not a prime"%prime)

    if level == 1:
        return Gamma0(prime).dimension_modular_forms(2)

    #list of genus(X_0(level)) equal to zero
    #elif (level in [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 16, 18, 25]):
    #compute basis

    else:
        raise NotImplementedError


def supersingular_D(prime):
    r"""
    This function returns a fundamental discriminant `D` of an
    imaginary quadratic field, where the given prime does not split
    (see Silverman's Advanced Topics in the Arithmetic of Elliptic
    Curves, page 184, exercise 2.30(d).)

    INPUT:

    - prime -- integer, prime

    OUTPUT:
        D -- integer, negative

    EXAMPLES:

    These examples return *supersingular discriminants* for 7,
    15073 and 83401::

        sage: supersingular_D(7)
        -4

        sage: supersingular_D(15073)
        -15

        sage: supersingular_D(83401)
        -7

    AUTHORS:

    - David Kohel - kohel@maths.usyd.edu.au

    - Iftikhar Burhanuddin - burhanud@usc.edu
    """
    if not(rings.Integer(prime).is_prime()):
        raise ValueError("%s is not a prime"%prime)

    #Making picking D more intelligent
    D = -1
    while True:
        Dmod4 = rings.Mod(D,4)
        if Dmod4 in (0,1) and (kronecker(D,prime) != 1):
            return D
        D = D - 1

def supersingular_j(FF):
    r"""
    This function returns a supersingular j-invariant over the finite
    field FF.

    INPUT:

    - ``FF``  -- finite field with p^2 elements, where p is a prime number

    OUTPUT:
       finite field element -- a supersingular j-invariant
       defined over the finite field FF

    EXAMPLES:

    The following examples calculate supersingular j-invariants for a
    few finite fields::

        sage: supersingular_j(GF(7^2, 'a'))
        6

    Observe that in this example the j-invariant is not defined over
    the prime field::

        sage: supersingular_j(GF(15073^2, 'a'))
        4443*a + 13964
        sage: supersingular_j(GF(83401^2, 'a'))
        67977

    AUTHORS:

    - David Kohel -- kohel@maths.usyd.edu.au

    - Iftikhar Burhanuddin -- burhanud@usc.edu
    """
    if not(FF.is_field()) or not(FF.is_finite()):
        raise ValueError("%s is not a finite field"%FF)
    prime = FF.characteristic()
    if not(rings.Integer(prime).is_prime()):
        raise ValueError("%s is not a prime"%prime)
    if not(rings.Integer(FF.cardinality())) == rings.Integer(prime**2):
        raise ValueError("%s is not a quadratic extension"%FF)
    if kronecker(-1, prime) != 1:
        j_invss = 1728                 #(2^2 * 3)^3
    elif kronecker(-2, prime) != 1:
        j_invss = 8000                 #(2^2 * 5)^3
    elif kronecker(-3, prime) != 1:
        j_invss = 0                    #0^3
    elif kronecker(-7, prime) != 1:
        j_invss = 16581375             #(3 * 5 * 17)^3
    elif kronecker(-11, prime) != 1:
        j_invss = -32768               #-(2^5)^3
    elif kronecker(-19, prime) != 1:
        j_invss = -884736              #-(2^5 * 3)^3
    elif kronecker(-43, prime) != 1:
        j_invss = -884736000           #-(2^6 * 3 * 5)^3
    elif kronecker(-67, prime) != 1:
        j_invss = -147197952000        #-(2^5 * 3 * 5 * 11)^3
    elif kronecker(-163, prime) != 1:
        j_invss = -262537412640768000  #-(2^6 * 3 * 5 * 23 * 29)^3
    else:
        D = supersingular_D(prime)
        hc_poly = FF['x'](pari(D).polclass())
        root_hc_poly_list = list(hc_poly.roots())

        j_invss = root_hc_poly_list[0][0]
    return FF(j_invss)


@richcmp_method
class SupersingularModule(hecke.HeckeModule_free_module):
    r"""
    The module of supersingular points in a given characteristic, with
    given level structure.

    The characteristic must not divide the level.

    NOTE: Currently, only level 1 is implemented.

    EXAMPLES::

        sage: S = SupersingularModule(17)
        sage: S
        Module of supersingular points on X_0(1)/F_17 over Integer Ring
        sage: S = SupersingularModule(16)
        Traceback (most recent call last):
        ...
        ValueError: the argument prime must be a prime number
        sage: S = SupersingularModule(prime=17, level=34)
        Traceback (most recent call last):
        ...
        ValueError: the argument level must be coprime to the argument prime
        sage: S = SupersingularModule(prime=17, level=5)
        Traceback (most recent call last):
        ...
        NotImplementedError: supersingular modules of level > 1 not yet implemented
    """
    def __init__(self, prime=2, level=1, base_ring=rings.IntegerRing()):
        r"""
        Create a supersingular module.

        EXAMPLES::

            sage: SupersingularModule(3)
            Module of supersingular points on X_0(1)/F_3 over Integer Ring
        """

        if not prime.is_prime():
            raise ValueError("the argument prime must be a prime number")
        if prime.divides(level):
            raise ValueError("the argument level must be coprime to the argument prime")
        if level != 1:
            raise NotImplementedError("supersingular modules of level > 1 not yet implemented")
        self.__prime = prime
        from sage.rings.all import FiniteField
        self.__finite_field = FiniteField(prime**2,'a')
        self.__level = level
        self.__hecke_matrices = {}
        hecke.HeckeModule_free_module.__init__(
            self, base_ring, prime*level, weight=2)

    def _repr_(self):
        """
        String representation of self

        EXAMPLES::

            sage: SupersingularModule(11)._repr_()
            'Module of supersingular points on X_0(1)/F_11 over Integer Ring'
        """

        return "Module of supersingular points on X_0(%s)/F_%s over %s"%(
            self.__level, self.__prime, self.base_ring())

    def __richcmp__(self, other, op):
        r"""
        Compare ``self`` to ``other``.

        EXAMPLES::

            sage: SupersingularModule(37) == ModularForms(37, 2)
            False
            sage: SupersingularModule(37) == SupersingularModule(37, base_ring=Qp(7))
            False
            sage: SupersingularModule(37) == SupersingularModule(37)
            True
        """
        if not isinstance(other, SupersingularModule):
            return NotImplemented
        return richcmp((self.__level, self.__prime, self.base_ring()),
                       (other.__level, other.__prime, other.base_ring()), op)

    def free_module(self):
        """
        EXAMPLES::

            sage: X = SupersingularModule(37)
            sage: X.free_module()
            Ambient free module of rank 3 over the principal ideal domain Integer Ring

        This illustrates the fix at :trac:`4306`::

            sage: X = SupersingularModule(389)
            sage: X.basis()
            ((1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0),
             (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1))
        """
        return rings.ZZ**self.dimension()

    def dimension(self):
        r"""
        Return the dimension of the space of modular forms of weight 2
        and level equal to the level associated to self.

        INPUT:

        - ``self`` -- SupersingularModule object

        OUTPUT:
            integer -- dimension, nonnegative

        EXAMPLES::

            sage: S = SupersingularModule(7)
            sage: S.dimension()
            1

            sage: S = SupersingularModule(15073)
            sage: S.dimension()
            1256

            sage: S = SupersingularModule(83401)
            sage: S.dimension()
            6950

        NOTES:
           The case of level > 1 has not yet been implemented.

        AUTHORS:

        - David Kohel -- kohel@maths.usyd.edu.au

        - Iftikhar Burhanuddin -- burhanud@usc.edu
        """
        try:
            return self.__dimension
        except AttributeError:
            pass
        if self.__level == 1:
            G = Gamma0(self.__prime)
            self.__dimension = G.dimension_modular_forms(2)
        else:
            raise NotImplementedError
        return self.__dimension

    rank = dimension

    def level(self):
        r"""
        This function returns the level associated to self.

        INPUT:

        - ``self`` -- SupersingularModule object

        OUTPUT:
            integer -- the level, positive

        EXAMPLES::

            sage: S = SupersingularModule(15073)
            sage: S.level()
            1

        AUTHORS:

        - David Kohel -- kohel@maths.usyd.edu.au

        - Iftikhar Burhanuddin -- burhanud@usc.edu
        """
        return self.__level

    def prime(self):
        r"""
        This function returns the characteristic of the finite field
        associated to self.

        INPUT:

        - ``self`` -- SupersingularModule object

        OUTPUT:

        - integer -- characteristic, positive

        EXAMPLES::

            sage: S = SupersingularModule(19)
            sage: S.prime()
            19

        AUTHORS:

        - David Kohel -- kohel@maths.usyd.edu.au

        - Iftikhar Burhanuddin -- burhanud@usc.edu
        """
        return self.__prime

    def weight(self):
        r"""
        This function returns the weight associated to self.

        INPUT:

        - ``self`` -- SupersingularModule object

        OUTPUT:
            integer -- weight, positive

        EXAMPLES::

            sage: S = SupersingularModule(19)
            sage: S.weight()
            2

        AUTHORS:

        - David Kohel -- kohel@maths.usyd.edu.au

        - Iftikhar Burhanuddin -- burhanud@usc.edu
        """
        return 2

    def supersingular_points(self):
        r"""
        This function computes the supersingular j-invariants over the
        finite field associated to self.

        INPUT:

        -  ``self`` -- SupersingularModule object

        OUTPUT: list_j, dict_j -- list_j is the list of supersingular
            j-invariants, dict_j is a dictionary with these
            j-invariants as keys and their indexes as values. The
            latter is used to speed up j-invariant look-up. The
            indexes are based on the order of their *discovery*.

        EXAMPLES:

        The following examples calculate supersingular j-invariants
        over finite fields with characteristic 7, 11 and 37::

            sage: S = SupersingularModule(7)
            sage: S.supersingular_points()
            ([6], {6: 0})

            sage: S = SupersingularModule(11)
            sage: S.supersingular_points()
            ([1, 0], {0: 1, 1: 0})

            sage: S = SupersingularModule(37)
            sage: S.supersingular_points()
            ([8, 27*a + 23, 10*a + 20], {8: 0, 10*a + 20: 2, 27*a + 23: 1})

        AUTHORS:

        - David Kohel -- kohel@maths.usyd.edu.au

        - Iftikhar Burhanuddin -- burhanud@usc.edu
        """
        try:
            return (self._ss_points_dic, self._ss_points)
        except AttributeError:
            pass
        Fp2 = self.__finite_field
        level = self.__level
        prime = Fp2.characteristic()
        X = Fp2['x'].gen()
        jinv = supersingular_j(Fp2)

        dim = dimension_supersingular_module(prime, level)

        pos = int(0)
        #using list to keep track of explored nodes using pos
        ss_points = [jinv]

        #using  to keep track of index of the previous node
        ss_points_pre = [-1]

        #using dictionary for fast j-invariant look-up
        ss_points_dic = {jinv:pos}

        T2_matrix = MatrixSpace(rings.Integers(), dim, sparse=True)(0)

        while pos < len(ss_points):
            if pos == 0:
                neighbors = Phi_polys(2,X,ss_points[pos]).roots()
            else:
                j_prev = ss_points_pre[pos]
                # TODO: These are quadratic polynomials -- maybe we should use the
                # quadratic formula and fast square root finding (??)
                neighbors = Phi2_quad(X, ss_points[j_prev], ss_points[pos]).roots()

            for (xj,ej) in neighbors:
                if xj not in ss_points_dic:
                    j = len(ss_points)
                    ss_points += [xj]
                    ss_points_pre += [pos]
                    ss_points_dic[xj] = j
                else:
                    j = ss_points_dic[xj]
                T2_matrix[pos, j] += ej
            # end for
            if pos != 0:
                # also record the root from j_prev
                T2_matrix[pos, j_prev] += 1
            pos += int(1)

        self.__hecke_matrices[2] = T2_matrix
        return (ss_points, ss_points_dic)


    def upper_bound_on_elliptic_factors(self, p=None, ellmax=2):
        r"""
        Return an upper bound (provably correct) on the number of
        elliptic curves of conductor equal to the level of this
        supersingular module.

        INPUT:

        - ``p`` - (default: 997) prime to work modulo

        ALGORITHM: Currently we only use `T_2`.  Function will be
        extended to use more Hecke operators later.

        The prime p is replaced by the smallest prime that doesn't
        divide the level.

        EXAMPLES::

            sage: SupersingularModule(37).upper_bound_on_elliptic_factors()
            2

        (There are 4 elliptic curves of conductor 37, but only 2 isogeny
        classes.)
        """
        # NOTE: The heuristic runtime is *very* roughly `p^2/(2\cdot 10^6)`.
        #ellmax -- (default: 2) use Hecke operators T_ell with ell <= ellmax
        if p is None:
            p = 997

        while self.level() % p == 0:
             p = next_prime(p)

        ell = 2
        t = self.hecke_matrix(ell).change_ring(rings.GF(p))

        # TODO: temporarily try using sparse=False
        # turn this off when sparse rank is optimized.
        t = t.dense_matrix()

        B = 2*math.sqrt(ell)
        bnd = 0
        lower = -int(math.floor(B))
        upper = int(math.floor(B))+1
        for a in range(lower, upper):
            tm = verbose("computing T_%s - %s"%(ell, a))
            if a == lower:
                c = a
            else:
                c = 1
            for i in range(t.nrows()):
                t[i,i] += c
            tm = verbose("computing kernel",tm)
            #dim = t.kernel().dimension()
            dim = t.nrows() - t.rank()
            bnd += dim
            verbose('got dimension = %s; new bound = %s'%(dim, bnd), tm)
        return bnd

    def hecke_matrix(self,L):
        r"""
        This function returns the `L^{\text{th}}` Hecke matrix.

        INPUT:

        - ``self`` -- SupersingularModule object

        - ``L`` -- integer, positive

        OUTPUT:
            matrix -- sparse integer matrix

        EXAMPLES:

        This example computes the action of the Hecke operator `T_2`
        on the module of supersingular points on `X_0(1)/F_{37}`::

            sage: S = SupersingularModule(37)
            sage: M = S.hecke_matrix(2)
            sage: M
            [1 1 1]
            [1 0 2]
            [1 2 0]

        This example computes the action of the Hecke operator `T_3`
        on the module of supersingular points on `X_0(1)/F_{67}`::

            sage: S = SupersingularModule(67)
            sage: M = S.hecke_matrix(3)
            sage: M
            [0 0 0 0 2 2]
            [0 0 1 1 1 1]
            [0 1 0 2 0 1]
            [0 1 2 0 1 0]
            [1 1 0 1 0 1]
            [1 1 1 0 1 0]

        .. note::

            The first list --- list_j --- returned by the supersingular_points
            function are the rows *and* column indexes of the above hecke
            matrices and its ordering should be kept in mind when interpreting
            these matrices.

        AUTHORS:

        - David Kohel -- kohel@maths.usyd.edu.au

        - Iftikhar Burhanuddin -- burhanud@usc.edu
        """
        if L in self.__hecke_matrices:
            return self.__hecke_matrices[L]
        SS, II = self.supersingular_points()
        if L == 2:
            # since T_2 gets computed as a side effect of computing the supersingular points
            return self.__hecke_matrices[2]
        Fp2 = self.__finite_field
        h = len(SS)
        R = self.base_ring()
        T_L = MatrixSpace(R,h)(0)
        S, X = Fp2['x'].objgen()

        for i in range(len(SS)):
            ss_i = SS[i]
            phi_L_in_x = Phi_polys(L, X, ss_i)
            rts = phi_L_in_x.roots()
            for r in rts:
                T_L[i,int(II[r[0]])] = r[1]

        self.__hecke_matrices[L] = T_L
        return T_L
