use crate::Registry;
use anyhow::Result;
use hurrycurry_protocol::{
    Gamedata, MapMetadata, PacketC, PacketS, PlayerClass, PlayerID, Serverdata, TileIndex, VERSION,
    glam::{IVec2, Vec2, ivec2, vec2},
    movement::MovementBase,
    registry::Entry,
};
use log::{error, info, warn};
use rocket::futures::{SinkExt, StreamExt};
use std::{
    collections::{BTreeMap, HashMap, HashSet},
    f32::consts::PI,
    net::SocketAddr,
    sync::Arc,
    time::Instant,
};
use tokio::{
    net::{TcpListener, TcpStream},
    spawn,
    sync::RwLock,
};
use tokio_tungstenite::tungstenite::Message;

pub(crate) async fn lobby_wrapper(registry: Arc<RwLock<Registry>>, saddr: SocketAddr) {
    let Err(e) = lobby(registry, saddr).await else {
        unreachable!()
    };
    error!("lobby crashed: {e}");
}

const TILES: &[(&str, bool)] = &[("grass", false), ("black-hole", false)];

async fn lobby(registry: Arc<RwLock<Registry>>, saddr: SocketAddr) -> Result<()> {
    let ws_listener = TcpListener::bind(saddr).await?;

    loop {
        let (sock, addr) = ws_listener.accept().await?;
        let r = registry.clone();
        spawn(async move {
            let entries = r.read().await.entries.clone();
            if let Err(e) = handle_conn(sock, addr, &entries).await {
                warn!("client error: {e}");
            }
        });
    }
}

async fn handle_conn(sock: TcpStream, addr: SocketAddr, entries: &[Entry]) -> Result<()> {
    let sock = tokio_tungstenite::accept_async(sock).await?;
    info!("{addr} connected via websocket");

    let ecount = entries.len().max(2) as f32;
    let radius = ecount * 5. / PI;
    let portal_location = |i: usize| {
        let t = i as f32 / ecount * PI * 2.;
        vec2(t.sin() * radius, t.cos() * radius).round().as_ivec2()
    };

    let mut tiles = HashMap::<IVec2, Vec<TileIndex>>::new();
    let bounds = (radius + 5.).ceil() as i32;
    for x in -bounds..bounds {
        for y in -bounds..bounds {
            tiles.insert(ivec2(x, y), vec![TileIndex(0)]);
        }
    }
    for (i, _) in entries.iter().enumerate() {
        tiles.insert(portal_location(i), vec![TileIndex(1)]);
    }

    let mut out = Vec::new();
    out.push(PacketC::Version {
        major: VERSION.0,
        minor: VERSION.1,
    });
    out.push(PacketC::ServerData(Box::new(Serverdata {
        bot_algos: vec![],
        maps: vec![],
        motd: None,
        name: "Registry".to_string(),
    })));
    out.push(PacketC::GameData(Box::new(Gamedata {
        tile_collide: (0..TILES.len()).map(TileIndex).collect(),
        tile_placeable_items: BTreeMap::new(),
        tile_names: TILES.iter().map(|(s, _)| s.to_string()).collect(),
        metadata: MapMetadata {
            name: "registry".to_string(),
            display_name: "Registry Lobby".to_string(),
            ..Default::default()
        },
        ..Default::default()
    })));
    let walkable = HashSet::from_iter(
        tiles
            .iter()
            .filter(|(_, v)| !v.iter().any(|p| TILES[p.0].1))
            .map(|(k, _)| *k),
    );
    out.push(PacketC::UpdateMap {
        changes: tiles.clone().into_iter().collect(),
    });
    out.push(PacketC::SetIngame {
        state: true,
        lobby: false, // very ironic
    });

    let (mut write, mut read) = sock.split();

    let mut joined = false;
    let mut redirected = false;
    let mut movement = MovementBase::new(Vec2::ZERO);
    let mut last_movement = Instant::now();

    loop {
        for p in out.drain(..) {
            write
                .send(tokio_tungstenite::tungstenite::Message::Text(
                    serde_json::to_string(&p).unwrap().into(),
                ))
                .await?;
        }

        let Some(message) = read.next().await.transpose()? else {
            break;
        };
        let packet = match message {
            Message::Text(line) => match serde_json::from_str::<PacketS>(&line) {
                Ok(p) => p,
                Err(e) => {
                    warn!("Invalid json packet: {e}");
                    break;
                }
            },
            Message::Close(_) => break,
            _ => continue,
        };

        match packet {
            PacketS::Join {
                character, name, ..
            } if !joined => {
                out.push(PacketC::Joined { id: PlayerID(0) });
                out.push(PacketC::AddPlayer {
                    id: PlayerID(0),
                    position: movement.position,
                    class: PlayerClass::Chef,
                    character,
                    name,
                });
                for (i, e) in entries.iter().enumerate() {
                    out.push(PacketC::ServerHint {
                        position: Some(portal_location(i)),
                        message: Some(hurrycurry_protocol::Message::Text(e.name.clone())),
                        player: PlayerID(0),
                    });
                }
                joined = true;
            }
            PacketS::Leave { .. } if joined => {
                out.push(PacketC::RemovePlayer { id: PlayerID(0) });
                joined = false;
            }
            PacketS::Movement {
                player,
                dir,
                boost,
                pos,
            } => {
                let dt = last_movement.elapsed();
                last_movement += dt;
                movement.position = pos.unwrap_or(movement.position);
                movement.input(dir, boost);
                movement.update(&walkable, dt.as_secs_f32());
                out.push(movement.movement_packet_c(player, false));
                if !redirected {
                    for (i, e) in entries.iter().enumerate() {
                        if movement
                            .position
                            .distance(portal_location(i).as_vec2() + 0.5)
                            < 0.5
                        {
                            redirected = true;
                            out.push(PacketC::Redirect {
                                uri: e.address.clone(),
                            });
                            break;
                        }
                    }
                }
            }
            _ => (),
        }
    }
    Ok(())
}
