class BasicModule():
    '''Basic Module: literally just a tin can'''
    def __init__(self, logger=None):
        self.id = util.register(self)     
        print self.id
        if not hasattr(self,'size'): self.size = np.array([ 3 , 2 , 2 ])
        self.stowage = Stowage(10) #things floating around in the module
        self.exterior_stowage = Stowage(0) #things strapped to the outside of the module
        self.sprite = None 
        self.gravity = np.array([ 0 , 0 , 0 ])
        self.max_gravity = 0.01
        self.min_gravity = 0
        self.orientation = np.array([ math.pi/4 , 0 ])
        self.location = np.array([ 0 , 0 , 0 ])
        self.composition = {'Al' : 14500}      
        self.package_material = [] #if a list of filters, material put in this will not be removed
        self.station = None
        self.manifest=None
        self.logger = logging.getLogger(logger.name + '.' + self.name) if logger else util.generic_logger
        self.loggername = self.logger.name
        
        self.atmo = Atmosphere()
        self.atmo.volume= math.pi * 2*self.size[0] * pow (self.size[1], 2)   
        self.atmo.initialize('Air')  
        
        self.equipment=dict() #miscellaneous (or unremovable) non-rack equipment
        self.player_installable = True
        
        self.paths = nx.Graph() 
        
        self.nodes=dict()
        
        self.touched = False        
        
        self.refresh_image()
     
    def __getstate__(self):
        d = dict(self.__dict__)
        del d['logger']
        del d['sprite']
        return d    
        
    def __setstate__(self, d):
        self.__dict__.update(d)   
        self.logger = logging.getLogger(self.loggername) if self.loggername else util.generic_logger    
        self.sprite=None
        self.refresh_image() 
     
    def refresh_image(self):
        if not gv.config['GRAPHICS']: return                            
                
        if gv.config['GRAPHICS'] == 'pyglet':           
            if self.sprite: self.sprite.delete()
            self.sprite = util.make_solid_sprite(int(2*self.size[0]*gv.config['ZOOM']),int(2*self.size[1]*gv.config['ZOOM']),(128,128,128,255),None,None,self.location[0],self.location[1], self.orientation[0],batch = util.station_batch)
            self.sprite.owner = self
        elif gv.config['GRAPHICS'] == 'cocos2d':
            self.sprite = util.make_solid_sprite(int(2*self.size[0]*gv.config['ZOOM']),int(2*self.size[1]*gv.config['ZOOM']),(128,128,128,255),None,None,self.location[0],self.location[1], self.orientation[0])
            if self.station: self.station.sprite.add(self.sprite)
            
    def new_manifest(self):
        self.manifest = manifest.Manifest(self)    
     
    def search(self, filter_, **kwargs):
        hits=[]
        if "Equipment" in filter_.comparison_type or 'All' in filter_.comparison_type:
            hits.extend([[self.equipment[e][3], self.node( e ), filter_.compare(self.equipment[e][3]) ]  for e in self.equipment.keys() if self.equipment[e][3]])
            
            s = self.stowage.search( filter_ )
            if hasattr(s,'local_coords'):
                snode = self.node( self.nearest_hall_node(s.local_coords) )
            else:
                snode = self.filterNode( self.node('Inside') )
            hits.append( [ s, snode, s != None ] )
            #hits.append( self.exterior_stowage.find_resource( filter_ ) )

        if "Equipment Slot" in filter_.comparison_type or 'All' in filter_.comparison_type:
            hits.extend([[self.equipment[e][2], self.node( e ), filter_.compare(self.equipment[e][2]) ]  for e in self.equipment.keys()  if not self.equipment[e][3] and self.player_installable])
 
        if "Clutter" in filter_.comparison_type:
            s = self.stowage.search( filter_ )
            if hasattr(s,'local_coords'):
                snode = self.node( self.nearest_hall_node(s.local_coords) )
            else:
                snode = self.filterNode( self.node('Inside') )
            hits.append( [ s, snode, s != None ] )
            
        random.shuffle(hits)    
        hits.sort(key=lambda tup: tup[2], reverse=True)
        
        return hits[0] if hits and hits[0][2] else [None, None, False]        
                     
    def get_living_space(self): return self.stowage.free_space       
    living_space = property(get_living_space, None, None, "Living space" ) 
    #exterior_space = self.exterior_stowage.free_space
        
    def compute_short_id(self): return string.upper(self.id[0:6])       
    short_id = property(compute_short_id, None, None, "Short ID" )     
        
    def get_mass(self): return sum([self.composition.values()])        
    mass = property(get_mass, None, None, "Total mass" )     
        
    def node(self, to_append):
        return ''.join( [ self.id, '|', to_append ] )
        
    def getXYZ(self,offset):
        return absolute_xyz(self.location, offset, self.orientation, self.size)       
        
    def filterNode(self,node):
        [ module, name ] = separate_node(node)
        if module != self.id: return None
        if name == "Inside": return random.choice(self.nodes.keys())    
        return node
        
    def mix_neighbor(self,neighbor,amt=1.0):
        self.atmo.mix( neighbor.atmo, 1.0 )     
        
    def update(self, dt):
        for e in self.equipment:            
            if self.equipment[e][3]: 
                self.equipment[e][3].update( dt )
        self.stowage.update(dt)
        self.exterior_stowage.update(dt)
        #if 'Equipment' not in self.package_material:
        #    for c in self.stowage.contents:
        #        if isinstance(c,Equipment) and not c.installed and not c.task:
        #            c.install_task(self.station)
        if self.manifest: 
            satisfaction = self.manifest.check_satisfied()
            #print satisfaction    
            
        if random.random() < 0.05:
            #atmo mix with neighbors
            for n in self.get_neighbors():
                self.mix_neighbor(n)
        
    def get_random_dock(self, side_port_allowed=True, unused = True, used = False):
        docks=[]
        if unused: docks.extend([f for f in self.equipment.keys() if self.equipment[f][2] in DOCK_EQUIPMENT and self.equipment[f][3] and not self.equipment[f][3].docked and ( side_port_allowed or not ( '2' in f or '3' in f ) ) and self.equipment[f][3].player_usable ])
        if used: docks.extend([f for f in self.equipment.keys() if self.equipment[f][2] in DOCK_EQUIPMENT and self.equipment[f][3] and self.equipment[f][3].docked and ( side_port_allowed or not ( '2' in f or '3' in f ) ) and self.equipment[f][3].player_usable ])
        if not docks: return None
        return random.choice(docks)    
            
    def get_neighbors(self, equipment=False):
        out=[]
        for e in [f for f in self.equipment.keys() if self.equipment[f][2] in DOCK_EQUIPMENT and self.equipment[f][3] and self.equipment[f][3].docked ]:
            if not equipment:
                out.append(self.equipment[e][3].partner.installed)
            else:
                out.append(self.equipment[e][3])
        return out
        
    def get_neighbor(self,station=None):
        for n in self.get_neighbors():
            if station and n.station == station: return n
        return None
        
    def percolate(self):
        if self.touched: return []
        out = [self]
        self.touched=True
        for n in self.get_neighbors():
            out.extend(n.percolate())
        return out    
            
    def percolate_location(self):
        if self.touched: return
        self.touched = True
        for e in self.get_neighbors(equipment=True):
             #update e's partner's module's location
             n = e.partner.installed
             if not n.touched:                
                n.adjust_location(e.partner.get_name(),self,e.get_name())        
                n.refresh_image()     
             #call n
             n.percolate_location()           
            
    def get_empty_slot(self,slot_type='LSS'):
        if not self.player_installable: return None
        slots = [s for s in self.equipment.keys() if ( self.equipment[s][2] == slot_type or slot_type=='ANY' ) and not self.equipment[s][3]]
        if not slots: return None
        return random.choice(slots)
            
    def get_node(self,equip):
        for f in self.equipment.keys():
            if self.equipment[f][3] == equip:
                return self.node(f)
        return None        
            
    def uninstall_equipment(self,equip):
        for f in self.equipment.keys():
            if self.equipment[f][3] == equip:
                self.equipment[f][3] = None
                self.stowage.add( equip )
                return True
        return False                            
        
    def adjust_location(self,my_node,neighbor,their_node):
        self.orientation = ( neighbor.orientation + neighbor.equipment[their_node][1] - self.equipment[my_node][1] ) + np.array([math.pi, 0])
        self.orientation %= 2*math.pi
        
        #calculate location
        self.location=np.array([ 0,0,0 ])
        loc_offset = self.getXYZ(self.equipment[my_node][0])
        self.location = neighbor.getXYZ(neighbor.equipment[their_node][0]) - loc_offset        
        
            
    def dock(self, my_node, neighbor, their_node):
        if not neighbor or not my_node or not their_node: return False, "Docking cancelled: pointers missing" 
        if not my_node in self.equipment or not their_node in neighbor.equipment: return False, "Docking cancelled: wrong module, I guess?"
        if not self.equipment[my_node][2] in DOCK_EQUIPMENT or not neighbor.equipment[their_node][2] in DOCK_EQUIPMENT: return False, "Docking cancelled: requested interface(s) are not docking equipment!"
        if self.equipment[my_node][2] != neighbor.equipment[their_node][2]: return False, "Docking cancelled: incompatible docking mechanisms"
        if self.equipment[my_node][3].docked or neighbor.equipment[their_node][3].docked: return False, "Docking cancelled: at least one module already docked elsewhere"
        
        self.adjust_location(my_node,neighbor,their_node)        
        
        #collision detection   
                        
        if neighbor.station:    
            if not self.station:     
                self.station = neighbor.station 
                self.station.paths.add_nodes_from(self.paths.nodes())
                self.station.paths.add_edges_from(self.paths.edges(data=True))
                self.refresh_station()                
        else:             
            neighbor.station = self.station                                    
            if self.station:
                self.station.paths.add_nodes_from(neighbor.paths.nodes())
                self.station.paths.add_edges_from(neighbor.paths.edges(data=True))
                neighbor.refresh_station()         
                     
        neighbor.refresh_image()    
        self.refresh_image()
            
        return True        
        
    def connect(self, my_node, neighbor, their_node, instant=False):    
        #dock, finally
        self.equipment[my_node][3].dock( neighbor, neighbor.equipment[their_node][3], instant)
        neighbor.equipment[their_node][3].dock( self, self.equipment[my_node][3], instant )
        
    
    def disconnect(self, my_node, their_node, instant=False):    
        #dock, finally
        a = self.equipment[my_node][3].undock( instant )
        b = their_node.undock( instant )
        return a and b
            
        
    def add_edge(self,one,two):
        delta = self.nodes[two] - self.nodes[one] #numpy vector subtraction, I hope
        delta *= self.size
        mag = abs( np.sqrt( np.vdot( delta , delta ) ) )
        self.paths.add_edge(one,two,weight=mag)
        
    def add_edge_list(self,edges):
        for e in range(1,len(edges)):            
            self.add_edge( self.node( edges[ e - 1 ] ), self.node( edges[ e ] ) )
        
    def nearest_hall_node(self,coords):
        all_nodes = [separate_node(n)[1] for n in self.nodes.keys()]
        hall_nodes = [n for n in all_nodes if not n in self.equipment.keys()]            
        hall_nodes.sort(key=lambda t: util.vec_dist( self.nodes[self.node( t )] , coords ), reverse=False)
        return hall_nodes[0]
        
    def add_equipment(self, eq_node, eq_obj, eq_coords, hall_node=None, eq_orientation=np.array([ 0 , 0]), eq_type='MISC' ):
        if not hall_node: hall_node = self.nearest_hall_node(eq_coords)
        node_coords = self.nodes[ self.node( hall_node ) ] + ( eq_coords - self.nodes[ self.node( hall_node ) ] ) 
        self.nodes[self.node(eq_node)] = node_coords
        self.add_edge( self.node(hall_node), self.node(eq_node) )
        self.equipment[ eq_node ] = [ eq_coords, eq_orientation, eq_type, eq_obj]

    def refresh_station(self, station=None):
        if station and station != self.station: self.station = station
        if gv.config['GRAPHICS'] == 'cocos2d': self.station.sprite.add(self.sprite)
        if self.manifest: self.manifest.refresh_station(self.station)
        for e in self.equipment:            
            if self.equipment[e][3]: 
                self.equipment[e][3].refresh_station()
                
    def draw(self,window):
        zoom=gv.config['ZOOM']
        #self.img.blit(zoom*self.location[0]+window.width // 2, zoom*self.location[1]+window.height // 2, 0)
        if hasattr(self.sprite, 'update_sprite'):
            l=self.location
            self.sprite.update_sprite(zoom*l[0], zoom*l[1],-180*(self.orientation[0])/math.pi)
        if self.sprite: self.sprite.draw()
        
        for e in self.equipment.keys():
            if self.equipment[e][3] and self.equipment[e][3].visible:
                l=self.getXYZ(self.equipment[e][0]) 
                #rotimg=self.equipment[e][3].img.get_transform
                self.equipment[e][3].sprite.update_sprite(zoom*l[0], zoom*l[1],-180*(self.equipment[e][1][0]+self.orientation[0])/math.pi)
                #self.equipment[e][3].sprite.set_position(zoom*l[0], zoom*l[1])
                #self.equipment[e][3].sprite.rotation = -180*(self.equipment[e][1][0]+self.orientation[0])/math.pi
                self.equipment[e][3].sprite.draw()#img.blit(zoom*l[0]+window.width // 2, zoom*l[1]+window.height // 2, 0)

        for c in self.stowage.contents:
            if hasattr(c,'sprite') and hasattr(c,'local_coords') and c.sprite:
                loc_xyz = self.getXYZ( 1.0*c.local_coords )
                c.sprite.set_position(zoom*loc_xyz[0],zoom*loc_xyz[1])
                #c.sprite.rotation = (-180/math.pi)*self.orientation[0]
                c.sprite.visible=True
                c.sprite.draw()         
class BasicModule():
    '''Basic Module: literally just a tin can'''
    def __init__(self):
        self.id = str(uuid.uuid4())        
        if not hasattr(self,'size'): self.size = np.array([ 3 , 2 , 2 ])
        self.stowage = Stowage(10) #things floating around in the module
        self.exterior_stowage = Stowage(0) #things strapped to the outside of the module
        self.sprite = None 
        self.gravity = np.array([ 0 , 0 , 0 ])
        self.max_gravity = 0.01
        self.min_gravity = 0
        self.orientation = np.array([ math.pi/4 , 0 ])
        self.location = np.array([ 0 , 0 , 0 ])
        self.composition = {'Al' : 14500}      
        self.package_material = [] #if a list of filters, material put in this will not be removed
        self.station = None
        
        self.atmo = Atmosphere()
        self.atmo.volume= math.pi * 2*self.size[0] * pow (self.size[1], 2)   
        self.atmo.initialize('Air')  
        
        self.equipment=dict() #miscellaneous (or unremovable) non-rack equipment
        self.paths = nx.Graph() 
        
        self.nodes=dict()
        
        
        self.refresh_image()
     
    def refresh_image(self):
        if not util.GRAPHICS: return                            
                
        if True:#not hasattr(self,'imgfile'):
            if util.GRAPHICS:
                self.img = util.make_solid_image(int(2*self.size[0]*util.ZOOM),int(2*self.size[1]*util.ZOOM),(128,128,128,255))
            else: 
                self.imgfile = "images/module_placeholder.jpg"            

        #if hasattr(self,'imgfile'):
        #    self.img = util.load_image(self.imgfile)
        
        '''if math.sin(self.orientation[0]) < 0:
            self.img = self.img.get_transform(flip_y=True)
            #TODO replace with different image altogether
        if math.cos(self.orientation[0]) < 0:
            self.img = self.img.get_transform(flip_x=True)
           ''' 
        self.sprite = util.image_to_sprite(self.img,self.location[0],self.location[1], self.orientation[0])
            
        
     
    def search(self, filter_):
        hits=[]
        if "Equipment" in filter_.comparison_type or 'All' in filter_.comparison_type:
            hits.extend([[self.equipment[e][3], self.node( e ), filter_.compare(self.equipment[e][3]) ]  for e in self.equipment.keys() if self.equipment[e][3]])
            
            hits.append( [ self.stowage.search( filter_ ), self.filterNode( self.node('Inside') ), self.stowage.search( filter_ ) != None ] )
            #hits.append( self.exterior_stowage.find_resource( filter_ ) )

        if "Equipment Slot" in filter_.comparison_type or 'All' in filter_.comparison_type:
            hits.extend([[self.equipment[e][2], self.node( e ), filter_.compare(self.equipment[e][2]) ]  for e in self.equipment.keys()  if not self.equipment[e][3]])
 
        if "Clutter" in filter_.comparison_type:
            hits.append( [ self.stowage.search( filter_ ), self.filterNode( self.node('Inside') ), self.stowage.search( filter_ ) != None ] )
            
        random.shuffle(hits)    
        hits.sort(key=lambda tup: tup[2], reverse=True)
        #print hits        
        return hits[0] if hits and hits[0][2] else [None, None, False]        
                     
    def get_living_space(self): return self.stowage.free_space       
    living_space = property(get_living_space, None, None, "Living space" ) 
    #exterior_space = self.exterior_stowage.free_space
        
    def compute_short_id(self): return string.upper(self.id[0:6])       
    short_id = property(compute_short_id, None, None, "Short ID" )     
        
    def get_mass(self): return sum([self.composition.values()])        
    mass = property(get_mass, None, None, "Total mass" )     
        
    def node(self, to_append):
        return ''.join( [ self.id, '|', to_append ] )
        
    def getXYZ(self,offset):
        return absolute_xyz(self.location, offset, self.orientation, self.size)       
        
    def filterNode(self,node):
        [ module, name ] = separate_node(node)
        if module != self.id: return None
        if name == "Inside": return random.choice(self.nodes.keys())    
        return node
        
    def update(self, dt):
        for e in self.equipment:            
            if self.equipment[e][3]: 
                self.equipment[e][3].update( dt )
        self.stowage.update(dt)
        self.exterior_stowage.update(dt)
        if 'Equipment' not in self.package_material:
            for c in self.stowage.contents:
                if isinstance(c,Equipment) and not c.installed and not c.task:
                    c.install_task(self.station)
                
        
    def get_random_dock(self, side_port_allowed=True):
        docks=[f for f in self.equipment.keys() if self.equipment[f][2] in DOCK_EQUIPMENT and self.equipment[f][3] and not self.equipment[f][3].docked and ( side_port_allowed or not ( '2' in f or '3' in f ) ) ]
        if not docks: return None
        return random.choice(docks)    
            
    def get_empty_slot(self,slot_type='LSS'):
        slots = [s for s in self.equipment.keys() if ( self.equipment[s][2] == slot_type or slot_type=='ANY' ) and not self.equipment[s][3]]
        if not slots: return None
        return random.choice(slots)
            
    def uninstall_equipment(self,equip):
        for f in self.equipment.keys():
            if self.equipment[f][3] == equip:
                self.equipment[f][3] = None
                self.stowage.add( equip )
                return True
        return False                            
        
    
            
    def berth(self, my_node, neighbor, their_node, instant=False):
        if not neighbor or not my_node or not their_node: return False, "Docking cancelled: pointers missing" 
        if not my_node in self.equipment or not their_node in neighbor.equipment: return False, "Docking cancelled: wrong module, I guess?"
        if not self.equipment[my_node][2] in DOCK_EQUIPMENT or not neighbor.equipment[their_node][2] in DOCK_EQUIPMENT: return False, "Docking cancelled: requested interface(s) are not docking equipment!"
        if self.equipment[my_node][2] != neighbor.equipment[their_node][2]: return False, "Docking cancelled: incompatible docking mechanisms"
        if self.equipment[my_node][3].docked or neighbor.equipment[their_node][3].docked: return False, "Docking cancelled: at least one module already docked elsewhere"
        
        #calculate orientation
        self.orientation = ( neighbor.orientation + neighbor.equipment[their_node][1] - self.equipment[my_node][1] ) + np.array([math.pi, 0])
        
        #calculate location
        self.location=np.array([ 0,0,0 ])
        loc_offset = self.getXYZ(self.equipment[my_node][0])
        self.location = neighbor.getXYZ(neighbor.equipment[their_node][0]) - loc_offset
        self.orientation %= 2*math.pi
        
        #collision detection

        #dock, finally
        self.equipment[my_node][3].dock( neighbor, neighbor.equipment[their_node][3], instant)
        neighbor.equipment[their_node][3].dock( self, self.equipment[my_node][3], instant )
        
        # map graphs together
        
        if neighbor.station:    
            if self.station:
                print self.station, neighbor.station
                assert False, "Station merging not in yet" #TODO replace with station merge
            else:         
                self.station = neighbor.station 
                self.station.paths.add_nodes_from(self.paths.nodes())
                self.station.paths.add_edges_from(self.paths.edges(data=True))
                self.station.paths.add_edge(self.node(my_node),neighbor.node(their_node),weight=1)
                self.refresh_equipment()
                
        else:             
            neighbor.station = self.station
                                    
            if self.station:
                self.station.paths.add_nodes_from(neighbor.paths.nodes())
                self.station.paths.add_edges_from(neighbor.paths.edges(data=True))
                self.station.paths.add_edge(self.node(my_node),neighbor.node(their_node),weight=1)
                neighbor.refresh_equipment()
            
        neighbor.refresh_image()    
        self.refresh_image()
            
        return True        
        
    def add_edge(self,one,two):
        delta = self.nodes[two] - self.nodes[one] #numpy vector subtraction, I hope
        delta *= self.size
        mag = abs( np.sqrt( np.vdot( delta , delta ) ) )
        self.paths.add_edge(one,two,weight=mag)
        
    def add_edge_list(self,edges):
        for e in range(1,len(edges)):            
            self.add_edge( self.node( edges[ e - 1 ] ), self.node( edges[ e ] ) )
        
    def add_equipment(self, eq_node, eq_obj, eq_coords, hall_node=None, eq_orientation=np.array([ 0 , 0]), eq_type='MISC' ):
        if not hall_node:
            all_nodes = [separate_node(n)[1] for n in self.nodes.keys()]
            hall_nodes = [n for n in all_nodes if not n in self.equipment.keys()]            
            hall_nodes.sort(key=lambda t: util.vec_dist( self.nodes[self.node( t )] , eq_coords ), reverse=False)
            #print [util.vec_dist( self.nodes[self.node( t )] , eq_coords ) for t in hall_nodes]
            hall_node = hall_nodes[0]
        node_coords = self.nodes[ self.node( hall_node ) ] + ( eq_coords - self.nodes[ self.node( hall_node ) ] ) 
        self.nodes[self.node(eq_node)] = node_coords
        self.add_edge( self.node(hall_node), self.node(eq_node) )
        self.equipment[ eq_node ] = [ eq_coords, eq_orientation, eq_type, eq_obj]

    def refresh_equipment(self):
        for e in self.equipment:            
            if self.equipment[e][3]: 
                self.equipment[e][3].refresh_station()
                
    def draw(self,window):
        zoom=util.ZOOM
        #self.img.blit(zoom*self.location[0]+window.width // 2, zoom*self.location[1]+window.height // 2, 0)
        self.sprite.draw()
        for c in self.stowage.contents:
            if hasattr(c,'sprite') and hasattr(c,'local_coords') and c.sprite:
                loc_xyz = self.getXYZ( 0.8*c.local_coords )
                c.sprite.set_position(zoom*loc_xyz[0],zoom*loc_xyz[1])
                c.sprite.rotation = (-180/math.pi)*self.orientation[0]
                c.sprite.draw()
        for e in self.equipment.keys():
            if self.equipment[e][3] and self.equipment[e][3].visible:
                l=self.getXYZ(self.equipment[e][0]) 
                #rotimg=self.equipment[e][3].img.get_transform
                self.equipment[e][3].sprite.update_sprite(zoom*l[0], zoom*l[1],-180*(self.equipment[e][1][0]+self.orientation[0])/math.pi)