# sage_setup: distribution = sagemath-combinat
# sage.doctest: needs sage.combinat sage.groups
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 ([LZ2004]_ 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).

EXAMPLES::

    sage: c = Constellation(['(1,2)', '(1,3)', None])
    sage: c
    Constellation of length 3 and degree 3
    g0 (1,2)(3)
    g1 (1,3)(2)
    g2 (1,3,2)
    sage: C = Constellations(3,4); C
    Connected constellations of length 3 and degree 4 on {1, 2, 3, 4}
    sage: C.cardinality()  # long time
    426

    sage: C = Constellations(3, 4, domain=('a', 'b', 'c', 'd'))
    sage: C
    Connected constellations of length 3 and degree 4 on {'a', 'b', 'c', 'd'}
    sage: c = C(('a','c'),(('b','c'),('a','d')), None)
    sage: c
    Constellation of length 3 and degree 4
    g0 ('a','c')('b')('d')
    g1 ('a','d')('b','c')
    g2 ('a','d','c','b')
    sage: c.is_connected()
    True
    sage: c.euler_characteristic()
    2
    sage: TestSuite(C).run()
"""

# ****************************************************************************
#       Copyright (C) 2015-2016 Vincent Delecroix <20100.delecroix@gmail.com>
#                               Frédéric Chapoton <fchapoton2@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#                  https://www.gnu.org/licenses/
# ****************************************************************************
from itertools import repeat, product

from sage.structure.element import parent
from sage.structure.parent import Parent
from sage.structure.element import Element
from sage.structure.unique_representation import UniqueRepresentation
from sage.structure.richcmp import (op_NE, op_EQ, richcmp_not_equal,
                                    rich_to_bool)

from sage.rings.integer import Integer
from sage.combinat.partition import Partition
from sage.misc.misc_c import prod
from sage.misc.lazy_import import lazy_import
from sage.categories.groups import Groups

lazy_import('sage.graphs.graph', 'Graph')
lazy_import('sage.graphs.digraph', 'DiGraph')
lazy_import('sage.groups.perm_gps.permgroup_named', 'SymmetricGroup')

# 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: Constellations(4,2)
        Connected constellations of length 4 and degree 2 on {1, 2}

        sage: Constellations([[3,2,1],[3,3],[3,3]])
        Connected constellations with profile ([3, 2, 1], [3, 3], [3, 3]) on {1, 2, 3, 4, 5, 6}
    """
    profile = options.get('profile', None)
    length = options.get('length', None)
    degree = options.get('degree', None)
    connected = options.get('connected', True)
    domain = options.get('domain', None)
    if domain is not None:
        domain = tuple(domain)

    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, domain, bool(connected))
    elif degree is not None and length is not None:
        if domain is None:
            sym = SymmetricGroup(degree)
        else:
            sym = SymmetricGroup(domain)
            if len(sym.domain()) != degree:
                raise ValueError("the size of the domain should be equal to the degree")

        return Constellations_ld(Integer(length), Integer(degree),
                                 sym, bool(connected))
    else:
        raise ValueError("you must either provide a profile or a pair (length, degree)")


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

    INPUT:

    - ``g`` -- list of permutations

    - ``mutable`` -- boolean (default: ``False``); whether the result is
      mutable or not

    - ``connected`` -- boolean (default: ``True``); whether the result should
      be connected

    - ``check`` -- boolean (default: ``True``); whether or not to check. If it
      is ``True``, then the list ``g`` must contain no ``None``.

    EXAMPLES:

    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)
    """
    l = len(g)
    sym, _ = perms_sym_init([x for x in g if x is not None])
    d = len(sym.domain())
    return Constellations(l, d,
                          domain=sym.domain(),
                          connected=connected)(g, mutable=mutable, check=check)

# 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, connected, mutable, check):
        r"""
        TESTS::

            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: c = Constellation(([0,2,1],[2,1,0],[1,2,0]), mutable=False)
            sage: hash(c) == hash(tuple(c._g))
            True
        """
        if self._mutable:
            raise ValueError("cannot hash mutable constellation")
        return hash(tuple(self._g))

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

        EXAMPLES::

            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: 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"""
        Perform the multiplication by the transposition `(j0, j1)` between the
        permutations `g_i` and `g_{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}`. The new
        constellation is

        .. MATH::

            (g_0, \ldots, g_{i-1}, g_{i} (j0 j1), (j0 j1) g_{i+1}, g_{i+2}, \ldots, g_k)

        EXAMPLES::

            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.is_mutable()
            True
            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)
            sage: c._check()
            sage: c.switch(2,1,3); c
            Constellation of length 3 and degree 5
            g0 (0,1,4,2,3)
            g1 (0)(1,4)(2,3)
            g2 (0,3,4)(1)(2)
            sage: c._check()
            sage: c.switch(0,0,1); c
            Constellation of length 3 and degree 5
            g0 (0)(1,4,2,3)
            g1 (0,4,1)(2,3)
            g2 (0,3,4)(1)(2)
            sage: c._check()
        """
        if not self._mutable:
            raise ValueError("this constellation is immutable."
                             " Take a mutable copy first.")
        S = SymmetricGroup(list(range(self.degree())))
        tr = S((j0, j1))
        i = int(i)
        if i < 0 or i >= len(self._g):
            raise ValueError("index out of range")

        ii = i + 1
        if ii == len(self._g):
            ii = 0
        self._g[i] = self._g[i] * tr
        self._g[ii] = tr * self._g[ii]

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

        ALGORITHM:

        Hurwitz formula

        EXAMPLES::

            sage: c = Constellation(['(0,1)', '(0,2)', None])
            sage: c.euler_characteristic()
            2

            sage: c = Constellation(['(0,1,2,3)','(1,3,0,2)', '(0,3,1,2)', None])
            sage: c.euler_characteristic()
            -4

        TESTS::

            sage: parent(c.euler_characteristic())
            Integer Ring
        """
        return Integer(self.degree() * 2 -
                       sum(sum(j - 1 for j in self.profile(i))
                           for i in range(self.length())))

    def genus(self):
        r"""
        Return the genus of the surface.

        EXAMPLES::

            sage: c = Constellation(['(0,1)', '(0,2)', None])
            sage: c.genus()
            0

            sage: c = Constellation(['(0,1)(2,3,4)','(1,3,4)(2,0)', None])
            sage: c.genus()
            1

        TESTS::

            sage: parent(c.genus())
            Integer Ring
        """
        return 1 - self.euler_characteristic() // 2

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

        TESTS::

            sage: c = Constellation([[0,1],[1,0]], mutable=True, check=False)
            sage: c._check()
            Traceback (most recent call last):
            ...
            ValueError: the product is not identity

            sage: c = Constellation([[0,1],[0,1]], mutable=True, check=False)
            sage: c._check()
            Traceback (most recent call last):
            ...
            ValueError: not connected
        """
        d = self.degree()
        Sd = self.parent()._sym

        if prod(self._g, Sd.one()) != Sd.one():
            raise ValueError("the product is not identity")

        if self._connected and not perms_are_connected(self._g, d):
            raise ValueError("not connected")

    def __copy__(self):
        r"""
        Return a copy of ``self``.

        TESTS::

            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()(list(self._g),
                             check=False,
                             mutable=self._mutable)

    copy = __copy__

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

        EXAMPLES::

            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()(list(self._g),
                             check=False,
                             mutable=True)

    # GENERAL PROPERTIES

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

        EXAMPLES::

            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_connected(self._g, self.degree())

    def connected_components(self):
        """
        Return the connected components.

        OUTPUT: list of connected constellations

        EXAMPLES::

            sage: c = Constellation(['(0,1)(2)', None, '(0,1)(2)'], connected=False)
            sage: cc = c.connected_components(); cc
            [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: all(c2.is_connected() for c2 in cc)
            True

            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()
        G.add_vertices(list(range(self.degree())))
        for p in self._g:
            G.add_edges(enumerate(p.domain()), loops=False)
        m = G.connected_components(sort=False)
        if len(m) == 1:
            return [self]
        for mm in m:
            mm.sort()
        m.sort()
        g = [[] for _ in repeat(None, len(m))]
        m_inv = [None] * self.degree()
        for t, mt in enumerate(m):
            for i, mti in enumerate(mt):
                m_inv[mti] = i
            for k in range(self.length()):
                tmp = [None] * len(mt)
                for i, mti in enumerate(mt):
                    tmp[i] = m_inv[self._g[k](mti)]
                g[t].append(tmp)
        return [Constellation(g=g[i], check=False) for i in range(len(m))]

    def _richcmp_(self, other, op):
        r"""
        Do the comparison.

        TESTS::

            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

            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

            sage: c1 = Constellation([[1,2,0],None])
            sage: c2 = Constellation([[2,0,1],None])
            sage: c1 < c2
            True
            sage: c2 > c1
            True
        """
        if not isinstance(other, Constellation_class):
            return op == op_NE
        if op == op_EQ:
            return self._g == other._g
        if op == op_NE:
            return self._g != other._g

        lx = self.length()
        rx = other.length()
        if lx != rx:
            return richcmp_not_equal(lx, rx, op)

        lx = self.degree()
        rx = other.degree()
        if lx != rx:
            return richcmp_not_equal(lx, rx, op)

        for i in range(self.length() - 1):
            lx = self._g[i]
            rx = other._g[i]
            if lx != rx:
                return richcmp_not_equal(lx, rx, op)
        return rich_to_bool(op, 0)

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

        Return ``True`` if the constellations are isomorphic
        (*i.e.* related by a common conjugacy) and return 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: 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 not (self.degree() == other.degree() and
                   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, sn_map * ~on_map

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

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

        EXAMPLES::

            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 {} and degree {}".format(self.length(),
                                                              self.degree())
        for i in range(self.length()):
            s += "\ng{} {}".format(i, self._g[i].cycle_string(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: c = Constellation([])
            sage: c.degree()
            0
            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

        TESTS::

            sage: parent(c.degree())
            Integer Ring
        """
        return self.parent()._degree

    def length(self):
        r"""
        Return the number of permutations.

        EXAMPLES::

            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

        TESTS::

            sage: parent(c.length())
            Integer Ring
        """
        return Integer(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.

        This is also called the passport.

        EXAMPLES::

            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:
            parts = [len(cy) for cy in self._g[i].cycle_tuples(True)]
            return Partition(sorted(parts, reverse=True))

    passport = profile

    # ACCESS TO INDIVIDUAL PERMUTATION

    def g(self, i=None):
        r"""
        Return the permutation `g_i` of the constellation.

        INPUT:

        - ``i`` -- integer or ``None`` (default)

        If ``None`` , return instead the list of all `g_i`.

        EXAMPLES::

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

    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: 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: S5 = SymmetricGroup(range(5))
            sage: d = c.relabel(S5([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 = S5.random_element()
            sage: cc = c.relabel(perm=p)
            sage: cc.is_isomorphic(c)
            True

        Check that it works for "non standard" labels::

            sage: c = Constellation([(('a','b'),('c','d','e')),('b','d'), None])
            sage: c.relabel()
            Constellation of length 3 and degree 5
            g0 ('a','b')('c','d','e')
            g1 ('a')('b','c')('d')('e')
            g2 ('a','b','e','d','c')
        """
        if perm is not None:
            g = [[None] * self.degree() for _ in range(self.length())]
            for i in range(len(perm.domain())):
                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")

        # get the permutations on {0, 1, ..., d-1}
        # compute the canonical labels
        # map it back to the domain
        # TODO: a lot of time is lost here!
        domain = list(self.parent()._sym.domain())
        index = {e: i for i, e in enumerate(domain)}
        g = [[index[gg(i)] for i in domain] for gg in self._g]
        c_win, m_win = perms_canonical_labels(g)
        c_win = [[domain[i] for i in gg] for gg in c_win]
        m_win = self.parent()._sym([domain[i] for i in m_win])
        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"""
        Act 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: 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():
            txt = "i should be between 0 and {}"
            raise ValueError(txt.format(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] = (~sj * si) * sj
        return h

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

        The action is considered up to isomorphism of constellation.

        EXAMPLES::

            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
        """
        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_ld(UniqueRepresentation, Parent):
    r"""
    Constellations of given length and degree.

    EXAMPLES::

        sage: C = Constellations(2,3); C
        Connected constellations of length 2 and degree 3 on {1, 2, 3}
        sage: C([[2,3,1],[3,1,2]])
        Constellation of length 2 and degree 3
        g0 (1,2,3)
        g1 (1,3,2)
        sage: C.cardinality()
        2
        sage: Constellations(2,3,connected=False).cardinality()
        6
    """
    Element = Constellation_class

    def __init__(self, length, degree, sym=None, connected=True):
        """
        TESTS::

            sage: TestSuite(Constellations(length=6,degree=4)).run(skip='_test_cardinality')
        """
        from sage.categories.finite_enumerated_sets import FiniteEnumeratedSets
        Parent.__init__(self, category=FiniteEnumeratedSets())
        self._length = length
        self._degree = degree
        if self._length < 0:
            raise ValueError("length should be a nonnegative integer")
        if self._degree < 0:
            raise ValueError("degree should be a nonnegative integer")

        self._sym = sym

        self._connected = bool(connected)

    def is_empty(self):
        r"""
        Return whether this set of constellations is empty.

        EXAMPLES::

            sage: Constellations(2, 3).is_empty()
            False
            sage: Constellations(1, 2).is_empty()
            True
            sage: Constellations(1, 2, connected=False).is_empty()
            False
        """
        return self._connected and self._length == 1 and self._degree > 1

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

            sage: C = Constellations(2, 3, connected=True)
            sage: D = Constellations(2, 3, connected=False)
            sage: e1 = [[3,1,2], None]
            sage: e2 = [[1,2,3], None]
            sage: C(e1) in C
            True
            sage: D(e1) in C
            True
            sage: D(e1) in D
            True
            sage: D(e2) in C
            False
            sage: D(e2) in D
            True

            sage: e1 in C and e1 in D
            True
            sage: e2 in C
            False
            sage: e2 in D
            True
        """
        if isinstance(elt, (tuple, list)):
            try:
                self(elt, check=True)
            except (ValueError, TypeError):
                return False
            else:
                return True
        elif not isinstance(elt, Constellation_class):
            return False
        return (elt.parent() is self or
                (elt.length() == self._length and
                 elt.degree() == self._degree and
                 (not self._connected or elt.is_connected())))

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

            sage: Constellations(3,3)._repr_()
            'Connected constellations of length 3 and degree 3 on {1, 2, 3}'
            sage: Constellations(3,3,connected=False)._repr_()
            'Constellations of length 3 and degree 3 on {1, 2, 3}'
        """
        s = "of length {} and degree {} on {}".format(self._length,
                                                      self._degree,
                                                      self._sym.domain())
        if self._connected:
            return "Connected constellations " + s
        else:
            return "Constellations " + s

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

        EXAMPLES::

            sage: const = Constellations(3,3); const
            Connected constellations of length 3 and degree 3 on {1, 2, 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 itertools import product

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

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

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

        This is found by trial and rejection, starting from
        a random list of permutations.

        EXAMPLES::

            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 import PermutationGroup

        l = self._length
        d = self._degree
        Sd = self._sym

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

        return self([sigma.domain() for sigma in g] + [None], mutable=mutable)

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

        EXAMPLES::

            sage: C = Constellations(2,3)
            sage: C([[2,3,1],[3,1,2]])
            Constellation of length 2 and degree 3
            g0 (1,2,3)
            g1 (1,3,2)
            sage: C([[3,2,1],[3,2,1]])
            Traceback (most recent call last):
            ...
            ValueError: not connected
        """
        if len(data) == 1 and isinstance(data[0], (list, tuple)) and \
           len(data[0]) == self._length:
            g = list(data[0])
        else:
            g = list(data)

        if len(g) != self._length:
            raise ValueError("must be a list of length {}".format(self._length))

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

        g = [self._sym(w) for w in g]

        if i is not None:
            h = self._sym.one()
            for p in g[i:]:
                h *= p
            for p in g[:i]:
                h *= p
            g.insert(i, ~h)

        mutable = options.pop('mutable', False)
        if options.pop('check', True):
            c = self.element_class(self, g, self._connected, mutable, True)
            if c.degree() != self._degree:
                raise ValueError("degree is not {}".format(self._degree))
            if c.length() != self._length:
                raise ValueError("length is not {}".format(self._length))
            return c
        else:
            return self.element_class(self, g, self._connected, mutable, False)

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

        EXAMPLES::

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

            sage: Constellations(3, 5,domain='abcde').an_element()
            Constellation of length 3 and degree 5
            g0 ('a','e','d','c','b')
            g1 ('a','b','c','d','e')
            g2 ('a')('b')('c')('d')('e')

            sage: Constellations(0, 0).an_element()
            Constellation of length 0 and degree 0

            sage: Constellations(1, 1).an_element()
            Constellation of length 1 and degree 1
            g0 (1)

            sage: Constellations(1, 2).an_element()
            Traceback (most recent call last):
            ...
            EmptySetError
        """
        if self.is_empty():
            from sage.categories.sets_cat import EmptySetError
            raise EmptySetError

        if self._degree == 0 and self._length == 0:
            return self([])
        elif self._length == 1:
            return self(self._sym.one())

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

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

        OUTPUT: list of graphs

        EXAMPLES::

            sage: C = Constellations(3,3)
            sage: C.braid_group_action()
            [Looped multi-digraph on 3 vertices,
             Looped multi-digraph on 1 vertex,
             Looped multi-digraph on 3 vertices]
        """
        G = []
        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"""
        Return the orbits under the action of braid group.

        EXAMPLES::

            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]]
            [([3], [3], [3])]
            sage: [x.profile() for x in O[2]]
            [([2, 1], [2, 1], [3]), ([2, 1], [3], [2, 1]), ([3], [2, 1], [2, 1])]
        """
        return [g.vertices(sort=True) for g in self.braid_group_action()]


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

    EXAMPLES::

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

    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, domain=None, connected=True):
        r"""
        OPTIONS:

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

        - ``connected`` -- boolean (default: ``True``); whether we consider
          only connected constellations

        TESTS::

            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:
            if sum(p) != d:
                raise ValueError("all partition in the passport should "
                                 "have the same sum.")
        if domain is None:
            sym = SymmetricGroup(d)
        else:
            sym = SymmetricGroup(domain)
            if len(sym.domain()) != d:
                raise ValueError("the size of the domain should be equal to the degree")

        self._cd = Constellations_ld(l, d, sym, connected)
        self._profile = profile
        from sage.categories.finite_enumerated_sets import FiniteEnumeratedSets
        Parent.__init__(self, category=FiniteEnumeratedSets())

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

            sage: Constellations(profile=[[3,2,1],[3,3],[3,3]])
            Connected constellations with profile ([3, 2, 1], [3, 3], [3, 3]) on {1, 2, 3, 4, 5, 6}
        """
        s = "with profile {} on {}".format(self._profile,
                                           self._cd._sym.domain())
        if self._cd._connected:
            return "Connected constellations " + s
        return "Constellations " + s

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

        EXAMPLES::

            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 (1,2,3,4,5)
            g1 (1)(2,3,4,5)
            g2 (1,5,3)(2,4)
            sage: ir[1]
            Constellation of length 3 and degree 5
            g0 (1,2,3,4,5)
            g1 (1)(2,5,3,4)
            g2 (1,5)(2,3,4)
        """
        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: C = Constellations([(3,1),(3,1),(2,2)])
            sage: c = C([(2,3,4),(1,2,3),((1,2),(3,4))]); c
            Constellation of length 3 and degree 4
            g0 (1)(2,3,4)
            g1 (1,2,3)(4)
            g2 (1,2)(3,4)
            sage: C([(1,2,3),(3,2,4),None])
            Traceback (most recent call last):
            ...
            ValueError: wrong profile
        """
        c = self._cd(*data, **options)
        if options.get('check', True) and c.profile() != self._profile:
            raise ValueError("wrong profile")
        return c

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

        TESTS::

            sage: C = Constellations([(3,1),(3,1),(2,2)])
            sage: for c in C: print(c)
            Constellation of length 3 and degree 4
            g0 (1)(2,3,4)
            g1 (1,2,3)(4)
            g2 (1,2)(3,4)
            Constellation of length 3 and degree 4
            g0 (1)(2,3,4)
            g1 (1,4,2)(3)
            g2 (1,4)(2,3)
            ...
            Constellation of length 3 and degree 4
            g0 (1,4,3)(2)
            g1 (1,2,3)(4)
            g2 (1,4)(2,3)
            Constellation of length 3 and degree 4
            g0 (1,4,3)(2)
            g1 (1,4,2)(3)
            g2 (1,2)(3,4)

            sage: C = Constellations([(3,1),(3,1),(2,2)], domain='abcd')
            sage: for c in sorted(C): print(c)
            Constellation of length 3 and degree 4
            g0 ('a')('b','c','d')
            g1 ('a','b','c')('d')
            g2 ('a','b')('c','d')
            ...
            Constellation of length 3 and degree 4
            g0 ('a','d','c')('b')
            g1 ('a','d','b')('c')
            g2 ('a','b')('c','d')
        """
        if self._cd._length == 1:
            if self._cd._degree == 1:
                yield self([[0]])
            return

        S = self._cd._sym
        profile = list(self._profile)[:-1]
        for p in product(*[S.conjugacy_class(pi) for pi in profile]):
            if self._cd._connected and not perms_are_connected(p, self._cd._degree):
                continue
            c = self._cd(list(p) + [None], check=False)
            if c.profile() == self._profile:
                yield c

# *************************************************************************
#                          auxiliary functions
# *************************************************************************


def perm_sym_domain(g):
    r"""
    Return the domain of a single permutation (before initialization).

    EXAMPLES::

        sage: from sage.combinat.constellation import perm_sym_domain
        sage: perm_sym_domain([1,2,3,4])
        {1, 2, 3, 4}
        sage: perm_sym_domain(((1,2),(0,4)))
        {0, 1, 2, 4}
        sage: sorted(perm_sym_domain('(1,2,0,5)'))
        [0, 1, 2, 5]
    """
    if isinstance(g, (tuple, list)):
        if isinstance(g[0], tuple):
            return set().union(*g)
        else:
            return set(g)
    elif isinstance(g, str):  # perms given as strings of cycles
        assert g.startswith('(') and g.endswith(')')
        domain = set().union(*[a for cyc in g[1:-1].split(')(')
                               for a in cyc.split(',')])
        if all(s.isdigit() for s in domain):
            return [int(x) for x in domain]
        else:
            return domain
    elif parent(g) in Groups:
        return g.domain()
    else:
        raise TypeError


def perms_sym_init(g, sym=None):
    r"""
    Initialize a list of permutations (in the same symmetric group).

    OUTPUT:

    - ``sym`` -- a symmetric group

    - ``gg`` -- list of permutations

    EXAMPLES::

        sage: from sage.combinat.constellation import perms_sym_init
        sage: S, g = perms_sym_init([[0,2,1,3], [1,3,2,0]])
        sage: S.domain()
        {0, 1, 2, 3}
        sage: g
        [(1,2), (0,1,3)]

        sage: S, g = perms_sym_init(['(2,1)', '(0,3)'])
        sage: S.domain()
        {0, 1, 2, 3}
        sage: g
        [(1,2), (0,3)]

        sage: S, g = perms_sym_init([(1,0), (2,1)])
        sage: S.domain()
        {0, 1, 2}
        sage: g
        [(0,1), (1,2)]

        sage: S, g = perms_sym_init([((1,0),(2,3)), '(0,1,4)'])
        sage: S.domain()
        {0, 1, 2, 3, 4}
        sage: g
        [(0,1)(2,3), (0,1,4)]
    """
    if g is None or len(g) == 0:
        if sym is None:
            sym = SymmetricGroup(0)
        return sym, [sym([])]

    if sym is None:
        domain = set().union(*[perm_sym_domain(gg) for gg in g])
        if all(isinstance(s, (int, Integer)) and s > 0
               for s in domain):
            domain = max(domain)
        else:
            domain = sorted(domain)
        sym = SymmetricGroup(domain)

    try:
        return sym, [sym(u) for u in g]
    except (ValueError, TypeError):
        return sym, None


def perms_are_connected(g, n):
    """
    Check that the action of the generated group is transitive.

    INPUT:

    - ``g`` -- list of permutations of `[0, n-1]` (in a SymmetricGroup)

    - ``n`` -- integer

    EXAMPLES::

        sage: from sage.combinat.constellation import perms_are_connected
        sage: S = SymmetricGroup(range(3))
        sage: perms_are_connected([S([0,1,2]),S([0,2,1])],3)
        False
        sage: perms_are_connected([S([0,1,2]),S([1,2,0])],3)
        True
    """
    G = Graph()
    if g:
        G.add_vertices(g[0].domain())
    for p in g:
        G.add_edges(p.dict().items(), loops=False)
    return G.is_connected()


def perms_canonical_labels_from(x, y, j0, verbose=False):
    r"""
    Return canonical labels for ``x``, ``y`` that starts at ``j0``.

    .. WARNING::

        The group generated by ``x`` and the elements of ``y`` should be
        transitive.

    INPUT:

    - ``x`` -- list; a permutation of `[0, ..., n]` as a list

    - ``y`` -- list of permutations of `[0, ..., n]` as a list of lists

    - ``j0`` -- an index in [0, ..., n]

    OUTPUT: mapping: a permutation that specify the new labels

    EXAMPLES::

        sage: from sage.combinat.constellation import perms_canonical_labels_from
        sage: perms_canonical_labels_from([0,1,2],[[1,2,0]], 0)
        [0, 1, 2]
        sage: perms_canonical_labels_from([1,0,2], [[2,0,1]], 0)
        [0, 1, 2]
        sage: perms_canonical_labels_from([1,0,2], [[2,0,1]], 1)
        [1, 0, 2]
        sage: perms_canonical_labels_from([1,0,2], [[2,0,1]], 2)
        [2, 1, 0]
    """
    n = len(x)

    k = 0
    mapping = [None] * n
    waiting = [[] for _ in repeat(None, len(y))]

    while k < n:
        if verbose:
            print("complete from {}".format(j0))
        # initialize at j0
        mapping[j0] = k
        waiting[0].append(j0)
        k += 1
        # complete x cycle from j0
        j = x[j0]
        while j != j0:
            mapping[j] = k
            waiting[0].append(j)
            k += 1
            j = x[j]
        if verbose:
            print("completed cycle mapping = {}".format(mapping))

        # find another guy
        if verbose:
            print("try to find somebody in {}".format(waiting))
        l = 0
        while l < len(waiting):
            i = 0
            while i < len(waiting[l]):
                j1 = waiting[l][i]
                if mapping[y[l][j1]] is None:
                    break
                i += 1

            if i == len(waiting[l]):  # not found: go further in waiting
                if l < len(waiting) - 1:
                    waiting[l + 1].extend(waiting[l])
                waiting[l] = []
                l += 1
                i = 0

            else:  # found: complete cycle from new guy
                j0 = y[l][j1]
                if l < len(waiting) - 1:
                    waiting[l + 1].extend(waiting[l][:i + 1])
                del waiting[l][:i + 1]
                break

    return mapping


def perm_invert(p):
    """
    Return the inverse of the permutation `p`.

    INPUT:

    - ``p`` -- a permutation of {0,..,n-1} given by a list of values

    OUTPUT: a permutation of {0,..,n-1} given by a list of values

    EXAMPLES::

        sage: from sage.combinat.constellation import perm_invert
        sage: perm_invert([3,2,0,1])
        [2, 3, 1, 0]
    """
    q = [None] * len(p)
    for i, j in enumerate(p):
        q[j] = i
    return q


def perm_conjugate(p, s):
    """
    Return the conjugate of the permutation `p` by the permutation `s`.

    INPUT:

    - ``p``, ``s`` -- two permutations of {0,..,n-1} given by lists of values

    OUTPUT: a permutation of {0,..,n-1} given by a list of values

    EXAMPLES::

        sage: from sage.combinat.constellation import perm_conjugate
        sage: perm_conjugate([3,1,2,0], [3,2,0,1])
        [0, 3, 2, 1]
    """
    q = [None] * len(p)
    for i in range(len(p)):
        q[s[i]] = s[p[i]]
    return q


def perms_canonical_labels(p, e=None):
    """
    Relabel a list with a common conjugation such that two conjugated
    lists are relabeled the same way.

    INPUT:

    - ``p`` -- list of at least 2 permutations

    - ``e`` -- ``None`` or a list of integer in the domain of the
      permutations. If provided, then the renumbering algorithm is
      only performed from the elements of ``e``.

    OUTPUT: a pair made of a list of permutations (as a list of lists) and a
    list that corresponds to the conjugacy used.

    EXAMPLES::

        sage: from sage.combinat.constellation import perms_canonical_labels
        sage: l0 = [[2,0,3,1], [3,1,2,0], [0,2,1,3]]
        sage: l, m = perms_canonical_labels(l0); l
        [[1, 2, 3, 0], [0, 3, 2, 1], [2, 1, 0, 3]]

        sage: S = SymmetricGroup(range(4))
        sage: [~S(m) * S(u) * S(m) for u in l0] == list(map(S, l))
        True

        sage: perms_canonical_labels([])
        Traceback (most recent call last):
        ...
        ValueError: input must have length >= 2
    """
    if not len(p) > 1:
        raise ValueError('input must have length >= 2')

    n = len(p[0])

    c_win = None
    m_win = list(range(n))

    x = p[0]
    y = p[1:]

    if e is None:
        e = list(range(n))

    # get canonical label from i in to_test and compare
    while e:
        i = e.pop()
        m_test = perms_canonical_labels_from(x, y, i)
        c_test = [perm_conjugate(u, m_test) for u in p]
        if c_win is None or c_test < c_win:
            c_win = c_test
            m_win = m_test

    return c_win, m_win
