def __init__(self, account, name, village_id, coord):
     from account import Account
     
     assert isinstance(account, Account)
     self.account = account
     self.name = name
     self.village_id = village_id
     self.coord = coord
     
     self.resources = None
     self.storage_capacity = None
     self.production = None
     self.resource_fields = None
     self.buildings = None
     self.resource_fields = None
     
     self.events = EventSet()
     self.next_refresh_time = datetime(1900,1,1)
     
     self.event_handlers = []
     self.triggers = []
     self.conditions = []
     
     self.suppress_refresh = False
class Village():

    '''
    Stores the state of a village. Collects events for the village and
    fires them on listeners.
    '''
    
    # maps important urls to more friendly names
    url_mapping = {
            'resources': '/dorf1.php',
            'village': '/dorf2.php'
        }
    
    event_refresh_pages = {
        'build': ('resources', 'village'),
        'quest_reward': ('resources',),
        'resources_spent': ('resources',),
    }
    
    def __init__(self, account, name, village_id, coord):
        from account import Account
        
        assert isinstance(account, Account)
        self.account = account
        self.name = name
        self.village_id = village_id
        self.coord = coord
        
        self.resources = None
        self.storage_capacity = None
        self.production = None
        self.resource_fields = None
        self.buildings = None
        self.resource_fields = None
        
        self.events = EventSet()
        self.next_refresh_time = datetime(1900,1,1)
        
        self.event_handlers = []
        self.triggers = []
        self.conditions = []
        
        self.suppress_refresh = False

    def has_enough_resources(self, resr):
        return +(self.resources - resr) == (self.resources - resr)

    def has_enough_space(self, resr):
        return +(self.resources + resr - self.storage_capacity) == Resources((0,0,0,0))

    def get_production_time(self, resr):
        needed = +(self.resources - resr) - (self.resources - resr)
        try:
            return timedelta(hours=max(needed / (self.production + Resources((0.00001,)*4))))
        except:
            logger.log_error("production calculation failed", "resr: %s \n self.resources: %s \n self.production: %s" % (resr, self.resources, self.production))
            raise
            return timedelta(minutes=1)
        
    def get_build_events_for_slot(self, slot_id):
        if slot_id == 0:
            return self.events.build
        
        if slot_id == 1:
            predicate = lambda event: event.building <= 4
        else:
            predicate = lambda event: event.building > 4
            
        return { event for event in self.events.build if predicate(event) }
    
    def read_events(self, pages:dict):
        '''
        Reads all available events from the given pages
        possible keys:
        - trader_movement (marketplace)
        - troop_movement (rally_place)
        - infantry (barracks)
        - cavalry (stable)
        - siege (workshop)
        - traps (trapper)
        - akademy_research (academy)
        - upgrade_research (smithy)
        '''
        if 'resources' in pages:
            doc = pages['resources']
            server_time = reader.read_server_time(doc)
            build_queue = reader.read_build_queue(doc)
            self.events.build = { Event(village = self, 
                                        name    = 'build', 
                                        time    = build_item['timer'] + server_time, 
                                        building= build_item['building'],
                                        level   = build_item['level']) for build_item in build_queue }
        self.events.update_next()
        
    def read_content(self, pages:dict):
        '''
        Reads the content of the given pages and writes them into the village.
        possible keys:
        - resources   (dorf1.php)
            resource_fields
            resources
            production
        - village (dorf2.php)
        - troops (rally_place)
        '''
        
        # dorf1.php:
        if 'resources' in pages: 
            doc = pages['resources']
            self.resource_fields = reader.read_resource_fields(doc)
            self.resources = Resources(reader.read_resources(doc))
            self.production = Resources(reader.read_production(doc))
            if not self.resources or not self.production:
                logger.log_error("page error", [ "%s: %s" % (t, p.find("html").html().strip()) for t,p in pages.items() ], 'Could not fetch village data')
                
            try:
                self.storage_capacity = Resources(reader.read_storage_capacity(doc))
                self.name = reader.read_villages(doc, True)[0]['name']
            except Exception as e:
                traceback.print_exc()
                self.storage_capacity = []
                self.name = ""
        
        # dorf2.php
        if 'village' in pages:
            doc = pages['village']
            self.buildings = reader.read_buildings(doc)
    
    def save_status(self):
        status = db.status.find_one({'village': self.village_id}) or {}
        status['village'] = self.village_id
        status['resource_fields'] = self.resource_fields
        status['buildings'] = self.buildings
        status['resources'] = self.resources
        status['production'] = self.production
        status['storage_capacity'] = self.storage_capacity
        status['name'] = self.name
        status['build_events'] = [ dict(village=event.village.name, name=event.name, time=event.time, **event.data) for event in self.events.build ]
        db.status.save(status)
    
    def refresh(self, *page_names):
        '''
        Refreshes all pages related with this village, or only 
        with the given pages.
        '''
        if self.suppress_refresh:
            return
        logger.log_note('refresh', 'refreshing %s' % str(page_names))
        pages = {}
        page_names = page_names or self.url_mapping.keys()
        for page_name in page_names:
            doc = self.account.request_GET(self.url_mapping[page_name])
            if 'type="password"' in doc.find("html").html():
                logger.log_info("logout", "Logout detected")
                self.account.clear_cookie()
                self.account.login()
                self.account.save_cookie()
                self.next_refresh_time = datetime.now() + timedelta(seconds=30)
                
                return
                
            pages[page_name] = doc
            
        self.read_content(pages)
        self.read_events(pages)
        
        if not self.resources:
            self.account.login()
            self.refresh(*page_names)
            
        print(self.events.build)
        
        if not self.resources or not self.production or not self.storage_capacity:
            logger.log_error("page error", [ "%s: %s" % (t, p.find("html").html().strip()) for t,p in pages.items() ], 'Could not fetch village data')
            
        self.save_status()
        
        self.new_refresh_time()
        
        return pages
        
    def new_refresh_time(self):
        self.next_refresh_time = datetime.now() + timedelta(minutes=random.random()*3+2)
            
    def update(self):
        '''
        Updates the village by one "tick"
        Fires events and eventually refreshes the events/content
        '''
        
        now = datetime.now()
        if self.events.next and self.events.next.time <= now:
            next_event = self.events.next
            self.events.remove(next_event)
            self.events.update_next()
            self.fire_event(next_event) # asynchronously 
            
        for trigger in self.triggers:
            trigger.update()
            
        for cond in self.conditions:
            cond.update()
            
        if self.next_refresh_time <= now:
            self.refresh()
            
    def fire_event(self, event):
        logger.log_note("event fired", "event %s got fired! %s" % (event.name, event))
        
        pages = self.event_refresh_pages.get(event.name, None)
        if pages is not None:
            try:
                self.refresh(*pages)
            except Exception as e:
                print("could not refresh due to connection issues.")
                print(traceback.format_exc())
        for event_handler in self.event_handlers:
            event_handler.__on_event__(event)
            
        for trigger in self.triggers:
            trigger.__on_event__(event)
        
        for condition in self.conditions:
            condition.__on_event__(event)
            
    def build_building(self, bld_gid, bld_lvl):
        from_lvl = bld_lvl - 1

        bld_bid = None
        bld_new = None
        for bid, gid, lvl in self.buildings + self.resource_fields:
            if gid == bld_gid and lvl == from_lvl:
                bld_bid = bid
                bld_new = False
                break

        if bld_bid is None:
            for bid, gid, lvl in self.buildings + self.resource_fields:
                if gid == 0:
                    bld_bid = bid
                    bld_new = True
                    break
                
        if bld_new is None:
            logger.log_error("build not found", "Nothing to build found! ")
            return False

        if bld_new == True:
            logger.log_info("build", "Building %s new lvl %s at %s" % (db.buildings[bld_gid]['gname'], bld_lvl, bld_bid) )
            action.action_build_new(self.account, bld_bid, bld_gid)
        else:
            logger.log_info("build", "Building %s up from lvl %s to %s at %s" % (db.buildings[bld_gid]['gname'], from_lvl, bld_lvl, bld_bid) )
            action.action_build_up(self.account, bld_bid)
            
        self.fire_event(Event(self, 'resources_spent', datetime.now()))
        
    def get_bid_by_gid(self, search_gid):
        for bid, gid, lvl in self.buildings:
            if search_gid == gid:
                return bid
        return None
        
    def get_bid_by_name(self, name):
        return self.get_bid_by_gid(db.building_names[name])
        
    def get_state(self):
        return VillageState(self.account.get_state(), self.resources, self.resource_fields+self.buildings, None)
        
    def __repr__(self):
        return str(self)
            
    def __str__(self):
        return ("Village: \n" +
                "  Resources:  " + str(self.resources) + "\n" +
                "  Production: " + str(self.production) + "\n" +
                "  Buildings:  " + str(self.buildings) + "\n" +
                "  ResourceFields: " + str(self.resource_fields) + "\n" +
                "  Events:     " + str(self.events) + "\n")