class Backend:
    def __init__(self):
        self.rules = Rules()
        self.curr_state = State(idle=True)
        self.pending_subactions = {}
        self.last_player_move = ('','')
        self.frac = 0
        self.next_act_lambda = None
        self.message = None

    def load(self,filename):
        # TODO: need copy?
        self.curr_room = room_data.load_room(filename).copy()
        self.rules.update(self.curr_room.get_rules())
        self.player = self.curr_room.player
    
    def do(self,verb,*arg_objs):
        # print (verb,)+tuple(obj.name for obj in arg_objs)
        if verb=='move':
            assert len(arg_objs)==2
            obj = arg_objs[0]
            target = arg_objs[1]
            if target.can_support:
                self.move_obj(target.x,target.y,obj)
            else:
                self.display_msg("I can't put it there")
        if verb=='push':
            assert len(arg_objs)==1
            offset = arg_objs[0].map_pos() - self.player.map_pos()
            dir = dir_from_offset( offset )
            if isinstance(dir,Error):
                self.display_msg("I need to be next to it to push it")
            else:
                dir = dir.unwrap()
                assert arg_objs[0].pushable
                objs = self._pushable_objects(dir)
                if objs:
                    for obj in reversed(objs):
                        new_pos = obj.map_pos()+offset
                        self.move_obj(new_pos.x,new_pos.y,obj)
                    self.curr_state = State(
                        contexts = {self.player.map_rect():dir},
                        offsets = {obj.map_rect():dir for obj in objs+[self.player,]},
                        cleanup_funcs = [lambda:self._finish_move(dir)],
                        chainable=True)
                    self.next_act_lambda = lambda: self._begin_facing(dir) # What does this do?
                else:
                    self.display_msg("I can't push it")
        elif verb=='examine':
            assert len(arg_objs)==1
            msg = arg_objs[0].get_examine_text()
            self.display_msg( msg if msg else "I don't see anything special")
        else:
            rule = self.rules.get_rule(verb,*arg_objs)
            if not rule:
                self.do_default_rule_failure()
            else:
                # print(self.curr_room.map)
                self._do_rule(rule,arg_objs)
                # print(self.curr_room.map)
    
    def _pushable_objects(self,dir):
        pos = self.player.map_pos()
        objs = []
        while True:
            pos += offset_from_dir(dir)
            obj = self.get_obj_at(pos)
            if obj.walkable:
                return objs
            elif obj.pushable:
                objs += [obj]
            else:
                return []

    def do_undo(self,undo_event):
        print "UNDO"
    
    def do_redo(self,redo_event):
        print "REDO"

    def _do_rule(self,rule,arg_objs):
        new_obj_packs = rule.out_obj_packs
        event = Event()
        event.verb = rule.verb
        event.old_objs = []
        event.new_objs = []
        event.other_prereqs = []
        event.sentence = arg_objs[0].get_verb_sentence_ncase(rule.verb,*arg_objs[1:])
        for i,new_pack in enumerate(new_obj_packs):
            assert i < len(arg_objs) # TODO: Create completely new objects, eg. wood shavings
            old_obj = arg_objs[i]
            if new_pack == '_pass':
                old_obj.used_in_events_past.append(event)
                event.other_prereqs.append(old_obj.copy())
            elif new_pack == '_del':
                old_obj.used_in_events_past.append(event)
                self.remove_obj(old_obj)
                event.old_objs.append(old_obj.copy())
            else:
                new_obj = self.curr_room.defs[new_pack.key].copy()
                new_obj.set_state(new_pack.state)
                new_obj.created_by_event = [event]
                old_obj.used_in_events_past.append(event)
                event.old_objs.append(old_obj.copy())
                event.new_objs.append(new_obj.copy())
                self.convert_obj(old_obj,new_obj)
                # TODO: create extra new objs, eg. shavings
                # TODO: also work if object is carried
        # TODO: create any entirely new objs
        for action in rule.out_actions:
            # TODO: replace old rules with implementation of actions
            if isinstance(action, Endgame):
                self.end_game(action.text)
        self.store_new_event(event)
        if rule.get_msg():
            self.display_msg( rule.get_msg() )
    
    def clear_message(self):
        self.message = None

    def display_msg(self,msg):
        self.message = msg
        self.curr_state = MessageState(self)
    
    def store_new_event(self,event):
        # currently only stored in links from objects, but may want a list of "initial events" which don't depend on any
        pass
        
    def do_default_rule_failure(self):
        self.display_msg("I don't know how to do that")
    
    def move_obj(self,x,y,obj):
        self.curr_room.map.move_to(x,y,obj)
        
    def delete_obj(self,obj):
        self.curr_room.map.remove_obj(obj)
        
    def convert_obj(self,old_obj,new_obj):
        self.curr_room.map.convert_obj(old_obj,new_obj)

    def end_game(self, msg):
        self.message = msg
        self.curr_state = EndState(self)

    def get_obj_at(self,pos):
        return self.curr_room.map.get_mapsquare_at(pos).get_combined_mainobj()

    def advance_state(self):
        if self.curr_state.endgame:
            return
        for cleanup_func in self.curr_state.cleanup_funcs:
            cleanup_func()
        self.curr_state = State(idle=True)
        if self.next_act_lambda:
            self.next_act_lambda()
            self.next_act_lambda = None
        else:
            self._idle_fidget()
       
    def state_is_idle(self):
       return self.curr_state.idle
       
    def state_is_chainable(self):
        return self.curr_state.chainable
        
    def start_move(self,dir):
        assert dir in 'lrud'
        if self.state_is_chainable():
            self.next_act_lambda = lambda: self.start_move(dir)
        else:
            if self.get_obj_at(self.player.map_pos() + offset_from_dir(dir)).walkable:
                self.curr_state = State(
                    contexts = {self.player.map_rect():dir},
                    offsets = {self.player.map_rect():dir},
                    cleanup_funcs = [lambda:self._finish_move(dir)],
                    chainable=True)
                self.next_act_lambda = lambda: self._begin_facing(dir) # What does this do?
            else:
                self._begin_facing(dir)
    
    def finished_state(self,frac):
        return self.curr_state.is_done(frac)
    
    def _idle_fidget(self):
        # Only do at random intervals
        random.seed()
        fidget = random.choice(('x','x','x','x','x','x1','x2'))
        self.curr_state = State( contexts={self.player.map_rect():fidget},idle=True)
    
    def _finish_move(self,dir):
        self.player.x, self.player.y = self.player.map_pos() + offset_from_dir(dir)
        # TODO: Use map.move_char()
        
    def _begin_facing(self,dir):
        self.curr_state = State(
            contexts={self.player.map_rect():"f"+dir},
            idle=True,
            is_done = lambda frac:frac>=3 )
    
    def get_blit_surfaces(self, frac, tile_size, window=Rect(0,0,15,7)):
        blit_surfaces = []
        for stratum in self.curr_room.map.get_strata_by_rows():
            for row in stratum:
                for obj_tuple in row:
                    for obj in obj_tuple:
                        contexts = self.curr_state.contexts.iteritems()  
                        offsets = self.curr_state.offsets.iteritems()  
                        curr_contexts =tuple( context for rect,context in contexts if obj.is_in(rect) )
                        curr_offsets =tuple( offset for rect,offset in offsets if obj.is_in(rect) )
                        blit_surface = obj.get_surface( tile_size,curr_contexts,curr_offsets,frac )
                        blit_surface.add_external_offset( obj.map_pos()*tile_size )
                        blit_surfaces.append(blit_surface)
        return blit_surfaces
        
    def get_map_size(self):
        return self.curr_room.map.map_size()
class Backend:
    def __init__(self):
        self.rules = Rules()
        self.default_idle_state = ({}, [], dict(idle=True), lambda frac: self._idle_done(frac))
        self.curr_state = self.default_idle_state
        self.pending_subactions = {}
        self.last_player_move = ("", "")
        self.frac = 0
        self.next_act_lambda = None

    def load(self, filename):
        # TODO: need copy?
        self.curr_room = room_data.load_room(filename)
        self.rules.update(self.curr_room.get_rules())
        self.player = self.curr_room.player

    def do(self, verb, *arg_objs):
        print (verb,) + tuple(obj.name for obj in arg_objs)
        if verb == "move":
            assert len(arg_objs) == 2
            obj = arg_objs[0]
            target = arg_objs[1]
            if target.can_support:
                self.move_obj(target.x, target.y, obj)
            else:
                self.print_msg("I can't put it there")
        elif verb == "examine":
            assert len(arg_objs) == 1
            msg = arg_objs[0].get_examine_text()
            self.print_msg(msg if msg else "I don't see anything special")
        else:
            rule = self.rules.get_rule(verb, *arg_objs)
            if not rule:
                self.do_default_rule_failure()
            else:
                self._do_rule(rule, arg_objs)

    def do_undo(self, undo_event):
        print "UNDO"

    def do_redo(self, redo_event):
        print "REDO"

    def _do_rule(self, rule, arg_objs):
        new_obj_packs = rule.out_obj_packs
        event = Event()
        event.verb = rule.verb
        event.old_objs = []
        event.new_objs = []
        event.other_prereqs = []
        event.sentence = arg_objs[0].get_verb_sentence_ncase(rule.verb, *arg_objs[1:])
        for i, new_pack in enumerate(new_obj_packs):
            assert i < len(arg_objs)  # TODO: Create completely new objects, eg. wood shavings
            old_obj = arg_objs[i]
            if new_pack == "_pass":
                old_obj.used_in_events_past.append(event)
                event.other_prereqs.append(old_obj.copy())
            elif new_pack == "_del":
                old_obj.used_in_events_past.append(event)
                self.remove_obj(old_obj)
                event.old_objs.append(old_obj.copy())
            else:
                new_obj = self.curr_room.defs[new_pack.key].copy()
                new_obj.set_state(new_pack.state)
                new_obj.created_by_event = [event]
                old_obj.used_in_events_past.append(event)
                event.old_objs.append(old_obj.copy())
                event.new_objs.append(new_obj.copy())
                self.convert_obj(old_obj, new_obj)
                # TODO: create extra new objs, eg. shavings
                # TODO: also work if object is carried
        # TODO: create any entirely new objs
        self.store_new_event(event)
        if rule.get_msg():
            self.print_msg(rule.get_msg())

    def print_msg(self, msg):
        print ">>> " + msg

    def store_new_event(self, event):
        # currently only stored in links from objects, but may want a list of "initial events" which don't depend on any
        pass

    def do_default_rule_failure(self):
        self.print_msg("I don't know how to do that")

    def move_obj(self, x, y, obj):
        self.curr_room.map.move_to(x, y, obj)

    def delete_obj(self, obj):
        self.curr_room.map.remove_obj(obj)

    def convert_obj(self, old_obj, new_obj):
        self.curr_room.map.convert_obj(old_obj, new_obj)

    def get_obj_at(self, pos):
        return self.curr_room.map.get_mapsquare_at(pos).get_combined_mainobj()

    def advance_state(self):
        for func in self.curr_state[1]:
            func()
        self.curr_state = self.default_idle_state
        if self.next_act_lambda:
            self.next_act_lambda()
            self.next_act_lambda = None
        else:
            self._idle_fidget()

    def state_is_idle(self):
        return self.curr_state[2].get("idle")

    def state_is_chainable(self):
        return self.curr_state[2].get("chainable")

    def start_move(self, dir):
        assert dir in "lrud"
        if self.state_is_chainable():
            self.next_act_lambda = lambda: self.start_move(dir)
        else:
            if self.get_obj_at(self.player.map_pos() + offset_from_dir(dir)).walkable:
                self.curr_state = (
                    {self.player.map_rect(): dir},
                    [lambda: self._finish_move(dir)],
                    dict(chainable=True),
                )
                self.next_act_lambda = lambda: self._begin_facing(dir)
            else:
                self._begin_facing(dir)

    def finished_state(self, frac):
        if len(self.curr_state) >= 4:
            return self.curr_state[3](frac)
        else:
            return frac >= 1

    def _idle_done(self, frac):
        return frac >= 1

    def _idle_fidget(self):
        # Only do at random intervals
        random.seed()
        fidget = random.choice(("x", "x", "x", "x", "x", "x1", "x2"))
        self.curr_state = ({self.player.map_rect(): fidget}, [], dict(idle=True))

    def _finish_move(self, dir):
        self.player.x, self.player.y = self.player.map_pos() + offset_from_dir(dir)
        # TODO: Use map.move_char()

    def _begin_facing(self, dir):
        self.curr_state = ({self.player.map_rect(): "f" + dir}, [], dict(idle=True), lambda frac: frac >= 3)

    def get_blit_surfaces(self, frac, tile_size, window=Rect(0, 0, 15, 7)):
        blit_surfaces = []
        for stratum in self.curr_room.map.get_strata_by_rows():
            for row in stratum:
                for obj_tuple in row:
                    for obj in obj_tuple:
                        blit_surface = obj.get_surface(tile_size, self.curr_state[0], frac)
                        blit_surface.add_external_offset(obj.map_pos() * tile_size)
                        blit_surfaces.append(blit_surface)
        return blit_surfaces

    def get_map_size(self):
        return self.curr_room.map.map_size()