import numpy as np

class Configuration:
    """
    A :any:`Configuration` describes a specific arrangement of objects in the vector space of possible positions.
    Objects are represented by integers, with indistinguishable objects denoted by identical integers.
    This class subclasses `numpy.matrix <https://docs.scipy.org/doc/numpy/reference/generated/numpy.matrix.html>`_. 
    Each configuration in the vector space of positions is represented as a column vector.

    Attributes:
        count (int): If symmetry-inequivalent configurations have been generated for a `configuration space`,
                     this records the number of configurations equivalent to this one. 
                     Value at initialisation is  ``None``.
        lowest_numeric_representation (int): If the numeric representations for the set of equivalent 
                     configurations are calculated, this can be used to store the lowest valued numeric 
                     representation, for use as a simple hash.
    
    Example:

        >>> Configuration( [1, 1, 0] )
        Configuration([1, 1, 0])

    """ 

    def __init__( self, vector ):
        self.count = None
        self.lowest_numeric_representation = None
        self.vector = np.array( vector )

    def matches( self, test_configuration ):
        """
        Test whether this configuration is equal to another configuration.

        Args:
            test_configuration (:any:`Configuration`): The configuration to compare against.

        Returns:
            (bool): True | False.
        """
        if not isinstance( test_configuration, Configuration ):
            raise TypeError
        return ( self.vector == test_configuration.vector ).all()

    def is_equivalent_to( self, test_configuration, symmetry_operations ):
        """
        Test whether this configuration is equivalent to another configuration
        under one or more of a set of symmetry operations.

        Args:
            test_configuration (Configuration): The configuration to compare against.
            symmetry_operations (list(SymmetryOperation): A list of SymmetryOperation objects.

        Returns:
            (bool): True | False
        """
        for symmetry_operation in symmetry_operations:
            if ( symmetry_operation.operate_on( self ).matches( test_configuration ) ):
                return True 
        else:
            return False

    def is_in_list( self, the_list ):
        """
        Test whether this configuration is in a list of configurations.

        Args:
            list (list(bsym.Comfiguration)): A list of Configuration instances.

        Returns:
            (bool): True | False
        """
        return next( ( True for c in the_list if self.matches( c ) ), False )

    def has_equivalent_in_list( self, the_list, symmetry_operations ):
        """
        Test whether this configuration is equivalent by symmetry to one or more
        in a list of configurations.

        Args:
            list (list(bsym.Configuration)): A list of :any:`Configuration` instances.
            symmetry_operations (list(bsym.SymmetryOperation)): A list of :any:`SymmetryOperation` objects.

        Returns:
            (bool): True | False 
        """
        return next( ( True for c in the_list if self.is_equivalent_to( c, symmetry_operations ) ), False )

    def set_lowest_numeric_representation( self, symmetry_operations ):
       """
       Sets `self.lowest_numeric_representation` to the lowest value numeric representation of this configuration when permutated under a set of symmetry operations.

       Args:
           symmetry_operations (list): A list of :any:`SymmetryOperation` instances.

       Returns:
           None
       """
       self.lowest_numeric_representation = min( [ symmetry_operation.operate_on( self ).as_number for symmetry_operation in symmetry_operations ] )

    def numeric_equivalents( self, symmetry_operations ):
        """
        Returns a list of all symmetry equivalent configurations generated by a set of symmetry operations
        with each configuration given in numeric representation.

        Args:
            symmetry_operations (list): A list of :any:`SymmetryOperation` instances.

        Returns:
            (list(int)): A list of numbers representing each equivalent configuration.
        """
        return [ symmetry_operation.operate_on( self ).as_number for symmetry_operation in symmetry_operations ]
        #return [ as_number( symmetry_operation.operate_on( self, config=False ) ) for symmetry_operation in symmetry_operations ]

    @property
    def as_number( self ):
        """
        A numeric representation of this configuration.

        Examples:
            >>> c = Configuration( [ 1, 2, 0 ] )
            >>> c.as_number
            120

        """
        return as_number( self.vector )

    @classmethod
    def from_tuple( cls, this_tuple ):
        """
        Construct a :any:`Configuration` from a `tuple`,
        e.g.::
     
            Configuration.from_tuple( ( 1, 1, 0 ) )
 
        Args:
            this_tuple (tuple): The tuple used to construct this :any:`Configuration`.

        Returns:
            (:any:`Configuration`): The new :any:`Configuration`.
        """
        return( cls( this_tuple ) )

    def tolist( self ):
        """
        Returns the configuration data as a list.
        
        Args:
            None

        Returns:
            (List)
        """
        return list( self.vector )

    def pprint( self ):
        print( ' '.join( [ str(e) for e in self.tolist() ] ) )

    def position( self, label ):
        """
        Returns the vector indices where elements are equal to `label`.

        Args:
            label (int): The label used to select the vector positions.

        Returns:
            (list): A list of all positions that match `label`.
        """
        return [ i for i,x in enumerate( self.tolist() ) if x == label ]

    def __repr__( self ):
        to_return = "Configuration({})\n".format(self.vector)
        return to_return

    def map_objects( self, objects ):
        """
        Returns a dict of objects sorted according to this configuration.

        Args:
            objects [list]: A list of objects.

        Returns:
            sorted_objects [dict]: A dictionary of sorted objects.
        """
        if len( objects ) != len( self.vector ):
            raise ValueError
        sorted_objects = {}
        for key in set( self.vector ):
            sorted_objects[key] = [ o for k, o in zip( self.vector, objects ) if k == key ]
        return sorted_objects 

def as_number( a ):
    tot = 0
    for num in a:
        tot *= 10
        tot += int( num )
    return tot

