use std::thread;
use std::time::{Duration, Instant};

use anyhow::{Context, Result};
use crossterm::event::{poll, read};

use crate::input::InputState;
use crate::net::client::{ClientEvent, ClientNet};
use crate::net::protocol::PlayerSnapshot;
use crate::render::{RenderStats, render};
use crate::terminal::Terminal;
use crate::world::{GameMap, apply_input};

const FRAME_TIME: Duration = Duration::from_millis(16);

pub fn run_client(server: &str, name: &str) -> Result<()> {
    let mut net = ClientNet::connect(server, name)?;
    let map = GameMap::parse(&net.map_text).context("failed to parse server map")?;
    let mut terminal = Terminal::enter()?;
    let mut input = InputState::default();

    let mut players = vec![net.initial.clone()];
    let mut local = net.initial.clone();
    let mut last_frame = Instant::now();
    let started = last_frame;
    let mut client_tick = 0_u64;
    let mut fire_seq = 0_u32;
    let mut last_ping = last_frame - Duration::from_secs(1);
    let mut ping_ms = None;

    let mut fps_frames = 0_u32;
    let mut fps_value = 0_u32;
    let mut fps_window = last_frame;

    loop {
        let frame_start = Instant::now();
        while poll(Duration::from_millis(0))? {
            input.handle_event(read()?, frame_start);
        }

        let input_frame = input.frame(frame_start);
        if input_frame.quit {
            break;
        }

        let dt = frame_start
            .duration_since(last_frame)
            .as_secs_f32()
            .min(0.05);
        last_frame = frame_start;
        client_tick = client_tick.wrapping_add(1);
        fire_seq = fire_seq.wrapping_add(input_frame.fire_presses);

        if local.alive {
            let (position, angle) = apply_input(
                &map,
                local.position,
                local.angle,
                input_frame.buttons,
                input_frame.turn,
                dt,
            );
            local.position = position;
            local.angle = angle;
        }

        net.send_input(client_tick, input_frame.buttons, input_frame.turn, fire_seq)?;

        if frame_start.duration_since(last_ping) >= Duration::from_secs(1) {
            let elapsed_ms = frame_start.duration_since(started).as_millis() as u64;
            net.send_ping(elapsed_ms, frame_start)?;
            last_ping = frame_start;
        }

        for event in net.recv_packets(frame_start)? {
            match event {
                ClientEvent::Snapshot {
                    players: snapshot_players,
                } => {
                    players = snapshot_players;
                    if let Some(authoritative) =
                        players.iter().find(|player| player.id == net.player_id)
                    {
                        local = authoritative.clone();
                    }
                }
                ClientEvent::Ping(duration) => {
                    ping_ms = Some(duration.as_millis());
                }
            }
        }

        let render_players = players_with_local(&players, &local);
        let stats = RenderStats {
            fps: fps_value,
            ping_ms,
            server_addr: net.server_addr().to_string(),
        };
        render(terminal.stdout(), &map, &local, &render_players, &stats)?;

        fps_frames += 1;
        if frame_start.duration_since(fps_window) >= Duration::from_secs(1) {
            fps_value = fps_frames;
            fps_frames = 0;
            fps_window = frame_start;
        }

        let elapsed = frame_start.elapsed();
        if elapsed < FRAME_TIME {
            thread::sleep(FRAME_TIME - elapsed);
        }
    }

    net.disconnect();
    Ok(())
}

fn players_with_local(players: &[PlayerSnapshot], local: &PlayerSnapshot) -> Vec<PlayerSnapshot> {
    let mut output = players.to_vec();
    if let Some(existing) = output.iter_mut().find(|player| player.id == local.id) {
        *existing = local.clone();
    } else {
        output.push(local.clone());
    }
    output
}
