use std::io::Write;

use anyhow::Result;
use crossterm::{
    cursor::MoveTo,
    queue,
    style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor},
    terminal::{Clear, ClearType, size},
};

use crate::net::protocol::PlayerSnapshot;
use crate::world::{FOV, GameMap, MAX_VIEW_DISTANCE, RaySide, Vec2, angle_diff};

const SHADES: &[u8] = b" .:-=+*#%@";

#[derive(Debug, Clone)]
pub struct RenderStats {
    pub fps: u32,
    pub ping_ms: Option<u128>,
    pub server_addr: String,
}

#[derive(Debug, Clone, Copy)]
struct Cell {
    ch: char,
    fg: Color,
    bg: Color,
}

pub struct FrameBuffer {
    width: u16,
    height: u16,
    cells: Vec<Cell>,
}

impl FrameBuffer {
    pub fn new(width: u16, height: u16) -> Self {
        Self {
            width,
            height,
            cells: vec![
                Cell {
                    ch: ' ',
                    fg: Color::White,
                    bg: Color::Black,
                };
                width as usize * height as usize
            ],
        }
    }

    fn clear_scene(&mut self) {
        let horizon = self.height / 2;
        for y in 0..self.height {
            let cell = if y < horizon {
                Cell {
                    ch: '.',
                    fg: Color::DarkBlue,
                    bg: Color::Black,
                }
            } else {
                Cell {
                    ch: ',',
                    fg: Color::DarkGrey,
                    bg: Color::Black,
                }
            };
            for x in 0..self.width {
                self.set(x as i32, y as i32, cell);
            }
        }
    }

    fn set(&mut self, x: i32, y: i32, cell: Cell) {
        if x < 0 || y < 0 || x >= self.width as i32 || y >= self.height as i32 {
            return;
        }
        self.cells[y as usize * self.width as usize + x as usize] = cell;
    }

    fn text(&mut self, x: i32, y: i32, text: &str, fg: Color) {
        for (offset, ch) in text.chars().enumerate() {
            self.set(
                x + offset as i32,
                y,
                Cell {
                    ch,
                    fg,
                    bg: Color::Black,
                },
            );
        }
    }

    pub fn flush<W: Write>(&self, out: &mut W) -> Result<()> {
        queue!(out, MoveTo(0, 0), Clear(ClearType::All))?;
        let mut last_fg = None;
        let mut last_bg = None;
        for y in 0..self.height {
            queue!(out, MoveTo(0, y))?;
            for x in 0..self.width {
                let cell = self.cells[y as usize * self.width as usize + x as usize];
                if last_fg != Some(cell.fg) {
                    queue!(out, SetForegroundColor(cell.fg))?;
                    last_fg = Some(cell.fg);
                }
                if last_bg != Some(cell.bg) {
                    queue!(out, SetBackgroundColor(cell.bg))?;
                    last_bg = Some(cell.bg);
                }
                queue!(out, Print(cell.ch))?;
            }
        }
        queue!(out, ResetColor)?;
        out.flush()?;
        Ok(())
    }
}

pub fn render<W: Write>(
    out: &mut W,
    map: &GameMap,
    viewer: &PlayerSnapshot,
    players: &[PlayerSnapshot],
    stats: &RenderStats,
) -> Result<()> {
    let (width, height) = size()?;
    let mut frame = FrameBuffer::new(width, height);

    if width < 80 || height < 24 {
        frame.text(2, 2, "Resize terminal to at least 80x24", Color::Yellow);
        frame.flush(out)?;
        return Ok(());
    }

    frame.clear_scene();
    let mut zbuffer = vec![MAX_VIEW_DISTANCE; width as usize];
    render_walls(&mut frame, map, viewer.position, viewer.angle, &mut zbuffer);
    render_players(&mut frame, viewer, players, &zbuffer);
    render_hud(&mut frame, viewer, stats);
    frame.flush(out)
}

fn render_walls(
    frame: &mut FrameBuffer,
    map: &GameMap,
    position: Vec2,
    angle: f32,
    zbuffer: &mut [f32],
) {
    for x in 0..frame.width {
        let t = if frame.width <= 1 {
            0.5
        } else {
            x as f32 / (frame.width - 1) as f32
        };
        let ray_angle = angle - FOV / 2.0 + FOV * t;
        let hit = map.raycast(position, ray_angle, MAX_VIEW_DISTANCE);
        let corrected = (hit.distance * angle_diff(ray_angle, angle).cos()).max(0.001);
        zbuffer[x as usize] = corrected;

        let wall_height = ((frame.height as f32 * 0.9) / corrected) as i32;
        let center = frame.height as i32 / 2;
        let start = (center - wall_height / 2).max(1);
        let end = (center + wall_height / 2).min(frame.height as i32 - 2);
        let shade_index = ((1.0 - (corrected / MAX_VIEW_DISTANCE)).clamp(0.0, 1.0)
            * (SHADES.len() - 1) as f32) as usize;
        let mut shade = SHADES[shade_index] as char;
        if hit.side == RaySide::Horizontal && shade != ' ' {
            shade = match shade {
                '@' => '#',
                '%' => '*',
                '#' => '+',
                other => other,
            };
        }

        let fg = if corrected < 4.0 {
            Color::White
        } else if corrected < 9.0 {
            Color::Grey
        } else {
            Color::DarkGrey
        };

        for y in start..=end {
            frame.set(
                x as i32,
                y,
                Cell {
                    ch: shade,
                    fg,
                    bg: Color::Black,
                },
            );
        }
    }
}

fn render_players(
    frame: &mut FrameBuffer,
    viewer: &PlayerSnapshot,
    players: &[PlayerSnapshot],
    zbuffer: &[f32],
) {
    let mut others: Vec<&PlayerSnapshot> = players
        .iter()
        .filter(|player| player.id != viewer.id && player.alive)
        .collect();
    others.sort_by(|a, b| {
        b.position
            .distance(viewer.position)
            .partial_cmp(&a.position.distance(viewer.position))
            .unwrap_or(std::cmp::Ordering::Equal)
    });

    for player in others {
        let dx = player.position.x - viewer.position.x;
        let dy = player.position.y - viewer.position.y;
        let distance = (dx * dx + dy * dy).sqrt().max(0.001);
        let target_angle = dy.atan2(dx);
        let diff = angle_diff(target_angle, viewer.angle);
        if diff.abs() > FOV / 2.0 {
            continue;
        }

        let screen_x = ((diff + FOV / 2.0) / FOV * frame.width as f32) as i32;
        if screen_x < 0 || screen_x >= frame.width as i32 {
            continue;
        }

        let sprite_h = ((frame.height as f32 * 0.85) / distance) as i32;
        let sprite_w = (sprite_h / 2).max(1);
        let top = frame.height as i32 / 2 - sprite_h / 2;
        let bottom = top + sprite_h;
        let left = screen_x - sprite_w / 2;
        let right = screen_x + sprite_w / 2;

        for x in left..=right {
            if x < 0 || x >= frame.width as i32 {
                continue;
            }
            if distance >= zbuffer[x as usize] {
                continue;
            }
            for y in top..=bottom {
                let ch = if y == top || y == bottom { 'A' } else { '@' };
                frame.set(
                    x,
                    y,
                    Cell {
                        ch,
                        fg: Color::Red,
                        bg: Color::Black,
                    },
                );
            }
        }
    }
}

fn render_hud(frame: &mut FrameBuffer, viewer: &PlayerSnapshot, stats: &RenderStats) {
    let ping = stats
        .ping_ms
        .map(|value| format!("{value}ms"))
        .unwrap_or_else(|| "--".to_owned());
    let status = if viewer.alive {
        format!(
            "HP {:>3} | score {} | deaths {} | ping {} | fps {} | {}",
            viewer.health, viewer.score, viewer.deaths, ping, stats.fps, stats.server_addr
        )
    } else {
        format!(
            "DEAD | respawn {}ms | score {} | deaths {} | ping {} | fps {} | {}",
            viewer.respawn_ms, viewer.score, viewer.deaths, ping, stats.fps, stats.server_addr
        )
    };
    frame.text(1, 0, &status, Color::Green);

    let cx = frame.width as i32 / 2;
    let cy = frame.height as i32 / 2;
    frame.set(
        cx,
        cy,
        Cell {
            ch: '+',
            fg: Color::Yellow,
            bg: Color::Black,
        },
    );

    let weapon = "     /-[=]-\\";
    let wx = frame.width as i32 / 2 - weapon.len() as i32 / 2;
    let wy = frame.height as i32 - 2;
    frame.text(wx, wy, weapon, Color::Grey);

    frame.text(
        1,
        frame.height as i32 - 1,
        "WASD move | Q/E or arrows turn | Space fire | Esc quit",
        Color::DarkGrey,
    );
}
