# -*- coding: utf-8 -*-
r"""
Tiling of the plane by isometric copies of a polygon

EXAMPLES::

    sage: F = AffineGroup(2, AA)
    sage: F([1,2,3,4],[5,6])
          [1 2]     [5]
    x |-> [3 4] x + [6]

AUTHORS:

 - Fait avec Lucas <lfx337@gmail.com>, le 28 novembre 2024.

"""
#*****************************************************************************
#       Copyright (C) 2024 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.groups.affine_gps.affine_group import AffineGroup

class PolygonTiling:
    r"""
    A periodic tiling by one polygon

    TODO::

    - It would maybe be better to use polyhedron instead of list of
      vertices?

    - Use the graph Eulerian paths and add a tikz method.

    - Code more (all?) of the 15 pentagons
    """
    def __init__(self, polygon, patch_symmetries=None, translations=None, ring=None):
        r"""

        If ``T`` denotes the group generated by the translations, then
        the input must be such that

            ``[t*s(polygon) for s in patch_symmetries for t in T]``

        is a tiling of the space.

        INPUT:

        - ``polygon`` -- list of vertices
        - ``patch_symmetries`` -- list of transformations (affine maps)
        - ``translations`` -- list of translations (affine maps),
          generators of a subgroup of translations
        - ``ring`` -- ring (default:``None``), if ``None``, then is uses ``AA``

        EXAMPLES::

            sage: from slabbe.polygon_tiling import PolygonTiling
            sage: jennifer = [(0,0), (1,0), (1+sqrt(3),1), (1+sqrt(3)/2,3/2), (0,1)]
            sage: F = AffineGroup(2, AA)
            sage: T = [F.translation((1,0))]
            sage: J = PolygonTiling(jennifer, translations=T)
            sage: J
            Tiling by the polygon [(0, 0), (1, 0), (2.732050807568878?,
            1), (1.866025403784439?, 3/2), (0, 1)]

        """
        if ring is None:
            from sage.rings.qqbar import AA
            ring = AA

        from sage.modules.free_module_element import vector
        self._polygon = [vector(ring, p) for p in polygon]

        if not self._polygon:
            raise ValueError('polygon(={}) must be non empty'.format(self._polygon))

        self._dimension = len(self._polygon[0])
        self._affine_group = AffineGroup(self._dimension, ring)

        if patch_symmetries is None:
            self._patch_symmetries = [self._affine_group.one()]
        else:
            self._patch_symmetries = patch_symmetries

        if translations is None:
            self._translations = [self._affine_group.one()]
        else:
            self._translations = translations

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

            sage: from slabbe.polygon_tiling import PolygonTiling
            sage: jennifer = [(0,0), (1,0), (1+sqrt(3),1), (1+sqrt(3)/2,3/2), (0,1)]
            sage: J = PolygonTiling(jennifer)
            sage: J
            Tiling by the polygon [(0, 0), (1, 0), (2.732050807568878?,
            1), (1.866025403784439?, 3/2), (0, 1)]

        """
        return "Tiling by the polygon {}".format(self._polygon)

    def affine_group(self):
        r"""
        Return the affine group containing the isometries

        EXAMPLES::

            sage: from slabbe.polygon_tiling import PolygonTiling
            sage: jennifer = [(0,0), (1,0), (1+sqrt(3),1), (1+sqrt(3)/2,3/2), (0,1)]
            sage: J = PolygonTiling(jennifer)
            sage: J.affine_group()
            Affine Group of degree 2 over Algebraic Real Field

        """
        return self._affine_group

    def dimension(self):
        r"""
        Return the dimension of the space

        EXAMPLES::

            sage: from slabbe.polygon_tiling import PolygonTiling
            sage: jennifer = [(0,0), (1,0), (1+sqrt(3),1), (1+sqrt(3)/2,3/2), (0,1)]
            sage: J = PolygonTiling(jennifer)
            sage: J.dimension()
            2

        """
        return self._dimension

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

            sage: from slabbe.polygon_tiling import PolygonTiling
            sage: jennifer = [(0,0), (1,0), (1+sqrt(3),1), (1+sqrt(3)/2,3/2), (0,1)]
            sage: J = PolygonTiling(jennifer)
            sage: J.polygon_angles()
            [1/2*pi,
             pi - arccos(0.866025403784439?),
             1/3*pi,
             pi - arccos(0.7071067811865475?),
             pi - arccos(0.2588190451025208?)]

        """
        from sage.functions.trig import arccos
        N = len(self._polygon)
        edges = [self._polygon[(i+1)%N]-self._polygon[i] for i in range(N)]

        L = [arccos(-edges[i]*edges[(i-1)%N] /
             (edges[i].norm()*edges[(i-1)%N].norm())) for i in range(N)]
        return L

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

            sage: from slabbe.polygon_tiling import PolygonTiling
            sage: jennifer = [(0,0), (1,0), (1+sqrt(3),1), (1+sqrt(3)/2,3/2), (0,1)]
            sage: J = PolygonTiling(jennifer)
            sage: J.polygon_vertex_distances()
            [1, 2.000000000000000?, 1.000000000000000?, 1.931851652578137?, 1]

        """
        N = len(self._polygon)
        distances = []
        for i in range(N):
            p = self._polygon[i]
            q = self._polygon[(i+1)%N]
            v = q - p
            distances.append(v.norm())
        return distances

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

            sage: from slabbe.polygon_tiling import PolygonTiling
            sage: jennifer = [(0,0), (1,0), (1+sqrt(3),1), (1+sqrt(3)/2,3/2), (0,1)]
            sage: J = PolygonTiling(jennifer)
            sage: J.polygon_area()
            2.549038105676658?

        """
        from sage.geometry.polyhedron.constructor import Polyhedron
        p = Polyhedron(self._polygon)
        return p.volume()

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

            sage: from slabbe.polygon_tiling import PolygonTiling
            sage: jennifer = [(0,0), (1,0), (1+sqrt(3),1), (1+sqrt(3)/2,3/2), (0,1)]
            sage: J = PolygonTiling(jennifer)
            sage: J.pp()
            Angles of the polygon:
             A = 90.000 degrees
             B = 150.000 degrees
             C = 60.000 degrees
             D = 135.000 degrees
             E = 105.000 degrees
            Side-lengths of the polygon:
             a = 1.000
             b = 1.000
             c = 2.000
             d = 1.000
             e = 1.932
        """
        if len(self._polygon) != 5:
            raise NotImplementedError

        from sage.symbolic.constants import pi
        angles_rad = self.polygon_angles()
        angles_degrees = [a.n()/pi.n()*180 for a in angles_rad]

        print("Angles of the polygon:")
        print(" A = {:.3f} degrees".format(angles_degrees[0]))
        print(" B = {:.3f} degrees".format(angles_degrees[1]))
        print(" C = {:.3f} degrees".format(angles_degrees[2]))
        print(" D = {:.3f} degrees".format(angles_degrees[3]))
        print(" E = {:.3f} degrees".format(angles_degrees[4]))

        (b,c,d,e,a) = self.polygon_vertex_distances()
        print("Side-lengths of the polygon:")
        print(" a = {:.3f}".format(a.n()))
        print(" b = {:.3f}".format(b.n()))
        print(" c = {:.3f}".format(c.n()))
        print(" d = {:.3f}".format(d.n()))
        print(" e = {:.3f}".format(e.n()))


    def patch(self):
        r"""
        Return a list of list of vertices.

        EXAMPLES::

            sage: from slabbe.polygon_tiling import PolygonTiling
            sage: jennifer = [(0,0), (1,0), (1+sqrt(3),1), (1+sqrt(3)/2,3/2), (0,1)]
            sage: J = PolygonTiling(jennifer)
            sage: list(J.patch())
            [[(0, 0),
              (1, 0),
              (2.732050807568878?, 1),
              (1.866025403784439?, 1.5000000000000000?),
              (0, 1)]]

        """
        for s in self._patch_symmetries:
            yield [s*v for v in self._polygon] 

    def iter_polygons(self, depth, region=None, round=None):
        r"""
        Iterator of the polygons of the tiling

        INPUT:

        - ``depth`` -- integer
        - ``region`` -- ``None`` or polyhedron, polygons in the output are
          restricted to this region
        - ``round`` -- rounding map to avoid 2 very close vertices to be
          considered different

        EXAMPLES::

            sage: from slabbe.polygon_tiling import PolygonTiling
            sage: jennifer = [(0,0), (1,0), (1+sqrt(3),1), (1+sqrt(3)/2,3/2), (0,1)]
            sage: F = AffineGroup(2, AA)
            sage: T = [F.translation((1,0))]
            sage: J = PolygonTiling(jennifer, translations=T)
            sage: list(J.iter_polygons(depth=2))
            [[(-2, 0),
              (-1, 0),
              (0.732050807568878?, 1),
              (-0.1339745962155614?, 1.5000000000000000?),
              (-2, 1)],
             [(-1, 0),
              (0, 0),
              (1.732050807568878?, 1),
              (0.866025403784439?, 1.5000000000000000?),
              (-1, 1)],
             [(0, 0),
              (1, 0),
              (2.732050807568878?, 1),
              (1.866025403784439?, 1.5000000000000000?),
              (0, 1)],
             [(1, 0),
              (2, 0),
              (3.732050807568878?, 1),
              (2.866025403784439?, 1.5000000000000000?),
              (1, 1)],
             [(2, 0),
              (3, 0),
              (4.732050807568877?, 1),
              (3.866025403784439?, 1.5000000000000000?),
              (2, 1)]]

        Restricted to a region::

            sage: box = polytopes.hypercube(dim=2, intervals=[(-2,2), (-2,2)])
            sage: list(J.iter_polygons(depth=2, region=box))
            [[(-2, 0), (-1, 0), ..., (-1, 1)]]

        """
        if round is None:
            round = lambda x:x

        import itertools
        from sage.misc.misc_c import prod
        k = len(self._translations)
        for r in itertools.product(range(-depth, depth+1), repeat=k):
            t_r = prod(t**ri for (t,ri) in zip(self._translations, r))
            for polygon in self.patch():
                t_r_polygon = [t_r*v for v in polygon] 
                if region is None or all(v in region for v in t_r_polygon):
                    polygon = [v.apply_map(round) for v in t_r_polygon]
                    yield polygon

    def is_edge_to_edge(self, verbose=False, round=None):
        r"""
        Return whether the tiling is edge to edge

        INPUT:

        - ``round`` -- rounding map to avoid 2 very close vertices to be
          considered different
        """
        from collections import Counter

        c_depth0 = Counter()
        for p in self.iter_polygons(depth=0, round=round):
            for v in p:
                v.set_immutable()
            len_p = len(p)
            for i in range(len_p):
                edge = (p[i],p[(i+1) % len_p]) 
                c_depth0[frozenset(edge)] += 1

        c_depth1 = Counter()
        for p in self.iter_polygons(depth=1, round=round):
            for v in p:
                v.set_immutable()
            len_p = len(p)
            for i in range(len_p):
                edge = (p[i],p[(i+1) % len_p]) 
                c_depth1[frozenset(edge)] += 1

        Sd0_mul1 = set(edge for (edge,mul) in c_depth0.items() if mul == 1) 
        Sd1_mul2 = set(edge for (edge,mul) in c_depth1.items() if mul == 2) 

        if Sd0_mul1 <= Sd1_mul2:
            return True
        else:
            if verbose:
                print(Sd0_mul1 - Sd1_mul2)
            return False

    def graph(self, depth, region=None, round=None):
        r"""
        Return the graph of the tiling.

        INPUT:

        - ``depth`` -- integer
        - ``region`` -- ``None`` or polyhedron, polygons in the output are
          restricted to this region
        - ``round`` -- rounding map to avoid 2 very close vertices to be
          considered different

        EXAMPLES::

            sage: from slabbe.polygon_tiling import PolygonTiling
            sage: jennifer = [(0,0), (1,0), (1+sqrt(3),1), (1+sqrt(3)/2,3/2), (0,1)]
            sage: F = AffineGroup(2, AA)
            sage: T = [F.translation((1,0))]
            sage: J = PolygonTiling(jennifer, translations=T)
            sage: J.graph(depth=1)
            Graph on 13 vertices

        Restricted to a region::

            sage: box = polytopes.hypercube(dim=2, intervals=[(-2,2), (-2,2)])
            sage: J.graph(depth=2, region=box)
            Graph on 9 vertices

        Rounding avoids having doubled copies of the same vertices::

            sage: from slabbe.polygon_tiling import pentagonal_tilings
            sage: t = pentagonal_tilings.type_7(a=2, B=4*pi/5, ring=RealField(53))
            sage: t.graph(depth=0, round=lambda x:round(2^10*x)/2^10)
            Graph on 22 vertices
            sage: t.graph(depth=0, round=None)
            Graph on 38 vertices

        """
        from sage.graphs.graph import Graph
        G = Graph()
        for p in self.iter_polygons(depth=depth, region=region, round=round):
            for v in p:
                v.set_immutable()
            len_p = len(p)
            G.add_edges((p[i],p[(i+1) % len_p]) for i in range(len_p))
        return G

    def eulerian_paths(self, depth, region=None, round=None):
        r"""
        Iterator of the paths forming a partition of the edges of the tiling.

        INPUT:

        - ``depth`` -- integer
        - ``region`` -- ``None`` or polyhedron, polygons in the output are
          restricted to this region
        - ``round`` -- rounding map to avoid 2 very close vertices to be
          considered different

        EXAMPLES::

            sage: from slabbe.polygon_tiling import PolygonTiling
            sage: jennifer = [(0,0), (1,0), (1+sqrt(3),1), (1+sqrt(3)/2,3/2), (0,1)]
            sage: F = AffineGroup(2, AA)
            sage: T = [F.translation((1,0))]
            sage: J = PolygonTiling(jennifer, translations=T)
            sage: J.eulerian_paths(depth=1)
            [[(-1, 0), ..., (0, 0), (-1, 0)]]

        Restricted to a region::

            sage: box = polytopes.hypercube(dim=2, intervals=[(-2,2), (-2,2)])
            sage: J.eulerian_paths(depth=2, region=box)
            [[(-2, 0), (-2, 1), ..., (-1, 0), (-2, 0)]]

        """
        from slabbe.graph import eulerian_paths
        G = self.graph(depth=depth, region=region, round=round)
        return eulerian_paths(G)

    def vertices(self, depth, region=None):
        r"""
        Return the set of vertices of the tiling.

        INPUT:

        - ``depth`` -- integer
        - ``region`` -- ``None`` or polyhedron, polygons in the output are
          restricted to this region

        EXAMPLES::

            sage: from slabbe.polygon_tiling import PolygonTiling
            sage: jennifer = [(0,0), (1,0), (1+sqrt(3),1), (1+sqrt(3)/2,3/2), (0,1)]
            sage: J = PolygonTiling(jennifer)
            sage: J.vertices(1)                               # abs tol 0.0001
            {(0, 0), (0, 1), (1, 0), (1.866025403784439?, 3/2), (2.732050807568878?, 1)}
            sage: sorted(v.n() for v in J.vertices(1))        # abs tol 0.0001
            [(0.000000000000000, 0.000000000000000),
             (0.000000000000000, 1.00000000000000),
             (1.00000000000000, 0.000000000000000),
             (1.86602540378444, 1.50000000000000),
             (2.73205080756888, 1.00000000000000)]

        Restricted to a region::

            sage: box = polytopes.hypercube(dim=2, intervals=[(-5,5), (-5,5)])
            sage: sorted(v.n() for v in J.vertices(2, region=box))  # abs tol 0.0001
            [(0.000000000000000, 0.000000000000000),
             (0.000000000000000, 1.00000000000000),
             (1.00000000000000, 0.000000000000000),
             (1.86602540378444, 1.50000000000000),
             (2.73205080756888, 1.00000000000000)]

        """
        s = set()
        for p in self.iter_polygons(depth, region=region):
            for v in p:
                v.set_immutable()
                s.add(v)
        return s

    def plot(self, depth, region=None):
        r"""
        Return a graphics 2d of the polygon of the tiling.

        INPUT:

        - ``depth`` -- integer
        - ``region`` -- ``None`` or polyhedron, polygons in the output are
          restricted to this region

        EXAMPLES::

            sage: from slabbe.polygon_tiling import PolygonTiling
            sage: jennifer = [(0,0), (1,0), (1+sqrt(3),1), (1+sqrt(3)/2,3/2), (0,1)]
            sage: J = PolygonTiling(jennifer)
            sage: J.plot(1)
            Graphics object consisting of 3 graphics primitives


        ::

            sage: jennifer = [(0,0), (1,0), (1+sqrt(3),1), (1+sqrt(3)/2,3/2), (0,1)]
            sage: F = AffineGroup(2, AA)
            sage: patch_symmetries = [F.one(), F.translation((2,2))]
            sage: J = PolygonTiling(jennifer, patch_symmetries)
            sage: J.plot(1)
            Graphics object consisting of 6 graphics primitives

        ::

            sage: jennifer = [(0,0), (1,0), (1+sqrt(3),1), (1+sqrt(3)/2,3/2), (0,1)]
            sage: F = AffineGroup(2, AA)
            sage: T = [F.translation((1,0))]
            sage: J = PolygonTiling(jennifer, translations=T)
            sage: J.plot(1)
            Graphics object consisting of 3 graphics primitives

        Restricted to a region::

            sage: box = polytopes.hypercube(dim=2, intervals=[(-2,2), (-2,2)])
            sage: J.plot(1, region=box)
            Graphics object consisting of 1 graphics primitive

        """
        from sage.plot.graphics import Graphics
        from sage.plot.polygon import polygon2d
        G = Graphics()
        for p in self.iter_polygons(depth, region=region):
            G += polygon2d(p, fill=False, thickness=4, color='orange')
        return G

    def tikz(self, depth, region=None, color='red', round=None, verbose=False):
        r"""
        Return a graphics 2d of the polygon of the tiling.

        INPUT:

        - ``depth`` -- integer
        - ``region`` -- ``None`` or polyhedron, polygons in the output are
          restricted to this region
        - ``color`` -- string (default: ``'red'``) 
        - ``round`` -- rounding map to avoid 2 very close vertices to be
          considered different
        - ``verbose`` -- bool (default: ``False``) 

        EXAMPLES::

            sage: from slabbe.polygon_tiling import PolygonTiling
            sage: jennifer = [(0,0), (1,0), (1+sqrt(3),1), (1+sqrt(3)/2,3/2), (0,1)]
            sage: F = AffineGroup(2, AA)
            sage: T = [F.translation((1,0))]
            sage: J = PolygonTiling(jennifer, translations=T)
            sage: tikz = J.tikz(depth=1); tikz             # known bug
            \documentclass[tikz]{standalone}
            \begin{document}
            \begin{tikzpicture}
            \draw[red] (-1.00000,0.00000) -- (-1.00000,1.00000) --
            (0.86603,1.50000) -- (1.73205,1.00000) -- (0.00000,0.00000) --
            (0.00000,1.00000) -- (1.86603,1.50000) -- (2.73205,1.00000) --
            (1.00000,0.00000) -- (1.00000,1.00000) -- (2.86603,1.50000) --
            (3.73205,1.00000) -- (2.00000,0.00000) -- (1.00000,0.00000) --
            (0.00000,0.00000) -- (-1.00000,0.00000);
            \end{tikzpicture}
            \end{document}

        Restricted to a region::

            sage: box = polytopes.hypercube(dim=2, intervals=[(-2,2), (-2,2)])
            sage: tikz = J.tikz(depth=2, region=box); tikz       # known bug
            \documentclass[tikz]{standalone}
            \begin{document}
            \begin{tikzpicture}
            \draw[red] (-2.00000,0.00000) -- (-2.00000,1.00000) --
            (-0.13397,1.50000) -- (0.73205,1.00000) -- (-1.00000,0.00000)
            -- (-1.00000,1.00000) -- (0.86603,1.50000) -- (1.73205,1.00000)
            -- (0.00000,0.00000) -- (-1.00000,0.00000) --
            (-2.00000,0.00000);
            \end{tikzpicture}
            \end{document}

        Use a rounding map to avoid precision issues ::

            sage: from slabbe.polygon_tiling import pentagonal_tilings
            sage: t = pentagonal_tilings.type_7(a=2, B=4*pi/5, ring=RealField(53))
            sage: tikz = t.tikz(depth=1, round=lambda x:round(2^10*x)/2^10)

        """
        lines = []
        lines.append(r"\begin{tikzpicture}")

        if self.is_edge_to_edge(round=round):
            paths = self.eulerian_paths(depth, region=region, round=round)
            if verbose:
                from collections import Counter
                print("Decomposition of the tiling graph into {} eulerian"
                      " paths\nwhose distribution of lengths is:\n"
                      "{}".format(len(paths), dict(Counter(len(path) for path in paths))))
            for path in paths:
                s = " -- ".join(["({:.5f},{:.5f})".format(*v.n()) for v in path])
                lines.append(r"\draw[{}] {};".format(color, s))
        else:
            raise NotImplementedError("graph is not edge to edge (you may want"
                    " to use 'round' input: see doc)")

        lines.append(r"\end{tikzpicture}")
        from sage.misc.latex_standalone import TikzPicture
        return TikzPicture('\n'.join(lines))

def symmetrie(p1, p2, ring=None, check=False):
    r"""
    Return la transformation lineraire qui fait la symmetrie dans la droite
    passant par p1 et p2.
    
    EXAMPLES::

        sage: from slabbe.polygon_tiling import symmetrie
        sage: p1 = (12,166)
        sage: p2 = (45,227)
        sage: T = symmetrie(p1, p2)
        sage: T
              [-1316/2405  2013/2405]     [-289506/2405]
        x |-> [ 2013/2405  1316/2405] x + [ 156618/2405]
        sage: T(p1)
        (12, 166)
        sage: T(p2)
        (45, 227)

    """
    from sage.matrix.constructor import matrix
    if ring is None:
        from sage.rings.qqbar import AA
        F = AffineGroup(2, AA)
    else:
        F = AffineGroup(2, ring)
    F_p1 = F.translation(p1)
    (x2,y2) = F_p1.inverse()(p2)
    F_rotate_p2 = F(matrix.column([(x2,y2),(-y2,x2)]))
    F_symmetry = F(matrix.column([(1,0),(0,-1)]))
    T = (F_p1 * F_rotate_p2 * F_symmetry * F_rotate_p2.inverse() * F_p1.inverse())
    if check:
        assert (T(p1)-p1).norm()<1e-10, "{} == {}, {}".format(p1, T(p1), (T(p1)-p1).norm())
        assert (T(p2)-p2).norm()<1e-10, "{} == {}, {}".format(p2, T(p2), (T(p2)-p2).norm())
    return T

def symmetrie_mediatrice(p1, p2, ring=None, check=False):
    r"""
    Return la transformation lineraire qui fait la symmetrie dans la
    mediatrice du segment passant par p1 et p2.
    
    EXAMPLES::

        sage: from slabbe.polygon_tiling import symmetrie_mediatrice
        sage: p1 = (12,166)
        sage: p2 = (45,227)
        sage: T = symmetrie_mediatrice(p1, p2)
        sage: T
              [ 1316/2405 -2013/2405]     [426591/2405]
        x |-> [-2013/2405 -1316/2405] x + [788547/2405]
        sage: T(p1)
        (45, 227)
        sage: T(p2)
        (12, 166)

    """
    from sage.matrix.constructor import matrix
    if ring is None:
        from sage.rings.qqbar import AA
        F = AffineGroup(2, AA)
    else:
        F = AffineGroup(2, ring)
    F_p1 = F.translation(p1)
    (x2,y2) = F_p1.inverse()(p2)
    F_rotate_p2 = F(matrix.column([(x2,y2),(-y2,x2)]))
    F_symmetry = F(matrix.column([(-1,0),(0,1)]),(1,0))
    T = (F_p1 * F_rotate_p2 * F_symmetry * F_rotate_p2.inverse() * F_p1.inverse())
    if check:
        assert (T(p1)-p2).norm()<1e-10, "{} == {}, {}".format(T(p1), p2, (T(p1)-p2).norm())
        assert (T(p2)-p1).norm()<1e-10, "{} == {}, {}".format(T(p2), p1, (T(p2)-p1).norm())
    return T


def rotation_180(VS, p):
    r"""
    Return the 180-degrees rotation around point p.

    INPUT:

    - ``VS`` - vector space
    - ``point`` - point in the vector space
    
    EXAMPLES::

        sage: from slabbe.polygon_tiling import rotation_180
        sage: p = (10, 200)
        sage: T = rotation_180(QQ^2, p)
        sage: T
              [-1  0]     [ 20]
        x |-> [ 0 -1] x + [400]
        sage: T(p)
        (10, 200)

    """
    F = AffineGroup(VS)
    T = F.translation(p)
    R = F([-1,0,0,-1])
    return T * R * T.inverse()

class PentagonalTilings:
    def type_3(self, c, d):
        r"""
        Return a type 3 pentagonal tiling.

        INPUT:

        - ``c`` -- length of side c
        - ``d`` -- length of side d

        .. NOTE::

            Length constraints: a=b, d=c+e
            Angle constraints: A=C=D=120

        EXAMPLES::

            sage: from slabbe.polygon_tiling import pentagonal_tilings
            sage: t = pentagonal_tilings.type_3(d=2, c=1/3)
            sage: t
            Tiling by the polygon [(0, 0), (-7/6, -1.443375672974065?), 
            (-1, -1.732050807568878?), (-2/3, -1.732050807568878?), (1, -1.732050807568878?),
            (11/6, -0.2886751345948129?)]

        TESTS::

            sage: t.is_edge_to_edge()
            True

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

        z = polygen(QQ, 'z')
        K = NumberField(z**2-3, 'sqrt3', embedding=RR(1.7))
        sqrt3 = K.gen()
        one = K.one()

        VS = K**2 # (2-dim vector space)

        c_d = c/d

        vA = VS((0,0))
        vD = d * VS((one/2, -sqrt3/2))
        vC = d * VS((-one/2, -sqrt3/2))
        vB = (1-c_d)*vC + c_d*VS((-d,0))
        vE = c_d*vD + (1-c_d)*VS((d,0))

        vCD = (1-c_d)*vC + c_d*vD   # so that the tiling is edge to edge

        pentagon = [vA, vB, vC, vCD, vD, vE]

        # transformations
        F = AffineGroup(VS)
        rotate60 = F([one/2, sqrt3/2, -sqrt3/2, one/2])

        patch = [F.one(),
                 rotate60**2,
                 rotate60**4]

        t1 = F.translation(vD + VS((d,0)))
        t2 = F.translation((0,-d*sqrt3))
        translations = [t1, t2]

        return PolygonTiling(pentagon, patch, translations)

    def type_5(self, a, c, B):
        r"""
        Return a type 5 pentagonal tiling.

        INPUT:

        - ``a`` -- length of side a
        - ``c`` -- length of side c
        - ``B`` -- angle of vertex B

        .. NOTE::

            Length constraints: a=b, c=d
            Angle constraints: A=120, C=60

        EXAMPLES::

            sage: from slabbe.polygon_tiling import pentagonal_tilings
            sage: t = pentagonal_tilings.type_5(a=1, c=3, B=3*pi/5)
            sage: t
            Tiling by the polygon [(-3.309016994374948?,
            0.9510565162951536?), (-3, 0), (0, 0), (-3/2,
            2.598076211353316?), (-2.639886388016089?, 1.694201341772548?)]

        TESTS::

            sage: t.is_edge_to_edge()
            True

        """
        from sage.misc.functional import sqrt
        from sage.rings.qqbar import AA
        from sage.functions.trig import cos, sin

        sqrt3 = AA(sqrt(3))
        one = AA.one()

        VS = AA**2 # (2-dim vector space)

        # transformations
        F = AffineGroup(VS)
        rotate60 = F([one/2, sqrt3/2, -sqrt3/2,one/2])
        rotateB = F([cos(B), -sin(B), sin(B), cos(B)])

        vC = VS((0,0))
        vB = VS((-c,0))
        vD = c*VS((-one/2,sqrt3/2))
        vA = vB + rotateB(a*VS((1,0)))
        vE = vB + rotateB(a*VS((3*one/2, -sqrt3/2)))

        pentagon = [vA, vB, vC, vD, vE]

        patch = [F.one(),
                 rotate60**1,
                 rotate60**2,
                 rotate60**3,
                 rotate60**4,
                 rotate60**5]

        t1 = F.translation(vE + vD)
        t2 = F.translation(rotate60(vE + vD))
        translations = [t1, t2]

        return PolygonTiling(pentagon, patch, translations)


    def type_6(self, a, c, A, B):
        r"""
        Return a type 6 pentagonal tiling.

        INPUT:

        - ``a`` -- length of side a
        - ``c`` -- length of side c
        - ``A`` -- angle of vertex A
        - ``B`` -- angle of vertex B

        .. NOTE::

            Length constraints: a=b=e, c=d
            Angle constraints: A+B+D=360, A=2C

        EXAMPLES::

            sage: from slabbe.polygon_tiling import pentagonal_tilings
            sage: t = pentagonal_tilings.type_6(a=1, c=3, A=pi/2, B=4*pi/5)
            Traceback (most recent call last):
            ...
            AssertionError: 1.86459371714417

        TESTS::

            sage: t.is_edge_to_edge() # not tested
            True

        """
        from sage.misc.functional import sqrt
        from sage.rings.qqbar import AA
        from sage.functions.trig import cos, sin

        sqrt3 = AA(sqrt(3))
        one = AA.one()
        C = A/2

        # transformations
        VS = AA**2 # (2-dim vector space)
        F = AffineGroup(VS)
        rotateA = F([cos(A), -sin(A), sin(A), cos(A)])
        rotateB = F([cos(B), -sin(B), sin(B), cos(B)])
        rotateC = F([cos(C), -sin(C), sin(C), cos(C)])
        rotateCinv = rotateC.inverse()

        vC = VS((0,0))
        vB = VS((-c,0))
        vD = rotateC.inverse()(vB)
        vA = vB + rotateB(a*VS((1,0)))
        vE = vB + rotateB(a*VS((1,0)) + rotateA(a*VS((-1,0))))

        pentagon = [vA, vB, vC, vD, vE]

        patch2 = [F.one(),
                rotateCinv]

        R = rotation_180(VS,(vD+rotateCinv(vA))/2)

        patch4 = [t for t in patch2]
        patch4.extend(R*t for t in patch2)

        t1 = F.translation(vE + vD)
        #t2 = F.translation(rotate60(vE + vD))
        #translations = [t1, t2]
        translations = [F.one()]

        p = PolygonTiling(pentagon, patch4, translations)

        A,B,C,D,E = p.polygon_angles()
        from sage.symbolic.constants import pi
        assert (A-2*C) < 1e-10, (A-2*C).n()
        assert ((A+B+D)/pi).n() == 2, ((A+B+D)/pi).n()

        (b,c,d,e,a) = p.polygon_vertex_distances()
        assert a == b == e
        assert c == d

        return p


    def type_7(self, a, B, ring=None, check=False):
        r"""
        Return a type 7 pentagonal tiling.

        INPUT:

        - ``a`` -- length of side a
        - ``B`` -- angle of vertex B

        .. NOTE::

            Length constraints: a=b=c=d
            Angle constraints: 2B+C=360, 2D+A=360

        EXAMPLES::

            sage: from slabbe.polygon_tiling import pentagonal_tilings
            sage: t = pentagonal_tilings.type_7(a=1, B=4*pi/5)
            sage: t
            Tiling by the polygon [(0.000000000000000, 0.000000000000000),
                    (1.00000000000000, 0.000000000000000),
                    (1.80901699437495, 0.587785252292473),
                    (1.00000000000000, 1.17557050458495),
                    (-0.0255593590340535, 0.999673306218471)]

        TESTS::

            sage: t.is_edge_to_edge()
            False
            sage: t.is_edge_to_edge(round=lambda x:round(2^10*x)/2^10)
            True

        """
        from sage.misc.functional import sqrt
        from sage.rings.qqbar import AA
        from sage.functions.trig import cos, sin
        from sage.symbolic.constants import pi

        if ring is None:
            from sage.rings.real_mpfr import RR
            ring = RR

        sqrt3 = ring(sqrt(3))
        one = ring.one()
        C = 2*pi - 2*B

        # transformations
        VS = ring**2 # (2-dim vector space)
        F = AffineGroup(VS)
        rotateB = F([cos(B), -sin(B), sin(B), cos(B)])
        rotateC = F([cos(C), -sin(C), sin(C), cos(C)])
        rotateBinv = rotateB.inverse()
        rotateCinv = rotateC.inverse()
        R = rotation_180(VS, (0,0))

        vA = VS((0,0))
        vB = VS((a,0))
        vC = vB + a*VS((-cos(B), sin(B)))
        vD = vB + R*rotateBinv(a*VS((1,0)) + rotateCinv(a*VS((-1,0))))

        # solving to find vertex E
        from sage.functions.trig import arccos
        from sage.calculus.var import var
        from sage.modules.free_module_element import vector
        from sage.numerical.optimize import find_root
        theta = var('theta')
        xy = a*vector((cos(theta),sin(theta)))
        u = vC-vD
        v = xy-vD
        angleD = arccos(u*v / (u.norm()*v.norm()))
        A = find_root(angleD == pi - theta/2, a=0,b=2*pi)
        vE = a*VS((cos(A), sin(A)))

        pentagon = [vA, vB, vC, vD, vE]

        s1 = F(symmetrie(vD, vE, ring=ring))
        patch2 = [F.one(), s1]

        pB = F.translation(s1(vB))
        r1 = pB*F(rotateB.inverse())*pB.inverse()
        patch4 = [t for t in patch2]
        patch4.extend(r1*t for t in patch2)

        r2 = rotation_180(VS, (s1(vB)+r1*s1(vA))/2)
        also = F.translation(vD - s1(vB))
        patch8 = [t for t in patch4]
        patch8.extend(also*r2*t for t in patch4)

        t1 = F.translation(also*r2*r1(vD) - vA)
        t2 = F.translation(r1(vB) - vB)
        translations = [t1, t2]

        p = PolygonTiling(pentagon, patch8, translations, ring=ring)

        if check:
            A,B,C,D,E = p.polygon_angles()
            assert ((A+2*D)/pi).n() - 2 < 1e-10, ((A+2*D)/pi).n() 
            assert ((C+2*B)/pi).n() - 2 < 1e-10, ((C+2*B)/pi).n()

            (b,c,d,e,a) = p.polygon_vertex_distances()
            assert (a - b)**2 + (b - c)**2 + (c - d)**2 < 1e-6, (a,b,c,d)

        return p


    def type_10(self, c, e, B):
        r"""
        Return a type 10 pentagonal tiling.

        INPUT:

        - ``c`` -- length of side c
        - ``e`` -- length of side e
        - ``B`` -- angle of vertex B

        .. NOTE::

            Length constraints: a=b=c+e.
            Angle constraints: A=90, B+E=180, B+2C=360, (C+D=270).

        .. TODO::

            Not all inputs are valid. Can we remove one argument and turn
            this into a function?

        EXAMPLES::

            sage: from slabbe.polygon_tiling import pentagonal_tilings
            sage: t = pentagonal_tilings.type_10(c=1, e=1, B=pi/2)
            sage: t
            Tiling by the polygon [(0, 0), (2, 0), (2, 1), (1, 2), (0, 2)]

        Bad inputs::

            sage: t = pentagonal_tilings.type_10(c=1, e=2, B=3*pi/5)
            Traceback (most recent call last):
            ...
            AssertionError: 1.90538514345210

        TESTS::

            sage: t = pentagonal_tilings.type_10(c=1, e=1, B=pi/2)
            sage: t.is_edge_to_edge()
            False

        """
        from sage.rings.qqbar import AA
        from sage.functions.trig import cos, sin

        VS = AA**2 # (2-dim vector space)

        vA = VS((0,0))
        vB = VS((c+e, 0))
        vC = VS((c+e-c*cos(B),c*sin(B)))
        vD = VS((e*sin(B), c+e+e*cos(B)))
        vE = VS((0, c+e))

        pentagon = [vA, vB, vC, vD, vE]

        # transformations
        F = AffineGroup(VS)
        rotate90 = F([0,-1,1,0])
        rotate180 = rotate90**2
        s = symmetrie(vC, vD)

        patch = [F.one(), 
                rotate90,
                rotate180,
                rotate90**3,
                s,
                rotate180 * s,
                ]

        p = (rotate180 * s) (vE)
        q = (rotate180 * s) (vB)

        t1 = F.translation(vD) * F.translation(p).inverse()
        t2 = F.translation(vC) * F.translation(q).inverse()
        translations = [t1, t2]

        p = PolygonTiling(pentagon, patch, translations)

        A,B,C,D,E = p.polygon_angles()

        from sage.symbolic.constants import pi
        assert (2*A/pi).n()     == 1, (2*A/pi).n()
        assert ((B+E)/pi).n()   == 1, ((B+E)/pi).n()
        assert ((B+2*C)/pi).n() == 2, ((B+2*C)/pi).n()
        assert (2*(C+D)/pi).n() == 3, (2*(C+D)/pi).n()

        return p



    def type_15(self, a):
        r"""
        Return a type 15 pentagonal tiling.

        INPUT:

        - ``a`` -- length of side a

        EXAMPLES::

            sage: from slabbe.polygon_tiling import pentagonal_tilings
            sage: t = pentagonal_tilings.type_15(a=1)
            sage: t
            Tiling by the polygon [(0, 0), (1, 0), (1.866025403784439?, 1/2),
            (2.732050807568878?, 1), (1.866025403784439?, 3/2), (0, 1)]

        TESTS::

            sage: t.is_edge_to_edge()
            True

        """
        from sage.misc.functional import sqrt
        from sage.rings.qqbar import AA

        sqrt3 = AA(sqrt(3))
        one = AA.one()

        VS = AA**2 # (2-dim vector space)

        A  = a*VS((0,0))
        B1 = a*VS((1,0))
        B2 = a*VS((1+sqrt3/2,one/2))
        C  = a*VS((1+sqrt3,1))
        D  = a*VS((1+sqrt3/2,3*one/2))
        E  = a*VS((0,1))

        pentagon = [A, B1, B2, C, D, E]

        # transformations
        F = AffineGroup(VS)
        rotate30 = F([sqrt3/2,one/2,-one/2,sqrt3/2])
        exchange_xy = F([0,1,1,0])

        patch3 = [F.one(), 
                F.translation(D)*rotate30*exchange_xy,
                symmetrie(D, E),
                ]

        g = F.translation((-a/2, a*sqrt3/2 + a))
        patch6 = [t for t in patch3]
        patch6.extend(g*rotate30.inverse()*exchange_xy*t for t in patch3)

        h1 = a*VS((-one/2, sqrt3*3/2 + 4))
        h2 = a*VS((sqrt3/2 - one/2, sqrt3*3/2 + 9*one/2))
        h  = symmetrie(h1, h2)
        hm = symmetrie_mediatrice(h1, h2)
        patch12 = [t for t in patch6]
        patch12.extend(hm*h*t for t in patch6)

        t1 = F.translation((a*sqrt3/2 + a/2, a*sqrt3/2 + 3*a/2))
        t2 = F.translation((-5*a*sqrt3/2 - 4*a, 2*a*sqrt3 + 9*a/2))
        translations = [t1,t2]

        return PolygonTiling(pentagon, patch12, translations)

pentagonal_tilings = PentagonalTilings()

