"""Demonstration of a shared camera and basic fighting mechanics.

Shows LDtk level setup, one-way platforms, and simple combat interactions."""

from __future__ import annotations

import math
from typing import List

from Box2D import b2ContactListener, b2PolygonShape, b2Vec2
import pyray as rl

from engine.framework import GameObject, Scene
from engine.math_extensions import vec_add, vec_div, vec_mul, vec_sub, v2
from engine.prefabs.components import (AnimationController, BodyComponent, MultiComponent,
                                       PlatformerMovementComponent, PlatformerMovementParams,
                                       SoundComponent)
from engine.prefabs.game_objects import CameraObject, CharacterParams, StaticBox
from engine.prefabs.services import LevelService, PhysicsService, SoundService, TextureService


class FightingCharacter(GameObject):
    """Basic fighting character with attacks and one-way platform logic."""
    def __init__(self, params: CharacterParams, player_number: int = 1) -> None:
        """Create a player-controlled fighter.

        Args:
            params: Character sizing and physics parameters.
            player_number: 1-based index used to map input/skins.

        Returns:
            None
        """
        super().__init__()
        self.p = params
        self.player_number = player_number
        self.gamepad = player_number - 1
        self.width = params.width
        self.height = params.height
        self.physics: PhysicsService = None  # type: ignore[assignment]
        self.level: LevelService = None  # type: ignore[assignment]
        self.body: BodyComponent = None  # type: ignore[assignment]
        self.movement: PlatformerMovementComponent = None  # type: ignore[assignment]
        self.animation: AnimationController = None  # type: ignore[assignment]
        self.sounds: MultiComponent = None  # type: ignore[assignment]
        self.jump_sound: SoundComponent = None  # type: ignore[assignment]
        self.hit_sound: SoundComponent = None  # type: ignore[assignment]
        self.die_sound: SoundComponent = None  # type: ignore[assignment]
        self.fall_through = False
        self.fall_through_timer = 0.0
        self.fall_through_duration = 0.2
        self.attack_display_timer = 0.0
        self.attack_display_duration = 0.1
        self.attack = False

    def init(self) -> None:
        """Initialize body, movement, sounds, and animations.

        Returns:
            None
        """
        self.physics = self.scene.get_service(PhysicsService)

        def build_body(component: BodyComponent):
            """Build body.

            Args:
                component: Parameter.

            Returns:
                Result of the operation.
            """
            world = self.physics.world
            body = world.CreateDynamicBody(position=(self.physics.convert_to_meters(self.p.position).x,
                                                     self.physics.convert_to_meters(self.p.position).y),
                                           fixedRotation=True,
                                           bullet=True)
            shape = b2PolygonShape(box=(self.physics.convert_length_to_meters(self.p.width / 2.0),
                                        self.physics.convert_length_to_meters(self.p.height / 2.0)))
            body.CreateFixture(shape=shape, density=self.p.density, friction=self.p.friction,
                               restitution=self.p.restitution)
            component.body = body

        self.body = self.add_component(BodyComponent(build=build_body))

        movement_params = PlatformerMovementParams()
        movement_params.width = self.p.width
        movement_params.height = self.p.height
        self.movement = self.add_component(PlatformerMovementComponent(movement_params))

        self.level = self.scene.get_service(LevelService)

        self.sounds = self.add_component(MultiComponent())
        self.jump_sound = self.sounds.add_component("jump", SoundComponent, "assets/sounds/jump.wav")
        self.hit_sound = self.sounds.add_component("hit", SoundComponent, "assets/sounds/hit.wav")
        self.die_sound = self.sounds.add_component("die", SoundComponent, "assets/sounds/die.wav")

        self.animation = self.add_component(AnimationController(self.body))
        if self.player_number == 1:
            self.animation.add_animation_from_files("run",
                                                   ["assets/sunnyland/fox/run-1.png",
                                                    "assets/sunnyland/fox/run-2.png",
                                                    "assets/sunnyland/fox/run-3.png",
                                                    "assets/sunnyland/fox/run-4.png",
                                                    "assets/sunnyland/fox/run-5.png",
                                                    "assets/sunnyland/fox/run-6.png"],
                                                   10.0)
            self.animation.add_animation_from_files("idle",
                                                   ["assets/sunnyland/fox/idle-1.png",
                                                    "assets/sunnyland/fox/idle-2.png",
                                                    "assets/sunnyland/fox/idle-3.png",
                                                    "assets/sunnyland/fox/idle-4.png"],
                                                   5.0)
            self.animation.add_animation_from_files("jump", ["assets/sunnyland/fox/jump-1.png"], 0.0)
            self.animation.add_animation_from_files("fall", ["assets/sunnyland/fox/jump-2.png"], 0.0)
            self.animation.origin.y += 4
        elif self.player_number == 2:
            self.animation.add_animation_from_files("run",
                                                   ["assets/sunnyland/bunny/run-1.png",
                                                    "assets/sunnyland/bunny/run-2.png",
                                                    "assets/sunnyland/bunny/run-3.png",
                                                    "assets/sunnyland/bunny/run-4.png",
                                                    "assets/sunnyland/bunny/run-5.png",
                                                    "assets/sunnyland/bunny/run-6.png"],
                                                   10.0)
            self.animation.add_animation_from_files("idle",
                                                   ["assets/sunnyland/bunny/idle-1.png",
                                                    "assets/sunnyland/bunny/idle-2.png",
                                                    "assets/sunnyland/bunny/idle-3.png",
                                                    "assets/sunnyland/bunny/idle-4.png"],
                                                   10.0)
            self.animation.add_animation_from_files("jump", ["assets/sunnyland/bunny/jump-1.png"], 0.0)
            self.animation.add_animation_from_files("fall", ["assets/sunnyland/bunny/jump-2.png"], 0.0)
            self.animation.origin.y += 8
        elif self.player_number == 3:
            self.animation.add_animation_from_files("run",
                                                   ["assets/sunnyland/squirrel/run-1.png",
                                                    "assets/sunnyland/squirrel/run-2.png",
                                                    "assets/sunnyland/squirrel/run-3.png",
                                                    "assets/sunnyland/squirrel/run-4.png",
                                                    "assets/sunnyland/squirrel/run-5.png",
                                                    "assets/sunnyland/squirrel/run-6.png"],
                                                   10.0)
            self.animation.add_animation_from_files("idle",
                                                   ["assets/sunnyland/squirrel/idle-1.png",
                                                    "assets/sunnyland/squirrel/idle-2.png",
                                                    "assets/sunnyland/squirrel/idle-3.png",
                                                    "assets/sunnyland/squirrel/idle-4.png",
                                                    "assets/sunnyland/squirrel/idle-5.png",
                                                    "assets/sunnyland/squirrel/idle-6.png",
                                                    "assets/sunnyland/squirrel/idle-7.png",
                                                    "assets/sunnyland/squirrel/idle-8.png"],
                                                   8.0)
            self.animation.add_animation_from_files("jump",
                                                   ["assets/sunnyland/squirrel/jump-1.png",
                                                    "assets/sunnyland/squirrel/jump-2.png",
                                                    "assets/sunnyland/squirrel/jump-3.png",
                                                    "assets/sunnyland/squirrel/jump-4.png"],
                                                   15.0)
            self.animation.origin.y += 7
        elif self.player_number == 4:
            self.animation.add_animation_from_files("run",
                                                   ["assets/sunnyland/imp/run-1.png",
                                                    "assets/sunnyland/imp/run-2.png",
                                                    "assets/sunnyland/imp/run-3.png",
                                                    "assets/sunnyland/imp/run-4.png",
                                                    "assets/sunnyland/imp/run-5.png",
                                                    "assets/sunnyland/imp/run-6.png",
                                                    "assets/sunnyland/imp/run-7.png",
                                                    "assets/sunnyland/imp/run-8.png"],
                                                   10.0)
            self.animation.add_animation_from_files("idle",
                                                   ["assets/sunnyland/imp/idle-1.png",
                                                    "assets/sunnyland/imp/idle-2.png",
                                                    "assets/sunnyland/imp/idle-3.png",
                                                    "assets/sunnyland/imp/idle-4.png"],
                                                   10.0)
            self.animation.add_animation_from_files("jump", ["assets/sunnyland/imp/jump-1.png"], 0.0)
            self.animation.add_animation_from_files("fall", ["assets/sunnyland/imp/jump-4.png"], 0.0)
            self.animation.origin.y += 10

    def update(self, delta_time: float) -> None:
        """Handle input, jumping, attacks, and respawn logic.

        Args:
            delta_time: Seconds since last frame.

        Returns:
            None
        """
        deadzone = 0.1
        jump_pressed = rl.is_key_pressed(rl.KEY_W) or rl.is_gamepad_button_pressed(self.gamepad, rl.GAMEPAD_BUTTON_RIGHT_FACE_DOWN)
        jump_held = rl.is_key_down(rl.KEY_W) or rl.is_gamepad_button_down(self.gamepad, rl.GAMEPAD_BUTTON_RIGHT_FACE_DOWN)

        move_x = rl.get_gamepad_axis_movement(self.gamepad, rl.GAMEPAD_AXIS_LEFT_X)
        if abs(move_x) < deadzone:
            move_x = 0.0
        if rl.is_key_down(rl.KEY_D) or rl.is_gamepad_button_down(self.gamepad, rl.GAMEPAD_BUTTON_LEFT_FACE_RIGHT):
            move_x = 1.0
        elif rl.is_key_down(rl.KEY_A) or rl.is_gamepad_button_down(self.gamepad, rl.GAMEPAD_BUTTON_LEFT_FACE_LEFT):
            move_x = -1.0

        self.movement.set_input(move_x, jump_pressed, jump_held)

        if self.movement.grounded and jump_pressed:
            self.jump_sound.play()

        if abs(self.movement.move_x) > 0.1:
            self.animation.play("run")
            self.animation.flip_x = self.movement.move_x < 0.0
        else:
            self.animation.play("idle")

        if not self.movement.grounded:
            if self.player_number != 3:
                if self.body.get_velocity_meters().y < 0.0:
                    self.animation.play("jump")
                else:
                    self.animation.play("fall")
            else:
                self.animation.play("jump")

        move_y = rl.get_gamepad_axis_movement(self.gamepad, rl.GAMEPAD_AXIS_LEFT_Y)
        if rl.is_key_pressed(rl.KEY_S) or rl.is_gamepad_button_pressed(self.gamepad, rl.GAMEPAD_BUTTON_LEFT_FACE_DOWN) or move_y > 0.5:
            self.fall_through = True
            self.fall_through_timer = self.fall_through_duration

        if self.fall_through_timer > 0.0:
            self.fall_through_timer = max(0.0, self.fall_through_timer - delta_time)
            if self.fall_through_timer == 0.0:
                self.fall_through = False

        if rl.is_key_pressed(rl.KEY_SPACE) or rl.is_gamepad_button_pressed(self.gamepad, rl.GAMEPAD_BUTTON_RIGHT_FACE_RIGHT):
            self.attack = True
            self.attack_display_timer = self.attack_display_duration
            position = self.body.get_position_pixels()
            position.x += (self.width / 2.0 + 8.0) * (-1.0 if self.animation.flip_x else 1.0)
            bodies = self.physics.circle_overlap(position, 8.0, self.body.body)
            for other_body in bodies:
                if other_body == self.body.body:
                    continue
                impulse = b2Vec2(-10.0 if self.animation.flip_x else 10.0, -10.0)
                other_body.ApplyLinearImpulse(impulse=impulse, point=other_body.worldCenter, wake=True)
                self.hit_sound.play()

        if self.attack_display_timer > 0.0:
            self.attack_display_timer = max(0.0, self.attack_display_timer - delta_time)
            if self.attack_display_timer == 0.0:
                self.attack = False

        if self.body.get_position_pixels().y > self.level.get_size().y + 200.0:
            self.body.set_position(self.p.position)
            self.body.set_velocity(v2(0.0, 0.0))
            self.die_sound.play()

    def draw(self) -> None:
        """Draw attack indicator (animations are drawn by controller).

        Returns:
            None
        """
        if self.attack:
            position = self.body.get_position_pixels()
            position.x += (self.width / 2.0 + 8.0) * (-1.0 if self.animation.flip_x else 1.0)
            rl.draw_circle_v(position, 8.0, rl.Color(230, 41, 55, 128))

    def pre_solve(self, body_a, body_b, contact, platforms: List[StaticBox]) -> bool:
        """Custom pre-solve for one-way platforms.

        Args:
            body_a: First body in the contact.
            body_b: Second body in the contact.
            contact: Box2D contact instance.
            platforms: List of one-way platform StaticBox objects.

        Returns:
            True to enable contact, False to disable it.
        """
        normal = contact.worldManifold.normal
        other = None
        sign = 0.0
        if body_a == self.body.body:
            sign = 1.0
            other = body_b
        elif body_b == self.body.body:
            sign = -1.0
            other = body_a
        if sign * normal.y < 0.5:
            return False
        if self.fall_through:
            for platform in platforms:
                if other == platform.body:
                    return False
        return True


class FightingContactListener(b2ContactListener):
    """Routes Box2D PreSolve callbacks to the owning character."""
    def __init__(self, scene: "FightingScene") -> None:
        """Capture the scene so contacts can be filtered.

        Args:
            scene: Scene containing fighters and one-way platforms.

        Returns:
            None
        """
        super().__init__()
        self.scene = scene

    def PreSolve(self, contact, old_manifold):
        """Dispatch pre-solve handling to the matching character.

        Args:
            contact: Box2D contact.
            old_manifold: Previous contact manifold.

        Returns:
            None
        """
        body_a = contact.fixtureA.body
        body_b = contact.fixtureB.body
        for character in self.scene.characters:
            if body_a == character.body.body or body_b == character.body.body:
                enabled = character.pre_solve(body_a, body_b, contact, self.scene.platforms)
                contact.enabled = enabled
                return


class FightingScene(Scene):
    """Scene demonstrating shared camera and arena combat."""
    def __init__(self) -> None:
        """Initialize scene storage for platforms, fighters, and services.

        Returns:
            None
        """
        super().__init__()
        self.platforms: List[StaticBox] = []
        self.characters: List[FightingCharacter] = []
        self.level: LevelService = None  # type: ignore[assignment]
        self.physics: PhysicsService = None  # type: ignore[assignment]
        self.camera: CameraObject = None  # type: ignore[assignment]
        self.renderer = None
        self.render_rect = None

    def init_services(self) -> None:
        """Register services required by the scene.

        Returns:
            None
        """
        self.add_service(TextureService)
        self.add_service(SoundService)
        self.physics = self.add_service(PhysicsService)
        collision_names = ["walls"]
        self.level = self.add_service(LevelService, "assets/levels/fighting.ldtk", "Stage", collision_names)

    def init(self) -> None:
        """Create platforms, players, camera, and render target.

        Returns:
            None
        """
        platform_entities = self.level.get_entities_by_name("One_way_platform")
        for platform_entity in platform_entities:
            position = self.level.convert_to_pixels(platform_entity.getPosition())
            size = self.level.convert_to_pixels(platform_entity.getSize())
            platform = self.add_game_object(StaticBox.from_vectors(vec_add(position, vec_div(size, 2.0)), size))
            platform.is_visible = False
            platform.add_tag("platform")
            self.platforms.append(platform)

        if self.physics.world:
            self.physics.world.contactListener = FightingContactListener(self)

        player_entities = self.level.get_entities_by_name("Start")
        for i, player_entity in enumerate(player_entities[:4]):
            params = CharacterParams()
            params.position = self.level.convert_to_pixels(player_entity.getPosition())
            params.width = 16
            params.height = 24
            character = self.add_game_object(FightingCharacter(params, i + 1))
            character.add_tag("character")
            self.characters.append(character)

        self.camera = self.add_game_object(CameraObject(self.level.get_size(), v2(0.0, 0.0), v2(300.0, 300.0), 0.0, 0.0, 0.0, 0.0))
        self.camera.target = vec_div(self.level.get_size(), 2.0)

        self.level.set_layer_visibility("Background", False)
        self.renderer = rl.load_render_texture(int(self.level.get_size().x), int(self.level.get_size().y))

    def update(self, delta_time: float) -> None:
        """Update camera framing and compute render placement.

        Args:
            delta_time: Seconds since last frame.

        Returns:
            None
        """
        avg_position = v2(0.0, 0.0)
        min_point = v2(float("inf"), float("inf"))
        max_point = v2(float("-inf"), float("-inf"))
        for character in self.characters:
            char_pos = character.body.get_position_pixels()
            avg_position = vec_add(avg_position, char_pos)
            min_point.x = min(min_point.x, char_pos.x)
            min_point.y = min(min_point.y, char_pos.y)
            max_point.x = max(max_point.x, char_pos.x)
            max_point.y = max(max_point.y, char_pos.y)
        avg_position = vec_div(avg_position, float(len(self.characters)))
        self.camera.target = avg_position
        self.camera.target.x = math.floor(self.camera.target.x)
        self.camera.target.y = math.floor(self.camera.target.y)

        distance = math.sqrt((max_point.x - min_point.x) ** 2 + (max_point.y - min_point.y) ** 2)
        level_diag = math.sqrt(self.level.get_size().x ** 2 + self.level.get_size().y ** 2)
        zoom = level_diag / (distance + 400.0)
        zoom = max(0.5, min(2.0, zoom))
        self.camera.camera.zoom += (zoom - self.camera.camera.zoom) * min(1.0, delta_time * 5.0)

        aspect_ratio = self.level.get_size().x / self.level.get_size().y
        render_scale = rl.get_screen_height() / self.level.get_size().y
        render_size = v2(self.level.get_size().y * render_scale * aspect_ratio, self.level.get_size().y * render_scale)
        pos = vec_div(vec_sub(v2(float(rl.get_screen_width()), float(rl.get_screen_height())), render_size), 2.0)
        self.render_rect = rl.Rectangle(pos.x, pos.y, render_size.x, render_size.y)

        # Trigger scene change on Enter key or gamepad start button.
        if rl.is_key_pressed(rl.KEY_ENTER) or rl.is_gamepad_button_pressed(0, rl.GAMEPAD_BUTTON_MIDDLE_RIGHT):
            self.game.go_to_scene_next()

    def draw_scene(self) -> None:
        """Render the world to a texture and draw it centered.

        Returns:
            None
        """
        rl.begin_texture_mode(self.renderer)
        rl.clear_background(rl.MAGENTA)
        self.level.draw_layer("Background")
        self.camera.draw_begin()
        super().draw_scene()
        self.camera.draw_end()
        rl.end_texture_mode()

        rl.draw_texture_pro(self.renderer.texture,
                       rl.Rectangle(0.0, 0.0, float(self.renderer.texture.width), -float(self.renderer.texture.height)),
                       self.render_rect,
                       v2(0.0, 0.0),
                       0.0,
                       rl.Color(255, 255, 255, 255))
