# -*- coding: utf-8 -*-
# vi:ts=4:et
#
# $Date: 2003/08/15 13:01:05 $
# $Revision: 1.2 $
# =====================================================

"""library for permutation groups"""

import sys
import itertools

# young
import combination
import error

__version__ = '0.08'

__all__ = [
           'to_cycle',
           'get_sgn',
           'get_inv',
           'permgroup',
           'PermGroup',
           'symmgroup',
           'SymmetricGroup',
           ]

"""
identity :単位元
inverse  : 逆元
inversion number : 転倒数
cycle : 巡回置換
cycle decomposition : 巡回置換分解

"""
# TODO
# move pretty-print related functions

def int2str(seq, separator = " "):
    """convert a sequence of int to a sequence of string"""
    # some kind of pretty printer?

    # sample
    # [3,1,2] --> "3 1 2"

    # convert to string type
    seq = map(str, seq)

    return separator.join(seq)

def pretty_print_permgroup(group, prefix="(", suffix = ")"):
    """Pretty print permutation group"""
    return prefix + int2str(group) + suffix

def pretty_print_cycyle(group, Ignore_Id=False, prefix="(", suffix=")"):
    """pretty print cycle"""
    # Ignore_Id == False
    # [[1,4,],[2,3]]
    # -->
    # (1 4)(2 3)
    #
    # [[1,4],[2],[3]]
    # -->
    # (1 4)(2)(3)
    # 
    # Ignore_Id == True
    # [[1,4],[2],[3]]
    # -->
    # (1 4)
    # 
    # [[1,2,3],[4]]
    # -->
    # (1 2 3)

    def not_id(cycle):
        # Tests whether or not the length of cycle is more than one
        return len(cycle)>1

    if Ignore_Id:
        group = filter(not_id, group)

    buff = []
    for cycle in group:
        buff.append(prefix + int2str(cycle) + suffix)

    return " ".join(buff)


class BaseGroup(tuple):
    """base class for group"""
    def __init__(self, element):
        assert self.__class__ != BaseGroup, "BaseGroup is an abstract class"
        self.order = len(element)

    def get_order(self):
        """Return the order of the group"""
        return self.order

    def get_inverse(self):
        """Return the inverse"""
        raise NotImplementedError

    def __repr__(self):
        #return "%s"%self.__class__.__name__ + "([" + int2str(self, ",") + "])"
        raise NotImplementedError

    __str__ = __repr__

class Cycle(tuple):
    """class for cycle"""
    def __repr__(self):
        return "%s"%self.__class__.__name__ + "([" + pretty_print_cycyle(self) +"])"
    __str__ = __repr__


class PermGroup(BaseGroup):
    """class for permutation group"""

    # NOTE
    # "inverse" and "inversion number" might be confusing.

    def __init__(self, group):
        BaseGroup.__init__(self, group)
        self.inversion_number = 0
        self.signature = 0

    def get_inverse(self):
        """Return the inverse"""
        # TODO
        # need clean-up

        lst = [None] * self.order
        for i in range(self.order):
            lst[self[i]-1] = i+1

        return PermGroup(lst)

    def to_cycle(self):
        """Decomposite permutation group into cycles"""
        # Cycle decomposition

        # XXX
        # built-in function, tuple, is used
        # to make an instance of Cycle hashable.

        group = [tuple(g) for g in cycle_decomposition_generator(self)]
        return Cycle(group)

    def to_matrix(self):
        """matrix representation"""
        # initialize (n,n) matrix with 0
        matrix = [[0] * self.order for row in range(self.order)]

        for i in range(self.order):
            matrix[i][self[i]-1] = 1

        return matrix

    def isodd(self):
        """Tests whether or not the signature is odd"""
        return self.get_sgn() == -1

    def iseven(self):
        """Tests whether or not the signature is even"""
        return self.get_sgn() == 1

    def get_sgn(self):
        """Return the signature of the permutation group"""
        # odd  : -1
        # even : 1

        if not self.signature:
            if (self.get_inv() %2) == 0:
                self.signature = 1
            else:
                self.signature = -1

        return self.signature

    def get_inv(self):
        """Return the inversion number"""
        # NOTE
        # get_inverse / get_inv might be confusing.

        if not self.inversion_number:
            self.inversion_number = self._get_inv_number()

        return self.inversion_number

    def _get_inv_number(self):
        """Return the inversion number of the permutation"""
        index = 0

        for i, element in enumerate(self):
            def _cmp(x):
                return self[i] > x
            result = filter(_cmp, self[i+1:])
            index += len(result)

        return index

    def __repr__(self):
        return "%s"%self.__class__.__name__ + "([" + int2str(self) + "])"

    __str__ = __repr__

    def __mul__(self, other):
        """composition of map"""
        # composite map

        if not isinstance(other,PermGroup):
            raise TypeError, "unsupported operand type(other) for *: %r and %r"%(repr(self), repr(other))

        #assert len(self) == len(other), "order must be same"
        if len(self) != len(other):
            raise error.GroupError, "orders must be same" +\
                    "\n" + "\t%r and %r"%(repr(self), repr(other))
        return PermGroup([other[x-1] for x in self])

# You can give permutation in two ways.
# [1] permgroup((3,1,2))
# [2] permgroup(3,1,2)

def permgroup(*seq):
    """Return the PermGroup instance"""
    if len(seq) == 1:
        seq = seq[0]
    return PermGroup(seq)


def get_inv(*permutation):
    """Return the inversion number of the permutation"""
    p = permgroup(*permutation)
    return p.get_inv()

def get_sgn(*permutation):
    """Return the signature of the permutation"""
    return permgroup(*permutation).get_sgn()


def to_cycle(*permutation):
    """decomposite permutation group into cycle"""
    return permgroup(*permutation).to_cycle()

def cycle_decomposition_generator(group):
    """generator for cycle decomposition"""

    if not group:
        # trivial
        return

    candidate = list(group)
    buff = []

    next = candidate[0]
    candidate.remove(next)
    #next = candidate.pop(0) # one line

    buff.append(next)

    while candidate:
        next = group[next-1]
        if next not in candidate:

            buff.sort()
            yield buff

            # try the next candidate
            next = candidate[0]
            buff = []

        candidate.remove(next)
        buff.append(next)

    if buff:
        buff.sort()
        yield buff

class AlternatingGroup(object):
    # needs to be implemented
    pass

class SymmetricGroup(object):
    """class for symmetric group"""
    def __init__(self, degree):
        # degree
        assert isinstance(degree, int) and degree >= 0, "degree must be non-negative integer"
        self.degree = degree

        # order
        self.order  = combination.factorial(degree)

        self.element = [PermGroup(grp) for grp in combination.permutation(range(1, self.degree+1))]
        self.element.sort()

        self._cycle_cache = {}

    def __iter__(self):
        return iter(self.element)

    def sort_by_cycle(self):
        """sort each permutation by its cycle"""
        if not self._cycle_cache:

            for perm in self.element:
                cycle = perm.to_cycle()
                self._cycle_cache.setdefault(cycle, []).append(perm)

        #return self._cycle_cache
        return self._cycle_cache.items()

    def get_order(self):
        """order of the symmetric group"""
        return self.order

    def get_degree(self):
        """degree of the symmetric group"""
        return self.degree

    def get_odd(self):
        """Return only odd permutaions"""
        return filter(PermGroup.isodd, self)

    def get_even(self):
        """Return only even permutaions"""
        return filter(PermGroup.iseven, self)

    def __repr__(self):
        return "%s"%self.__class__.__name__ + "([" + "degree:%d"%self.degree +\
                "|" + "order:%d"%self.order+ "])"
    __str__ = __repr__

    def display(self):
        print "\n".join(map(str, self.element))

def symmgroup(num):
    return SymmetricGroup(num)


