use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};

pub const MAX_PLAYERS: usize = 4;
pub const PLAYER_HEALTH: i16 = 100;
pub const PLAYER_RADIUS: f32 = 0.18;
pub const MOVE_SPEED: f32 = 3.0;
pub const TURN_SPEED: f32 = 2.35;
pub const FOV: f32 = std::f32::consts::FRAC_PI_3;
pub const MAX_VIEW_DISTANCE: f32 = 24.0;

pub const BUTTON_FORWARD: u16 = 1 << 0;
pub const BUTTON_BACK: u16 = 1 << 1;
pub const BUTTON_STRAFE_LEFT: u16 = 1 << 2;
pub const BUTTON_STRAFE_RIGHT: u16 = 1 << 3;

#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
pub struct Vec2 {
    pub x: f32,
    pub y: f32,
}

impl Vec2 {
    pub const fn new(x: f32, y: f32) -> Self {
        Self { x, y }
    }

    pub fn distance(self, other: Vec2) -> f32 {
        let dx = self.x - other.x;
        let dy = self.y - other.y;
        (dx * dx + dy * dy).sqrt()
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tile {
    Wall,
    Floor,
}

#[derive(Debug, Clone)]
pub struct GameMap {
    pub width: usize,
    pub height: usize,
    tiles: Vec<Tile>,
    pub spawns: Vec<Vec2>,
    pub source: String,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RayHit {
    pub distance: f32,
    pub hit: bool,
    pub side: RaySide,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RaySide {
    Vertical,
    Horizontal,
    None,
}

impl GameMap {
    pub fn parse(source: &str) -> Result<Self> {
        let lines: Vec<&str> = source
            .lines()
            .filter(|line| !line.trim().is_empty())
            .collect();
        if lines.is_empty() {
            bail!("map is empty");
        }

        let height = lines.len();
        let width = lines
            .iter()
            .map(|line| line.chars().count())
            .max()
            .context("map has no width")?;
        if width == 0 {
            bail!("map has no width");
        }

        let mut tiles = vec![Tile::Wall; width * height];
        let mut spawns = Vec::new();

        for (y, line) in lines.iter().enumerate() {
            for (x, ch) in line.chars().enumerate() {
                let tile = match ch {
                    '#' => Tile::Wall,
                    '.' => Tile::Floor,
                    '1'..='4' => {
                        spawns.push(Vec2::new(x as f32 + 0.5, y as f32 + 0.5));
                        Tile::Floor
                    }
                    ' ' => Tile::Wall,
                    other => bail!("unsupported map character '{other}' at {x},{y}"),
                };
                tiles[y * width + x] = tile;
            }
        }

        if spawns.len() < MAX_PLAYERS {
            bail!("map needs at least {MAX_PLAYERS} spawn points");
        }

        Ok(Self {
            width,
            height,
            tiles,
            spawns,
            source: source.to_owned(),
        })
    }

    pub fn spawn_for(&self, id: usize) -> Vec2 {
        self.spawns[id % self.spawns.len()]
    }

    pub fn tile_at(&self, x: i32, y: i32) -> Tile {
        if x < 0 || y < 0 || x >= self.width as i32 || y >= self.height as i32 {
            return Tile::Wall;
        }
        self.tiles[y as usize * self.width + x as usize]
    }

    pub fn is_wall_cell(&self, x: i32, y: i32) -> bool {
        self.tile_at(x, y) == Tile::Wall
    }

    pub fn is_blocked(&self, pos: Vec2, radius: f32) -> bool {
        let checks = [
            Vec2::new(pos.x - radius, pos.y - radius),
            Vec2::new(pos.x + radius, pos.y - radius),
            Vec2::new(pos.x - radius, pos.y + radius),
            Vec2::new(pos.x + radius, pos.y + radius),
        ];

        checks
            .iter()
            .any(|p| self.is_wall_cell(p.x.floor() as i32, p.y.floor() as i32))
    }

    pub fn raycast(&self, origin: Vec2, angle: f32, max_distance: f32) -> RayHit {
        let dir_x = angle.cos();
        let dir_y = angle.sin();
        let mut map_x = origin.x.floor() as i32;
        let mut map_y = origin.y.floor() as i32;

        let delta_x = if dir_x.abs() < 0.000_001 {
            f32::INFINITY
        } else {
            (1.0 / dir_x).abs()
        };
        let delta_y = if dir_y.abs() < 0.000_001 {
            f32::INFINITY
        } else {
            (1.0 / dir_y).abs()
        };

        let (step_x, mut side_x) = if dir_x < 0.0 {
            (-1, (origin.x - map_x as f32) * delta_x)
        } else {
            (1, (map_x as f32 + 1.0 - origin.x) * delta_x)
        };

        let (step_y, mut side_y) = if dir_y < 0.0 {
            (-1, (origin.y - map_y as f32) * delta_y)
        } else {
            (1, (map_y as f32 + 1.0 - origin.y) * delta_y)
        };

        let mut distance = 0.0;

        while distance <= max_distance {
            let side = if side_x < side_y {
                side_x += delta_x;
                map_x += step_x;
                distance = if dir_x.abs() < 0.000_001 {
                    max_distance
                } else {
                    (map_x as f32 - origin.x + (1 - step_x) as f32 * 0.5) / dir_x
                };
                RaySide::Vertical
            } else {
                side_y += delta_y;
                map_y += step_y;
                distance = if dir_y.abs() < 0.000_001 {
                    max_distance
                } else {
                    (map_y as f32 - origin.y + (1 - step_y) as f32 * 0.5) / dir_y
                };
                RaySide::Horizontal
            };

            if self.is_wall_cell(map_x, map_y) {
                return RayHit {
                    distance: distance.abs().min(max_distance),
                    hit: true,
                    side,
                };
            }
        }

        RayHit {
            distance: max_distance,
            hit: false,
            side: RaySide::None,
        }
    }
}

pub fn normalize_angle(mut angle: f32) -> f32 {
    while angle > std::f32::consts::PI {
        angle -= std::f32::consts::TAU;
    }
    while angle < -std::f32::consts::PI {
        angle += std::f32::consts::TAU;
    }
    angle
}

pub fn angle_diff(target: f32, source: f32) -> f32 {
    normalize_angle(target - source)
}

pub fn apply_input(
    map: &GameMap,
    position: Vec2,
    angle: f32,
    buttons: u16,
    turn: i8,
    dt: f32,
) -> (Vec2, f32) {
    let next_angle = normalize_angle(angle + turn as f32 * TURN_SPEED * dt);
    let forward = button_axis(buttons, BUTTON_FORWARD, BUTTON_BACK);
    let strafe = button_axis(buttons, BUTTON_STRAFE_RIGHT, BUTTON_STRAFE_LEFT);

    let mut move_x = next_angle.cos() * forward - next_angle.sin() * strafe;
    let mut move_y = next_angle.sin() * forward + next_angle.cos() * strafe;

    let len = (move_x * move_x + move_y * move_y).sqrt();
    if len > 1.0 {
        move_x /= len;
        move_y /= len;
    }

    let dx = move_x * MOVE_SPEED * dt;
    let dy = move_y * MOVE_SPEED * dt;
    let mut next = position;

    let try_x = Vec2::new(next.x + dx, next.y);
    if !map.is_blocked(try_x, PLAYER_RADIUS) {
        next.x = try_x.x;
    }

    let try_y = Vec2::new(next.x, next.y + dy);
    if !map.is_blocked(try_y, PLAYER_RADIUS) {
        next.y = try_y.y;
    }

    (next, next_angle)
}

fn button_axis(buttons: u16, positive: u16, negative: u16) -> f32 {
    let pos = if buttons & positive != 0 { 1.0 } else { 0.0 };
    let neg = if buttons & negative != 0 { 1.0 } else { 0.0 };
    pos - neg
}

#[cfg(test)]
mod tests {
    use super::*;

    const MAP: &str = "\
#####
#1..#
#.#2#
#3.4#
#####
";

    #[test]
    fn parses_map_and_spawns() {
        let map = GameMap::parse(MAP).unwrap();
        assert_eq!(map.width, 5);
        assert_eq!(map.height, 5);
        assert_eq!(map.spawns.len(), 4);
        assert!(!map.is_wall_cell(1, 1));
        assert!(map.is_wall_cell(2, 2));
    }

    #[test]
    fn blocks_wall_collision() {
        let map = GameMap::parse(MAP).unwrap();
        assert!(map.is_blocked(Vec2::new(0.5, 0.5), PLAYER_RADIUS));
        assert!(!map.is_blocked(Vec2::new(1.5, 1.5), PLAYER_RADIUS));
    }

    #[test]
    fn raycast_finds_wall_distance() {
        let map = GameMap::parse(MAP).unwrap();
        let hit = map.raycast(Vec2::new(1.5, 1.5), 0.0, 10.0);
        assert!(hit.hit);
        assert!((hit.distance - 2.5).abs() < 0.01);
    }
}
