r"""
Submodules and subquotients of free modules

Free modules and submodules of a free module (of finite rank) over a principal
ideal domain have well-defined notion of rank, and they are implemented in
:mod:`sage.modules.free_module`. Here submodules with no rank are
implemented. For example, submodules of free modules over multivariate
polynomial rings with more than one variables have no notion of rank.

EXAMPLES::

    sage: S.<x,y,z> = PolynomialRing(QQ)
    sage: M = S**2
    sage: N = M.submodule([vector([x - y, z]), vector([y * z, x * z])])
    sage: N
    Submodule of Ambient free module of rank 2 over the integral domain Multivariate Polynomial Ring in x, y, z over Rational Field
    Generated by the rows of the matrix:
    [x - y     z]
    [  y*z   x*z]

AUTHORS:

- Kwankyu Lee (2022-05): initial version
"""

# ****************************************************************************
#       Copyright (C) 2022 Kwankyu Lee <ekwankyu@gmail.com>
#
# 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.
#                  https://www.gnu.org/licenses/
# ****************************************************************************

from sage.modules.free_module import (
    FreeModule_ambient,
    Module_free_ambient,
    basis_seq,
)
from sage.modules.quotient_module import QuotientModule_free_ambient


class Submodule_free_ambient(Module_free_ambient):
    """
    Base class of submodules of ambient modules.

    The ambient module is either a free module or a quotient of a free module
    by a submodule.

    Note that if the ambient module is a quotient module, submodules
    of the quotient module are called subquotients.

    INPUT:

    - ``ambient`` -- an ambient module

    - ``gens`` -- vectors of the ambient free module generating this submodule

    - ``check`` -- boolean; if ``True``, vectors in ``gens`` are checked whether
      they belong to the ambient free module

    - ``already_echelonized`` -- ignored; for compatibility with other submodules

    EXAMPLES::

        sage: S.<x,y,z> = PolynomialRing(QQ)
        sage: M = S**2
        sage: N = M.submodule([vector([x - y, z]), vector([y * z, x * z])])
        sage: N
        Submodule of Ambient free module of rank 2 over the integral domain
        Multivariate Polynomial Ring in x, y, z over Rational Field
        Generated by the rows of the matrix:
        [x - y     z]
        [  y*z   x*z]

    ::

        sage: M.coerce_map_from(N)
        Coercion map:
          From: Submodule of Ambient free module of rank 2 over the integral domain
        Multivariate Polynomial Ring in x, y, z over Rational Field
        Generated by the rows of the matrix:
        [x - y     z]
        [  y*z   x*z]
          To:   Ambient free module of rank 2 over the integral domain
        Multivariate Polynomial Ring in x, y, z over Rational Field
    """
    def __init__(self, ambient, gens, check=True, already_echelonized=False):
        r"""
        Initialize.

        TESTS::

            sage: S.<x,y,z> = PolynomialRing(QQ)
            sage: M = S**2
            sage: N = M.submodule([vector([x - y, z]), vector([y * z, x * z])])
            sage: N.is_submodule(M)
            True

        ::

            sage: R.<x,y> = QQ[]
            sage: S = R.quotient([x*y])
            sage: M = FreeModule(S, 2)
            sage: M.zero_submodule()
            Submodule of Ambient free module of rank 2 over Quotient of Multivariate Polynomial Ring in x, y
             over Rational Field by the ideal (x*y)
              Generated by the rows of the matrix:
              []
        """
        if not isinstance(ambient, (FreeModule_ambient, QuotientModule_free_ambient)):
            raise TypeError("ambient (=%s) must be ambient or a quotient" % ambient)
        R = ambient.base_ring()
        degree = ambient.degree()
        sparse = ambient.is_sparse()
        self._ambient = ambient

        if check:
            try:
                # convert generator vectors to elements of the ambient module
                gens = [ambient(x) for x in gens]
            except TypeError:
                raise TypeError("each element of basis must be in the ambient free module")

        Module_free_ambient.__init__(self, base_ring=R, degree=degree, sparse=sparse)

        C = self.element_class
        w = [C(self, x.list(), coerce=False, copy=False) for x in gens if x]
        self.__gens = basis_seq(self, w)

    def _groebner_basis_contains(self, v):
        r"""
        Check membership of ``v`` in ``self`` using Gröbner bases.

        This works for submodules over polynomial rings and quotient rings
        of polynomial rings. The algorithm lifts elements to the covering
        polynomial ring (if necessary), augments the generator matrix with
        the ideal relation generators, computes a Gröbner basis for the
        resulting submodule of the free module over the polynomial ring,
        and then uses Singular's ``reduce`` to test if `v` reduces to zero.

        INPUT:

        - ``v`` -- a vector in the ambient free module

        OUTPUT: ``True`` if ``v`` is in the submodule, ``False`` otherwise

        EXAMPLES::

            sage: P.<a,b> = PolynomialRing(QQ, 2)
            sage: q = P.quotient([a^11, b^10])
            sage: M = FreeModule(q, 10)
            sage: s = M.submodule([M.0])
            sage: s._groebner_basis_contains(M.0)
            True
            sage: s._groebner_basis_contains(M.1)
            False

        Over a polynomial ring (no quotient)::

            sage: S.<x,y,z> = PolynomialRing(QQ)
            sage: M = S**2
            sage: N = M.submodule([vector([x - y, z]), vector([y*z, x*z])])
            sage: N._groebner_basis_contains(vector([x - y, z]))
            True
            sage: N._groebner_basis_contains(vector([1, 0]))
            False

        When the ambient module is a quotient module (subquotient)::

            sage: S.<x,y,z> = PolynomialRing(QQ)
            sage: M = S**2
            sage: N = M.submodule([vector([x - y, z]), vector([y*z, x*z])])
            sage: Q = M.quotient_module(N)
            sage: NQ = Q.submodule([Q([1, x])])
            sage: NQ._groebner_basis_contains(Q([1, x]).list())
            True
            sage: NQ._groebner_basis_contains(Q([0, 1]).list())
            False

        TESTS::

            sage: P.<a,b> = PolynomialRing(QQ, 2)
            sage: q = P.quotient([a^11, b^10])
            sage: M = FreeModule(q, 3)
            sage: s = M.submodule([M.0, M.1])
            sage: s._groebner_basis_contains(M.0 + M.1)
            True
            sage: s._groebner_basis_contains(M.2)
            False
        """
        from sage.libs.singular.function import singular_function
        from sage.libs.singular.option import opt_verb
        from sage.matrix.constructor import matrix
        from sage.rings.quotient_ring import QuotientRing_generic
        from sage.structure.element import Vector

        R = self.base_ring()
        n = self.degree()

        # Determine the polynomial ring and ideal generators
        if isinstance(R, QuotientRing_generic):
            poly_ring = R.cover_ring()
            ideal_gens = R.defining_ideal().gens()
            do_lift = True
        else:
            from sage.rings.polynomial.multi_polynomial_ring_base import \
                MPolynomialRing_base
            if isinstance(R, MPolynomialRing_base):
                poly_ring = R
                ideal_gens = []
                do_lift = False
            else:
                raise NotImplementedError(
                    "Gröbner basis membership test is not implemented for "
                    "modules over {}".format(R)
                )

        # Suppress "_ is no standard basis" warning from Singular
        opt_verb['not_warn_sb'] = True

        std = singular_function('std')
        reduce_func = singular_function('reduce')

        # Lift generators of self to the polynomial ring
        gen_rows = []
        for g in self.gens():
            if do_lift:
                row = [c.lift() for c in g]
            else:
                row = list(g)
            gen_rows.append(row)

        # Add ideal relation generators: for each ideal generator f and
        # each standard basis vector e_j, add f * e_j as a row
        for f in ideal_gens:
            for j in range(n):
                row = [poly_ring.zero()] * n
                row[j] = f
                gen_rows.append(row)

        if not gen_rows:
            # Zero submodule: only zero vector is contained
            if isinstance(v, (list, tuple, Vector)):
                return all(c == 0 for c in v)
            return v == 0

        # Build the matrix and compute Gröbner basis
        # Singular's std() for modules expects the matrix transposed
        # (columns = generators)
        gen_matrix = matrix(poly_ring, gen_rows).transpose()
        gb = std(gen_matrix, ring=poly_ring)

        # Lift the target vector
        if do_lift:
            v_lifted = matrix(poly_ring, [[c.lift() for c in v]]).transpose()
        else:
            v_lifted = matrix(poly_ring, [list(v)]).transpose()

        # Reduce v modulo the Gröbner basis; zero result means v is in the submodule
        remainder = reduce_func(v_lifted, gb, ring=poly_ring)
        return matrix(remainder).is_zero()

    def _check_element_membership(self, x):
        r"""
        Check that ``x`` belongs to this submodule using Gröbner bases.

        This overrides the no-op in the base class to verify actual submodule
        membership. For rings where Gröbner basis computation is not
        implemented, this falls back to no check.

        EXAMPLES::

            sage: P.<a,b> = PolynomialRing(QQ, 2)
            sage: q = P.quotient([a^11, b^10])
            sage: M = FreeModule(q, 10)
            sage: s = M.submodule([M.0])
            sage: s._check_element_membership(M.0.list())  # no error
            sage: s._check_element_membership(M.1.list())
            Traceback (most recent call last):
            ...
            TypeError: element (0, 1, 0, 0, 0, 0, 0, 0, 0, 0) is not in this submodule
        """
        try:
            if not self._groebner_basis_contains(x):
                raise ArithmeticError
        except NotImplementedError:
            # For rings where we can't do the Gröbner basis check,
            # fall back to no check (old behavior)
            pass
        except ArithmeticError:
            raise TypeError(
                "element {} is not in this submodule".format(
                    tuple(x)
                )
            )

    def _repr_(self):
        """
        Return a string representation of ``self``.

        EXAMPLES::

            sage: S.<x,y,z> = PolynomialRing(QQ)
            sage: M = S**2
            sage: N = M.submodule([vector([x - y, z]), vector([y*z, x*z])])
            sage: N
            Submodule of Ambient free module of rank 2 over the integral domain
            Multivariate Polynomial Ring in x, y, z over Rational Field
            Generated by the rows of the matrix:
            [x - y     z]
            [  y*z   x*z]

            sage: Q = M.quotient_module(N)
            sage: NQ = Q.submodule([Q([1,x])])
            sage: NQ
            Subquotient of Ambient free module of rank 2 over the integral domain Multivariate Polynomial Ring in x, y, z over Rational Field
            Generated by the rows of the matrix:
            [1 x]
            With relations matrix:
            [x - y     z]
            [  y*z   x*z]
        """
        if isinstance(self._ambient, QuotientModule_free_ambient):
            return ("Subquotient of %s\n" % self.ambient_module().free_cover() +
                    "Generated by the rows of the matrix:\n%s\n" % self.matrix() +
                    "With relations matrix:\n%s" % self._ambient.free_relations().matrix())
        return ("Submodule of %s\n" % self.ambient_module() +
                "Generated by the rows of the matrix:\n%s" % self.matrix())

    def matrix(self):
        """
        Return the matrix defining ``self``.

        EXAMPLES::

            sage: S.<x,y,z> = PolynomialRing(QQ)
            sage: M = S**2
            sage: N = M.submodule([vector([x - y, z]), vector([y*z, x*z])])
            sage: N.matrix()
            [x - y     z]
            [  y*z   x*z]
        """
        from sage.matrix.matrix_space import MatrixSpace
        MAT = MatrixSpace(self.base_ring(), len(self.gens()), self.degree(),
                          sparse=self.is_sparse())
        A = MAT(self.gens())
        A.set_immutable()
        return A

    generators_matrix = matrix

    def relations(self):
        r"""
        Return the relations module of the ambient module.

        If the ambient module is free, then the relations module is trivial.

        EXAMPLES::

            sage: S.<x,y,z> = PolynomialRing(QQ)
            sage: M = S**2
            sage: N = M.submodule([vector([x - y, z]), vector([y * z, x * z])])
            sage: N.relations() == M.zero_submodule()
            True

            sage: Q = M.quotient(N)
            sage: Q.zero_submodule().relations() == N
            True
        """
        return self._ambient.relations()

    def gens(self) -> list:
        """
        Return the generators of this submodule.

        EXAMPLES::

            sage: S.<x,y,z> = PolynomialRing(QQ)
            sage: M = S**2
            sage: N = M.submodule([vector([x - y, z]), vector([y * z, x * z])])
            sage: N.gens()
            [(x - y, z), (y*z, x*z)]
        """
        return self.__gens

    def gen(self, i=0):
        """
        Return the `i`-th generator of this module.

        EXAMPLES::

            sage: S.<x,y,z> = PolynomialRing(QQ)
            sage: M = S**2
            sage: N = M.submodule([vector([x - y, z]), vector([y*z, x*z])])
            sage: N.gen(0)
            (x - y, z)
        """
        if i < 0 or i >= len(self.__gens):
            raise ValueError('no generator with index %s' % i)
        return self.__gens[i]

    def ambient_module(self):
        """
        Return the ambient module of ``self``.

        EXAMPLES::

            sage: S.<x,y,z> = PolynomialRing(QQ)
            sage: M = S**2
            sage: N = M.submodule([vector([x - y, z]), vector([y * z, x * z])])
            sage: N.ambient_module() is M
            True
            sage: N.zero_submodule().ambient_module() is M
            True
            sage: Q = M / N
            sage: Q.zero_submodule().ambient_module() is Q
            True
        """
        return self._ambient
