import random, math
from functools import reduce
from collections import deque

class ElementaryAbelianSubgroup:
    """
    A class to represent an elementary abelian subgroup of the symmetric group Sn.
    The subgroup is generated by a set of permutations.
    """

    def __init__(self, n, generators):
        """
        Initializes an ElementaryAbelianSubgroup instance.
        
        Parameters:
            n (int): The number of elements in the symmetric group Sn.
            generators (dict): A dictionary of generators for the subgroup.
        """
        self.generators = generators  # Store all generators
        self.n = n  # Number of elements in the symmetric group Sn
        self.identity = list(range(self.n))  # Identity permutation
        self.m = len(self.generators)  # Number of generators in the subgroup

    def is_valid_permutation(self, perm):
        """
        Checks if a given permutation is valid.
        
        Parameters:
            perm (list): A permutation.
        
        Returns:
            bool: True if the permutation is valid, False otherwise.
        """
        return sorted(perm) == list(range(self.n)) 

    def transpose(self, perm, i, j):
        """
        Transposes two elements in the permutation.
        
        Parameters:
            perm (list): The permutation.
            i (int): Index of the first element.
            j (int): Index of the second element.
        
        Returns:
            list: The permutation after transposition.
        """
        if not (0 <= i < len(perm) and 0 <= j < len(perm)):
            raise IndexError("Indices out of range")
        perm[i], perm[j] = perm[j], perm[i]
        return perm

    def multiply_permutations(self, perm1, perm2):
        """
        Multiplies two permutations.
        
        Parameters:
            perm1 (list): The first permutation.
            perm2 (list): The second permutation.
        
        Returns:
            list: The resulting permutation after multiplication.
        """
        return [perm1[perm2[i]] for i in range(self.n)]

    def multiply_generators(self):
        """
        Multiplies all generators to produce a single permutation.
        
        Returns:
            list: The resulting permutation after multiplying all generators.
        """
        def apply_permutation(perm, array):
            return [array[i] for i in perm]
        # Apply permutations in sequence
        result = reduce(apply_permutation, reversed(self.generators), self.identity)
        
        return result

    def inverse(self, perm):
        """
        Computes the inverse of a permutation.
        
        Parameters:
            perm (list): The permutation to be inverted.
        
        Returns:
            ElementaryAbelianSubgroup: A new instance with the inverse permutation.
        """
        inverse_perm = [0] * self.n
        for i in range(self.n):
            inverse_perm[perm[i]] = i
        return ElementaryAbelianSubgroup([inverse_perm])

    def random_binary_array(self):
        """
        Generates a random binary array.
        
        Returns:
            list: A list of random 0s and 1s.
        """
        array = [random.randint(0, 1) for _ in range(self.m)]
        return array

    def generate_element_from_bitarray(self, bit_array):
        """
        Generates an element of the subgroup from a binary array.
        
        Parameters:
            bit_array (list): A binary array indicating which generators to use.
        
        Returns:
            tuple: A tuple containing the generated permutation and the bit array.
        """
        combined_permutation = self.identity[:]
        temp = math.ceil(len(bit_array)/2)
        for i in range(temp):
            if bit_array[2*i] == 1:
                combined_permutation = self.multiply_permutations(combined_permutation, self.generators[(i,"t")])
            if bit_array[2*i+1] == 1:
                combined_permutation = self.multiply_permutations(combined_permutation, self.generators[(i,"f")])
        return combined_permutation, bit_array

    def random_element(self):
        """
        Generates a random element of the subgroup.
        
        Returns:
            tuple: A tuple containing the random permutation and the bit array used to generate it.
        """
        t_r = self.random_binary_array()
        combined_permutation, _ = self.generate_element_from_bitarray(t_r)
        return combined_permutation, t_r

    def solve(self, g):
        """
        Solves the subgroup distance problem by finding the element closest to a given permutation.
        
        Parameters:
            g (list): The target permutation.
        
        Returns:
            tuple: The closest element in the subgroup and the Hamming distance.
        """
        all_elements = self.generate_all_elements()
        mindist = self.n + 1
        chosenelem = None
        for elem in all_elements:
            dist = self.hamming_distance(g, elem)
            if dist < mindist:
                chosenelem = elem
                mindist = dist
        return chosenelem, mindist

    def hamming_distance(self, p1, p2):
        """
        Calculates the Hamming distance between two permutations.
        
        Parameters:
            p1 (list): The first permutation.
            p2 (list): The second permutation.
        
        Returns:
            int: The Hamming distance.
        """
        assert len(p1) == len(p2), "Permutations must be of the same length"
        return sum(1 for i in range(len(p1)) if p1[i] != p2[i])

    def generate_all_elements(self):
        """
        Generates all elements of the subgroup.
        
        Returns:
            list: A list of all permutations in the subgroup.
        """
        visited = set()
        queue = deque([self.identity])
        all_elements = []
        
        while queue:
            current = queue.popleft()
            perm_tuple = tuple(current)
            if perm_tuple not in visited:
                visited.add(perm_tuple)
                all_elements.append(current)
                for gen in self.generators.values():
                    next_perm = self.multiply_permutations(current, gen)
                    queue.append(next_perm)
        
        return all_elements

    def print_all_elements(self):
        """
        Prints all elements of the subgroup.
        """
        print("All elements in the permutation group:")
        all_elements = self.generate_all_elements()
        
        for elem in all_elements:
            print("elem: ", elem[-12:])

    def inverse_permutation(self, perm):
        """
        Returns the inverse of a permutation. 
        Since we use only transpositions, the elements are their own inverses.
        
        Parameters:
            perm (list): The permutation.
        
        Returns:
            list: The inverse permutation.
        """
        return perm

    def apply_generators(self, bitmask):
        """
        Applies the generators to the identity permutation based on a bitmask.
        
        Parameters:
            bitmask (int): A bitmask indicating which generators to apply.
        
        Returns:
            list: The resulting permutation.
        """
        current_perm = self.identity[:]
        for i in range(len(self.generators)):
            if bitmask & (1 << i):
                current_perm = self.multiply_permutations(current_perm, self.generators[i])
        return current_perm

    def __repr__(self):
        """
        Returns a string representation of the subgroup.
        
        Returns:
            str: A string representing the subgroup.
        """
        return f"PermutationGroup(n={self.n}, permutation={self.generators})"

    def __eq__(self, other):
        """
        Checks if two subgroups are equal.
        
        Parameters:
            other (ElementaryAbelianSubgroup): Another subgroup to compare with.
        
        Returns:
            bool: True if the subgroups are equal, False otherwise.
        """
        return self.permutation == other.permutation


class ElementaryAbelianSubgroupWithSolution(ElementaryAbelianSubgroup):
    """
    A subclass of ElementaryAbelianSubgroup that includes a solution to the subgroup distance problem.
    """

    def __init__(self, n, generators, solution_t_h):
        """
        Initializes an ElementaryAbelianSubgroupWithSolution instance.
        
        Parameters:
            n (int): The number of elements in the symmetric group Sn.
            generators (dict): A dictionary of generators for the subgroup.
            solution_t_h (list): A bit array that generates the solution.
        """
        super().__init__(n, generators)
        self.solution_t_h = solution_t_h  # Store generators that produce the solution
        self.h, bit_array = self.multiply_solution_generators()  # The solution to the subgroup distance problem

    def multiply_solution_generators(self):
        """
        Multiplies the generators specified by the solution bit array.
        
        Returns:
            tuple: A tuple containing the resulting permutation and the bit array.
        """
        combined_permutation, bit_array = self.generate_element_from_bitarray(self.solution_t_h)
        return combined_permutation, bit_array
