use rand::seq::IteratorRandom;
use roast2d::{
    prelude::{glam::Vec3Swizzles, *},
    renderer::post::RetroShader,
};

#[cfg(target_arch = "wasm32")]
fn init_wasm_console() {
    use console_error_panic_hook::set_once as set_panic_hook;
    use log::Level;

    let _ = console_log::init_with_level(Level::Info);
    set_panic_hook();
}

const BALL_MIN_VEL: f32 = 120.0;
const BALL_MAX_VEL: f32 = BALL_MIN_VEL * 2.5;
const MAX_SPEED: f32 = 400.0;
const FRICTION: f32 = 4.0;
const BRICK_SIZE: Vec2 = Vec2::new(64., 32.);
const BRICK_DYING: f32 = 0.3;
const BALL_Z: f32 = 0.1;
const BRICK_Z: f32 = 0.2;
const UI_Z: f32 = 0.3;
const VIEW_SIZE: Vec2 = Vec2::new(800., 600.);
const SHAKE_DURATION: f32 = 0.25;
const GRAVITY: f32 = 35.0;

#[derive(Clone)]
pub struct Particle {
    transform: Transform,
    velocity: Vec2,
    color: Color,
    life: f32,
    max_life: f32,
}

#[derive(Clone)]
pub struct ShakeSample {
    time: f32,
    offset: Vec2,
}

pub struct World {
    pub player: Player,
    pub ball: Ball,
    pub bricks: Vec<Brick>,
    pub score: usize,
    pub shake_samples: Vec<ShakeSample>,
    pub shake_current_index: usize,
    pub shake_total_time: f32,
    pub particles: Vec<Particle>,
}

impl World {
    pub fn setup(view: Vec2) -> Self {
        let lb_pos = -view * 0.5;

        let player = Player::new(lb_pos + Vec2::new(108.0, 8.0));
        let ball = Ball::new(lb_pos + Vec2::new(40.0, 64.0));

        let padding = 5.;
        let row_gap = 5.;
        let offset_x = view.x % (BRICK_SIZE.x + padding) * 0.5;
        let cols = (view.x / (BRICK_SIZE.x + padding)) as i32;
        let rows = 6;

        let mut bricks = Vec::new();
        for i in 0..cols {
            for j in 0..rows {
                let pos = Vec2::new(
                    (i as f32 + 0.5) * (BRICK_SIZE.x + padding) + offset_x,
                    view.y - (j as f32 + 0.5) * (BRICK_SIZE.y + row_gap),
                );
                let brick = Brick::new(lb_pos + pos);
                bricks.push(brick);
            }
        }

        // Start with empty shake samples
        let shake_samples = Vec::new();

        Self {
            player,
            ball,
            bricks,
            score: 0,
            shake_samples,
            shake_current_index: 0,
            shake_total_time: 0.0,
            particles: Vec::new(),
        }
    }
}

#[derive(Clone)]
pub struct Velocity {
    accel: Vec2,
    friction: Vec2,
    velocity: Vec2,
}

pub struct Ball {
    color: Color,
    transform: Transform,
    size: Vec2,
    velocity: Velocity,
}

impl Ball {
    pub fn new(pos: Vec2) -> Self {
        let size = Vec2::new(32., 32.0);
        let color = Color::rgb_u8(0xfb, 0xf2, 0x36);

        Self {
            color,
            transform: Transform::new(pos.extend(BALL_Z)),
            size,
            velocity: Velocity {
                accel: Vec2::ZERO,
                velocity: Vec2::new(0.0, -BALL_MAX_VEL),
                friction: Vec2::splat(0.9),
            },
        }
    }

    fn draw(g: &mut Engine, w: &World) -> Result<()> {
        let ball = &w.ball;
        let transform = ball.transform;

        // Draw trail effect
        let vel = ball.velocity.velocity;
        let speed = vel.length();
        let trail_length = (speed / BALL_MAX_VEL).min(1.0) * 5.0;

        for i in 1..=3 {
            let alpha = 0.3 / i as f32;
            let offset = -vel.normalize() * trail_length * i as f32 * 4.0;
            let trail_pos = transform.pos.xy() + offset;
            let trail_color = Color::rgba_u8(0xfb, 0xf2, 0x36, (alpha * 255.0) as u8);
            let trail_transform = Transform::new(trail_pos.extend(BALL_Z - 0.01));
            g.draw_rect(
                trail_color,
                ball.size * (1.0 - 0.1 * i as f32),
                None,
                trail_transform,
            );
        }

        // Draw main ball
        let color = ball.color;
        g.draw_rect(color, ball.size, None, transform);
        Ok(())
    }

    fn handle_player_collision(
        old_pos: Vec3,
        half_size: Vec2,
        bounds: Rect,
        vel: &mut Vec2,
        player_vel: Vec2,
    ) -> Vec3 {
        let ball_center = old_pos.xy();
        let player_bounds = bounds;
        let mut new_pos = old_pos;

        // Ensure ball is coming from above (moving downward)
        if vel.y <= 0.0 {
            // Calculate where on the paddle the ball hit (-1 to 1)
            let paddle_center_x = (player_bounds.min.x + player_bounds.max.x) * 0.5;
            let hit_position =
                (ball_center.x - paddle_center_x) / (player_bounds.max.x - player_bounds.min.x);

            // Angle-based deflection with upward bias
            let max_angle = std::f32::consts::PI * 0.75; // Max angle of deflection (135 degrees)
            let bounce_angle = hit_position * max_angle;

            // Calculate new velocity based on angle - always upward
            let speed = vel.length().clamp(BALL_MIN_VEL, BALL_MAX_VEL);
            let mut new_vel_x = speed * bounce_angle.sin();
            let new_vel_y = speed * bounce_angle.cos().abs(); // Always positive (upward)

            // Add spin effect based on paddle movement
            let spin_factor = 0.3;
            new_vel_x += player_vel.x * spin_factor;

            // Ensure minimum horizontal velocity
            if new_vel_x.abs() < BALL_MIN_VEL * 0.3 {
                new_vel_x = new_vel_x.signum() * BALL_MIN_VEL * 0.3;
            }

            vel.x = new_vel_x;
            vel.y = new_vel_y;

            // Position correction - place ball just above paddle
            new_pos.y = player_bounds.max.y + half_size.y + 1.0; // Add small offset to prevent sticking
        } else {
            // Ball is moving upward, treat as side collision
            let mut new_vel_x = vel.x;

            // Add spin effect
            let spin_factor = 0.5;
            new_vel_x += player_vel.x * spin_factor;

            vel.x = new_vel_x;
            vel.y *= 0.8; // Reduce upward velocity slightly

            // Position correction based on side hit
            if ball_center.x < player_bounds.min.x {
                new_pos.x = player_bounds.min.x - half_size.x;
            } else if ball_center.x > player_bounds.max.x {
                new_pos.x = player_bounds.max.x + half_size.x;
            }
        }

        new_pos
    }

    fn handle_brick_collision(
        old_pos: Vec3,
        half_size: Vec2,
        bounds: Rect,
        vel: &mut Vec2,
    ) -> Vec3 {
        let mut new_pos = old_pos;
        let restitution = 1.05; // Slightly reduced for more controlled bounces
        let ball_center = old_pos.xy();

        // More accurate collision response based on collision side
        let overlap_x = if vel.x > 0.0 {
            bounds.min.x - (ball_center.x + half_size.x)
        } else {
            bounds.max.x - (ball_center.x - half_size.x)
        };

        let overlap_y = if vel.y > 0.0 {
            bounds.min.y - (ball_center.y + half_size.y)
        } else {
            bounds.max.y - (ball_center.y - half_size.y)
        };

        // Determine which side was hit based on overlap depth
        if overlap_x.abs() < overlap_y.abs() {
            // Horizontal collision
            new_pos.x = if vel.x > 0.0 {
                bounds.min.x - half_size.x
            } else {
                bounds.max.x + half_size.x
            };
            vel.x = -vel.x * restitution;

            // Add slight vertical variation based on hit position
            let brick_center_y = (bounds.min.y + bounds.max.y) * 0.5;
            let hit_offset = (ball_center.y - brick_center_y) / (bounds.max.y - bounds.min.y);
            vel.y += hit_offset * 50.0;
        } else {
            // Vertical collision
            new_pos.y = if vel.y > 0.0 {
                bounds.min.y - half_size.y
            } else {
                bounds.max.y + half_size.y
            };
            vel.y = -vel.y * restitution;

            // Add slight horizontal variation based on hit position
            let brick_center_x = (bounds.min.x + bounds.max.x) * 0.5;
            let hit_offset = (ball_center.x - brick_center_x) / (bounds.max.x - bounds.min.x);
            vel.x += hit_offset * 50.0;
        }

        // Clamp velocity to maintain reasonable speeds
        let speed = vel.length();
        if speed > BALL_MAX_VEL * 1.2 {
            *vel = vel.normalize() * BALL_MAX_VEL * 1.2;
        }

        new_pos
    }

    fn update(g: &mut Engine, w: &mut World) -> Result<()> {
        let ball = &mut w.ball;
        let view = g.view_size();
        let vel = &mut ball.velocity;

        // update velocity
        vel.velocity += vel.accel * g.tick;
        let new_vel = vel.velocity;
        vel.velocity *= vel.friction;
        let mut vel = new_vel;

        // update position
        let tf = &mut ball.transform;
        let old_pos = tf.pos;
        let mut new_pos = old_pos + (vel * g.tick).extend(0.0);

        // Create a swept bounds that encompasses the entire movement path
        let half_size = (ball.size * tf.scale) * 0.5;
        let min_x = old_pos.x.min(new_pos.x) - half_size.x;
        let max_x = old_pos.x.max(new_pos.x) + half_size.x;
        let min_y = old_pos.y.min(new_pos.y) - half_size.y;
        let max_y = old_pos.y.max(new_pos.y) + half_size.y;
        let swept_bounds = Rect {
            min: Vec2::new(min_x, min_y),
            max: Vec2::new(max_x, max_y),
        };

        // Check player collisions first
        let player = &w.player;
        let bounds = player
            .transform
            .bounds_with_anchor_and_size(Vec2::splat(0.5), player.size * player.transform.scale);
        let player_vel = player.velocity.velocity;
        if swept_bounds.is_touching(&bounds) {
            new_pos =
                Self::handle_player_collision(old_pos, half_size, bounds, &mut vel, player_vel);
            new_pos += (vel * g.tick).extend(0.0);
        }

        // Then check brick collisions
        let mut touched_brick = None;
        for (i, brick) in w.bricks.iter().enumerate() {
            let bounds = brick
                .transform
                .bounds_with_anchor_and_size(Vec2::splat(0.5), BRICK_SIZE * brick.transform.scale);
            if swept_bounds.is_touching(&bounds) {
                new_pos = Self::handle_brick_collision(old_pos, half_size, bounds, &mut vel);
                new_pos += (vel * g.tick).extend(0.0);
                touched_brick = Some(i);
                break;
            }
        }

        // touch brick
        if let Some(id) = touched_brick {
            Brick::touch(w, id)?;
        }

        // Handle x bounds
        let half_view = view * 0.5;
        if new_pos.x > half_view.x {
            new_pos.x = half_view.x - half_size.x;
            vel.x = -BALL_MAX_VEL;
        } else if new_pos.x < -half_view.x {
            new_pos.x = -half_view.x + half_size.x;
            vel.x = BALL_MAX_VEL;
        }

        // Handle y bounds
        if new_pos.y > half_view.y {
            new_pos.y = half_view.y - half_size.y;
            vel.y = -BALL_MAX_VEL;
        } else if new_pos.y < -half_view.y {
            new_pos.y = -half_view.y + half_size.y;
            vel.y = BALL_MAX_VEL;
        }

        // Apply slight gravity for more natural feel
        vel.y -= GRAVITY * g.tick;

        // Clamp velocity
        let speed = vel.length();
        if speed > BALL_MAX_VEL {
            vel = vel.normalize() * BALL_MAX_VEL;
        }
        if speed < BALL_MIN_VEL {
            vel = vel.normalize() * BALL_MIN_VEL;
        }

        // Update transform and velocity separately
        let ball = &mut w.ball;
        ball.transform.pos = new_pos;
        ball.velocity.velocity = vel;

        Ok(())
    }
}

pub struct Brick {
    transform: Transform,
    hit: bool,
    dying: f32,
    dead_pos: Vec2,
    color: Color,
}

impl Brick {
    pub fn new(pos: Vec2) -> Self {
        let color = Color::rgb_u8(0x5b, 0x6e, 0xe1);
        let transform = Transform::new(pos.extend(BRICK_Z));
        Self {
            hit: false,
            dying: 0.0,
            dead_pos: Vec2::default(),
            color,
            transform,
        }
    }

    fn draw(g: &mut Engine, w: &World, id: usize) -> Result<()> {
        let brick = &w.bricks[id];
        let t = brick.transform;
        let color = brick.color;
        g.draw_rect(color, BRICK_SIZE, None, t);
        Ok(())
    }

    fn is_dead(&self) -> bool {
        self.dying > BRICK_DYING
    }

    fn generate_particles(w: &mut World, brick_pos: Vec2, brick_color: Color) {
        const PARTICLE_COUNT: usize = 8;
        for _ in 0..PARTICLE_COUNT {
            let angle = rand::random::<f32>() * 2.0 * std::f32::consts::PI;
            let speed = 250.0 + rand::random::<f32>() * 100.0;
            let velocity = Vec2::new(angle.cos(), angle.sin()) * speed;
            let particle = Particle {
                transform: Transform::new(brick_pos.extend(BRICK_Z + 0.01)),
                velocity,
                color: brick_color,
                life: 1.35,
                max_life: 1.35,
            };
            w.particles.push(particle);
        }
    }

    fn update(g: &mut Engine, w: &mut World, id: usize) -> Result<()> {
        let brick = &mut w.bricks[id];
        let is_hit = brick.hit;
        if is_hit {
            brick.dying += g.tick;

            let progress = (brick.dying / BRICK_DYING).powi(2);
            let color = {
                let c1 = Color::rgb_u8(0x5b, 0x6e, 0xe1);
                let c2 = RED;
                let r = c1.r + ((c1.r - c2.r) * progress).abs();
                let g = c1.g + ((c1.g - c2.g) * progress).abs();
                let b = c1.b + ((c1.b - c2.b) * progress).abs();
                Color::rgb(r, g, b)
            };
            let scale = {
                let start = 1.0;
                let end = start * 0.5;
                start - (start - end) * progress
            };
            brick.color = color;
            brick.transform.scale = Vec2::splat(scale);
        }
        Ok(())
    }

    fn touch(w: &mut World, id: usize) -> Result<()> {
        let brick = &mut w.bricks[id];
        if !brick.hit {
            brick.hit = true;
            brick.dead_pos = brick.transform.pos.xy();

            // Create particles at brick position
            let brick_pos = brick.transform.pos.xy();
            let brick_color = brick.color;
            Brick::generate_particles(w, brick_pos, brick_color);
            generate_screen_shake_samples(w);
        }
        Ok(())
    }
}

pub struct Player {
    color: Color,
    transform: Transform,
    size: Vec2,
    velocity: Velocity,
}

impl Player {
    pub fn new(pos: Vec2) -> Self {
        let size = Vec2::new(120.0, 32.0);
        let color = Color::rgb_u8(0x37, 0x94, 0x6e);
        Self {
            color,
            transform: Transform::new(pos.extend(UI_Z)),
            size,
            velocity: Velocity {
                accel: Vec2::ZERO,
                velocity: Vec2::ZERO,
                friction: Vec2::splat(FRICTION),
            },
        }
    }

    fn draw(g: &mut Engine, w: &World) -> Result<()> {
        let player = &w.player;
        let t = player.transform;
        let color = player.color;
        g.draw_rect(color, player.size, None, t);
        Ok(())
    }

    fn update(g: &mut Engine, w: &mut World) -> Result<()> {
        let player = &mut w.player;
        let vel = &mut player.velocity;

        let input = g.input();

        // Apply acceleration based on input
        const ACCELERATION: f32 = 800.0;
        const FRICTION: f32 = 8.0;

        // Calculate desired acceleration
        let mut acceleration = Vec2::ZERO;
        let right = input.key_down(KeyCode::Right) || input.key_down(KeyCode::KeyD);
        let left = input.key_down(KeyCode::Left) || input.key_down(KeyCode::KeyA);
        if right {
            acceleration.x += ACCELERATION;
        }
        if left {
            acceleration.x -= ACCELERATION;
        }

        // Apply friction when no input
        if acceleration.x == 0.0 {
            let friction_force = FRICTION * vel.velocity.x.signum() * vel.velocity.x.abs();
            acceleration.x -= friction_force;
        }

        // Update velocity with acceleration
        vel.velocity += acceleration * g.tick;

        // Clamp to max speed
        vel.velocity.x = vel.velocity.x.clamp(-MAX_SPEED, MAX_SPEED);

        // update position
        let tf = &mut player.transform;
        tf.pos += (vel.velocity * g.tick).extend(0.0);

        // clamp player position to screen
        let half_view = g.view_size() * 0.5;
        let half_size = (player.size * tf.scale) * 0.5;
        tf.pos = tf
            .pos
            .xy()
            .clamp(-half_view + half_size, half_view - half_size)
            .extend(tf.pos.z);

        Ok(())
    }
}

fn generate_screen_shake_samples(w: &mut World) {
    let sample_count = (5..10).choose(&mut rand::rng()).unwrap();
    w.shake_samples.clear();
    w.shake_current_index = 0;
    w.shake_total_time = 0.0;

    let mut current_time = 0.0;
    let time_step = SHAKE_DURATION / (sample_count - 1) as f32;

    for i in 0..sample_count {
        let intensity = (1.0 - (i as f32 / (sample_count - 1) as f32)) * 6.0;
        let x = (rand::random::<f32>() - 0.5) * 2.0 * intensity;
        let y = (rand::random::<f32>() - 0.5) * 2.0 * intensity;

        w.shake_samples.push(ShakeSample {
            time: current_time,
            offset: Vec2::new(x, y),
        });

        current_time += time_step;
    }

    // Ensure last sample brings camera back to 0
    w.shake_samples.push(ShakeSample {
        time: SHAKE_DURATION,
        offset: Vec2::ZERO,
    });
}

pub struct Demo {
    frames: f32,
    timer: f32,
    interval: f32,
    fps: f32,
    w: World,
}

impl Demo {
    pub fn new(w: World) -> Self {
        Self {
            frames: 0.0,
            timer: 0.0,
            interval: 1.0,
            fps: 0.0,
            w,
        }
    }
}

impl Scene for Demo {
    fn init(&mut self, g: &mut Engine) {
        log::info!("Init Demo {:?}", g.view_size());
    }

    fn update(&mut self, g: &mut Engine) {
        let w = &mut self.w;

        // Exit on ESC
        if g.input().key_down(KeyCode::Escape) {
            g.exit();
            return;
        }

        // Update camera shake
        if !w.shake_samples.is_empty() {
            w.shake_total_time += g.tick;

            // Find and apply the next sample
            while w.shake_current_index < w.shake_samples.len() {
                let sample = &w.shake_samples[w.shake_current_index];
                if w.shake_total_time >= sample.time {
                    // Apply this sample's offset
                    g.camera_mut().set_pos(sample.offset);
                    w.shake_current_index += 1;
                } else {
                    break;
                }
            }

            // Reset camera and clear samples when done
            if w.shake_current_index >= w.shake_samples.len() {
                g.camera_mut().set_pos(Vec2::ZERO);
                w.shake_samples.clear();
                w.shake_current_index = 0;
                w.shake_total_time = 0.0;
            }
        }

        // update player
        Player::update(g, w).unwrap();

        // update ball
        Ball::update(g, w).unwrap();

        // update bricks
        let mut removed = Vec::new();
        for id in 0..w.bricks.len() {
            Brick::update(g, w, id).unwrap();
            if w.bricks[id].is_dead() {
                removed.push(id);
            }
        }

        // remove bricks in reverse order
        removed.sort();
        for id in removed.iter().rev() {
            w.score += 1;
            w.bricks.swap_remove(*id);
        }

        // update particles
        let dt = g.tick;
        let mut to_remove = Vec::new();
        for (i, particle) in w.particles.iter_mut().enumerate() {
            // Update life
            particle.life -= dt;
            if particle.life <= 0.0 {
                to_remove.push(i);
                continue;
            }

            // Update position with velocity and gravity
            particle.transform.pos += (particle.velocity * dt).extend(0.0);
            particle.velocity.y -= 200.0 * dt; // Gravity
            particle.velocity *= 0.98; // Air resistance
        }

        // Remove dead particles
        for &id in to_remove.iter().rev() {
            w.particles.swap_remove(id);
        }

        // FPS
        self.frames += 1.0;
        self.timer += g.tick;
        if self.timer > self.interval {
            self.fps = self.frames / self.timer;
            self.timer = 0.;
            self.frames = 0.;
        }
    }

    fn draw(&mut self, g: &mut Engine) {
        let w = &mut self.w;

        // draw particles
        for particle in &w.particles {
            let alpha = (particle.life / particle.max_life).powf(2.0);
            let size = 32.0 * alpha;
            let color = particle.color.with_a(alpha);
            let transform = Transform::new(particle.transform.pos);
            g.draw_rect(color, Vec2::splat(size), None, transform);
        }

        // draw ball
        Ball::draw(g, w).unwrap();

        // draw bricks
        for id in 0..w.bricks.len() {
            Brick::draw(g, w, id).unwrap();
        }

        // draw player
        Player::draw(g, w).unwrap();

        g.with_ui(|g| {
            let view = g.view_size();
            let half_view = view * 0.5;
            // Score
            g.draw_text(
                &Text::new(format!("Score: {}", w.score), 20.0, WHITE),
                Some(Vec2::new(0.0, 1.0)),
                Transform::new(Vec3::new(-half_view.x, half_view.y, UI_Z)),
            );
            // FPS
            g.draw_text(
                &Text::new(format!("FPS: {:.2}", self.fps), 20.0, WHITE),
                Some(Vec2::new(0.0, 1.0)),
                Transform::new(Vec3::new(half_view.x - 160.0, half_view.y, UI_Z)),
            );
        });
    }

    fn cleanup(&mut self, _g: &mut Engine) {}
}

fn setup(g: &mut Engine) {
    // set resize and scale
    g.set_scale_mode(ScaleMode::Fixed {
        width: VIEW_SIZE.x as u32,
        height: VIEW_SIZE.y as u32,
    });
    g.set_scene(Demo::new(World::setup(VIEW_SIZE)));

    let arg = std::env::args().nth(1).unwrap_or_else(|| "crt".to_string());
    if arg == "crt" {
        g.set_post_renderer(Some(RetroShader::setup(
            Default::default(),
            g.backend_state().clone(),
        )));
    }
}

fn run() {
    let app = App::default()
        .title("Hello Roast2D".to_string())
        .window(VIEW_SIZE.as_uvec2())
        .vsync(true)
        .resizable(false);
    app.run(setup).expect("Start game");
}

fn main() {
    #[cfg(target_arch = "wasm32")]
    init_wasm_console();
    #[cfg(not(target_arch = "wasm32"))]
    env_logger::init();
    run();
}
