
import os.path, random

from game_object import GameObject, FACING_DIRS
from collision import CST_NONE, CST_CIRCLE, CST_AABB, CST_TILE, CT_NONE, CT_GENERIC_STATIC, CT_GENERIC_DYNAMIC, CT_PLAYER, CTG_STATIC, CTG_DYNAMIC

class GameObjectAttachment(GameObject):
    "GameObject that doesn't think about anything, just renders"
    collision_type = CT_NONE
    should_save = False
    selectable = False
    exclude_from_class_list = True
    physics_move = False
    offset_x, offset_y, offset_z = 0., 0., 0.
    "Offset from parent object's origin"
    fixed_z = False
    "If True, Z will not be locked to GO we're attached to"
    editable = GameObject.editable + ['offset_x', 'offset_y', 'offset_z']
    
    def attach_to(self, game_object):
        "Attach this object to given object."
        self.parent = game_object
    
    def update(self):
        # very minimal update!
        if not self.art.updated_this_tick:
            self.art.update()
    
    def post_update(self):
        # after parent has moved, snap to its location
        self.x = self.parent.x + self.offset_x
        self.y = self.parent.y + self.offset_y
        if not self.fixed_z:
            self.z = self.parent.z + self.offset_z


class BlobShadow(GameObjectAttachment):
    "Generic blob shadow attachment class"
    art_src = 'blob_shadow'
    alpha = 0.5

class StaticTileBG(GameObject):
    "Generic static world object with tile-based collision"
    collision_shape_type = CST_TILE
    collision_type = CT_GENERIC_STATIC
    physics_move = False

class StaticTileObject(GameObject):
    collision_shape_type = CST_TILE
    collision_type = CT_GENERIC_STATIC
    physics_move = False
    y_sort = True

class StaticBoxObject(GameObject):
    "Generic static world object with AABB-based (rectangle) collision"
    collision_shape_type = CST_AABB
    collision_type = CT_GENERIC_STATIC
    physics_move = False

class DynamicBoxObject(GameObject):
    collision_shape_type = CST_AABB
    collision_type = CT_GENERIC_DYNAMIC
    y_sort = True

class Pickup(GameObject):
    collision_shape_type = CST_CIRCLE
    collision_type = CT_GENERIC_DYNAMIC
    y_sort = True
    attachment_classes = { 'shadow': 'BlobShadow' }

class Projectile(GameObject):
    "Generic projectile class"
    fast_move_steps = 1
    collision_type = CT_GENERIC_DYNAMIC
    collision_shape_type = CST_CIRCLE
    move_accel_x = move_accel_y = 400.
    noncolliding_classes = ['Projectile']
    lifespan = 10.
    "Projectiles should be transient, limited max life"
    should_save = False
    
    def __init__(self, world, obj_data=None):
        GameObject.__init__(self, world, obj_data)
        self.fire_dir_x, self.fire_dir_y = 0, 0
    
    def fire(self, firer, dir_x=0, dir_y=1):
        self.set_loc(firer.x, firer.y, firer.z)
        self.reset_last_loc()
        self.fire_dir_x, self.fire_dir_y = dir_x, dir_y
    
    def update(self):
        if (self.fire_dir_x, self.fire_dir_y) != (0, 0):
            self.move(self.fire_dir_x, self.fire_dir_y)
        GameObject.update(self)

class Character(GameObject):
    "Generic character class"
    state_changes_art = True
    stand_if_not_moving = True
    move_state = 'walk'
    "Move state name - added to valid_states in init so subclasses recognized"
    collision_shape_type = CST_CIRCLE
    collision_type = CT_GENERIC_DYNAMIC
    
    def __init__(self, world, obj_data=None):
        if not self.move_state in self.valid_states:
            self.valid_states.append(self.move_state)
        GameObject.__init__(self, world, obj_data)
        # assume that character should start idling, if its art animates
        if self.art.frames > 0:
            self.start_animating()
    
    def update_state(self):
        GameObject.update_state(self)
        if self.state_changes_art and abs(self.vel_x) > 0.1 or abs(self.vel_y) > 0.1:
            self.state = self.move_state

class Player(Character):
    "Generic player class"
    log_move = False
    collision_type = CT_PLAYER
    editable = Character.editable + ['move_accel_x', 'move_accel_y',
                                         'ground_friction', 'air_friction',
                                         'bounciness', 'stop_velocity']
    
    def pre_first_update(self):
        if self.world.player is None:
            self.world.player = self
            if self.world.player_camera_lock:
                self.world.camera.focus_object = self
            else:
                self.world.camera.focus_object = None
    
    def button_pressed(self, button_index):
        pass
    
    def button_unpressed(self, button_index):
        pass


class TopDownPlayer(Player):
    
    y_sort = True
    attachment_classes = { 'shadow': 'BlobShadow' }
    facing_changes_art = True
    
    def get_facing_dir(self):
        return FACING_DIRS[self.facing]


class WorldPropertiesObject(GameObject):
    "Special magic singleton object that stores and sets GameWorld properties"
    art_src = 'world_properties_object'
    visible = deleteable = selectable = False
    locked = True
    physics_move = False
    exclude_from_object_list = True
    exclude_from_class_list = True
    world_props = ['game_title', 'gravity_x', 'gravity_y', 'gravity_z',
                   'hud_class_name', 'globals_object_class_name',
                   'camera_x', 'camera_y', 'camera_z',
                   'bg_color_r', 'bg_color_g', 'bg_color_b', 'bg_color_a',
                   'player_camera_lock', 'object_grid_snap', 'draw_hud',
                   'collision_enabled', 'show_collision_all', 'show_bounds_all',
                   'show_origin_all', 'show_all_rooms',
                   'room_camera_changes_enabled', 'draw_debug_objects'
    ]
    """
    Properties we serialize on behalf of GameWorld
    TODO: figure out how to make these defaults sync with those in GW?
    """
    serialized = world_props
    editable = []
    "All visible properties are serialized, not editable"
    def __init__(self, world, obj_data=None):
        GameObject.__init__(self, world, obj_data)
        world_class = type(world)
        for v in self.serialized:
            if obj_data and v in obj_data:
                # if world instance has property from loaded data, use it
                if hasattr(self.world, v):
                    setattr(self.world, v, obj_data[v])
                setattr(self, v, obj_data[v])
            # use world class (default) property if loaded data lacks it
            elif hasattr(world_class, v):
                setattr(self, v, getattr(world_class, v))
            else:
                # set explicitly as float, for camera & bg color
                setattr(self, v, 0.0)
        # special handling of bg color (a list)
        self.world.bg_color = [self.bg_color_r, self.bg_color_g, self.bg_color_b, self.bg_color_a]
        self.world.camera.set_loc(self.camera_x, self.camera_y, self.camera_z)
        # TODO: figure out why collision_enabled seems to default False!
    
    def set_object_property(self, prop_name, new_value):
        setattr(self, prop_name, new_value)
        # special handling for some values, eg bg color and camera
        if prop_name.startswith('bg_color_'):
            component = {'r': 0, 'g': 1, 'b': 2, 'a': 3}[prop_name[-1]]
            self.world.bg_color[component] = float(new_value)
        elif prop_name.startswith('camera_') and len(prop_name) == len('camera_x'):
            setattr(self.world.camera, prop_name[-1], new_value)
        # some properties have unique set methods in GW
        elif prop_name == 'show_collision_all':
            self.world.toggle_all_collision_viz()
        elif prop_name == 'show_bounds_all':
            self.world.toggle_all_bounds_viz()
        elif prop_name == 'show_origin_all':
            self.world.toggle_all_origin_viz()
        elif prop_name == 'player_camera_lock':
            self.world.toggle_player_camera_lock()
        # normal properties you can just set: set em
        elif hasattr(self.world, prop_name):
            setattr(self.world, prop_name, new_value)
    
    def update_from_world(self):
        self.camera_x = self.world.camera.x
        self.camera_y = self.world.camera.y
        self.camera_z = self.world.camera.z


class WorldGlobalsObject(GameObject):
    """
    Invisible object holding global state, variables etc in GameWorld.globals.
    Subclass can be specified in WorldPropertiesObject.
    NOTE: this object is spawned from scratch every load, it's never serialized!
    """
    should_save = False
    visible = deleteable = selectable = False
    locked = True
    exclude_from_object_list = True
    exclude_from_class_list = True
    physics_move = False
    serialized = []
    editable = []


class LocationMarker(GameObject):
    "Very simple GameObject that marks an XYZ location for eg camera points"
    art_src = 'loc_marker'
    serialized = ['name', 'x', 'y', 'z', 'visible', 'locked']
    editable = []
    alpha = 0.5
    physics_move = False
    is_debug = True


class StaticTileTrigger(GameObject):
    """
    Generic static trigger with tile-based collision.
    Overlaps but doesn't collide.
    """
    is_debug = True
    collision_shape_type = CST_TILE
    collision_type = CT_GENERIC_STATIC
    noncolliding_classes = ['GameObject']
    physics_move = False
    serialized = ['name', 'x', 'y', 'z', 'art_src', 'visible', 'locked']
    
    def started_overlapping(self, other):
        #self.app.log('Trigger overlapped with %s' % other.name)
        pass

class WarpTrigger(StaticTileTrigger):
    "Trigger that warps object to a room/marker when they touch it."
    is_debug = True
    art_src = 'trigger_default'
    alpha = 0.5
    destination_marker_name = None
    "If set, warp to this location marker"
    destination_room_name = None
    "If set, make this room the world's current"
    use_marker_room = True
    "If True, change to destination marker's room"
    warp_class_names = ['Player']
    "List of class names to warp on contact with us."
    serialized = StaticTileTrigger.serialized + ['destination_room_name',
                                                 'destination_marker_name',
                                                 'use_marker_room']
    
    def __init__(self, world, obj_data=None):
        StaticTileTrigger.__init__(self, world, obj_data)
        self.warp_classes = [self.world.get_class_by_name(class_name) for class_name in self.warp_class_names]
    
    def started_overlapping(self, other):
        if other.warped_recently():
            return
        # bail if object's class isn't allowed
        valid_class = False
        for c in self.warp_classes:
            if isinstance(other, c):
                valid_class = True
                break
        if not valid_class:
            return
        if self.destination_room_name:
            if other is self.world.player:
                # if overlapping object is player, change current room
                # to destination room
                self.world.change_room(self.destination_room_name)
            else:
                # if object is only in one room, move them to destination room
                if len(other.rooms) == 1:
                    old_room = other.rooms.values()[0]
                    old_room.remove_object(other)
                self.destination_room.add_object(other)
        elif self.destination_marker_name:
            marker = self.world.objects.get(self.destination_marker_name, None)
            if not marker:
                self.app.log('Warp destination object %s not found' % self.destination_marker_name)
                return
            other.set_loc(marker.x, marker.y, marker.z)
            # warp to marker's room if specified, pick a random one if multiple
            if self.use_marker_room and len(marker.rooms) == 1:
                room = random.choice(list(marker.rooms.values()))
                # warn if both room and marker are set but they conflict
                if self.destination_room_name and \
                   room.name != self.destination_room_name:
                    self.app.log("Marker %s's room differs from destination room %s" % (marker.name, self.destination_room_name))
                self.world.change_room(room.name)
        other.last_warp_update = self.world.updates


class ObjectSpawner(LocationMarker):
    "Simple object that spawns an object when triggered"
    is_debug = True
    spawn_class_name = None
    spawn_obj_name = ''
    spawn_random_in_bounds = False
    "If True, spawn somewhere in this object's bounds, else spawn at location"
    spawn_obj_data = {}
    "Dict of properties to set on newly spawned object"
    times_to_fire = -1
    "Number of times we can fire, -1 = infinite"
    trigger_on_room_enter = True
    "Set False for any subclass that triggers in some other way"
    destroy_on_room_exit = True
    "if True, spawned object will be destroyed when player leaves its room"
    serialized = LocationMarker.serialized + ['spawn_class_name', 'spawn_obj_name',
                                              'times_to_fire', 'destroy_on_room_exit'
    ]
    
    def __init__(self, world, obj_data=None):
        LocationMarker.__init__(self, world, obj_data)
        self.times_fired = 0
        # list of objects we've spawned
        self.spawned_objects = []
    
    def get_spawn_class_name(self):
        "Return class name of object to spawn."
        return self.spawn_class_name
    
    def get_spawn_location(self):
        "Return x,y location we should spawn a new object at."
        if not self.spawn_random_in_bounds:
            return self.x, self.y
        left, top, right, bottom = self.get_edges()
        x = left + random.random() * (right - left)
        y = top + random.random() * (bottom - top)
        return x, y
    
    def can_spawn(self):
        "Return True if spawner is allowed to spawn."
        return True
    
    def do_spawn(self):
        "Spawn and returns object."
        class_name = self.get_spawn_class_name()
        if not class_name:
            return None
        x, y = self.get_spawn_location()
        new_obj = self.world.spawn_object_of_class(class_name, x, y)
        if self.spawn_obj_name:
            self.world.rename_object(new_obj, self.spawn_obj_name)
        # new object should be in same rooms as us
        new_obj.rooms.update(self.rooms)
        self.spawned_objects.append(new_obj)
        # save a reference to us, the spawner
        new_obj.spawner = self
        # TODO: put new object in our room(s), apply spawn_obj_data
        return new_obj
    
    def trigger(self):
        "Poke this spawner to do its thing, returns an object if spawned"
        if self.times_to_fire != -1 and self.times_fired >= self.times_to_fire:
            return None
        if not self.can_spawn():
            return None
        if self.times_fired != -1:
            self.times_fired += 1
        return self.do_spawn()
    
    def room_entered(self, room, old_room):
        if self.trigger_on_room_enter:
            self.trigger()
    
    def room_exited(self, room, new_room):
        if not self.destroy_on_room_exit:
            return
        for obj in self.spawned_objects:
            obj.destroy()


class SoundBlaster(LocationMarker):
    "Simple object that plays sound when triggered"
    is_debug = True
    sound_name = ''
    "String name of sound to play, minus any extension"
    can_play = True
    "If False, won't play sound when triggered"
    play_on_room_enter = True
    loops = -1
    "Number of times to loop, if -1 loop indefinitely"
    serialized = LocationMarker.serialized + ['sound_name', 'can_play',
                                              'play_on_room_enter']
    
    def __init__(self, world, obj_data=None):
        LocationMarker.__init__(self, world, obj_data)
        # find file, try common extensions
        for ext in ['', '.ogg', '.wav']:
            filename = self.sound_name + ext
            if self.world.sounds_dir and os.path.exists(self.world.sounds_dir + filename):
                self.sound_filenames[self.sound_name] = filename
                return
        self.world.app.log("Couldn't find sound file %s for SoundBlaster %s" % (self.sound_name, self.name))
    
    def room_entered(self, room, old_room):
        self.play_sound(self.sound_name, self.loops)
    
    def room_exited(self, room, new_room):
        self.stop_sound(self.sound_name)
