

from __future__ import absolute_import, division, print_function
from __future__ import  unicode_literals

import re
import numpy as np
import warnings
from functools import partial
from numbers import Integral


from mmgroup import mat24
from mmgroup.generators import gen_leech2_type
from mmgroup.clifford12 import xsp2co1_elem_to_qs_i, xsp2co1_elem_to_qs 
from mmgroup.clifford12 import xsp2co1_qs_to_elem_i 
from mmgroup.clifford12 import xsp2co1_chain_short_3 
from mmgroup.clifford12 import xsp2co1_neg_elem
from mmgroup.clifford12 import error_string, chk_qstate12
from mmgroup.clifford12 import xsp2co1_unit_elem
from mmgroup.clifford12 import xsp2co1_mul_elem, xsp2co1_inv_elem
from mmgroup.clifford12 import xsp2co1_copy_elem
from mmgroup.clifford12 import xsp2co1_reduce_elem
from mmgroup.clifford12 import xsp2co1_elem_to_leech_op
from mmgroup.clifford12 import xsp2co1_elem_to_bitmatrix
from mmgroup.clifford12 import xsp2co1_set_elem_word 
from mmgroup.clifford12 import xsp2co1_mul_elem_word 
from mmgroup.clifford12 import xsp2co1_xspecial_vector
from mmgroup.clifford12 import xsp2co1_xspecial_conjugate
from mmgroup.clifford12 import xsp2co1_elem_xspecial
from mmgroup.clifford12 import xsp2co1_elem_to_word
from mmgroup.clifford12 import xsp2co1_involution_invariants
from mmgroup.clifford12 import xsp2co1_involution_orthogonal
from mmgroup.clifford12 import xsp2co1_conjugate_elem
from mmgroup.clifford12 import xsp2co1_elem_conjugate_involution
from mmgroup.clifford12 import xsp2co1_elem_conjugate_involution_Gx0
from mmgroup.clifford12 import xsp2co1_elem_subtype
from mmgroup.clifford12 import xsp2co1_traces_fast      
from mmgroup.clifford12 import bitmatrix64_to_numpy
    

from mmgroup.structures.abstract_group import singleton
from mmgroup.structures.abstract_mm_group import AbstractMMGroupWord
from mmgroup.structures.abstract_mm_group import AbstractMMGroup
from mmgroup.structures.parse_atoms import AtomDict    
from mmgroup.structures.construct_mm import iter_mm       

from mmgroup.structures.ploop import Cocode, PLoop
from mmgroup.structures.autpl import StdAutPlGroup, AutPL ,autpl_from_obj
from mmgroup.structures.xleech2 import XLeech2
from mmgroup.structures.qs_matrix import QStateMatrix

from mmgroup.structures.construct_mm import iter_mm       
from mmgroup.structures.construct_mm import load_group_name     
from mmgroup.structures.construct_mm import iter_strings_from_atoms


FORMAT_REDUCED = True



###########################################################################
# Word class for the group G_{x0}
###########################################################################


DICT_INVOLUTION_G_x0 = {
   0x1011 : '1A_x+',  # the neutral element :math:`x_1`
   0x3022 : '2B_x-',  # the central involution :math:`x_{-1}`
   0x0021 : '2A_x0',  # the element :math:`x_{\{2,3\}}`
   0x0022 : '2B_x0',  # the element :math:`x_{\Omega}`
   0x1121 : '2A_o+',  # the element :math:`y_o`
   0x1122 : '2B_o-',  # the element :math:`x_{-1} y_o`
   0x0122 : '2B_o0',  # the element :math:`y_o x_{\{8,9\}}`
   0x0322 : '2B_d0',  # the element :math:`y_D x_{\{0, 12\}}
}



import_construct_mm_pending = True


def import_construct_mm():
    global import_construct_mm_pending, iter_mm
    global iter_strings_from_atoms
    from mmgroup.structures.construct_mm import iter_mm       
    from mmgroup.structures.construct_mm import load_group_name     
    from mmgroup.structures.construct_mm import iter_strings_from_atoms
    load_group_name(StdXsp2_Co1_Group, "MX")
    import_construct_mm_pending = False



MM = None

def import_MM():
    global MM
    if MM is not None:
        return MM
    from mmgroup.mm_group import MM
    return MM


class Xsp2_Co1(AbstractMMGroupWord):
    r"""Models an element the subgroup :math:`G_{x0}` of the monster

    Here  the subgroup :math:`G_{x0}` is a subgroup of the
    monster of structure :math:`2^{1+24}.\mbox{Co}_1`. It is 
    generated by the elements 
    :math:`x_\delta, y_d, y_d, x_\pi, \xi` described in
    section :ref:`mmgroup-label`. 

    The constructor of this class works exactly as the constructor
    of class |MM|. Here all elements of the monster :math:`\mathbb{M}`
    occuring in the constructor must lie in the subgroup :math:`G_{x0}`
    of the monster. So in the constructor all tags are legal, except 
    for the tag ``'t'``. A instance of class  |MM| is accepted in the 
    constructor of this class and vice versa.

    The group operation and the operation on a vector of class
    |MMVector| is the same as in class |MM|.

    This class uses a considerably faster implementation of the
    group operation as in class |MM|, as described in section
    :ref:`group_g_x0_label`.

    The user hardly ever has to deal with this class, since the
    implementation of class |MM| uses the accelerated 
    functions in this class automatically where appropriate.
    """
    MIN_LEN = 16
    __slots__ =  "_data"
    def __init__(self,  tag = None, atom = None, *args, **kwds):
        if import_construct_mm_pending:
            import_construct_mm()
        atoms = iter_mm(self.group, tag, atom, in_G_x0 = True)
        a_atoms = np.fromiter(atoms, dtype = np.uint32) 
        self._data = np.zeros(26, dtype = np.uint64)
        xsp2co1_set_elem_word(self._data, a_atoms, len(a_atoms))
         
    @property
    def data(self):
        return list(map(int, self._data))

    @property
    def short3(self):
        return int(self._data[0])

    @property
    def qs_t(self):
        return QStateMatrix(xsp2co1_elem_to_qs_i(self._data))
        
    @property
    def qs(self):
        return QStateMatrix(xsp2co1_elem_to_qs(self._data))
        
    @property
    def leech_op(self):
        a = np.zeros(576, dtype = np.int8)
        xsp2co1_elem_to_leech_op(self._data, a) 
        return a.reshape((24,24))        

    @property
    def leech_mod2_op(self):
        a = np.zeros(24, dtype = np.uint64)
        xsp2co1_elem_to_bitmatrix(self._data, a) 
        return a        

    def __neg__(self):
        neg = Xsp2_Co1(self)
        xsp2co1_neg_elem(self._data)
        return neg

    def __pos__(self):
        return self
        
    def order(self, max_order = 119):
        """Return the order of the element of the monster group


        If the argument ``max_order`` is present then the order of the 
        element is checked up to (and including) ``max_order`` only.  
        Then the function returns ``0`` if the order is greater than 
        ``max_order``. By default, the function returns the exact 
        order of the element.
        """
        o = self.qs.order(max_order)
        if o & 1 == 0:
            o = o >> 1
        unit, pw = self.group(), self**o
        if pw == unit:
            return o
        for i in range(2):
            o, pw = 2*o, pw * pw
            if pw == unit:
                return o
        err = "Order of QStateMatrix object not found" 
        raise ValueError(err)


    def as_xsp(self):
        return chk_qstate12(xsp2co1_xspecial_vector(self._data))


    def as_Co1_bitmatrix(self):
        """Convert element to a 24 times 24 bit matrix

        The bit matrix is returned as a 24 times 24 bit numpy
        array of 8-bit unsigned integers, with each entry equal to
        0 or 1.

        That matrix operates by right multiplication on a vector
        representing an element of the Leech lattice mod 2.
        See method ``mmgroup.XLeech2.as_Leech2_bitvector`` for the 
        encoding of such a vector.
        """
        m = np.zeros(24, dtype = np.uint64)
        chk_qstate12(xsp2co1_elem_to_bitmatrix(self._data, m))
        bm = np.zeros(576, dtype = np.uint8)
        length = chk_qstate12(bitmatrix64_to_numpy(m, 24, 24, bm))
        assert length == 576
        return bm.reshape((24,24))

    as_Q_x0_atom = as_xsp

    def type_Q_x0(self):
        r"""Return type of element if it is in the subgroup :math:`Q_{x0}`

        If the element is in the subgroup :math:`Q_{x0}` of the monster
        then the function returns the type of the vector in the Leech 
        lattice modulo 2 corresponding to this element. That type is
        0, 2, 3, or 4.

        The function raises ValueError if the element is not
        in the subgroup :math:`Q_{x0}`. 
        """
        v = self.as_Q_x0_atom()
        return gen_leech2_type(v)

    def xsp_conjugate(self, v, sign = True):
        v = np.array(v, dtype = np.uint64, copy=True)
        shape = v.shape
        assert len(shape) <= 1
        v = np.ravel(v)
        chk_qstate12(
            xsp2co1_xspecial_conjugate(self._data, len(v), v, sign))
        if len(shape):
            return list(map(int,v))
        else:
            return int(v[0])

    def mul_data(self, data):
        a_atoms = np.array(data, dtype = np.uint32)
        xsp2co1_mul_elem_word(self._data, a_atoms, len(a_atoms))
        return self


    @property
    def mmdata(self):
        a = np.zeros(10, dtype = np.uint32)
        length = chk_qstate12(xsp2co1_elem_to_word(self._data, a))
        return a[:length]

    def _involution_invariants(self):
        """Wrapper for C function  xsp2co1_involution_invariants"""
        invar = np.zeros(12, dtype = np.uint64)
        xsp2co1_involution_invariants(self._data, invar)
        v1 = xsp2co1_involution_orthogonal(invar, 1)
        v0 = xsp2co1_involution_orthogonal(invar, 0)
        return invar, v1, v0


    def conjugate_involution(self, mmgroup = None):
        r"""Find an element conjugating an involution standard element

        If the element :math:`g` given by ``self`` is an involution 
        in  the monster then the method computes an element :math:`h` 
        of the monster   with  :math:`h^{-1} g h = z`, where  
        :math:`z` is define as follows:

        If :math:`g = 1`, we put :math:`h = z = 1`

        if :math:`g` is a 2A involution (in the monster) then we let
        :math:`z` be the involution in  :math:`Q_{x0}` corresponding 
        to the Golay cocode word with entries  :math:`2,3` being set.

        if :math:`g` is a 2B involution (in the monster) then we let 
        :math:`z` be the central involution in :math:`G_{x0}`

        The function returns a pair ``(I, h)``, where :math:`h` as an 
        element of the instance  ``MM`` of class ``MMGroup``. We put
        ``I = 0`` if :math:`g = 1`. We put ``I = 1, 2`` if 
        :math:`g` is a 2A or 2B involution, respectively.

        The function raises ``ValueError`` if :math:`g` is not an 
        involution. 

        This is a wrapper for the C 
        function ``xsp2co1_elem_conjugate_involution``.

        Parameter ``mmgroup`` is not for public use. In special cases
        it may specify an alternative class implementing the monster 
        group. Then the function returns :math:`h` as an instance of 
        that class.
        """
        if mmgroup is None:
             mmgroup = import_MM()            
        a = np.zeros(15, dtype = np.uint32)
        len_a = xsp2co1_elem_conjugate_involution(self._data, a)
        chk_qstate12(len_a)
        return (len_a >> 8), mmgroup('a', a[:len_a & 0xff])


    def conjugate_involution_G_x0(self, guide = 0, group = None):
        r"""Map an involution in :math:`G_{x0}` to a standard form.

        Assume that the element :math:`g` represented by ``self`` is
        an involution in the group :math:`G_{x0}`.

        The function computes an element :math:`a` in :math:`G_{x0}` 
        such that :math:`h = a^{-1} g a` is a (fixed) representative 
        of the class of :math:`g` in the group :math:`G_{x0}`.
        The the function returns a pair ``(iclass, a)``, where
        ``a`` is the computed element of :math:`G_{x0}`, and 
        ``iclass`` describes the representative :math:`h` of the
        involution class as given in the following list:

        ``iclass = '1A_x+'``: the neutral element :math:`x_1`

        ``iclass = '2B_x-'``: the central involution :math:`x_{-1}`
   
        ``iclass = '2A_x0'``: the element :math:`x_{\{2,3\}}`

        ``iclass = '2B_x0'``: the element :math:`x_{\Omega}`

        ``iclass = '2A_o+'``: the element :math:`y_o`

        ``iclass = '2B_o-'``: the element :math:`x_{-1} y_o`

        ``iclass = '2B_o0'``: the element :math:`x_{\{8,9\}} y_o`

        ``iclass = '2B_d0'``: the element :math:`x_{\{0, 12\}} y_d`


        Here in :math:`x_{\{i,j\}}` the index :math:`\{i,j\}` indicates
        a Golay cocode word of length 2 given by the entries :math:`i` 
        and :math:`j`. Octad :math:`o` is the standard octad 
        :math:`\{0,1,2,3,4,5,6,7\}`.
        Dodecad :math:`d` is the standard dodecad 
        :math:`\{0, 4, 8, 13, 14, 15, 17, 18, 19, 21, 22, 23\}`.

        The first two characters of the string ``iclass`` denote the 
        class in the Monster containing that class. The last character 
        of the string ``iclass`` denotes the sign of the character of 
        the class in the representation :math:`24_x \otimes 4096_x`.

        By default, ``a`` is an instance of this class. If
        parameter ``group`` is set to a class representing a suitable
        subgroup of the monster (e.g. ``group = mmgroup.MM``) then 
        ``a`` is returned as an instance of that class.
 
        Parameter ``guide`` should usually be zero. If ``guide`` is a
        type-4 vector :math:`v_4` in the Leech lattice mod 2 such that
        the two  conditions :math:`h = a^{-1} g a`
        and :math:`v_4 \cdot a = \Omega` can both be achieved then we
        compute an element :math:`a` satisfying these two conditions.
        Otherwise parameter ``guide`` is ignored. Here :math:`\Omega`
        is the standard frame in the Leech lattice.  
        """
        group = Xsp2_Co1 if group is None else group
        if not guide:
             guide = 0
        elif not isinstance(guide, Integral):
             guide = XLeech2(guide).ord
        a = np.zeros(10, dtype = np.uint32)
        len_a = xsp2co1_elem_conjugate_involution_Gx0(
            self._data, guide, a)
        chk_qstate12(len_a)
        iclass = DICT_INVOLUTION_G_x0[len_a >> 8]
        return iclass, group('a', a[:len_a & 0xff])


    def chi_G_x0(self):
        r"""Compute characters of element

        The function returns a tuple 
        :math:`(\chi_M, \chi_{299}, \chi_{24}, \chi_{4096})`
        of integers.

        Here :math:`\chi_M` is the character of the element in the
        196833-dimensional rep :math:`198883_x` of the monster.

        By Conway's construction of the monster we have:

        :math:`198883_x =  299_x \oplus 24_x \otimes  4096_x
        \oplus 98280_x`,

        for suitable irreducible representations 
        :math:`299_x, 24_x, 4096_x, 98280_x` of the group 
        :math:`G_{x0}`. The corresponding characters of the
        element of  :math:`G_{x0}` are returned in the tuple
        given above.  

        While the product :math:`\chi_{24} \cdot \chi_{4096}`
        is well defined, the factors  :math:`\chi_{24}` and 
        :math:`\chi_{4096}` are defined up to sign only. We
        normalize these factors such that the first nonzero value 
        of the pair :math:`(\chi_{24}, \chi_{4096})` is positive. 
        """
        a = np.zeros(4, dtype = np.int32)
        res = chk_qstate12(xsp2co1_traces_fast(self._data, a))
        chi24, chisq24, chi4096, chi98260 = map(int, a[:4])
        chi299 = (chi24**2 + chisq24) // 2 - 1
        chi_M = chi299 + chi98260 + chi24 * chi4096
        return chi_M, chi299, chi24, chi4096


    def _qs_v2(self):
        i = 26
        while i and int(self._data[i-1] == 0):
            i -= 1
        i -= 14
        assert i & 1 == 0 and 0 <= i <= 12
        return i >> 1
        
    @property
    def subtype(self):
        r"""Return subtype of an element

        Let :math:`g \in G_{x0}` be stored in ``self``. The function
        returns the subtype of a :math:`g`. If :math:`g` maps the 
        standard frame :math:`\Omega` of the Leech lattice modulo 2 
        to a frame of subtype :math:`t` then :math:`g` has 
        subtype :math:`t`.

        The subtype is returned as a pair of integers as in the 
        corresponding method in class |XLeech2|, see section 
        :ref:`computation-leech2` in the **guide** for background.

        Since the subtype is determined by the size of the denominators
        of the representation :math:`4096_x`, it can be computed very 
        fast.
        """
        res = xsp2co1_elem_subtype(self._data)
        assert res & 0x40 == 0x40
        return res >> 4, res & 0xf

    def str(self):
        """Convert group element to a string
        """
        atoms = self.mmdata
        strings = iter_strings_from_atoms(atoms, abort_if_error=0)
        s = "*".join(strings) 
        return "Xsp2_Co1<%s>" % (s if len(s) else "1")



###########################################################################
# The class representing the group G_x0
###########################################################################



def cocode_to_xsp2co1(g, c):
    res =  g.word_type(group = g)
    chk_qstate12(xsp2co1_elem_xspecial(res._data, c.value & 0xfff))
    return res

def ploop_to_xsp2co1(g, pl):
    res =  g.word_type(group = g)
    value = (c.value & 0x1fff)
    value = (value << 12) ^ mat24.ploop_theta(value)
    chk_qstate12(xsp2co1_elem_xspecial(res._data, value))
    return res


def autpl_to_xsp2co1(g, aut):
    res =  g.word_type(group = g)
    a = np.zeros(2, dtype = uint32)
    a[0] = 0x10000000 + (aut._cocode & 0xfff)   # tag 'd'
    a[1] = 0x20000000 + aut._perm_num           # tag 'p'
    chk_qstate12(xsp2co1_set_elem_word(res._data, a, 2))
    return res

def mmgroup_to_xsp2co1(g, mm):
    res =  g.word_type(group = g)
    chk_qstate12(xsp2co1_set_elem_word(res._data, mm._data, mm.length))
    return res


def xsp2co1_to_mm(mmgroup, xsp):
    g = mmgroup()
    g._extend(10)
    g.length = chk_qstate12(xsp2co1_elem_to_word(xsp._data, g._data))
    g.reduced = 0 
    return g



@singleton
class Xsp2_Co1_Group(AbstractMMGroup):
    r"""Model the subgroup :math:`G_{x0}` of the Monster
    
    The group :math:`G_{x0}` is the subgroup of the Monster group
    genertated by the elements with tags ``p, d, x, y, l`` in the  
    class |MMGroup| representing the Monster group.  :math:`G_{x0}`
    has structure :math:`2**(1+24).\mbox{Co}_1` in ATLAS
    notation, see :cite:`Con85`, :cite:`Atlas`.
       

    
    :param \*data:

      A variable number of arguments; each argument describes an
      element of the subgroup ``2**(1+24).Co_1`` of the monster.
      These elements are multiplied.  
        

    Depending on its type each parameter in **\*data** is  
    interpreted as follows:

    .. table:: Legal types for constructor of class ``Xsp2_Co1_Group``
      :widths: 25 75

      ===================== ==================================================
      type                  Evaluates to
      ===================== ==================================================
      tuple (``tag, data``) Create an element of the monster group as 
                            described in class |MMGroup|. Here legal tags
                            are: ``'p', 'd', 'x', 'y', 'z', 'l'``.  The
                            resulting element must lie in the subgroup
                            :math:`G_{x0}` of the monster.
                            
      class |MMGroup|       Create an element of the Monster group. That                
                            element must lie in the subgroup
                            :math:`G_{x0}` of the monster.

      class |AutPL|         Create an element of the subgroup of
                            :math:`G_{x0}` given by class |AutPL|.
                           
      class ``Xsp2_Co1``    Create a copy of an element of this class.
                           
      pair (``qstate, x``)  Deprecated and not implemented!!!!!
                            This kind of construction is for testing and
                            not recommended for public use. Here        
                            ``qstate`` must be an instance of class 
                            ``mmgroup.structures.qs_matrix.QStateMatrix``
                            and ``x`` must be an integer representing a
                            short Leech lattice vector modulo 3.
      ===================== ==================================================

        
    :raise:
        * TypeError if ``type(data)`` is not as expected.
        * ValueError if ``data`` cannot be converted to an
          instance of class  `` Xsp2_Co1``.
    
    
    """
    __instance = None
    __slots__ = "data"
    STD_V3  = 0x8000004
    word_type = Xsp2_Co1
    is_mmgroup = True


    def __init__(self):
        """ TODO: Yet to be documented     


        """
        super(Xsp2_Co1_Group, self).__init__()
        self.atom_parser = AtomDict(self.atom)

    def atom(self, tag = None, i = "r"):
        return self.word_type(tag, i)

    def _imul(self, g1, g2):
        chk_qstate12(xsp2co1_mul_elem(g1._data, g2._data, g1._data))
        return g1

    def _invert(self, g1):
        w = self.word_type()
        chk_qstate12(xsp2co1_inv_elem(g1._data, w._data))
        return w

    def copy_word(self, g1):
        w = self.word_type()
        xsp2co1_copy_elem(g1._data, w._data)
        return w

    def reduce(self, g1):
        chk_qstate12(xsp2co1_reduce_elem(g1._data))
        return self
       
    def _equal_words(self, g1, g2):
        chk_qstate12(xsp2co1_reduce_elem(g1._data))
        chk_qstate12(xsp2co1_reduce_elem(g2._data))
        return (g1._data == g2._data).all()

    def _embed_number(self, n):
        w = self.word_type()
        if (n == -1):
            xsp2co1_neg_elem(w._data)
            n = 1
        if n == 1:
            return w
        raise TypeError("Cannot convert a number to a group element")

        
    def from_qs(self, qs, x):  
        w = self.word_type()
        w0 =  xsp2co1_qs_to_elem_i (qs, x)
        for i in range(26):
             w._data[i] =  w0[i]
        return w             

    def str_word(self, v1):
        #return "Xsp2_Co1 " + str_xsp2_co1(v1._data)
        atoms = v1.mmdata
        strings = iter_strings_from_atoms(atoms, abort_if_error=0)
        s = "*".join(strings) 
        return "Xsp2_Co1<%s>" % (s if len(s) else "1")
 
    def from_xsp(self, x):
        w = self.word_type()
        chk_qstate12(xsp2co1_elem_xspecial(w._data, x))
        return w

    def from_data(self, data):
        """Create a group element from an array of generators

        Internally, an element of group is represented
        as an array of unsigned 32-bit integers, where each entry
        of the array describes a generator. See section
        :ref:`header-mmgroup-generators-label` for details.
 
        This function creates an element of the group from
        such an array of integers.

        :param data: An array-like object representing a 
                     word of generators of the monster group

        :return: An element of this instance of the group 
        :rtype:  an instance of class 
                 mmgroup.structures.xsp2_co1.Xsp2_Co1_Word

        """
        return self.word_type('a', data)




_dict_pm3 = {0: '0', 1:'+', 0x1000000:'-', 0x1000001:'0'}
def str_leech3(x):
    x = int(x)
    lst = [_dict_pm3[(x >> i) & 0x1000001] for i in range(24)]
    return "(" + "".join(lst) + ")"

def str_xsp2_co1(data, factor = 1, t = False):
    qs0 = xsp2co1_elem_to_qs_i(data) if t else xsp2co1_elem_to_qs(data)
    qs = QStateMatrix(qs0) / factor
    return str_leech3(data[0]) + " (x) " + str(qs)


try:
    from mmgroup.clifford12 import xsp2co1_error_pool
    def get_error_pool(length):
        assert length > 0
        a = np.zeros(length, dtype = np.uint64)
        length = xsp2co1_error_pool(a, length)
        return list(map(int, a[:length])) 
except:
    def get_error_pool(length):        
        return []    





StdXsp2_Co1_Group = Xsp2_Co1_Group()
Xsp2_Co1.group = StdXsp2_Co1_Group



   