r"""
Constellations

A constellation is a tuple `(g_0, g_1, \dots, g_k)` of permutations
such that the product `g_0 g_1 ... g_k` is the identity. One often
assumes that the group generated by `g_0, g_1, \dots, g_k` acts
transitively ([LaZv04]_ definition 1). Geometrically, it corresponds
to a covering of the 2-sphere ramified over `k` points (the transitivity
condition corresponds to the connectivity of the covering).

.. NOTE::

    The permutations are on `[0, n-1]` and not on `[1, n]` for
    algorithmic and practical reasons.

EXAMPLES::

    sage: from surface_dynamics.misc.constellation import Constellation, Constellations
    sage: c = Constellation(['(0,1)', '(0,2)', None])
    sage: c
    Constellation of length 3 and degree 3
    g0 (0,1)(2)
    g1 (0,2)(1)
    g2 (0,2,1)
    sage: C = Constellations(3,4); C
    Connected constellations of length 3 and degree 4
    sage: C.cardinality()
    426

REFERENCES:

.. [LaZv04] S. Lando and A. Zvonkine, "Graphs on surfaces and their
            applications", Springer-Verlag, 2004.
"""
#*****************************************************************************
#       Copyright (C) 2010 Vincent Delecroix <20100.delecroix@gmail.com>
#
#  Distributed under the terms of the GNU General Public License (GPL)
#  as published by the Free Software Foundation; either version 2 of
#  the License, or (at your option) any later version.
#                  https://www.gnu.org/licenses/
#*****************************************************************************

from __future__ import print_function, absolute_import, division
from six.moves import range, zip, filter

from sage.structure.parent import Parent
from sage.structure.element import Element
from sage.structure.unique_representation import UniqueRepresentation

from sage.rings.integer import Integer
from sage.combinat.partition import Partition
from sage.structure.sage_object import SageObject
from sage.groups.perm_gps.permgroup_element import PermutationGroupElement
from sage.graphs.graph import Graph

# we import everything from sage.misc.permutation
from surface_dynamics.misc.permutation import *

ZZ_0 = Integer(0)

# constructors

def Constellations(*data, **options):
    r"""
    Build a set of constellations.

    INPUT:

    - ``profile`` -- an optional profile

    - ``length`` -- an optional length

    - ``degree`` -- an optional degree

    - ``connected`` -- an optional boolean

    EXAMPLES::

        sage: from surface_dynamics.misc.constellation import Constellation, Constellations

    The set of all constellations::

        sage: Constellations()
        Connected constellations

    Constellations with constrained length and degree::

        sage: Constellations(4,2)
        Connected constellations of length 4 and degree 2
    """
    profile = options.get('profile', None)
    length = options.get('length', None)
    degree = options.get('degree', None)
    connected = options.get('connected', True)

    if data:
        if len(data) == 1:
            if isinstance(data[0],(tuple,list)):
                profile = data[0]
            else:
                length = Integer(data[0])
        elif len(data) == 2:
            length = Integer(data[0])
            degree = Integer(data[1])

    if profile:
        profile = tuple(map(Partition,profile))
        return Constellations_p(profile, bool(connected))
    if degree is None and length is None:
        return Constellations_all(bool(connected))
    elif degree is not None and length is not None:
        return Constellations_ld(Integer(length), Integer(degree), bool(connected))
    else:
        raise NotImplementedError("one cannot give just degree or just length")

def Constellation(g=None, mutable=False, connected=True, check=True):
    r"""
    Constellation

    INPUT:

    - ``g`` -- a list of permutations

    - ``mutable`` -- whether the result is mutable or not. Default is ``False``.

    - ``connected`` -- whether the result should be connected. Default is
      ``True``.

    - ``check`` -- whether or not to check. If it is ``True``, then the
      list g must contains no ``None``.

    EXAMPLES::

        sage: from surface_dynamics.misc.constellation import Constellation

    Simple initialization::

        sage: Constellation(['(0,1)','(0,3)(1,2)','(0,3,1,2)'])
        Constellation of length 3 and degree 4
        g0 (0,1)(2)(3)
        g1 (0,3)(1,2)
        g2 (0,3,1,2)

    One of the permutation can be omitted::

        sage: Constellation(['(0,1)', None, '(0,4)(1,2,3)'])
        Constellation of length 3 and degree 5
        g0 (0,1)(2)(3)(4)
        g1 (0,3,2,1,4)
        g2 (0,4)(1,2,3)

    One can define mutable constellations::

        sage: Constellation(([0,2,1], [2,1,0], [1,2,0]), mutable=True)
        Constellation of length 3 and degree 3
        g0 (0)(1,2)
        g1 (0,2)(1)
        g2 (0,1,2)
    """
    return Constellations(connected=connected)(g,check=check,mutable=mutable)

# classes

class Constellation_class(Element):
    r"""
    Constellation

    A constellation or a tuple of permutations `(g_0,g_1,...,g_k)`
    such that the product `g_0 g_1 ... g_k` is the identity.
    """
    def __init__(self, parent, g, check=True, connected=True, mutable=False):
        r"""
        TESTS::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: c = Constellation([[1,2,0],[0,2,1],[1,0,2],None])
            sage: c == loads(dumps(c))
            True
            sage: g0 = '(0,1)(2,4)'
            sage: g1 = '(0,3)(1,4)'
            sage: g2 = '(2,4,3)'
            sage: g3 = '(0,3)(1,2)'
            sage: c0 = Constellation([g0,g1,g2,g3])
            sage: c0 == Constellation([None,g1,g2,g3])
            True
            sage: c0 == Constellation([g0,None,g2,g3])
            True
            sage: c0 == Constellation([g0,g1,None,g3])
            True
            sage: c0 == Constellation([g0,g1,g2,None])
            True
        """
        Element.__init__(self, parent)
        self._connected = connected
        self._mutable = mutable
        self._g = g
        if check:
            self._check()

    def __hash__(self):
        r"""
        Return a hash for ``self``

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: c = Constellation(([0,2,1],[2,1,0],[1,2,0]), mutable=False)
            sage: c.__hash__() # random
            2148362403144019871
        """
        if self._mutable:
            raise ValueError("can not hash mutable constellation")
        return hash(tuple(map(tuple, self._g)))

    def set_immutable(self):
        r"""
        Does nothing, as ``self`` is already immutable

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: c = Constellation(([0,2,1],[2,1,0],[1,2,0]), mutable=False)
            sage: c.set_immutable()
            sage: c.is_mutable()
            False
        """
        self._mutable = False

    def is_mutable(self):
        r"""
        Return ``False`` as ``self`` is immutable

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: c = Constellation(([0,2,1],[2,1,0],[1,2,0]), mutable=False)
            sage: c.is_mutable()
            False
        """
        return self._mutable

    def switch(self, i, j0, j1):
        r"""
        Swith the vertices and edges at `i` and `i+1`

        The modification is local in the sense that it modifies `g_i`
        and `g_{i+1}` but does not modify the product `g_i g_{i+1}`.

        |  j0 -- g_i   --> k0 -- g_i0  --> l0
        |  j1 -- g_i+1 --> k1 -- g_i+1 --> l1

        becomes

        |  j0 -- g_i   --> k1 -- g_i   --> l0
        |  j1 -- g_i+1 --> k1 -- g_i+1 --> l1

        EXAMPLES::

           sage: from surface_dynamics.misc.constellation import Constellation

            sage: c = Constellation(['(0,1)(2,3,4)','(1,4)',None], mutable=True); c
            Constellation of length 3 and degree 5
            g0 (0,1)(2,3,4)
            g1 (0)(1,4)(2)(3)
            g2 (0,1,3,2,4)
            sage: c.switch(1,2,3); c
            Constellation of length 3 and degree 5
            g0 (0,1)(2,3,4)
            g1 (0)(1,4)(2,3)
            g2 (0,1,3,4)(2)
        """
        if not self._mutable:
            raise ValueError("this constellation is immutable. Take a mutable copy first.")
        perm_switch(self._g[i], self._g[i+1], j0, j1)

    def _set_g_unsafe(self, i, j, k):
        r"""
        set gi[j] to k

        Advice: try a _check() after your modification

        TESTS::

            sage: from surface_dynamics import *
            sage: c = Constellation([[1,0],[0,1],[1,0]], mutable=True)
            sage: c
            Constellation of length 3 and degree 2
            g0 (0,1)
            g1 (0)(1)
            g2 (0,1)
            sage: c._set_g_unsafe(0,0,0)
            sage: c._set_g_unsafe(0,1,1)
            sage: c._set_g_unsafe(1,0,1)
            sage: c._set_g_unsafe(1,1,0)
            sage: c
            Constellation of length 3 and degree 2
            g0 (0)(1)
            g1 (0,1)
            g2 (0,1)
            sage: c._check()
        """
        if not self._mutable:
            raise ValueError("not mutable")
        self._g[i][j] = int(k)

    def euler_characteristic(self):
        r"""
        Return the Euler characteristic of the surface

        ALGORITHM:

        Hurwitz formula

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: c = Constellation(['(0,1)', '(0,2)', None])
            sage: c.euler_characteristic()
            2
        """
        return Integer(self.degree()*2 -
                       sum(sum(i-1 for i in self.profile(i))
                           for i in range(self.length())))

    def genus(self):
        r"""
        Returns the genus of the surface

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellation
            sage: c = Constellation(['(0,1)', '(0,2)', None])
            sage: c.genus()
            0
        """
        return 1 - self.euler_characteristic()/2

    def _check(self):
        r"""
        Check that the constellation is valid and if not raise ValueError

        TESTS::

            sage: from surface_dynamics import *
            sage: c = Constellation([[1,0],[1,0]], mutable=True)
            sage: c._set_g_unsafe(0,0,0)
            sage: c._check()
            Traceback (most recent call last):
            ...
            ValueError: permutation 0 invalid
            sage: c._set_g_unsafe(0,1,1)
            sage: c._check()
            Traceback (most recent call last):
            ...
            ValueError: The product is not identity
            sage: c._set_g_unsafe(1,0,0)
            sage: c._set_g_unsafe(1,1,1)
            sage: c._check()
            Traceback (most recent call last):
            ...
            ValueError: not connected
        """
        for i in range(self.length()):
            if not perm_check(self._g[i]):
                raise ValueError("permutation {} invalid".format(i))

        h = perm_one(self.degree())
        for p in self._g:
            h = perm_compose(h, p)
        if not perm_is_one(h):
            raise ValueError("The product is not identity")

        if self._connected and not perms_are_transitive(self._g):
            raise ValueError("not connected")

    def __copy__(self):
        r"""
        TESTS::

            sage: from surface_dynamics import *
            sage: c = Constellation([[0,2,1],[1,0,2],[2,1,0],None])
            sage: c == copy(c)
            True
            sage: c is copy(c)
            False
            sage: c = Constellation([[0,2,1],[1,0,2],[2,1,0],None],mutable=True)
            sage: c == copy(c)
            True
            sage: c is copy(c)
            False
        """
        return self.parent()([gg[:] for gg in self._g],
                             check=False,
                             mutable=self._mutable)

    copy = __copy__

    def mutable_copy(self):
        r"""
        Return a mutable copy of ``self``

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: c = Constellation(([0,2,1],[2,1,0],[1,2,0]), mutable=False)
            sage: d = c.mutable_copy()
            sage: d.is_mutable()
            True
        """
        return self.parent()([gg[:] for gg in self._g],
                             check=False,
                             mutable=True)

    ## GENERAL PROPERTIES

    def is_connected(self):
        r"""
        Test of connectedness

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: c = Constellation(['(0,1)(2)', None, '(0,1)(2)'], connected=False)
            sage: c.is_connected()
            False
            sage: c = Constellation(['(0,1,2)', None], connected=False)
            sage: c.is_connected()
            True
        """
        if self._connected:
            return True
        else:
            return perms_are_transitive(self._g)

    def connected_components(self):
        """
        Returns the connected components ?

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: c = Constellation(['(0,1)(2)', None, '(0,1)(2)'], connected=False)
            sage: c.connected_components()
            [Constellation of length 3 and degree 2
             g0 (0,1)
             g1 (0)(1)
             g2 (0,1), Constellation of length 3 and degree 1
             g0 (0)
             g1 (0)
             g2 (0)]

            sage: c = Constellation(['(0,1,2)', None], connected=False)
            sage: c.connected_components()
            [Constellation of length 2 and degree 3
            g0 (0,1,2)
            g1 (0,2,1)]
        """
        if self._connected:
            return [self]
        G = Graph(loops=True, multiedges=True)
        for p in self._g:
            G.add_edges(enumerate(p))
        m = G.connected_components()
        if len(m) == 1:
            return [self]
        for mm in m:
            mm.sort()
        m.sort()
        g = [[] for _ in range(len(m))]
        m_inv = [None]*self.degree()
        for t in range(len(m)):
            for i in range(len(m[t])):
                m_inv[m[t][i]] = i
            for k in range(self.length()):
                tmp = [None]*len(m[t])
                for i in range(len(m[t])):
                    tmp[i] = m_inv[self._g[k][m[t][i]]]
                g[t].append(tmp)
        return [Constellation(g=g[i], check=False) for i in range(len(m))]

    def __eq__(self, other):
        r"""
        TESTS::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: Constellation(['(0,1,2)', None]) == Constellation(['(0,1,2)', None])
            True
            sage: Constellation(['(0,1)','(0,2)',None]) == Constellation(['(0,1)',None,'(0,2)'])
            False
        """
        return type(self) == type(other) and self._g == other._g

    def __ne__(self, other):
        r"""

        TESTS::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: Constellation(['(0,1,2)', None]) != Constellation(['(0,1,2)', None])
            False
            sage: Constellation(['(0,1)','(0,2)',None]) != Constellation(['(0,1)',None,'(0,2)'])
            True
        """
        return type(self) != type(other) or self._g != other._g

    def is_isomorphic(self, other, return_map=False):
        r"""
        Test of isomorphism

        Returns ``True`` if the constellations are isomorphic (i.e. common conjugacy)
        and returns the permutation that conjugate the two permutations if
        return_map is ``True`` in such a way that self.relabel(m) == other

        ALGORITHM:

            uses canonical labels obtained from the method :meth:`relabel`.

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: c = Constellation([[1,0,2],[2,1,0],[0,2,1],None])
            sage: d = Constellation([[2,1,0],[0,2,1],[1,0,2],None])
            sage: answer, mapping = c.is_isomorphic(d,return_map=True)
            sage: answer
            True
            sage: c.relabel(mapping) == d
            True
        """
        if return_map:
            if (self.degree() != other.degree() or
                self.length() != other.length()):
                return False, None
            sn, sn_map = self.relabel(return_map=True)
            on, on_map = other.relabel(return_map=True)
            if sn != on:
                return False, None
            return True, perm_compose(sn_map, perm_invert(on_map))

        return (self.degree() == other.degree() and
                self.length() == other.length() and
                self.relabel() == other.relabel())

    def __lt__(self, other):
        r"""
        TESTS::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: c1 = Constellation([[1,2,0],None])
            sage: c2 = Constellation([[2,0,1],None])
            sage: c1 == c2
            False
            sage: c1 != c2
            True
            sage: c1 < c2 or c1 <= c2
            True
            sage: c1 > c2 or c1 >= c2
            False
            sage: c2 < c1 or c2 <= c1
            False
            sage: c2 > c1 and c2 >= c1
            True
        """
        if type(self) != type(other):
            raise TypeError

        if self.length() < other.length():
            return True
        elif self.length() > other.length():
            return False

        if self.degree() < other.degree():
            return True
        elif self.degree() > other.degree():
            return False

        for i in range(self.length()-1):
            for j in range(self.degree()-1):
                if self._g[i][j] < other._g[i][j]:
                    return True
                elif self._g[i][j] > other._g[i][j]:
                    return False

        # equality
        return False

    def __le__(self, other): return self == other or self < other
    def __ge__(self, other): return not self < other
    def __gt__(self, other): return not self <= other

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

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: c = Constellation([[1,0,2],[2,1,0],[0,2,1],None])
            sage: c._repr_()
            'Constellation of length 4 and degree 3\ng0 (0,1)(2)\ng1 (0,2)(1)\ng2 (0)(1,2)\ng3 (0,2)(1)'
        """
        s = "Constellation of length %d and degree %d" % (self.length(), self.degree())
        for i in range(self.length()):
            s += "\ng%d %s" % (i, perm_cycle_string(self._g[i], True))
        return s

    def degree(self):
        r"""
        Return the degree of the constellation

        The degree of a constellation is the number `n` that
        corresponds to the symmetric group `S(n)` in which the
        permutations of the constellation are defined.

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: c=Constellation(['(0,1)',None])
            sage: c.degree()
            2
            sage: c=Constellation(['(0,1)','(0,3,2)(1,5)',None,'(4,3,2,1)'])
            sage: c.degree()
            6
        """
        return ZZ_0 if len(self._g) == 0 else Integer(len(self._g[0]))

    def length(self):
        r"""
        Number of permutations

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: c = Constellation(['(0,1)','(0,2)','(0,3)',None])
            sage: c.length()
            4
            sage: c = Constellation(['(0,1,3)',None,'(1,2)'])
            sage: c.length()
            3
        """
        return len(self._g)

    def profile(self, i=None):
        r"""
        Return the profile of ``self``

        The profile of a constellation is the tuple of partitions
        associated to the conjugacy classes of the permutations of the
        constellation.

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: c = Constellation(['(0,1,2)(3,4)','(0,3)',None])
            sage: c.profile()
            ([3, 2], [2, 1, 1, 1], [5])
        """
        if i is None:
            return tuple(self.profile(j) for j in range(self.length()))
        else:
            return Partition(sorted(map(len, perm_cycles(self._g[i], True)), reverse=True))

    passport = profile

    ## ACCESS TO INDIVIDUAL PERMUTATION

    def g(self, i=None):
        r"""
        Return the permutation i of the constellation as a list

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: c = Constellation(['(0,1,2)(3,4)','(0,3)',None])
            sage: c.g(0)
            [1, 2, 0, 4, 3]
            sage: c.g(1)
            [3, 1, 2, 0, 4]
            sage: c.g(2)
            [4, 0, 1, 2, 3]
        """
        from copy import copy
        if i is None:
            return list(map(copy, self._g))
        else:
            return copy(self._g[i])

    def g_cycle_tuples(self, i, singletons=False):
        r"""
        Return the tuples associated to the cycles of the permutations of ``self``

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: c = Constellation(['(0,1)(2,3,4)','(0,4)(1,3)',None])
            sage: c.g_cycle_tuples(0)
            [[0, 1], [2, 3, 4]]
            sage: c.g_cycle_tuples(1)
            [[0, 4], [1, 3]]
        """
        if i is None:
            return [perm_cycles(p, singletons) for p in self._g]
        else:
            return perm_cycles(self._g[i], singletons)

    def g_cycle_string(self, i, singletons=False):
        r"""
        Return the permutations of a constellation, as cycles

        If `i` is ``None``, return all of them

        Otherwise, return just the permutation `g_i` as a cycle

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: c = Constellation(['(0,1)(2,3,4)','(0,4)(1,3)',None])
            sage: c.g_cycle_string(None)
            ['(0,1)(2,3,4)', '(0,4)(1,3)', '(0,3)(1,2,4)']
            sage: c.g_cycle_string(2)
            '(0,3)(1,2,4)'
        """
        if i is None:
            return [perm_cycle_string(p, singletons) for p in self._g]
        else:
            return perm_cycle_string(self._g[i], singletons)

    def g_next(self, i, j):
        r"""
        Return the image of `j` for the permutation `g_i` in constant time

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: c = Constellation(['(1,2,0,5)','(2,3,4)(5,1)',None])
            sage: c.g_next(0,0)
            5
            sage: all(c.g(i)[j] == c.g_next(i,j) for i in range(3) for j in range(6))
            True
        """
        if i is None:
            return [p[j] for p in  self._g]
        else:
            return self._g[i][j]

    def g_prev(self, i, j):
        r"""
        Return the image of `j` for the permutation `g_i` in constant time

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: c = Constellation(['(0,1,2)(3,4)','(0,4,2)',None])
            sage: c.g_prev(0,0)
            2
            sage: all(c.g(i)[c.g_prev(i,j)] == j for i in range(3) for j in range(5))
            True
        """
        if i is None:
            return [self.g_prev(k, j) for k in range(self.length())]
        else:
            for k in range(i+1, self.length()):
                j = self._g[k][j]
            for k in range(0, i):
                j = self._g[k][j]
            return j

    def g_orbit(self, i, j):
        r"""
        Returns the orbit of `j` under `g_i`

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: c = Constellation(['(0,1)(2,3,4)','(1,4)',None])
            sage: c.g_orbit(0,3)
            [3, 4, 2]
            sage: c.g_orbit(1,0)
            [0]
        """
        if i is None:
            return [self.g_orbit(k, j) for k in range(self.length())]
        else:
            return perm_orbit(self._g[i], j)

    def relabel(self, perm=None, return_map=False):
        r"""
        Relabel ``self``.

        If ``perm`` is provided then relabel with respect to ``perm``. Otherwise
        use canonical labels. In that case, if ``return_map`` is provided, the
        return also the map used for canonical labels.

        Algorithm:

        the cycle for g(0) are adjacent and the cycle are joined with
        respect to the other permutations. The minimum is taken for
        all possible renumerotations.

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: c = Constellation(['(0,1)(2,3,4)','(1,4)',None]); c
            Constellation of length 3 and degree 5
            g0 (0,1)(2,3,4)
            g1 (0)(1,4)(2)(3)
            g2 (0,1,3,2,4)
            sage: c2 = c.relabel(); c2
            Constellation of length 3 and degree 5
            g0 (0,1)(2,3,4)
            g1 (0)(1,2)(3)(4)
            g2 (0,1,4,3,2)

        The map returned when the option ``return_map`` is set to True can be
        used to set the relabelling::

            sage: c3, perm = c.relabel(return_map=True)
            sage: c3 == c2 and c3 == c.relabel(perm=perm)
            True

            sage: d = c.relabel([4,3,1,0,2]); d
            Constellation of length 3 and degree 5
            g0 (0,2,1)(3,4)
            g1 (0)(1)(2,3)(4)
            g2 (0,1,2,4,3)
            sage: d.is_isomorphic(c)
            True

        We check that after a random relabelling the new constellation is
        isomorphic to the initial one::

            sage: c = Constellation(['(0,1)(2,3,4)','(1,4)',None])
            sage: p = SymmetricGroup(c.degree()).random_element()
            sage: cc = c.relabel([x-1 for x in p.domain()]).relabel()
            sage: cc.is_isomorphic(c)
            True
        """
        if perm is not None:
            perm.extend(range(len(perm), self.degree()))
            g = [[None]*self.degree() for _ in range(self.length())]
            for i in range(len(perm)):
                for k in range(self.length()):
                    g[k][perm[i]] = perm[self._g[k][i]]
            return Constellation(g=g, check=False, mutable=self.is_mutable())

        if return_map:
            try:
                return self._normal_form, self._normal_form_map
            except AttributeError:
                pass
        else:
            try:
                return self._normal_form
            except AttributeError:
                pass


        # compute canonical labels
        if not self.is_connected():
            raise ValueError("No canonical labels implemented for non connected constellation")

        c_win, m_win = perms_canonical_labels(self._g)
        c_win = self.parent()(c_win, mutable=False, check=False)

        if not self.is_mutable():
            self._normal_form = c_win
            self._normal_form_map = m_win

        c_win._normal_form = c_win
        c_win._normal_form_map = m_win

        if return_map:
            return c_win, m_win
        else:
            return c_win

    # BRAID GROUP ACTION

    def braid_group_action(self, i):
        r"""
        Acts on self as the braid group generator that exchanges
        position `i` and `i+1`

        INPUT:

        - ``i`` -- integer in `[0, n-1]` where `n` is the length of self

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: sigma = lambda c, i: c.braid_group_action(i)

            sage: c = Constellation(['(0,1)(2,3,4)','(1,4)',None]); c
            Constellation of length 3 and degree 5
            g0 (0,1)(2,3,4)
            g1 (0)(1,4)(2)(3)
            g2 (0,1,3,2,4)
            sage: sigma(c, 1)
            Constellation of length 3 and degree 5
            g0 (0,1)(2,3,4)
            g1 (0,1,3,2,4)
            g2 (0,3)(1)(2)(4)

        Check the commutation relation::

            sage: c = Constellation(['(0,1)(2,3,4)','(1,4)','(2,5)(0,4)',None])
            sage: d = Constellation(['(0,1,3,5)','(2,3,4)','(0,3,5)',None])
            sage: c13 = sigma(sigma(c, 0), 2)
            sage: c31 = sigma(sigma(c, 2), 0)
            sage: c13 == c31
            True
            sage: d13 = sigma(sigma(d, 0), 2)
            sage: d31 = sigma(sigma(d, 2), 0)
            sage: d13 == d31
            True

        Check the braid relation::

            sage: c121 = sigma(sigma(sigma(c, 1), 2), 1)
            sage: c212 = sigma(sigma(sigma(c, 2), 1), 2)
            sage: c121 == c212
            True
            sage: d121 = sigma(sigma(sigma(d, 1), 2), 1)
            sage: d212 = sigma(sigma(sigma(d, 2), 1), 2)
            sage: d121 == d212
            True
        """
        if i < 0 or i >= self.length():
            raise ValueError("i should be between 0 and %d"%(self.length()-1))
        j = i+1
        if j == self.length():   # wrap around the cylinder
            j = 0
        h = self.copy()
        si = self._g[i]
        sj = self._g[j]
        h._g[i] = sj
        h._g[j] = perm_compose(perm_compose(perm_invert(sj), si), sj)
        return h

    def braid_group_orbit(self):
        r"""
        Returns the graph of the action of the braid group.

        The action is considered up to isomorphism of constellation.

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: c = Constellation(['(0,1)(2,3,4)','(1,4)',None]); c
            Constellation of length 3 and degree 5
            g0 (0,1)(2,3,4)
            g1 (0)(1,4)(2)(3)
            g2 (0,1,3,2,4)
            sage: G = c.braid_group_orbit()
            sage: G.num_verts()
            4
            sage: G.num_edges()
            12
        """
        from sage.graphs.digraph import DiGraph

        G = DiGraph(multiedges=True, loops=True)
        waiting = [self.relabel()]

        while waiting:
            c = waiting.pop()
            G.add_vertex(c)
            for i in range(self.length()):
                cc = self.braid_group_action(i).relabel()
                if cc not in G:
                    waiting.append(cc)
                G.add_edge(c, cc, i)
        return G

class Constellations_all(UniqueRepresentation, Parent):
    Element = Constellation_class

    def __init__(self, connected=True):
        r"""
        TESTS::

            sage: from surface_dynamics.misc.constellation import Constellations
            sage: TestSuite(Constellations()).run()
        """
        from sage.categories.infinite_enumerated_sets import InfiniteEnumeratedSets
        Parent.__init__(self, category=InfiniteEnumeratedSets())
        self._connected = connected

    def __contains__(self,elt):
        r"""
        TESTS::

            sage: from surface_dynamics.misc.constellation import Constellations

            sage: C = Constellations(connected=True)
            sage: D = Constellations(connected=False)
            sage: C([[1,0],None]) in C
            True
            sage: C([[1,0],None]) in D
            True
            sage: D([[1,0],None]) in C
            True
            sage: D([[1,0],None]) in D
            True
            sage: D([[0,1],None]) in C
            False
            sage: D([[0,1],None]) in D
            True
        """
        return isinstance(elt, Constellation_class) and (not self._connected or elt.is_connected())

    def _repr_(self):
        r"""
        String representation.

        TESTS::

            sage: from surface_dynamics.misc.constellation import Constellations

            sage: Constellations()    # indirect doctest
            Connected constellations
        """
        if self._connected:
            return "Connected constellations"
        else:
            return "Constellations"

    def _element_constructor_(self, g, check=True, mutable=False):
        r"""
        Returns a constellation.

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellation

            sage: Constellation([[1,0],[1,0]])  # indirect doctest
            Constellation of length 2 and degree 2
            g0 (0,1)
            g1 (0,1)
        """
        if g is None or len(g) == 0:
            gg = []

        else:
            if g.count(None) == 0:
                i = None
            elif g.count(None) == 1:
                i = g.index(None)
                del g[i]
            else:
                raise ValueError("only one permutation must be None")

            gg = list(map(perm_init, g))
            equalize_perms(gg)

            if i is not None:
                h = range(len(gg[0]))
                for p in gg[i:]:
                    h = perm_compose(h, p)
                for p in gg[:i]:
                    h = perm_compose(h, p)
                gg.insert(i, perm_invert(h))

        return self.element_class(self, gg, check, self._connected, mutable)

    def an_element(self):
        r"""
        Return a constellation.

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellations

            sage: Constellations().an_element()
            Constellation of length 2 and degree 2
            g0 (0,1)
            g1 (0,1)
        """
        return Constellations(2,2).an_element()

    def __iter__(self):
        r"""
        TESTS::

            sage: from surface_dynamics.misc.constellation import Constellations

            sage: I = iter(Constellations())
            sage: for _ in range(100): print(next(I))
            Constellation of length 1 and degree 1
            g0 (0)
            Constellation of length 2 and degree 1
            g0 (0)
            g1 (0)
            Constellation of length 3 and degree 1
            g0 (0)
            g1 (0)
            g2 (0)
            Constellation of length 2 and degree 2
            g0 (0,1)
            g1 (0,1)
            ...
            Constellation of length 4 and degree 3
            g0 (0)(1,2)
            g1 (0)(1,2)
            g2 (0,2,1)
            g3 (0,1,2)
            Constellation of length 4 and degree 3
            g0 (0)(1,2)
            g1 (0)(1,2)
            g2 (0,2)(1)
            g3 (0,2)(1)
        """
        n = 2
        while True:
            for d in range(1,n):
                l = n-d
                for c in Constellations(l,d,connected=self._connected):
                    yield c
            n += 1

class Constellations_ld(UniqueRepresentation, Parent):
    r"""
    Constellations of given length and degree.

    EXAMPLES::

        sage: from surface_dynamics.misc.constellation import Constellations

        sage: C = Constellations(2,3); C
        Connected constellations of length 2 and degree 3
        sage: C([[1,2,0],[2,0,1]])
        Constellation of length 2 and degree 3
        g0 (0,1,2)
        g1 (0,2,1)
        sage: C.cardinality()
        2
        sage: Constellations(2,3,connected=False).cardinality()
        6
    """
    def __init__(self, length, degree, connected=True):
        """
        TESTS::

            sage: from surface_dynamics.misc.constellation import Constellations

            sage: TestSuite(Constellations(length=3,degree=4)).run()
        """
        from sage.categories.finite_enumerated_sets import FiniteEnumeratedSets
        Parent.__init__(self, facade=Constellations_all(), category=FiniteEnumeratedSets())
        self._length = Integer(length)
        self._degree = Integer(degree)
        if self._length <= 0:
            raise ValueError("length should be a positive integer")
        if self._degree <= 0:
            raise ValueError("degree should be a positive integer")
        self._connected = bool(connected)

    def __contains__(self, elt):
        r"""
        TESTS::

            sage: from surface_dynamics.misc.constellation import Constellations

            sage: C = Constellations(2,3,connected=True)
            sage: D = Constellations(2,3,connected=False)
            sage: C([[2,0,1],None]) in C
            True
            sage: D([[2,0,1],None]) in C
            True
            sage: D([[2,0,1],None]) in D
            True
            sage: D([[0,1,2],None]) in C
            False
            sage: D([[0,1,2],None]) in D
            True
        """
        if elt not in Constellations(connected=self._connected):
            return False
        return elt.length() == self._length and elt.degree() == self._degree

    def first(self):
        r"""
        Returns the first element in lexicographic order

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellations

            sage: const = Constellations(3,3);const
            Connected constellations of length 3 and degree 3
            sage: const.first()
            Constellation of length 3 and degree 3
            g0 (0,1,2)
            g1 (0,1,2)
            g2 (0,1,2)
        """
        if self._connected:
            g = [str(tuple(range(self._degree)))] * self._length
        else:
            g = [range(d)] * self._length
        return self(g, check=False)

    def last(self):
        r"""
        Returns the last element in lexicographic order

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellations

            sage: const=Constellations(3,3);const
            Connected constellations of length 3 and degree 3
            sage: const.last()
            Constellation of length 3 and degree 3
            g0 (0,2,1)
            g1 (0,2,1)
            g2 (0,2,1)
        """
        if self._connected:
            g = [str(tuple(range(self._degree-1, -1, -1)))] * self._length
        else:
            g = [range(d-1,-1,-1)] * self._length
        return Constellation(g, check=False, mutable=False)

    def _repr_(self):
        """
        TESTS::

            sage: from surface_dynamics.misc.constellation import Constellations

            sage: Constellations(3,3)._repr_()
            'Connected constellations of length 3 and degree 3'
            sage: Constellations(3,3,connected=False)._repr_()
            'Constellations of length 3 and degree 3'
        """
        s = "of length %d and degree %d" % (self._length, self._degree)
        if self._connected:
            return "Connected constellations " + s
        return "Constellations " + s

    def __iter__(self):
        """
        iterator over all constellations of given degree and length

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellations

            sage: const = Constellations(3,3); const
            Connected constellations of length 3 and degree 3
            sage: len([v for v in const])
            26

        One can check the first few terms of sequence :oeis:`220754`::

            sage: Constellations(4,1).cardinality()
            1
            sage: Constellations(4,2).cardinality()
            7
            sage: Constellations(4,3).cardinality()
            194
            sage: Constellations(4,4).cardinality()  # long time
            12858
        """
        from sage.arith.srange import srange
        from itertools import product, permutations

        if self._length == 1:
            if self._degree == 1:
                yield self([[0]])
            return

        for p in product(permutations(srange(self._degree)), repeat=self._length-1):
            if self._connected and not perms_are_transitive(p):
                continue
            yield self(list(map(list,p))+[None], check=False)

    def random_element(self, mutable=False):
        r"""
        a random element

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellations

            sage: const = Constellations(3,3)
            sage: const.random_element()
            Constellation of length 3 and degree 3
            ...
            ...
            ...
            sage: c = const.random_element()
            sage: c.degree() == 3 and c.length() == 3
            True
        """
        from sage.groups.perm_gps.permgroup_named import SymmetricGroup
        from sage.groups.perm_gps.permgroup import PermutationGroup

        l = self._length
        d = self._degree

        g = [SymmetricGroup(d).random_element() for _ in range(l-1)]
        G = PermutationGroup(g)
        while not G.degree() == d or (self._connected and not G.is_transitive()):
            g = [SymmetricGroup(d).random_element() for _ in range(l-1)]
            G = PermutationGroup(g)

        return self(list(map(permutation_to_perm, g))+[None], mutable=mutable)

    def _element_constructor_(self, *data, **options):
        r"""
        Build an element of self.

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellations

            sage: C = Constellations(2,3)
            sage: C([[1,2,0],[2,0,1]])
            Constellation of length 2 and degree 3
            g0 (0,1,2)
            g1 (0,2,1)
            sage: C([[2,1,0],[2,1,0]])
            Traceback (most recent call last):
            ...
            ValueError: not connected
        """
        c = Constellations(connected=self._connected)(*data, **options)
        if c.degree() != self._degree:
            raise ValueError("not able to build a constellation of degree %d from the given data"%self._degree)
        if c.length() != self._length:
            raise ValueError("not able to build a constellation of length %d from the given data"%self._length)
        return c

    def an_element(self):
        r"""
        Return a constellation in ``self``.

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellations

            sage: Constellations(2,3).an_element()
            Constellation of length 2 and degree 3
            g0 (0,2,1)
            g1 (0,1,2)
            sage: Constellations(7,3).an_element()
            Constellation of length 7 and degree 3
            g0 (0,2,1)
            g1 (0,1,2)
            g2 (0)(1)(2)
            g3 (0)(1)(2)
            g4 (0)(1)(2)
            g5 (0)(1)(2)
            g6 (0)(1)(2)
        """
        if self._degree == 0 and self._length == 0:
            return self()([])
        elif self._length == 1:
            if self._degree > 1:
                from sage.categories.sets_cat import EmptySetError
                raise EmptySetError
            return self()([[0]])

        d = self._degree
        if self._connected:
            g = [[d-1] + list(range(d-1)), list(range(1,d)) + [0]] + [list(range(d))]*(self._length-2)
        else:
            g = [list(range(d))]*self._length
        return self(g)

    def braid_group_action(self):
        r"""
        List of graphs that corresponds to the braid group action on ``self`` up
        to isomorphism.


        OUTPUT:

        - list of graphs

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellations

            sage: C = Constellations(3,3)
            sage: C.braid_group_action()
            [Looped multi-digraph on 3 vertices,
             Looped multi-digraph on 3 vertices,
             Looped multi-digraph on 1 vertex]
        """
        G = []
        I = iter(self)
        for c in self:
            c = c.relabel()
            if any(c in g for g in G):
                continue
            G.append(c.braid_group_orbit())
        return G

    def braid_group_orbits(self):
        r"""
        Orbits under the action of braid group.

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellations

            sage: C = Constellations(3,3)
            sage: O = C.braid_group_orbits()
            sage: len(O)
            3
            sage: [x.profile() for x in O[0]]
            [([1, 1, 1], [3], [3]), ([3], [1, 1, 1], [3]), ([3], [3], [1, 1, 1])]
            sage: [x.profile() for x in O[1]]
            [([2, 1], [2, 1], [3]), ([2, 1], [3], [2, 1]), ([3], [2, 1], [2, 1])]
            sage: [x.profile() for x in O[2]]
            [([3], [3], [3])]
        """
        return [g.vertices() for g in self.braid_group_action()]

class Constellations_p(UniqueRepresentation, Parent):
    r"""
    Constellations with fixed profile.

    EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellations

        sage: C = Constellations([[3,1],[3,1],[2,2]]); C
        Constellations with profile ([3, 1], [3, 1], [2, 2])
        sage: C.cardinality()
        24
        sage: C.first()
        Constellation of length 3 and degree 4
        g0 (0)(1,2,3)
        g1 (0,1,2)(3)
        g2 (0,1)(2,3)
        sage: C.last()
        Constellation of length 3 and degree 4
        g0 (0,3,2)(1)
        g1 (0,3,1)(2)
        g2 (0,1)(2,3)

    Note that the cardinality can also be computed using characters of the
    symmetric group (Frobenius formula)::

        sage: P = Partitions(4)
        sage: p1 = Partition([3,1])
        sage: p2 = Partition([3,1])
        sage: p3 = Partition([2,2])
        sage: i1 = P.cardinality() - P.rank(p1) - 1
        sage: i2 = P.cardinality() - P.rank(p2) - 1
        sage: i3 = P.cardinality() - P.rank(p3) - 1
        sage: s = 0
        sage: for c in SymmetricGroup(4).irreducible_characters():
        ....:     v = c.values()
        ....:     s += v[i1] * v[i2] * v[i3] / v[0]
        sage: c1 = p1.conjugacy_class_size()
        sage: c2 = p2.conjugacy_class_size()
        sage: c3 = p3.conjugacy_class_size()
        sage: c1 * c2 * c3 / factorial(4)**2 * s
        1

    The number obtained above is up to isomorphism. And we can check::

        sage: len(C.isomorphism_representatives())
        1
    """
    def __init__(self, profile, connected=True):
        r"""
        OPTIONS:

        - ``profile`` -- a list of integer partitions of the same integer

        - ``connected`` -- a boolean (default: True) that specify if we consider
          only connected constellations.

        TESTS::

            sage: from surface_dynamics.misc.constellation import Constellations

            sage: C = Constellations([(3,1),(3,1),(2,2)])
            sage: TestSuite(C).run()
        """
        l = Integer(len(profile))
        d = Integer(sum(profile[0]))
        for p in profile[1:]:
            if sum(p) != d:
                raise ValueError("all partition in the passport should have the same sum.")

        from sage.categories.finite_enumerated_sets import FiniteEnumeratedSets
        Parent.__init__(self, facade=Constellations_all(), category=FiniteEnumeratedSets())
        self._profile = profile
        self._degree = d
        self._length = l
        self._connected = connected

    def _repr_(self):
        r"""
        TESTS::

            sage: from surface_dynamics.misc.constellation import Constellations

            sage: Constellations(([3,2],[2,2,1]))
            Constellations with profile ([3, 2], [2, 2, 1])
        """
        return "Constellations with profile %s"%(self._profile,)

    def isomorphism_representatives(self):
        r"""
        Return a set of isomorphism representative of ``self``.

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellations

            sage: C = Constellations([[5], [4,1], [3,2]])
            sage: C.cardinality()
            240
            sage: ir = sorted(C.isomorphism_representatives())
            sage: len(ir)
            2
            sage: ir[0]
            Constellation of length 3 and degree 5
            g0 (0,1,2,3,4)
            g1 (0)(1,2,3,4)
            g2 (0,4,2)(1,3)
            sage: ir[1]
            Constellation of length 3 and degree 5
            g0 (0,1,2,3,4)
            g1 (0)(1,4,2,3)
            g2 (0,4)(1,2,3)
        """
        result = set()
        for c in self:
            cc = c.relabel()
            if cc not in result:
                result.add(cc)
        return result

    def _element_constructor_(self, *data, **options):
        r"""
        Build an element of self.

        TESTS::

            sage: from surface_dynamics.misc.constellation import Constellations

            sage: C = Constellations([(3,1),(3,1),(2,2)])
            sage: c = C(['(1,2,3)','(0,1,2)','(0,1)(2,3)']); c
            Constellation of length 3 and degree 4
            g0 (0)(1,2,3)
            g1 (0,1,2)(3)
            g2 (0,1)(2,3)
            sage: C(['(1,2,3)','(0,1,3)',None])
            Traceback (most recent call last):
            ...
            ValueError: not able to build a constellation with profile ([3, 1], [3, 1], [2, 2]) from the given data
        """
        c = Constellations(connected=self._connected)(*data, **options)
        if options.get('check', True):
            if c.profile() != self._profile:
                raise ValueError("not able to build a constellation with profile %s from the given data"%(self._profile,))
        return c

    def __iter__(self):
        r"""
        Iterator of the elements in ``self``.

        .. NOTE::

            The algorithm is highly non optimized and is *very* slow for large
            entries! In order to make a better one, we need an iterator over
            conjugacy classes of the symmetric group.

        TESTS::

            sage: from surface_dynamics.misc.constellation import Constellations

            sage: C = Constellations([(3,1),(3,1),(2,2)])
            sage: for c in C: print(c)
            Constellation of length 3 and degree 4
            g0 (0)(1,2,3)
            g1 (0,1,2)(3)
            g2 (0,1)(2,3)
            Constellation of length 3 and degree 4
            g0 (0)(1,2,3)
            g1 (0,2,3)(1)
            g2 (0,2)(1,3)
            ...
            Constellation of length 3 and degree 4
            g0 (0,3,2)(1)
            g1 (0,1,2)(3)
            g2 (0,3)(1,2)
            Constellation of length 3 and degree 4
            g0 (0,3,2)(1)
            g1 (0,3,1)(2)
            g2 (0,1)(2,3)
        """
        from sage.arith.srange import srange
        from itertools import product, permutations

        if self._length == 1:
            if self._degree == 1:
                yield self([[0]])
            return

        for p in product(permutations(srange(self._degree)), repeat=self._length-1):
            if self._connected and not perms_are_transitive(p):
                continue
            c = Constellations(connected=self._connected)(list(map(list,p))+[None], check=False)
            if c.profile() == self._profile:
                yield c

    def braid_group_action(self):
        r"""
        List of graphs that corresponds to the braid group action on ``self`` up
        to isomorphism.

        OUTPUT:

        - list of graphs

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellations

            sage: C = Constellations(3,3)
            sage: C.braid_group_action()
            [Looped multi-digraph on 3 vertices,
             Looped multi-digraph on 3 vertices,
             Looped multi-digraph on 1 vertex]
        """
        G = []
        I = iter(self)
        for c in self:
            c = c.relabel()
            if any(c in g for g in G):
                continue
            G.append(c.braid_group_orbit())
        return G

    def braid_group_orbits(self):
        r"""
        Orbits under the action of braid group.

        EXAMPLES::

            sage: from surface_dynamics.misc.constellation import Constellations

            sage: C = Constellations(3,3)
            sage: O = C.braid_group_orbits()
            sage: len(O)
            3
            sage: [x.profile() for x in O[0]]
            [([1, 1, 1], [3], [3]), ([3], [1, 1, 1], [3]), ([3], [3], [1, 1, 1])]
            sage: [x.profile() for x in O[1]]
            [([2, 1], [2, 1], [3]), ([2, 1], [3], [2, 1]), ([3], [2, 1], [2, 1])]
            sage: [x.profile() for x in O[2]]
            [([3], [3], [3])]
        """
        return [g.vertices() for g in self.braid_group_action()]

