import pygame, math, datetime, os
from pygame.locals import *
from enum import Enum
import settings as s
import util as u

class PlayerStatus(Enum):
    alive = 0
    game_over  = 1
    level_over = 2

class Player:
    """Represents the player in the game world."""

    def __init__(self, high_score, selected_player):
        self.settings       = s.PLAYERS[selected_player]
        self.points         = 0
        self.high_score     = high_score
        self.next_milestone = s.POINT_MILESTONE

        self.reset()

    def reset(self, total_laps=s.LAPS_PER_LEVEL):
        """Resets player variables for the start of a new level."""
        self.status          = PlayerStatus.alive
        self.level_over_lag  = s.LEVEL_OVER_LAG
        self.x               = 0
        self.y               = 0
        self.position        = 0
        self.lap_percent     = 0
        self.direction       = 0
        self.acceleration    = 0
        self.speed           = 1
        self.speed_boost     = 1
        self.animation_frame = 1
        self.new_lap         = False
        self.lap_bonus       = 0
        self.time_bonus      = 0
        self.lap             = 1
        self.total_laps      = total_laps
        self.lap_time        = 0
        self.lap_margin      = 0
        self.blood_alpha     = 0
        self.in_tunnel       = False
        self.fastest_lap     = s.CHECKPOINT
        self.checkpoint      = s.CHECKPOINT
        self.time_left       = s.CHECKPOINT
        self.last_checkpoint = None
        self.crashed         = False
        self.special_text    = None
        self.screech_sfx     = None

        self.__set_checkpoint()

    def steer(self, segment):
        """Updates x to simulate steering."""
        bounds = s.TUNNEL_BOUNDS if self.in_tunnel else s.BOUNDS
        self.x = u.limit(self.x + self.direction, -bounds, bounds)

        # Apply centrifugal force if we are going around a corner.
        if segment.curve != 0 and self.status == PlayerStatus.alive:
            # Congratulate player if they've broken personal record.
            self.x -= (self.direction_speed() * self.speed_percent() * segment.curve * self.settings["centrifugal_force"])

    def climb(self, segment):
        """Updates y to simulate hill and valley ascension."""
        top_y    = segment.top["world"]["y"]
        bottom_y = segment.bottom["world"]["y"]

        self.y = top_y + (top_y - bottom_y) * self.speed_percent()

    def detect_collisions(self, segment):
        """Detects and handles player collisions with sprites."""
        if not self.crashed:
            for sp in segment.sprites:
                if sp.sprite.has_key("collision") and self.__collided_with_sprite(sp):
                    if sp.is_hooker():
                        if not sp.hit:
                            sp.hit = True
                            self.__hit_hooker()
                    elif sp.is_bonus():
                        segment.remove_sprite(sp)
                        self.__hit_bonus()
                    elif sp.is_speed_boost():
                        self.__hit_speed_boost()
                    else:
                        self.__hit_world_object()

                    break

            for comp in segment.competitors:
                if self.__collided_with_sprite(comp):
                    self.__hit_competitor()

                    break

    def render(self, window, segment):
        """Renders the player sprite to the given surface."""
        top    = segment.top
        bottom = segment.bottom
        width  = s.DIMENSIONS[0] / 2
        height = s.DIMENSIONS[1] / 2
        scale  = s.CAMERA_DEPTH / (s.CAMERA_HEIGHT * s.CAMERA_DEPTH)
        sprite = "straight"

        if self.direction > 0:
            sprite = "right"
        elif self.direction < 0:
            sprite = "left"

        if top["world"]["y"] > bottom["world"]["y"]:
            sprite = "uphill_" + sprite
        elif top["world"]["y"] < (bottom["world"]["y"] - 10):  # TODO: Fix this. Should not need -10 here.
            sprite = "downhill_" + sprite

        if self.speed > 0:
            self.animation_frame += 1

            if self.animation_frame > (s.PLAYER_ANIM_HOLD * 2):
                self.animation_frame = 1

        # Show smoke if player is fangin' it around a corner.
        if abs(segment.curve) > s.MINIMUM_CORNER_SMOKE and\
           self.direction != 0 and\
           self.speed > (self.settings["top_speed"] / 1.2):
            sprite += "_smoke"
            self.__run_screech()
        elif self.screech_sfx:
            self.__stop_screech()

        sprite += "1" if (self.animation_frame < s.PLAYER_ANIM_HOLD) else "2"

        sprite   = self.settings["sprites"][sprite]
        s_width  = int(sprite["width"] * scale * s.ROAD_WIDTH * 1.2)
        s_height = int(sprite["height"] * scale * s.ROAD_WIDTH * 1.2)

        p = pygame.image.load(os.path.join("lib", sprite["path"]))
        p = pygame.transform.scale(p, (s_width, s_height))

        self.rendered_area = [width - (s_width / 2), width + (s_width / 2)]

        window.blit(p, (width - (s_width / 2), s.DIMENSIONS[1] - s_height - s.BOTTOM_OFFSET))

        # Finish up the round.
        if self.status != PlayerStatus.alive:
            self.level_over_lag -= 1

    def render_hud(self, window):
        """Renders a Head-Up display on the active window."""
        center      = (75, s.DIMENSIONS[1] - 80)
        speedo_rect = (35, s.DIMENSIONS[1] - 120, 80, 80)
        orbit_pos   = (self.speed / (self.settings["top_speed"] / 4.7)) + 2.35
        start       = self.__circular_orbit(center, -10, orbit_pos)
        finish      = self.__circular_orbit(center, 36, orbit_pos)
        speed       = round((self.speed / s.SEGMENT_HEIGHT) * 1.5, 1)
        font        = pygame.font.Font(s.FONTS["retro_computer"], 16)
        st          = self.special_text
        time_colour = s.COLOURS["text"] if self.time_left > 5 else s.COLOURS["red"]

        # Speedometer.
        pygame.draw.circle(window, s.COLOURS["black"], center, 50, 2)
        pygame.draw.circle(window, s.COLOURS["black"], center, 4)
        pygame.draw.line(window, s.COLOURS["black"], start, finish, 3)
        pygame.draw.arc(window, s.COLOURS["black"], speedo_rect, 0.2, math.pi * 1.25, 5)
        pygame.draw.arc(window, s.COLOURS["red"], speedo_rect, -0.73, 0.2, 5)

        u.render_text("kmph", window, font, s.COLOURS["text"], (110, s.DIMENSIONS[1] - 24))
        u.render_text(str(speed), window, font, s.COLOURS["text"], (10, s.DIMENSIONS[1] - 24))
        u.render_text("Lap", window, font, s.COLOURS["text"], (s.DIMENSIONS[0] - 130, 10))
        u.render_text("%s/%s" % (self.lap, self.total_laps) , window, font, s.COLOURS["text"], (s.DIMENSIONS[0] - 58, 10))

        u.render_text("Time", window, font, time_colour, (10, 10))
        u.render_text(str(math.trunc(self.time_left)), window, font, time_colour, (90, 10))

        # Render special text.
        if st:
            td = (datetime.datetime.now() - st[0])

            if td.seconds > st[1]:
                self.special_text = None
            else:
                bonus_colour = "bonus_a" if (td.microseconds / 25000.0) % 10 > 5 else "bonus_b"
                u.render_text(st[2], window, font, s.COLOURS[bonus_colour], (10, 36))

        # Points rendering needs more care because it grows so fast.
        p_val_text  = font.render(str(math.trunc(self.points)), 1, s.COLOURS["text"])
        p_name_text = font.render("Points", 1, s.COLOURS["text"])
        p_val_x     = s.DIMENSIONS[0] - p_val_text.get_width() - 10

        window.blit(p_val_text, (p_val_x, s.DIMENSIONS[1] - 24))
        window.blit(p_name_text, (p_val_x - 112, s.DIMENSIONS[1] - 24))

        # Hit a point milestone.
        if self.points > self.next_milestone and self.status == PlayerStatus.alive:
            milestone_sfx = pygame.mixer.Sound(os.path.join("lib", "excellent.ogg"))
            milestone_sfx.play()

            self.next_milestone += s.POINT_MILESTONE

            self.__set_special_text("Nice driving!", 2)

        # On the leaderboard!
        if self.high_score > 0 and self.points > self.high_score:
            high_score_sfx = pygame.mixer.Sound(os.path.join("lib", "excellent.ogg"))
            high_score_sfx.play()

            self.high_score = 0

            self.__set_special_text("New High Score!", 2)

        if self.status == PlayerStatus.game_over:
            self.__game_over_overlay(window)
        elif self.status == PlayerStatus.level_over:
            self.__level_over_overlay(window)

        # Display lap difference (unless we've only done one lap).
        if self.lap_margin != 0 and self.lap > 2 and self.lap_percent < 20:
            diff = self.lap_margin

            if diff <= 0:
                colour = "red"
                sign   = "+"
            else:
                colour = "green"
                sign   = "-"

            u.render_text(sign + str(round(abs(diff), 1)), window, font, s.COLOURS[colour], (10, 40))

    def render_blood(self, window):
        """Renders a blood splatter if we've killed someone."""
        b = pygame.image.load(os.path.join("lib", "blood.png"))
        b.set_alpha(self.blood_alpha)

        x = (s.DIMENSIONS[0] - b.get_size()[0]) / 2
        y = ((s.DIMENSIONS[1] - b.get_size()[1]) / 2) - 30
 
        window.blit(b, (x, y))

        self.blood_alpha -= 1

    def accelerate(self):
        """Updates speed at appropriate acceleration level."""
        curr_speed = self.speed_boost * (self.speed + ((self.settings["top_speed"] / self.settings["acceleration_factor"]) * self.acceleration))
        self.speed = u.limit(curr_speed, 0, self.speed_boost * self.settings["top_speed"])

    def travel(self, track_length, window):
        """Updates position, reflecting how far we've travelled since the last frame."""
        pos        = self.position + (s.FRAME_RATE * self.speed)
        td         = (datetime.datetime.now() - self.last_checkpoint)
        total_secs = (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6 # td.total_seconds() not implemented in Python 2.6

        self.new_lap = False

        if self.speed_boost > 1:
            self.speed_boost -= s.SPEED_BOOST_DECREASE

        if self.status == PlayerStatus.alive:
            self.points   += (self.speed / s.SEGMENT_HEIGHT) / s.POINTS
            self.time_left = round(self.checkpoint - total_secs, 1) + self.lap_bonus

            if self.time_left <= 0:
                go_sfx = pygame.mixer.Sound(os.path.join("lib", "loser.ogg"))
                go_sfx.play()
                self.status = PlayerStatus.game_over
            elif self.time_left == 5:
                self.__set_special_text("Hurry up!", 2)

        # New lap.
        if pos >= track_length:
            self.__set_checkpoint()

            self.lap_bonus  = 0
            self.new_lap    = True
            self.lap_time   = total_secs
            self.lap_margin = self.fastest_lap - self.lap_time

            # Finished level.
            if self.status == PlayerStatus.alive and self.lap == self.total_laps:
                self.status = PlayerStatus.level_over
            else:    
                self.lap += 1

                lap_sfx = pygame.mixer.Sound(os.path.join("lib", "570.wav"))
                lap_sfx.play()

            if self.status != PlayerStatus.game_over:
                # Reduce checkpoint time every lap to increase difficulty.
                checkpoint_diff = (self.checkpoint - self.lap_time) / s.LAP_DIFFICULTY_FACTOR
                bonus_points    = self.time_left * s.POINTS * self.lap

                self.checkpoint -= max(checkpoint_diff, s.MINIMUM_DIFFICULTY)
                self.time_bonus += bonus_points
                self.points     += bonus_points

                if self.__fastest_lap():
                    # Congratulate player if they've broken personal record.
                    if self.lap > 2:
                        self.points += self.lap_margin * s.POINTS * self.lap
                        fast_lap_sfx = pygame.mixer.Sound(os.path.join("lib", "jim.ogg"))
                        fast_lap_sfx.play()

                    self.fastest_lap = self.lap_time

            pos -= track_length

        if pos < 0:
            pos += track_length

        self.position    = pos
        self.lap_percent = round((pos / track_length) * 100)

    def set_acceleration(self, keys):
        """Updates the acceleration factor depending on world conditions."""
        a = -s.FRAME_RATE

        # Slow player down if they are on the grass or crashed.
        if self.crashed:
            a = 0
        else:
            if (self.x > 1.0 or self.x < -1.0) and self.speed > (self.settings["top_speed"] / self.settings["offroad_top_speed_factor"]):
                a = a * 3
            else:
                if keys[K_UP] or keys[K_x] or s.AUTO_DRIVE or self.status != PlayerStatus.alive:
                    a = s.FRAME_RATE
                elif keys[K_DOWN]:
                    a = -(s.FRAME_RATE * self.settings["deceleration"])

        self.acceleration = a

    def set_direction(self, keys):
        """Updates the direction the player is going, accepts a key-map."""
        d = 0

        if self.status == PlayerStatus.alive:
            if keys[K_LEFT]:
                d = -self.direction_speed()
            elif keys[K_RIGHT]:
                d = self.direction_speed()

        self.direction = d

    def speed_percent(self):
        return self.speed / self.settings["top_speed"]

    def direction_speed(self):
        return (s.FRAME_RATE * 3 * self.speed_percent())

    def segment_percent(self):
        """Returns a value between 0 and 1 indicating how far through the current segment we are."""
        return ((self.position + s.PLAYER_Z) % s.SEGMENT_HEIGHT) / s.SEGMENT_HEIGHT

    def handle_crash(self):
        """Proceeds player through crash state."""
        if self.crashed:
            step = -0.025 if self.x > 0 else 0.025

            if round(self.x, 1) != 0:
                self.x += step
            else:
                pygame.mixer.music.set_volume(s.MUSIC_VOLUME)
                self.crashed = False

    def finished(self):
        return self.level_over_lag == 0

    def alive(self):
        return self.status != PlayerStatus.game_over

    def __set_special_text(self, text, time):
        """Defines the special text to show and for how long we should show it."""
        st = self.special_text

        if not st or st[2] != text:
            self.special_text = [datetime.datetime.now(), time, text]

    def __collided_with_sprite(self, sprite):
        r_area = list(sprite.rendered_area)
        p_area = sprite.sprite["collision"]
        width  = r_area[1] - r_area[0]

        # Apply offsets.
        r_area[0] += (width * p_area[0])
        r_area[1] -= (width * p_area[1])

        return (self.rendered_area[0] < r_area[1] and\
                self.rendered_area[1] > r_area[0])
 
    def __circular_orbit(self, center, radius, t):
        """Returns the X/Y coordinate for a given time (t) in a circular orbit."""
        theta = math.fmod(t, math.pi * 2)
        c     = math.cos(theta)
        s     = math.sin(theta)

        return center[0] + radius * c, center[1] + radius * s

    def __set_checkpoint(self):
        self.last_checkpoint = datetime.datetime.now()

    def __hit_hooker(self):
        crash_sfx        = pygame.mixer.Sound(os.path.join("lib", "scream.ogg"))
        splat_sfx        = pygame.mixer.Sound(os.path.join("lib", "blood.ogg"))
        self.blood_alpha = 255

        # Yeah, I'm a sicko....
        if self.status == PlayerStatus.alive:
            self.points += s.POINT_GAIN_PROSTITUTE
            self.__set_special_text("+%d points!" % s.POINT_GAIN_PROSTITUTE, 2)

        crash_sfx.play()
        splat_sfx.play()

    def __hit_bonus(self):
        if self.status == PlayerStatus.alive:
            self.lap_bonus += s.BONUS_AMOUNT

        bonus_sfx = pygame.mixer.Sound(os.path.join("lib", "oh_yeah.ogg"))
        bonus_sfx.play()

        self.__set_special_text("Bonus time!", 2)

    def __hit_speed_boost(self):
        if self.speed_boost == 1:
            boost_sfx = pygame.mixer.Sound(os.path.join("lib", "speed_boost.ogg"))
            boost_sfx.play()

            self.__set_special_text("Speed boost!", 2)

        self.speed_boost = 1.6

    def __hit_world_object(self):
        pygame.mixer.music.set_volume(0.2)

        crash_sfx        = pygame.mixer.Sound(os.path.join("lib", "you_fool.ogg"))
        self.crashed     = True
        self.speed       = 0
        self.speed_boost = 1

        crash_sfx.play()

        if self.status == PlayerStatus.alive:
            deduction    = self.points * s.POINT_LOSS_SPRITE
            self.points -= deduction
            self.__set_special_text("-%d points!" % deduction, 2)

    def __hit_competitor(self):
        crash_sfx = pygame.mixer.Sound(os.path.join("lib", "car_crash.ogg"))
        crash_sfx.play()

        self.speed = self.speed / s.CRASH_DIVISOR

        if self.status == PlayerStatus.alive:
            self.points -= self.points * s.POINT_LOSS_COMP

    def __fastest_lap(self):
        return self.status != PlayerStatus.game_over and self.lap_time < self.fastest_lap

    def __run_screech(self):
        if not self.screech_sfx:
            self.screech_sfx = pygame.mixer.Sound(os.path.join("lib", "screech_short.ogg"))
            self.screech_sfx.set_volume(0.4)
            self.screech_sfx.play(-1)

    def __stop_screech(self):
        self.screech_sfx.stop()
        self.screech_sfx = None

    def __game_over_overlay(self, window):
        go_font = pygame.font.Font(s.FONTS["retro_computer"], 44)
        txt_go  = go_font.render("Game Over", 1, s.COLOURS["red"])
        x       = (s.DIMENSIONS[0] - txt_go.get_size()[0]) / 2
        y       = (s.DIMENSIONS[1] - txt_go.get_size()[1]) / 2
        overlay = pygame.Surface(s.DIMENSIONS, pygame.SRCALPHA)

        overlay.fill((255, 255, 255, 90))
        overlay.blit(txt_go, (x, y))
        window.blit(overlay, (0,0))

    def __level_over_overlay(self, window):
        lo_font      = pygame.font.Font(s.FONTS["fipps"], 38)
        s_font       = pygame.font.Font(s.FONTS["retro_computer"], 30)
        txt_lo       = lo_font.render("Level Complete!", 1, s.COLOURS["dark_text"])
        txt_lap      = s_font.render("Best Lap", 1, s.COLOURS["dark_text"])
        txt_lap_v    = s_font.render("%.1fs" % round(self.fastest_lap, 1), 1, s.COLOURS["dark_text"])
        txt_bonus    = s_font.render("Time bonus", 1, s.COLOURS["dark_text"])
        txt_bonus_v  = s_font.render(str(math.trunc(self.time_bonus)), 1, s.COLOURS["dark_text"])
        txt_points   = s_font.render("Points", 1, s.COLOURS["dark_text"])
        txt_points_v = s_font.render(str(math.trunc(self.points)), 1, s.COLOURS["dark_text"])
        overlay      = pygame.Surface(s.DIMENSIONS, pygame.SRCALPHA)

        overlay.fill((255, 255, 255, 150))
        overlay.blit(txt_lo, (s.DIMENSIONS[0] / 2 - txt_lo.get_size()[0] / 2, 20))
        overlay.blit(txt_lap, (20, 180))
        overlay.blit(txt_lap_v, (s.DIMENSIONS[0] - txt_lap_v.get_size()[0] - 10, 190))
        overlay.blit(txt_bonus, (20, 260))
        overlay.blit(txt_bonus_v, (s.DIMENSIONS[0] - txt_bonus_v.get_size()[0] - 10, 270))
        overlay.blit(txt_points, (20, 340))
        overlay.blit(txt_points_v, (s.DIMENSIONS[0] - txt_points_v.get_size()[0] - 10, 350))

        window.blit(overlay, (0,0))