# -*- coding: utf-8 -*-
r"""
Cut and project schemes and model sets

Based on the definitions presented in the book [BG13]_.

AUTHORS:

    - Sébastien Labbé, initial version + first plot method, February 2023
      (Sage Days 117, Le Teich)
    - Carole Porrier and Sébastien Labbé, creation of cut-and-project from
      slopes, Golden-Octagonal, Ammann-Beenker, subperiods, February 2025
      (Sage Days 128, Le Teich)

REFERENCES:

.. [BG13] Baake, Michael, Uwe Grimm. Aperiodic order.  Vol. 1. Vol. 149.
   Encyclopedia of Mathematics and its Applications.  Cambridge University
   Press, Cambridge, 2013.
"""
#*****************************************************************************
#       Copyright (C) 2023 Sébastien Labbé <slabqc@gmail.com>
#
#  Distributed under the terms of the GNU General Public License version 2 (GPLv2)
#
#  The full text of the GPLv2 is available at:
#
#                  http://www.gnu.org/licenses/
#*****************************************************************************
from sage.misc.cachefunc import cached_method
from sage.structure.sage_object import SageObject

############################################
# Classes
############################################
class CutAndProjectScheme(SageObject):
    r"""
    INPUT:

    - ``base_ring`` -- ring
    - ``pi`` -- `n\times d` matrix, projection of the ambiant space to
        the physical space
    - ``pi_int`` -- `n\times (n-d)` matrix, projection of the ambiant space
        to the internal space
    - ``lattice`` -- `n\times n` matrix (default:``None``), the columns
      form a base of a lattice in R^n, if ``None``, it uses the `n\times n`
      identity matrix
    - ``check`` -- boolean (default:``True``), whether to check that
        dimensions of the matrices given as input are consistent

    EXAMPLES::

        sage: from slabbe import CutAndProjectScheme
        sage: z = polygen(QQ, 'z')
        sage: K = NumberField(z**2-z-1, 'phi', embedding=RR(1.6))
        sage: phi = K.gen()
        sage: pi = matrix([[phi, 1]])
        sage: pi_int = matrix([[-~phi, 1]])
        sage: lattice = identity_matrix(2)
        sage: cap = CutAndProjectScheme(K, pi, pi_int, lattice)

    """
    def __init__(self, base_ring, pi, pi_int, lattice=None, check=True):
        r"""
        Constructor
        """
        self._base_ring = base_ring
        self._pi = pi
        self._pi_int = pi_int

        if lattice is None:
            from sage.matrix.special import identity_matrix
            lattice = identity_matrix(self._pi.ncols())
        self._lattice = lattice

        if check:
            self._check()

    def _check(self):
        r"""
        Perform checks on the dimension of the matrices given as input
        and raise a ``ValueError`` in case of an inconsistency

        """
        if self._pi.ncols() != self._pi_int.ncols():
            raise ValueError("pi and pi_int matrices must have the same number of columns")

        if self._pi.ncols() != self._pi.nrows() +  self._pi_int.nrows():
            raise ValueError("pi and pi_int matrices must have the same number of columns")

    @classmethod
    def from_slope(self, E, projection='gram_schmidt', verbose=False):
        r"""
        Compute the orthogonal projection on the internal space (orthogonal
        of the slope)

        The *slope* refers to vector space in the ambiant space
        corresponding to kernel of the pi_int projection [FP24]_.

        The construction of the projection in the internal space
        from the slope using Gramm-Schmidt is made acording to Carole
        Porrier's code available at https://github.com/cporrier/Cyrenaic

        INPUT:

        - ``E`` -- matrix, whose rows span the slope
        - ``projection`` -- string (default:``'gram_schmidt'``), 
          possible values are ``'gram_schmidt'`` and ``'roots_of_unity'``
          (works only for n->2 tilings)
        - ``verbose`` -- bool (default:``False``)

        OUTPUT:

        A cut and project scheme whose kernel of the internal projection
        is the vector space E.

        .. TODO::

            Roots of unity are currently using algebraic numbers which are
            slow. Check if they can be turned into number field elements.

        EXAMPLES::

            sage: from slabbe import CutAndProjectScheme
            sage: E = matrix([[1,1,1,1], [1,2,3,4]])
            sage: E
            [1 1 1 1]
            [1 2 3 4]
            sage: c = CutAndProjectScheme.from_slope(E)
            sage: c
            4-to-2 cut and project scheme
            sage: print(c)
            4-to-2 cut and project scheme over
            Rational Field
            Projection to physical space:
            [   1    1    1    1]
            [-3/2 -1/2  1/2  3/2]
            Projection to internal space:
            [   1    0   -3    2]
            [-4/7    1 -2/7 -1/7]
            Lattice generated by the columns of:
            [1 0 0 0]
            [0 1 0 0]
            [0 0 1 0]
            [0 0 0 1]

        ::

            sage: E = matrix([[1,1,1,1], [1,2,3,4]])
            sage: c = CutAndProjectScheme.from_slope(E, projection='roots_of_unity')

        REFERENCES:

        .. [FP24] Thomas Fernique, Carole Porrier, Ammann Bars for
           Octagonal Tilings, Discrete Mathematics & Theoretical Computer
           Science 26 (2024), https://doi.org/10.46298/dmtcs.10764

        """
        import itertools
        from sage.symbolic.constants import pi
        from sage.functions.trig import cos, sin
        from sage.rings.number_field.number_field import CyclotomicField
        from sage.matrix.constructor import matrix

        (d,n) = E.dimensions()
        EF = E.right_kernel_matrix().echelon_form()
        pi_int,M1 = EF.gram_schmidt()
        base_ring = pi_int.base_ring()

        if projection == 'gram_schmidt':
            projection_phys, M2 = E.gram_schmidt()
            return CutAndProjectScheme(base_ring, projection_phys, pi_int)

        elif projection == 'roots_of_unity':
            if d != 2:
                raise ValueError(f"d(={d}) works for only for n->2 tilings")

            projection_phys = pi_int.right_kernel_matrix()
            c = CutAndProjectScheme(base_ring, projection_phys, pi_int)

            entries = [(cos(k*pi/n), sin(k*pi/n)) for k in range(2*n)]
            C_2n = CyclotomicField(2*n)
            C_2n_real, morphism = C_2n.maximal_totally_real_subfield()
            M = matrix.column(C_2n_real, entries)

            # Can we find p directly and avoid a for-loop in what follows?
            for p in itertools.combinations(range(2*n), n):
                projection_phys = M.matrix_from_columns(p)
                if c.is_valid(projection_phys):
                    return CutAndProjectScheme(base_ring, projection_phys, pi_int)
                else:
                    if verbose:
                        print(f'p(={p}) not valid at column indices')

        else:
            raise ValueError(f'projection(={projection}) unknown')

    def base_ring(self):
        r"""
        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: cap = cut_and_project_schemes.Fibonacci()
            sage: cap.base_ring()
            Number Field in phi with defining polynomial z^2 - z - 1 with phi = 1.618033988749895?

        """
        return self._base_ring

    def physical_space_projection(self):
        r"""
        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: cap = cut_and_project_schemes.Fibonacci()
            sage: cap.physical_space_projection()
            [      1 phi - 1]

        """
        return self._pi

    def internal_space_projection(self):
        r"""
        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: cap = cut_and_project_schemes.Fibonacci()
            sage: cap.internal_space_projection()
            [-phi + 1        1]

        """
        return self._pi_int

    def lattice(self):
        r"""
        Return the lattice

        OUTPUT:

            a matrix whose columns generate the lattice

        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: cap = cut_and_project_schemes.Fibonacci()
            sage: cap.lattice()
            [1 0]
            [0 1]

        """
        return self._lattice

    def lattice_base(self):
        r"""
        Return the lattice base

        OUTPUT:

            a matrix whose columns generate the lattice

        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: cap = cut_and_project_schemes.Fibonacci()
            sage: cap.lattice_base()
            [(1, 0), (0, 1)]

        """
        return self._lattice.columns()

    def ambiant_space(self):
        r"""
        Return the ambiant space

        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: cap = cut_and_project_schemes.Fibonacci()
            sage: cap.ambiant_space()
            Ambient free module of rank 2 over the principal ideal domain
            Integer Ring

        """
        return self.lattice().column_ambient_module()
    def physical_space(self):
        r"""
        Return the physical space

        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: cap = cut_and_project_schemes.Fibonacci()
            sage: cap.physical_space()
            Vector space of dimension 1 over Number Field in phi with
            defining polynomial z^2 - z - 1 with phi = 1.618033988749895?

        """
        return self.physical_space_projection().column_ambient_module()
    def internal_space(self):
        r"""
        Return the internal space

        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: cap = cut_and_project_schemes.Fibonacci()
            sage: cap.internal_space()
            Vector space of dimension 1 over Number Field in phi with
            defining polynomial z^2 - z - 1 with phi = 1.618033988749895?

        """
        return self.internal_space_projection().column_ambient_module()

    def ambiant_space_dimension(self):
        r"""
        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: cap = cut_and_project_schemes.Fibonacci()
            sage: cap.ambiant_space_dimension()
            2

        """
        return self._pi.ncols()

    def physical_space_dimension(self):
        r"""
        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: cap = cut_and_project_schemes.Fibonacci()
            sage: cap.physical_space_dimension()
            1

        """
        return self._pi.nrows()

    def internal_space_dimension(self):
        r"""
        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: cap = cut_and_project_schemes.Fibonacci()
            sage: cap.internal_space_dimension()
            1

        """
        return self._pi_int.nrows()

    def __repr__(self):
        r"""
        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: cut_and_project_schemes.Fibonacci()
            2-to-1 cut and project scheme

        """
        return "{}-to-{} cut and project scheme".format(
                self.ambiant_space_dimension(),
                self.physical_space_dimension())

    def __str__(self):
        r"""
        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: print(cut_and_project_schemes.Fibonacci())
            2-to-1 cut and project scheme over
            Number Field in phi with defining polynomial z^2 - z - 1 with phi = 1.618033988749895?
            Projection to physical space:
            [      1 phi - 1]
            Projection to internal space:
            [-phi + 1        1]
            Lattice generated by the columns of:
            [1 0]
            [0 1]

        """
        s = ("{}-to-{} cut and project scheme over\n{}\n"
             "Projection to physical space:\n{}\n"
             "Projection to internal space:\n{}\n"
             "Lattice generated by the columns of:\n{}")
        return s.format(
                self.ambiant_space_dimension(),
                self.physical_space_dimension(),
                self.base_ring(),
                self.physical_space_projection(), 
                self.internal_space_projection(), 
                self.lattice())

    def is_orthogonal(self):
        r"""
        Return whether the two projections are orthogonal.

        TODO: Is this the good name for this method?

        EXAMPLES::

            sage: from slabbe import CutAndProjectScheme
            sage: E = matrix([[1,1,1,1], [1,2,3,4]])
            sage: c = CutAndProjectScheme.from_slope(E)
            sage: c.is_orthogonal()
            True

        """
        return self._pi_int * self._pi.T == 0

    def is_valid(self, projection=None, verbose=False):
        r"""
        Return True if the physical space projection is valid.

        A projection is *valid* if the rhombi do not overlap after
        projection in the physical space, that is, tiles do not create
        accordions in the physical space [H2004]_.

        The computation of the validity made below comes from Carole
        Porrier's code available at https://github.com/cporrier/Cyrenaic

        .. NOTE::

            The current code checks that the orientation of the projection
            of each pair of vectors is the same in the slope and after
            projection in the physical space.

        INPUT:

        - ``projection`` -- matrix (default:``None``), if ``None`` it uses
          the physical space projection
        - ``verobse`` -- bool (default:``False``)

        OUTPUT:

            boolean

        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: c = cut_and_project_schemes.Penrose()
            sage: c.is_valid()
            True

        REFERENCE:

        .. [H2004] Edmund Harriss, On canonical substitution tilings,
           Ph. D. Thesis, Univeristy of London, 2004.

        """
        import itertools
        n = self.ambiant_space_dimension()
        d = self.physical_space_dimension()
        E = self.slope().matrix()

        if projection is None:
            projection = self.physical_space_projection()

        S1 = [m.sign() for m in projection.minors(d)]
        S2 = [m.sign() for m in E.minors(d)]

        if verbose:
            print("projection minors sign = ", S1)
            print("slope minors sign      = ", S2)
        return S1 == S2 or S1 == [-s for s in S2]

    def lattice_neighbors(self, v):
        r"""
        Return the neighbors of a point according
        to the base of the lattice

        INPUT:

        - ``v`` -- tuple or vector, in the ambiant space

        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: cap = cut_and_project_schemes.Fibonacci()
            sage: v = vector((10, 10))
            sage: cap.lattice_neighbors(v)
            [(11, 10), (10, 11), (9, 10), (10, 9)]

        """
        L = []
        L.extend(v+p for p in self.lattice_base())
        L.extend(v-p for p in self.lattice_base())
        for u in L:
            u.set_immutable()
        return L

    def star_map(self):
        pass
    def slope(self):
        r"""
        Return the slope of the cut-and project scheme.

        The *slope* refers to vector space in the ambiant space
        corresponding to kernel of the pi_int projection [FP24]_.

        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: c = cut_and_project_schemes.Penrose()
            sage: slope = c.slope(); slope
            Vector space of degree 5 and dimension 2 over Number Field in a with defining polynomial z^4 - 5*z^2 + 5 with a = 1.175570504584947?
            Basis matrix:
            [       1        0       -1  a^2 - 2 -a^2 + 2]
            [       0        1 -a^2 + 2  a^2 - 2       -1]
            sage: slope.matrix().n(digits=5)
            [  1.0000  0.00000  -1.0000 -0.61803  0.61803]
            [ 0.00000   1.0000  0.61803 -0.61803  -1.0000]

        ::

            sage: c = cut_and_project_schemes.JeandelRao()
            sage: c.slope()
            Vector space of degree 4 and dimension 2 over Number Field in phi with defining polynomial z^2 - z - 1 with phi = 1.618033988749895?
            Basis matrix:
            [         1          0 -3*phi - 1       -phi]
            [         0          1    phi + 2          1]

        """
        pi_int = self.internal_space_projection()
        return pi_int.right_kernel()

    def shadow_periods(self):
        r"""
        Return the periods of the shadows of the cut and project scheme.

        A *shadow period* is the integer entries of a subperiod of the cut and
        project scheme [BF2015]_.

        A *subperiod* is a vector of n entries where `d+1` are integers.

        The computation of the shadow periods made below comes from
        Carole Porrier's code available at https://github.com/cporrier/Cyrenaic

        OUTPUT: 

        list of pairs (t,s) where t is a tuple of indices where the entries
        are integers equal to s

        EXAMPLES::
            
            sage: from slabbe import cut_and_project_schemes
            sage: c = cut_and_project_schemes.Penrose()
            sage: c.shadow_periods()
            [((0, 1, 2), (1, 0, -1)),
             ((0, 1, 3), (1, -1, 0)),
             ((0, 1, 4), (0, 1, -1)),
             ((0, 2, 3), (0, 1, -1)),
             ((0, 2, 4), (1, 0, -1)),
             ((0, 3, 4), (1, -1, 0)),
             ((1, 2, 3), (1, 0, -1)),
             ((1, 2, 4), (1, -1, 0)),
             ((1, 3, 4), (0, 1, -1)),
             ((2, 3, 4), (1, 0, -1))]

        ::

             sage: c = cut_and_project_schemes.JeandelRao()
             sage: c.shadow_periods()
             [((0, 1, 2), (1, 3, 5)),
              ((0, 1, 3), (0, 1, 1)),
              ((0, 2, 3), (1, 0, 0)),
              ((1, 2, 3), (1, 0, 0))]

        REFERENCE:

        .. [BF2015] Bédaride, Nicolas, et Thomas Fernique. « When
           Periodicities Enforce Aperiodicity ». Communications in
           Mathematical Physics 335, nᵒ 3 (1 mai 2015): 1099‑1120.
           https://doi.org/10.1007/s00220-015-2334-8.

        """
        import itertools
        from sage.matrix.constructor import matrix
        n = self.ambiant_space_dimension()
        d = self.physical_space_dimension()
        E = self.slope().matrix()
        L = []
        for t in itertools.combinations(range(n), d + 1):
            G = [(-1)**i*E.matrix_from_columns([j for j in t if j != a]).det() for (i,a) in enumerate(t)]
            #print("G=",G)
            G = matrix([g.list() for g in G])
            for s in G.left_kernel().basis():
                L.append((t,s))
        return L

    def subperiods(self):
        r"""
        Return the subperiods of the cut and project scheme.

        A *subperiod* is a vector of n entries where `d+1` are integers
        [BF2015]_.

        The computation of the subperiods made below comes from
        Carole Porrier's code available at https://github.com/cporrier/Cyrenaic

        OUTPUT: 

            todo

        EXAMPLES::
            
            sage: from slabbe import cut_and_project_schemes
            sage: c = cut_and_project_schemes.Penrose()
            sage: P = c.subperiods()            # long time (3s)
            sage: P                             # long time
            [(1, 0, -1, a^2 - 2, -a^2 + 2),
             (1, -1, a^2 - 3, 0, -a^2 + 3),
             (0, 1, -a^2 + 2, a^2 - 2, -1),
             (0, -a^2 + 3, 1, -1, a^2 - 3),
             (1, -a^2 + 3, 0, a^2 - 3, -1),
             (1, -a^2 + 2, a^2 - 2, -1, 0),
             (-a^2 + 2, 1, 0, -1, a^2 - 2),
             (-a^2 + 3, 1, -1, a^2 - 3, 0),
             (a^2 - 3, 0, -a^2 + 3, 1, -1),
             (a^2 - 2, -a^2 + 2, 1, 0, -1)]

        The same in terms of the golden ratio::

            sage: K.<phi> = NumberField(x**2-x-1, 'phi', embedding=RR(1.6))
            sage: [p.change_ring(K) for p in P]    # long time
            [(1, 0, -1, -phi + 1, phi - 1),
             (1, -1, -phi, 0, phi),
             (0, 1, phi - 1, -phi + 1, -1),
             (0, phi, 1, -1, -phi),
             (1, phi, 0, -phi, -1),
             (1, phi - 1, -phi + 1, -1, 0),
             (phi - 1, 1, 0, -1, -phi + 1),
             (phi, 1, -1, -phi, 0),
             (-phi, 0, phi, 1, -1),
             (-phi + 1, phi - 1, 1, 0, -1)]

        """
        from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
        from sage.matrix.constructor import matrix
        from sage.rings.ideal import Ideal as ideal

        n = self.ambiant_space_dimension()
        d = self.physical_space_dimension()
        E = self.slope().matrix()
        L = self.shadow_periods()
        K = self.base_ring()
        Z = PolynomialRing(K, 'x', len(L)*(n-d-1))

        M = matrix([[s[t.index(i)] if i in t else Z.gens()[L.index((t,s))*(n-d-1)+i-len([j for j in t if j<i])] for i in range(n)] for (t,s) in L])

        I = ideal(Z, M.minors(d+1))
        if I.dimension() > 0:
            raise ValueError(f'slope(={E}) not determined by subperiods')
        for v in I.variety(K):
            z = M.subs(v).change_ring(K)
            if z.transpose().augment(E.transpose()).rank() == d:
                return z.rows()

    def is_determined_by_subperiods(self):
        r"""
        Return whether the slope of the cut-and-project scheme is determined by
        the subperiods.

        A *subperiod* is a vector of n entries where `d+1` are integers
        [BF2015]_.

        The computation made below comes from Carole Porrier's code available
        at https://github.com/cporrier/Cyrenaic

        OUTPUT: 

            todo

        EXAMPLES:

        The Penrose hull is determined by its subperiods::
            
            sage: from slabbe import cut_and_project_schemes
            sage: c = cut_and_project_schemes.Penrose()
            sage: c.is_determined_by_subperiods()
            True

        Ammann-Beenker is not determined by its subperiods::

            sage: c = cut_and_project_schemes.AmmannBeenker()
            sage: c.is_determined_by_subperiods()
            False

        Jeandel-Rao seems not to be determined by its subperiods (?)::

            sage: c = cut_and_project_schemes.JeandelRao()
            sage: c.is_determined_by_subperiods()
            False

        """
        from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
        from sage.matrix.constructor import matrix
        from sage.rings.ideal import Ideal as ideal

        n = self.ambiant_space_dimension()
        d = self.physical_space_dimension()
        E = self.slope().matrix()
        L = self.shadow_periods()
        K = self.base_ring()
        Z = PolynomialRing(K, 'x', len(L)*(n-d-1))

        M = matrix([[s[t.index(i)] if i in t else Z.gens()[L.index((t,s))*(n-d-1)+i-len([j for j in t if j<i])] for i in range(n)] for (t,s) in L])

        return ideal(Z, M.minors(d+1)).dimension() == 0

    def canonical_model_set(self, intervals='zero_one', shift=None):
        r"""
        Return the canoncial model set using a hypercube as window
        after projection in the internal space.

        INPUT:

        - ``intervals`` -- (default: ``'zero_one'``) intervals defining the
          hypercube
        - ``shift`` -- `n`-dimensional vector translating the internal window
          to avoid singular tilings and Conway worms, where `n` is the
          dimension of the ambiant space

        .. TODO::

            Verify if the hypercube over interval [-1,0] would not be a
            better choice for the canonical model set.

        EXAMPLES::

            sage: from slabbe import CutAndProjectScheme
            sage: E = matrix([[1,1,1,1], [1,2,3,4]])
            sage: c = CutAndProjectScheme.from_slope(E)
            sage: m = c.canonical_model_set()
            sage: m
            Model Set of a 4-to-2 cut and project scheme

        Golden-Octagonal::

            sage: z = polygen(QQ, 'z')
            sage: K = NumberField(z**2-z-1, 'phi', embedding=RR(1.6))
            sage: phi = K.gen()
            sage: E = matrix([[-1,0,phi,phi], [0,1,phi,1]])
            sage: c = CutAndProjectScheme.from_slope(E)
            sage: m = c.canonical_model_set()
            sage: m
            Model Set of a 4-to-2 cut and project scheme

        With shift::

            sage: from slabbe import cut_and_project_schemes
            sage: c = cut_and_project_schemes.Penrose()
            sage: shift = vector((1,-1,2,-1,-1)) / 1000
            sage: c.canonical_model_set(shift=shift)
            Model Set of a 5-to-2 cut and project scheme

        TESTS::

            sage: c = cut_and_project_schemes.GoldenOctagonal(projection='roots_of_unity')
            sage: m = c.canonical_model_set(shift=None)

        """
        from sage.geometry.polyhedron.library import polytopes
        dimension = self.ambiant_space_dimension()
        H = polytopes.hypercube(dimension, intervals=intervals)

        if shift is None:
            shift = self.ambiant_space().zero()
        else:
            try:
                shift = self.ambiant_space()(shift)
            except TypeError:
                from sage.modules.free_module_element import vector
                shift = vector(shift)

        H_shifted = H + shift
        W = self.internal_space_projection() * (H_shifted)
        return ModelSet(self, W)

    def change_physical_projection(self, pi):
        pass

    def orthogonal_physical_projection_cut_and_project_scheme(self):
        r"""
        EXAMPLES::

            sage: # TODO
        """
        K = self.base_ring()
        pi_int = self.internal_space_projection()
        pi = pi_int.right_kernel_matrix()   # correct?
        return CutAndProjectScheme(K, pi, pi_int)

class ModelSet(SageObject):
    r"""
    Regular Euclidean model set

    INPUT:

    - ``cut_and_project_schemes`` -- a cut and project scheme
    - ``window`` -- polyhedron

    EXAMPLES::

        sage: from slabbe import cut_and_project_schemes, ModelSet
        sage: cap = cut_and_project_schemes.Fibonacci()
        sage: phi = cap.base_ring().gen()
        sage: W = Polyhedron([(-1,),(phi-1,)])
        sage: m = ModelSet(cap, W)

    """
    def __init__(self, cut_and_project_scheme, window):
        r"""
        Construct the model set
        """
        self._cut_and_project_scheme = cut_and_project_scheme
        self._window = window

    def cut_and_project_scheme(self):
        return self._cut_and_project_scheme

    def window(self):
        return self._window

    def __repr__(self):
        r"""
        String representation
        
        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes, ModelSet
            sage: cap = cut_and_project_schemes.Fibonacci()
            sage: phi = cap.base_ring().gen()
            sage: W = Polyhedron([(-1,),(phi-1,)])
            sage: ModelSet(cap, W)
            Model Set of a 2-to-1 cut and project scheme

        """
        return "Model Set of a {}".format(repr(self._cut_and_project_scheme))

    def is_Meyer(self):
        raise NotImplementedError
    def is_regular(self):
        raise NotImplementedError
    def is_singular(self):
        raise NotImplementedError
    def is_generic(self):
        raise NotImplementedError
    def is_uniformly_discrete(self):
        raise NotImplementedError
    def is_relatively_dense(self):
        raise NotImplementedError

    def lattice_neighbors_projected_in_window(self, v):
        r"""
        Return the neighbors of a point that are projected in the (internal
        space) window

        INPUT:

        - ``v`` -- tuple or vector, in the ambiant space

        EXAMPLES::

            sage: from slabbe import model_sets
            sage: m = model_sets.Fibonacci()
            sage: v = vector((0,0))
            sage: m.lattice_neighbors_projected_in_window(v)
            [(1, 0), (-1, 0), (0, -1)]

        """
        cap = self.cut_and_project_scheme()
        M = cap.internal_space_projection()
        window = self.window()
        return [p for p in cap.lattice_neighbors(v) if M * p in window]

    def internal_space_window_preimage(self):
        r"""
        Return the preimage of the window in the internal space by the
        projection in the internal space.

        EXAMPLES::

            sage: from slabbe import model_sets
            sage: m = model_sets.Fibonacci()
            sage: m.internal_space_window_preimage()
            A 2-dimensional polyhedron in (Number Field in phi with defining polynomial z^2 - z - 1
            with phi = 1.618033988749895?)^2 defined as the convex hull of 2 vertices and 1 line

        Penrose tiling::

            sage: m = model_sets.Penrose()
            sage: strip = m.internal_space_window_preimage()
            sage: strip
            A 5-dimensional polyhedron in (Number Field in a with defining
            polynomial z^4 - 5*z^2 + 5 with a = 1.175570504584947?)^5
            defined as the convex hull of 22 vertices and 2 lines
            sage: vector((0,0,0,0,0)) in strip
            True

        We move the window to force a resolution of the Conway worms::

            sage: shift = vector((1,-1,2,-1,-1)) / 1000
            sage: m = model_sets.Penrose(shift)
            sage: strip = m.internal_space_window_preimage()
            sage: vector((0,0,0,0,0)) in strip
            False
            sage: vector((1,1,1,1,1)) in strip
            False

        TESTS::

            sage: from slabbe import cut_and_project_schemes
            sage: c = cut_and_project_schemes.GoldenOctagonal(projection='roots_of_unity')
            sage: m = c.canonical_model_set(shift=vector((1,1,1,1))/100)
            sage: m.internal_space_window_preimage()
            A 4-dimensional polyhedron in (Number Field in phi with
            defining polynomial z^2 - z - 1 with phi =
            1.618033988749895?)^4 defined as the convex hull of 8 vertices
            and 2 lines

        """
        from sage.geometry.polyhedron.constructor import Polyhedron
        from sage.matrix.special import identity_matrix
        from sage.matrix.constructor import matrix

        cap = self.cut_and_project_scheme()
        M_int = cap.internal_space_projection()
        n_d = cap.internal_space_dimension()
        d = cap.physical_space_dimension()

        # identity matrix augmented by zeros
        # first entries are for the physical space, last are for the internal
        rows = []
        row_of_zero = [0]*n_d
        for _ in range(d):
            rows.append(row_of_zero)
        rows.extend(identity_matrix(n_d).rows())
        Q = matrix(rows)

        vertices = [Q * v.vector() for v in self.window().vertices()]
        lines = M_int.right_kernel().basis()
        strip = Polyhedron(vertices=vertices, lines=lines)
        return strip

    def physical_space_window_preimage(self, physical_window):
        r"""
        Return the preimage of the window in the internal space by the
        projection in the internal space.

        INPUT:

        - ``physical_window`` -- polyhedron

        EXAMPLES::

            sage: from slabbe import model_sets
            sage: m = model_sets.Fibonacci()
            sage: W = Polyhedron([(0,),(10,)])
            sage: m.physical_space_window_preimage(W)
            A 2-dimensional polyhedron in (Number Field in phi with defining polynomial z^2 - z - 1
            with phi = 1.618033988749895?)^2 defined as the convex hull of 2 vertices and 1 line

        Penrose tiling::

            sage: m = model_sets.Penrose()
            sage: W = polytopes.hypercube(2, intervals=[(-10,10), (-10,10)])
            sage: strip = m.physical_space_window_preimage(W)
            sage: strip
            A 5-dimensional polyhedron in (Number Field in a with defining
            polynomial z^4 - 5*z^2 + 5 with a = 1.175570504584947?)^5
            defined as the convex hull of 4 vertices and 3 lines

        We move the window to force a resolution of the Conway worms::

            sage: shift = vector((1,-1,2,-1,-1)) / 1000
            sage: m = model_sets.Penrose(shift)
            sage: W = polytopes.hypercube(2, intervals=[(-10,10), (-10,10)])
            sage: strip = m.physical_space_window_preimage(W)
            sage: strip
            A 5-dimensional polyhedron in (Number Field in a with defining
            polynomial z^4 - 5*z^2 + 5 with a = 1.175570504584947?)^5
            defined as the convex hull of 4 vertices and 3 lines

        """
        from sage.geometry.polyhedron.constructor import Polyhedron
        from sage.matrix.special import identity_matrix
        from sage.matrix.constructor import matrix

        cap = self.cut_and_project_scheme()
        M = cap.physical_space_projection()
        n_d = cap.internal_space_dimension()
        d = cap.physical_space_dimension()

        # identity matrix augmented by zeros
        # first entries are for the physical space, last are for the internal
        rows = []
        rows.extend(identity_matrix(d).rows())
        row_of_zero = [0]*d
        for _ in range(n_d):
            rows.append(row_of_zero)
        Q = matrix(rows)

        vertices = [Q * v.vector() for v in physical_window.vertices()]
        lines = M.right_kernel().basis()
        strip = Polyhedron(vertices=vertices, lines=lines)
        return strip

    def ambiant_compact_strip(self, physical_window):
        r"""
        Return the preimage of the window in the internal space by the
        projection in the internal space.

        INPUT:

        - ``physical_window`` -- polyhedron

        EXAMPLES::

            sage: from slabbe import model_sets
            sage: m = model_sets.Fibonacci()
            sage: W = Polyhedron([(0,),(10,)])
            sage: m.ambiant_compact_strip(W)
            A 2-dimensional polyhedron in (Number Field in phi with
            defining polynomial z^2 - z - 1 with phi =
            1.618033988749895?)^2 defined as the convex hull of 4 vertices

        Penrose tiling::

            sage: m = model_sets.Penrose()
            sage: W = polytopes.hypercube(2, intervals=[(-10,10), (-10,10)])
            sage: m.ambiant_compact_strip(W)
            A 5-dimensional polyhedron in (Number Field in a with
            defining polynomial z^4 - 5*z^2 + 5 with a =
            1.175570504584947?)^5 defined as the convex hull of 88
            vertices

        We move the window to force a resolution of the Conway worms::

            sage: shift = vector((1,-1,2,-1,-1)) / 1000
            sage: m = model_sets.Penrose(shift)
            sage: W = polytopes.hypercube(2, intervals=[(-10,10), (-10,10)])
            sage: m.ambiant_compact_strip(W)
            A 5-dimensional polyhedron in (Number Field in a with defining
            polynomial z^4 - 5*z^2 + 5 with a = 1.175570504584947?)^5
            defined as the convex hull of 88 vertices

        """
        strip_i = self.internal_space_window_preimage()
        strip_p = self.physical_space_window_preimage(physical_window)
        return strip_i.intersection(strip_p)

    def some_element_of_lattice_in_the_strip(self, physical_window):
        r"""
        Return the lattice points that are projected in the internal space
        window (and that are projected to the provided physical space
        window).

        INPUT:

        - ``physical_window`` -- polyhedron

        OUTPUT:

            vector

        EXAMPLES::

            sage: from slabbe import model_sets
            sage: m = model_sets.Fibonacci()
            sage: W = Polyhedron([(0,),(10,)])
            sage: m.some_element_of_lattice_in_the_strip(W)
            (3, 2)

        Penrose tiling::

            sage: m = model_sets.Penrose()
            sage: W = polytopes.hypercube(2, intervals=[(-10,10), (-10,10)])
            sage: m.some_element_of_lattice_in_the_strip(W)
            (0, 0, 0, 0, 0)

        """
        from sage.modules.free_module_element import vector

        cap = self.cut_and_project_scheme()
        strip = self.ambiant_compact_strip(physical_window)
        c = strip.center()
        L = cap.lattice()
        Linv_c = L.inverse() * c
        v = vector((a.floor() for a in Linv_c))
        L_v = L*v
        L_v.set_immutable()

        # search for a seed lattice point in the strip
        from sage.sets.recursively_enumerated_set import RecursivelyEnumeratedSet
        seeds = [L_v]
        #successors = self.successor_map(physical_window)
        successors = cap.lattice_neighbors
        R = RecursivelyEnumeratedSet(seeds, successors, structure='symmetric')

        M_int = cap.internal_space_projection()
        M = cap.physical_space_projection()

        for p in R.breadth_first_search_iterator():
            if M*p in physical_window and M_int*p in self.window():
                return p

    def successor_map(self, physical_window):
        r"""
        Return the successor map of points in the lattice
        projected in both windows.

        INPUT:

        - ``physical_window`` -- polyhedron

        EXAMPLES::

            sage: from slabbe import model_sets
            sage: m = model_sets.Fibonacci()
            sage: W = Polyhedron([(0,),(10,)])
            sage: succ = m.successor_map(W)
            sage: succ(vector((0,0)))
            [(1, 0)]

        """
        cap = self.cut_and_project_scheme()
        M = cap.physical_space_projection()
        def successors(v): 
            return [p for p in self.lattice_neighbors_projected_in_window(v) 
                      if M * p in physical_window]
        return successors

    @cached_method
    def cut(self, physical_window):
        r"""
        Return the lattice points that are projected in the internal space
        window (and that are projected to the provided physical space
        window).

        INPUT:

        - ``physical_window`` -- polyhedron

        EXAMPLES::

            sage: from slabbe import model_sets
            sage: m = model_sets.Fibonacci()
            sage: W = Polyhedron([(0,),(10,)])
            sage: m.cut(W)
            [(3, 2), (4, 2), (3, 1), (4, 3), (2, 1), (5, 3), (1, 1), (6,
            3), (1, 0), (6, 4), (0, 0), (7, 4)]

        """
        from sage.sets.recursively_enumerated_set import RecursivelyEnumeratedSet
        p = self.some_element_of_lattice_in_the_strip(physical_window)
        seeds = [p]
        successors = self.successor_map(physical_window)
        R = RecursivelyEnumeratedSet(seeds, successors, structure='symmetric')
        return list(R)

    def cut_edges(self, physical_window):
        r"""
        Return the edges linking lattice points that are projected in the
        internal space window (and that are projected to the provided
        physical space window).

        INPUT:

        - ``physical_window`` -- polyhedron

        EXAMPLES::

            sage: from slabbe import model_sets
            sage: m = model_sets.Fibonacci()
            sage: W = Polyhedron([(0,),(10,)])
            sage: sorted(sorted(edge) for edge in m.cut_edges(W))
            [[(0, 0), (1, 0)],
             [(1, 0), (1, 1)],
             [(1, 1), (2, 1)],
             [(2, 1), (3, 1)],
             [(3, 1), (3, 2)],
             [(3, 2), (4, 2)],
             [(4, 2), (4, 3)],
             [(4, 3), (5, 3)],
             [(5, 3), (6, 3)],
             [(6, 3), (6, 4)],
             [(6, 4), (7, 4)]]

        """
        successors = self.successor_map(physical_window)

        edges = set()
        for p in self.cut(physical_window):
            for q in successors(p):
                p_q = frozenset((p,q))
                edges.add(p_q)
        return [tuple(edge) for edge in edges]

    def cut_and_project(self, physical_window):
        r"""
        Return the model set restricted to a window in the physical space

        INPUT:

        - ``physical_window`` -- polyhedron

        OUTPUT:

            list

        EXAMPLES::

            sage: from slabbe import model_sets
            sage: m = model_sets.Fibonacci()
            sage: W = Polyhedron([(0,),(10,)])
            sage: sorted(m.cut_and_project(W))
            [(0),
             (1),
             (phi),
             (phi + 1),
             (phi + 2),
             (2*phi + 1),
             (2*phi + 2),
             (3*phi + 1),
             (3*phi + 2),
             (3*phi + 3),
             (4*phi + 2),
             (4*phi + 3)]

        """
        L = self.cut(physical_window)
        cap = self.cut_and_project_scheme()
        M = cap.physical_space_projection()
        return [M*p for p in L]

    def plot_in_ambiant_space(self, physical_window, pointsize=100):
        r"""
        Return a Graphics representing the model set restricted to a window
        in the physical space (seen in the ambiant space)

        INPUT:

        - ``physical_window`` -- polyhedron
        - ``pointsize`` -- integer (default:20)

        EXAMPLES::

            sage: from slabbe import model_sets
            sage: m = model_sets.Fibonacci()
            sage: W = Polyhedron([(-10,),(10,)])
            sage: G = m.plot_in_ambiant_space(W)        # long time (1s)
            sage: G.show(aspect_ratio=1, figsize=20)    # long time

        TODO: The following needs a little fix since the slope is zero
        (some limit case for the region_plot)::

            sage: m = model_sets.Fibonacci_the_Minkowski_way()
            sage: W = Polyhedron([(-10,),(10,)])
            sage: G = m.plot_in_ambiant_space(W)        # long time (1s)
            sage: G.show(aspect_ratio=1, figsize=20)    # known bug

        TESTS::

            sage: m = model_sets.Fibonacci2D()
            sage: W = polytopes.hypercube(2, intervals=[(-10,10), (-10,10)])
            sage: G = m.plot_in_ambiant_space(W)
            Traceback (most recent call last):
            ...
            NotImplementedError: when physical space dimension is 2 and
            ambiant space dimension is 4

        """
        cap = self.cut_and_project_scheme()
        if (cap.physical_space_dimension() != 1 or cap.ambiant_space_dimension() != 2):
            raise NotImplementedError("when physical space dimension "
                    "is {} and ambiant space dimension is {}".format(
                        cap.physical_space_dimension(),
                        cap.ambiant_space_dimension()))

        from sage.plot.graphics import Graphics
        from sage.plot.point import point
        from sage.plot.arrow import arrow
        from sage.calculus.var import var
        from sage.plot.contour_plot import region_plot
        from sage.modules.free_module_element import vector

        G = Graphics()

        # the strip (alternate way using polyhedron)
        #strip = self.internal_space_window_preimage()
        #G += strip.plot(fill='lightyellow')

        wp_vertices = sorted(b for (b,) in physical_window.vertices())
        wp_min, wp_max = wp_vertices

        # the strip
        M_int = cap.internal_space_projection()
        [[a,b]] = M_int
        wi_vertices = sorted(b for (b,) in self.window().vertices())
        wi_min, wi_max = wi_vertices
        x,y = var('x,y')
        ineqs = [a*x+b*y<wi_max, a*x+b*y>wi_min]
        if b:
            y_max = ((wi_max-a*wp_max)/b).n()
            y_min = ((wi_min-a*wp_min)/b).n()
        else:
            raise NotImplementedError
        G += region_plot(ineqs, (x,wp_min,wp_max), (y,y_min,y_max),
                incol='lightcoral', 
                bordercol='coral', borderstyle='dotted',
                borderwidth=2, 
                alpha=.1)

        # points in the strip
        L = self.cut(physical_window)
        G += point(L, color='red', size=pointsize)

        # arrows and projected points
        M = cap.physical_space_projection()
        for p in L:
            (a,) = M_p = M*p
            a_0 = vector((a,0))
            edge = (p,a_0)
            if p != a_0:
                # see https://github.com/sagemath/sage/issues/35031
                G += arrow(p, a_0, linestyle='dashed', color='green',
                        arrowshorten=15)
            G += point([a_0], size=pointsize, color='blue')

        return G

    def plot_in_physical_space(self, physical_window, pointsize=20):
        r"""
        Return a Graphics representing the model set restricted to a window
        in the physical space

        INPUT:

        - ``physical_window`` -- polyhedron
        - ``pointsize`` -- integer (default:20)

        EXAMPLES::

            sage: from slabbe import model_sets
            sage: m = model_sets.Fibonacci2D()
            sage: W = polytopes.hypercube(2, intervals=[(-10,10), (-10,10)])
            sage: G = m.plot_in_physical_space(W)      # long time (1s)
            sage: G.show(aspect_ratio=1, figsize=20)   # long time

        Penrose tiling::

            sage: m = model_sets.Penrose()
            sage: W = polytopes.hypercube(2, intervals=[(-10,10), (-10,10)])
            sage: G = m.plot_in_physical_space(W)      # long time (5s)
            sage: G.show(aspect_ratio=1, figsize=20)   # long time

        We move the window to force a resolution of the Conway worms::

            sage: shift = vector((1,-1,2,-1,-1)) / 1000
            sage: m = model_sets.Penrose(shift)
            sage: W = polytopes.hypercube(2, intervals=[(-10,10), (-10,10)])
            sage: G = m.plot_in_physical_space(W)      # long time (4s)
            sage: G.show(aspect_ratio=1, figsize=20)   # long time

        TODO, Bug!!::

            sage: from slabbe import cut_and_project_schemes
            sage: c = cut_and_project_schemes.GoldenOctagonal(projection='roots_of_unity')
            sage: m = c.canonical_model_set(shift=vector((1,1,1,1))/100)
            sage: m.plot_in_physical_space(W).show(figsize=20)
            Traceback (most recent call last):
            ...
            ValueError: invalid base ring: Number Field in zeta80 with defining
            polynomial x^2 - 2 with zeta80 = 1.414213562373095? cannot be coerced to a real
            field

        TESTS::

            sage: m = model_sets.Fibonacci()
            sage: W = Polyhedron([(-10,),(10,)])
            sage: G = m.plot_in_physical_space(W)
            Traceback (most recent call last):
            ...
            NotImplementedError: when physical space dimension is 1

        .. TODO::

            The current method currently calls ``cut`` twice.

        """
        from sage.plot.point import point
        from sage.plot.line import line
        cap = self.cut_and_project_scheme()
        if cap.physical_space_dimension() != 2:
            raise NotImplementedError("when physical space dimension "
                    "is {}".format(cap.physical_space_dimension()))

        L = self.cut_and_project(physical_window)
        G = point(L, size=pointsize)

        M = cap.physical_space_projection()
        for (p,q) in self.cut_edges(physical_window):
            edge = (M*p, M*q)
            G += line(edge)
        return G

    def coding_region(self, P):
        r"""
        Return the region in the window associated with a subset of points
        of the lattice.

        INPUT:

        - ``P``-- list, subset of vertices of the lattice

        OUTPUT:

        a polytope

        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: c = cut_and_project_schemes.Penrose()
            sage: m = c.canonical_model_set(shift=vector((1,1,1,1,1))/100)
            sage: P = [(0,0,0,0,0), (0,1,0,0,0), (0,0,1,0,0), (0,0,0,1,0)]
            sage: m.coding_region(P)
            A 3-dimensional polyhedron in (Number Field in a with defining
            polynomial z^4 - 5*z^2 + 5 with a = 1.175570504584947?)^3 defined
            as the convex hull of 11 vertices

        The region may be empty::

            sage: P = [(0,0,0,0,0),(0,1,0,0,0),(0,0,1,0,0),(0,0,0,10,0)]
            sage: m.coding_region(P)
            The empty polyhedron in (Number Field in a with defining
            polynomial z^4 - 5*z^2 + 5 with a = 1.175570504584947?)^3

        """
        from sage.modules.free_module_element import vector
        from sage.geometry.polyhedron.constructor import Polyhedron

        ip = self.cut_and_project_scheme().internal_space_projection()
        W = self.window()

        ieqs = []
        for v in P:
            W_translated = W.translation(-ip*vector(v))
            ieqs.extend(W_translated.inequalities_list())

        return Polyhedron(ieqs=ieqs)

############################################
# Generator of well-known examples (classes)
############################################
class CutAndProjectSchemeGenerator():
    r"""
    Constructor of several famous cut and project schemes

    EXAMPLES::

        sage: from slabbe import cut_and_project_schemes
        sage: cut_and_project_schemes.Fibonacci()
        2-to-1 cut and project scheme
    """
    def Fibonacci(self):
        r"""
        Return the Fibonacci cut and project scheme

        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: cut_and_project_schemes.Fibonacci()
            2-to-1 cut and project scheme

        """
        from sage.rings.rational_field import QQ
        from sage.rings.real_mpfr import RR
        from sage.rings.polynomial.polynomial_ring import polygen
        from sage.rings.number_field.number_field import NumberField
        from sage.matrix.constructor import matrix

        z = polygen(QQ, 'z')
        K = NumberField(z**2-z-1, 'phi', embedding=RR(1.6))
        phi = K.gen()
        pi = matrix([[1, ~phi]])
        pi_int = matrix([[-~phi, 1]])
        return CutAndProjectScheme(K, pi, pi_int)

    def Fibonacci_the_Minkowski_way(self):
        r"""
        Return the Fibonacci cut and project scheme

        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: cut_and_project_schemes.Fibonacci_the_Minkowski_way()
            2-to-1 cut and project scheme
            sage: print(cut_and_project_schemes.Fibonacci_the_Minkowski_way())
            2-to-1 cut and project scheme over
            Number Field in phi with defining polynomial z^2 - z - 1 with
            phi = 1.618033988749895?
            Projection to physical space:
            [1 0]
            Projection to internal space:
            [0 1]
            Lattice generated by the columns of:
            [       1      phi]
            [       1 -phi + 1]


        """
        from sage.rings.rational_field import QQ
        from sage.rings.real_mpfr import RR
        from sage.rings.polynomial.polynomial_ring import polygen
        from sage.rings.number_field.number_field import NumberField
        from sage.matrix.constructor import matrix

        z = polygen(QQ, 'z')
        K = NumberField(z**2-z-1, 'phi', embedding=RR(1.6))
        phi = K.gen()
        pi = matrix([[1, 0]])
        pi_int = matrix([[0, 1]])
        lattice = matrix.column(2,[(1,1), (phi,-~phi)])
        return CutAndProjectScheme(K, pi, pi_int, lattice)


    def Fibonacci2D(self):
        r"""
        Return the 2D Fibonacci cut and project scheme

        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: cut_and_project_schemes.Fibonacci2D()
            4-to-2 cut and project scheme

        """
        from sage.rings.rational_field import QQ
        from sage.rings.real_mpfr import RR
        from sage.rings.polynomial.polynomial_ring import polygen
        from sage.rings.number_field.number_field import NumberField
        from sage.matrix.constructor import matrix

        z = polygen(QQ, 'z')
        K = NumberField(z**2-z-1, 'phi', embedding=RR(1.6))
        phi = K.gen()
        pi = matrix(K, [[1, ~phi, 0, 0], [0, 0, 1, ~phi]])
        pi_int = matrix(K, [[-~phi, 1, 0, 0], [0, 0, -~phi, 1]])
        return CutAndProjectScheme(K, pi, pi_int)

    def Penrose(self, algorithm='degree4'):
        r"""
        Return the Penrose cut and project scheme

        INPUT:

        - ``algorithm`` -- string (default:``'degree4'``), valid options are:

          - ``'degree4'`` -- computations are made in the number field of degree 4
          - ``'Carole_gram_schmidt'`` -- physical projection is not
            orthonormal
          - ``'Carole_roots_of_unity'`` -- computations are made in the
            Algebraic field (computations are slower)

        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: cut_and_project_schemes.Penrose()
            5-to-2 cut and project scheme

        ::

            sage: c = cut_and_project_schemes.Penrose(algorithm='Carole_gram_schmidt')
            sage: c
            5-to-2 cut and project scheme

        """
        from sage.rings.rational_field import QQ
        from sage.rings.real_mpfr import RR
        from sage.rings.polynomial.polynomial_ring import polygen
        from sage.rings.number_field.number_field import NumberField
        from sage.matrix.constructor import matrix
        from sage.symbolic.constants import pi
        from sage.functions.trig import cos, sin

        if algorithm == 'degree4':
            # unit vectors are algebraic numbers of degree 4
            z = polygen(QQ, 'z')
            K = NumberField(z**4 - 5*z**2 + 5, 'a', embedding=RR(1.17))
            #a = K.gen()
            entries = [(cos(2*pi*n/5), sin(2*pi*n/5)) for n in range(5)]
            projection_phys = matrix.column(K, entries)
            projection_int = projection_phys.right_kernel_matrix()
            return CutAndProjectScheme(K, projection_phys, projection_int)

        elif algorithm == 'Carole_gram_schmidt':
            z = polygen(QQ, 'z')
            K = NumberField(z**2-z-1, 'phi', embedding=RR(1.6))
            phi = K.gen()
            E = matrix(K, [[phi,0,-phi,-1, 1],
                           [-1, 1, phi, 0,-phi]])
            return CutAndProjectScheme.from_slope(E, projection='gram_schmidt')

        elif algorithm == 'Carole_roots_of_unity':
            z = polygen(QQ, 'z')
            K = NumberField(z**2-z-1, 'phi', embedding=RR(1.6))
            phi = K.gen()
            E = matrix(K, [[phi,0,-phi,-1, 1],
                           [-1, 1, phi, 0,-phi]])
            return CutAndProjectScheme.from_slope(E, projection='roots_of_unity')

        else:
            raise ValueError(f'algorithm(={algorithm}) unknown')

    def GoldenOctagonal(self, projection='gram_schmidt'):
        r"""
        Return the Golden-Octagonal cut and project scheme

        The choice of the matrix E whose rows generate the slope, that is,
        the kernel of the pi_int projection, is made acording to
        Carole Porrier's code available at https://github.com/cporrier/Cyrenaic

        INPUT:

        - ``projection`` -- string (default:``'gram_schmidt'``), 
          possible values are ``'gram_schmidt'`` and ``'roots_of_unity'``

        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: cut_and_project_schemes.GoldenOctagonal()
            4-to-2 cut and project scheme

        """
        from sage.rings.rational_field import QQ
        from sage.rings.real_mpfr import RR
        from sage.rings.polynomial.polynomial_ring import polygen
        from sage.rings.number_field.number_field import NumberField
        from sage.matrix.constructor import matrix

        z = polygen(QQ, 'z')
        K = NumberField(z**2-z-1, 'phi', embedding=RR(1.6))
        phi = K.gen()
        E = matrix(K, [[-1,0,phi,phi], [0,1,phi,1]])
        return CutAndProjectScheme.from_slope(E, projection=projection)

    def AmmannBeenker(self):
        r"""
        Return the Ammann-Beenker cut and project scheme

        The choice of the matrix E whose rows generate the slope, that is,
        the kernel of the pi_int projection, is made acording to
        Carole Porrier's code available at https://github.com/cporrier/Cyrenaic

        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: cut_and_project_schemes.AmmannBeenker()
            4-to-2 cut and project scheme

        ::

            sage: c = cut_and_project_schemes.AmmannBeenker()
            sage: m = c.canonical_model_set(shift=vector((1,1,1,1))/100)
            sage: W = polytopes.hypercube(2, intervals=[(-10,10), (-10,10)])
            sage: G = m.plot_in_physical_space(W)      # long time (1s)
            sage: G.show(aspect_ratio=1, figsize=20)   # long time

        """
        from sage.rings.rational_field import QQ
        from sage.rings.real_mpfr import RR
        from sage.rings.polynomial.polynomial_ring import polygen
        from sage.rings.number_field.number_field import NumberField
        from sage.matrix.constructor import matrix

        z = polygen(QQ, 'z')
        K = NumberField(z**2-2, 'sqrt2', embedding=RR(1.4))
        sqrt2 = K.gen()
        E = matrix(K, [[sqrt2,1,0,-1], [0,1,sqrt2,1]])
        return CutAndProjectScheme.from_slope(E)

    def Cyrenaic(self):
        r"""
        Return the Cyrenaic cut and proect scheme

        The choice of the matrix E whose rows generate the slope, that is,
        the kernel of the pi_int projection, is made acording to
        Carole Porrier's code available at https://github.com/cporrier/Cyrenaic

        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: cut_and_project_schemes.Cyrenaic()
            4-to-2 cut and project scheme

        ::

            sage: c = cut_and_project_schemes.Cyrenaic()
            sage: m = c.canonical_model_set(shift=vector((1,1,1,1))/100)
            sage: W = polytopes.hypercube(2, intervals=[(-10,10), (-10,10)])
            sage: G = m.plot_in_physical_space(W)      # long time (1s)
            sage: G.show(aspect_ratio=1, figsize=20)   # long time

        """
        from sage.rings.rational_field import QQ
        from sage.rings.real_mpfr import RR
        from sage.rings.polynomial.polynomial_ring import polygen
        from sage.rings.number_field.number_field import NumberField
        from sage.matrix.constructor import matrix

        z = polygen(QQ, 'z')
        K = NumberField(z**2-3, 'sqrt3', embedding=RR(1.7))
        sqrt3 = K.gen()
        E = matrix(K, [[sqrt3,0,1,1],[1,sqrt3-1,-1,1]])
        return CutAndProjectScheme.from_slope(E)

    def JeandelRao(self, algorithm=0):
        r"""
        Return the Jeandel-Rao cut and proect scheme as given in [L21]_.

        INPUT:

        - ``algorithm`` -- integer

        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: c = cut_and_project_schemes.JeandelRao()
            sage: c
            4-to-2 cut and project scheme

        ::

            sage: print(c)
            4-to-2 cut and project scheme over
            Number Field in phi with defining polynomial z^2 - z - 1 with
            phi = 1.618033988749895?
            Projection to physical space:
            [1 1 0 0]
            [0 0 1 1]
            Projection to internal space:
            [       1 -phi + 1        0  phi - 1]
            [       0        0        1 -phi - 2]
            Lattice generated by the columns of:
            [1 0 0 0]
            [0 1 0 0]
            [0 0 1 0]
            [0 0 0 1]

        The third version has valid projection in the physical space::

            sage: c = cut_and_project_schemes.JeandelRao()
            sage: c.is_valid(verbose=True)
            projection minors sign =  [0, 1, 1, 1, 1, 0]
            slope minors sign      =  [1, 1, 1, 1, 1, 0]
            False
            sage: c = cut_and_project_schemes.JeandelRao(3)
            sage: c.is_valid(verbose=True)
            projection minors sign =  [1, 1, 1, 1, 1, 0]
            slope minors sign      =  [1, 1, 1, 1, 1, 0]
            True

        REFERENCES:

        .. [L21] Labbé, Sébastien. Markov Partitions for Toral
           $\mathbb{Z}^2$-Rotations Featuring Jeandel–Rao Wang Shift and
           Model Sets, Annales Henri Lebesgue 4 (2021) 283‑324.
           https://doi.org/10.5802/ahl.73.

        """
        from sage.rings.rational_field import QQ
        from sage.rings.real_mpfr import RR
        from sage.rings.polynomial.polynomial_ring import polygen
        from sage.rings.number_field.number_field import NumberField
        from sage.matrix.constructor import matrix

        z = polygen(QQ, 'z')
        K = NumberField(z**2-z-1, 'phi', embedding=RR(1.6))
        phi = K.gen()

        if algorithm == 0:
            projection_int = matrix(K, [[1,-~phi,0,~phi],[0,0,1,-(phi+2)]])
            projection_phys = matrix(K, [[1,1,0,0],[0,0,1,1]])

        elif algorithm == 1:
            projection_int = matrix(K, [[1,-~phi,0,~phi],[0,0,1,-(phi+2)]])
            def galois_conjugate_matrix(M):
                return matrix([[a.galois_conjugate() for a in row] for row in M.rows()])
            projection_phys = galois_conjugate_matrix(projection_int)

        elif algorithm == 2:
            projection_int = matrix(K, [[1,-~phi,0,~phi],[0,0,1,-(phi+2)]])
            projection_phys = matrix([[1,phi,0,1-phi],[0,0,1,1]])

        elif algorithm == 3:
            projection_int = matrix(K, [[1,-~phi,0,~phi],[0,0,1,-(phi+2)]])
            J = matrix(K, [[1,phi,0,0], [-1,phi-1,5*phi,2*phi-1]])
            projection_phys = J

        else:
            raise ValueError()

        return CutAndProjectScheme(K, projection_phys, projection_int)

    def self_similar_19_tiles(self):
        r"""
        Return the self-similar 19 Wang tiles cut and project scheme from
        [L21]_.

        EXAMPLES::

            sage: from slabbe import cut_and_project_schemes
            sage: cut_and_project_schemes.self_similar_19_tiles()
            4-to-2 cut and project scheme

        """
        from sage.rings.rational_field import QQ
        from sage.rings.real_mpfr import RR
        from sage.rings.polynomial.polynomial_ring import polygen
        from sage.rings.number_field.number_field import NumberField
        from sage.matrix.constructor import matrix

        z = polygen(QQ, 'z')
        K = NumberField(z**2-z-1, 'phi', embedding=RR(1.6))
        phi = K.gen()
        pi = matrix(K, [[1, 1, 0, 0], [0, 0, 1, 1]])
        pi_int = matrix(K, [[~phi**2, -~phi, 0, 0], [0, 0, ~phi**2, -~phi]])
        return CutAndProjectScheme(K, pi, pi_int)

class ModelSetGenerator():
    r"""
    Constructor of several famous model sets

    EXAMPLES::

        sage: from slabbe import model_sets
        sage: model_sets.Fibonacci()
        Model Set of a 2-to-1 cut and project scheme

    """
    def Fibonacci(self):
        r"""
        Return the Fibonacci cut and project scheme

        EXAMPLES::

            sage: from slabbe import model_sets
            sage: model_sets.Fibonacci()
            Model Set of a 2-to-1 cut and project scheme

        """
        from sage.geometry.polyhedron.constructor import Polyhedron
        cap = cut_and_project_schemes.Fibonacci()
        K = cap.base_ring()
        phi = K.gen()
        W = Polyhedron([(-1,),(phi-1,)])
        return ModelSet(cap, W)

    def Fibonacci_the_Minkowski_way(self):
        r"""
        Return the Fibonacci cut and project scheme

        EXAMPLES::

            sage: from slabbe import model_sets
            sage: model_sets.Fibonacci_the_Minkowski_way()
            Model Set of a 2-to-1 cut and project scheme

        """
        from sage.geometry.polyhedron.constructor import Polyhedron
        cap = cut_and_project_schemes.Fibonacci_the_Minkowski_way()
        K = cap.base_ring()
        phi = K.gen()
        W = Polyhedron([(-1,),(phi-1,)])
        return ModelSet(cap, W)

    def Fibonacci2D(self):
        r"""
        Return the 2D Fibonacci cut and project scheme

        EXAMPLES::

            sage: from slabbe import model_sets
            sage: model_sets.Fibonacci2D()
            Model Set of a 4-to-2 cut and project scheme

        """
        from sage.geometry.polyhedron.library import polytopes
        cap = cut_and_project_schemes.Fibonacci2D()
        K = cap.base_ring()
        phi = K.gen()
        W = polytopes.hypercube(2, intervals=[(-1,phi-1), (-1,phi-1)])
        return ModelSet(cap, W)



    def Penrose(self, shift=None):
        r"""
        Return the Penrose cut and project scheme

        INPUT:

        - ``shift`` -- 5-dimensional vector translating the internal window
          to avoid singular tilings and Conway worms

        EXAMPLES::

            sage: from slabbe import model_sets
            sage: model_sets.Penrose()
            Model Set of a 5-to-2 cut and project scheme

        ::

            sage: shift = vector((1,-1,2,-1,-1)) / 1000
            sage: model_sets.Penrose(shift)
            Model Set of a 5-to-2 cut and project scheme

        """
        from sage.modules.free_module_element import vector
        from sage.geometry.polyhedron.library import polytopes
        cap = cut_and_project_schemes.Penrose()
        return cap.canonical_model_set(shift=shift)

    def GoldenOctagonal(self, shift=None):
        r"""
        Return the Golden-Octagonal cut and project scheme

        INPUT:

        - ``shift`` -- 4-dimensional vector translating the internal window
          to avoid singular tilings and Conway worms

        EXAMPLES::

            sage: from slabbe import model_sets
            sage: model_sets.GoldenOctagonal()
            Model Set of a 4-to-2 cut and project scheme

        ::

            sage: shift = vector((1,-1,2,-1)) / 1000
            sage: model_sets.GoldenOctagonal(shift)
            Model Set of a 4-to-2 cut and project scheme

        """
        from sage.modules.free_module_element import vector
        from sage.geometry.polyhedron.library import polytopes

        cap = cut_and_project_schemes.GoldenOctagonal()
        return cap.canonical_model_set(shift=shift)

################
# the generators
################
cut_and_project_schemes = CutAndProjectSchemeGenerator()
model_sets = ModelSetGenerator()


###


def hypercube_pet(M, i):
    r"""
    EXAMPLES::

        sage: from slabbe import cut_and_project_schemes
        sage: c = cut_and_project_schemes.GoldenOctagonal(projection='roots_of_unity')
        sage: M = c._pi_int.change_ring(RDF)
        sage: M
        [                1.0                 0.0                -1.0   1.618033988749895]
        [-0.9172881767044976                 1.0 -0.7007458120453971 0.13383054136359823]
        sage: from slabbe.cut_and_project_scheme import hypercube_pet
        sage: T = hypercube_pet(M, 0)
        sage: T
        Polyhedron Exchange Transformation of
        Polyhedron partition of 4 atoms with 4 letters
        with translations {'123': (1.0, -0.9172881767044976), '01':
        (-2.618033989, -0.8345763534), '03': (1.0000000000000002, 1.7007458117), '02':
        (-1.618033989, 0.8661694583)}

    """
    import itertools
    from sage.geometry.polyhedron.library import polytopes
    from sage.modules.free_module_element import vector
    from sage.geometry.polyhedron.constructor import Polyhedron
    from slabbe import PolyhedronPartition
    from slabbe import PolyhedronExchangeTransformation as PET

    H = polytopes.hypercube(4, intervals=[(-1,0) for _ in range(4)])

    # vector ei
    def e(i):
        L = [0] * 4
        L[i] = 1
        return vector(L)

    ei = e(i)

    # Equation x * ei = -1
    eqn = [1] + list(ei)
    eq = Polyhedron(eqns=[eqn])

    # lower-half of the hypercube
    H_eq = H & eq
    M_H_eq = M * H_eq
    
    M_ei = M*ei
    P_i = Polyhedron([(0,0), M_ei])

    F_pos = [f.as_polyhedron() for f in M_H_eq.facets() if f.normal_cone().rays()[0].vector() * M_ei >= 0]
    F_neg = [f.as_polyhedron() for f in M_H_eq.facets() if f.normal_cone().rays()[0].vector() * M_ei <= 0]

    lift_edge = {M*edge.as_polyhedron():edge for edge in H_eq.faces(1)}

    def find_edge_index(f):
        u,v = lift_edge[f].vertices()
        diff = v.vector() - u.vector()
        nonzero = [a for (a,b) in enumerate(diff) if b != 0]
        assert len(nonzero) == 1, str(nonzero)
        return nonzero[0]

    other_indices = [a for a in range(4) if a != i]
    concat = lambda L: ''.join(str(a) for a in sorted(L))

    d = {concat(other_indices):M_H_eq}
    translations = {concat(other_indices): M_ei}

    # computing the partition
    for f in F_pos:
        f_index = find_edge_index(f)
        key = concat([i,f_index])
        d[key] = f + P_i

        for g in F_neg:
            g_index = find_edge_index(g)
            if g_index == f_index:
                Me = M*e(f_index)
                f0 = min([v.vector() for v in f.vertices()], key=lambda a:a*Me)
                g0 = min([v.vector() for v in g.vertices()], key=lambda a:a*Me)     
                translations[key] = g0 - f0
                break
            else:
                continue
                
    P = PolyhedronPartition(d)

    return PET(P, translations)






