pub mod save;

use anyhow::{Result, anyhow};
use clap::Parser;
use futures_util::{SinkExt, StreamExt};
use hurrycurry_client_lib::network::sync::Network;
use hurrycurry_protocol::{
    Character, Gamedata, Hand, Message, PacketC, PacketS, PlayerClass, PlayerID, TileIndex,
    VERSION,
    glam::{IVec2, Vec2, ivec2},
    movement::MovementBase,
};
use log::{debug, info, warn};
use save::{export_state, import_state};
use std::{
    collections::{BTreeMap, HashMap, HashSet},
    fs::{File, read_to_string},
    io::Write,
    net::SocketAddr,
    process::exit,
    thread::{sleep, spawn},
    time::{Duration, Instant},
};
use tokio::net::{TcpListener, TcpStream};

#[derive(Parser)]
struct Args {
    /// Print version, then exit
    #[arg(short, long)]
    version: bool,
    #[arg(short, long, default_value = "127.0.0.1")]
    bind_addr: String,
    #[arg(short, long, default_value = "27035")]
    port: u16,
}

#[derive(Parser)]
#[clap(multicall = true)]
pub enum Command {
    /// Play the map on the game yerver
    #[clap(alias = "p")]
    Play,
    /// Save current map to permanent storage
    Save { name: Option<String> },
    /// Load map from storage
    Load { name: Option<String> },
    /// Teleport to spawnpoint
    Spawn {
        #[arg(short, long)]
        customer: bool,
    },
}

#[tokio::main]
async fn main() -> Result<()> {
    env_logger::init_from_env("LOG");
    let args = Args::parse();
    if args.version {
        println!("{}", env!("CARGO_PKG_VERSION"));
        exit(0);
    }
    let ws_listener = TcpListener::bind((args.bind_addr, args.port)).await?;
    let mut ms = None;
    loop {
        let (sock, addr) = ws_listener.accept().await?;
        if let Err(e) = handle_conn(sock, addr, &mut ms).await {
            warn!("client error: {e}");
        }
    }
}

const TILES: &[(&str, char, u8)] = &[
    ("grass", 'a', 1),
    ("floor", 'b', 1),
    ("counter", 'c', 0),
    ("oven", 'd', 0),
    ("stove", 'e', 0),
    ("cutting-board", 'f', 0),
    ("chair", 'g', 1),
    ("table", 'h', 0),
    ("wall", 'i', 2),
    ("tree", 'j', 2),
    ("counter-window", 'k', 0),
    ("tomato-crate", 'l', 0),
    ("leek-crate", 'm', 0),
    ("lettuce-crate", 'n', 0),
    ("cheese-crate", 'o', 0),
    ("potato-crate", 'p', 0),
    ("chili-crate", 'q', 0),
    ("fish-crate", 'r', 0),
    ("steak-crate", 's', 0),
    ("flour-crate", 't', 0),
    ("coconut-crate", 'u', 0),
    ("strawberry-crate", 'v', 0),
    ("rice-crate", 'w', 0),
    ("wall-window", 'x', 2),
    ("freezer", 'y', 0),
    ("trash", 'z', 0),
    ("sink", 'A', 0),
    ("street", 'B', 1),
    ("conveyor", 'C', 0),
    ("lamp", 'D', 1),
    ("fence", 'E', 2),
    ("door", 'F', 1),
    ("path", 'G', 1),
    ("book", 'H', 1),
    ("house-wall", 'I', 2),
    ("house-side", 'J', 2),
    ("house-balcony", 'K', 2),
    ("house-door", 'L', 2),
    ("house-oriel", 'M', 2),
    ("house-roof", 'N', 2),
    ("house-roof-chimney", 'O', 2),
];

#[allow(unused_assignments)]
async fn handle_conn(
    sock: TcpStream,
    addr: SocketAddr,
    mapname_save: &mut Option<String>,
) -> Result<()> {
    let sock = tokio_tungstenite::accept_async(sock).await?;
    info!("{addr} connected via websocket");

    let mut state = State {
        out: Vec::new(),
        tiles: HashMap::new(),
        walkable: HashSet::new(),
        joined: false,
        movement: MovementBase::new(Vec2::ZERO),
        last_movement: Instant::now(),
        tile: TileIndex(0),
        chef_spawn: ivec2(0, 0),
        customer_spawn: ivec2(0, 0),
        mapname: "editor".to_string(),
        dirty_tiles: HashSet::new(),
    };

    state.out.push(PacketC::Version {
        major: VERSION.0,
        minor: VERSION.1,
        supports_bincode: false,
    });
    state.out.push(PacketC::Data {
        data: Box::new(Gamedata {
            tile_walkable: (0..TILES.len()).map(TileIndex).collect(),
            tile_placeable_items: BTreeMap::new(),
            tile_names: TILES.iter().map(|(name, _, _)| name.to_string()).collect(),
            current_map: "editor".to_owned(),
            ..Default::default()
        }),
    });
    state.out.push(PacketC::SetIngame {
        state: true,
        lobby: false, // very ironic
    });

    if let Some(name) = mapname_save.clone() {
        state.load(&name)?;
    } else {
        state.build_start_platform();
    }
    state.flush();

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

    loop {
        for p in state.out.drain(..) {
            debug!("-> {p:?}");
            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 {
            tokio_tungstenite::tungstenite::Message::Text(line) => {
                match serde_json::from_str::<PacketS>(&line) {
                    Ok(p) => p,
                    Err(e) => {
                        warn!("Invalid json packet: {e}");
                        break;
                    }
                }
            }
            tokio_tungstenite::tungstenite::Message::Close(_) => break,
            _ => continue,
        };
        debug!("<- {packet:?}");

        if let Err(e) = state.handle_packet(packet) {
            state.out.push(PacketC::ServerMessage {
                message: Message::Text(format!("{e}")),
                error: true,
            });
        }
        state.flush();
        *mapname_save = Some(state.mapname.clone());
    }
    Ok(())
}

pub struct State {
    tiles: HashMap<IVec2, TileIndex>,
    walkable: HashSet<IVec2>,
    out: Vec<PacketC>,
    joined: bool,
    movement: MovementBase,
    last_movement: Instant,
    tile: TileIndex,

    customer_spawn: IVec2,
    chef_spawn: IVec2,

    dirty_tiles: HashSet<IVec2>,

    mapname: String,
}

impl State {
    pub fn set_tile(&mut self, pos: IVec2, tile: TileIndex) {
        self.tiles.insert(pos, tile);
        self.walkable.insert(pos);
        self.dirty_tiles.insert(pos);
    }
    pub fn clear(&mut self) {
        self.dirty_tiles.extend(self.tiles.keys());
        self.tiles.clear();
    }
    pub fn flush(&mut self) {
        if !self.dirty_tiles.is_empty() {
            for p in self.dirty_tiles.drain() {
                self.out.push(PacketC::UpdateMap {
                    tile: p,
                    kind: self.tiles.get(&p).copied(),
                    neighbors: [
                        self.tiles.get(&(p + IVec2::NEG_Y)).copied(),
                        self.tiles.get(&(p + IVec2::NEG_X)).copied(),
                        self.tiles.get(&(p + IVec2::Y)).copied(),
                        self.tiles.get(&(p + IVec2::X)).copied(),
                    ],
                })
            }
            self.out.push(PacketC::FlushMap);
        }
    }
    pub fn build_start_platform(&mut self) {
        for x in 0..10 {
            for y in 0..10 {
                self.set_tile(ivec2(x, y), TileIndex(1));
            }
        }
    }

    pub fn handle_packet(&mut self, packet: PacketS) -> Result<()> {
        match packet {
            PacketS::Join {
                character, name, ..
            } if !self.joined => {
                self.out.push(PacketC::Joined { id: PlayerID(0) });
                self.out.push(PacketC::AddPlayer {
                    id: PlayerID(0),
                    position: self.movement.position,
                    class: PlayerClass::Chef,
                    character,
                    name,
                });
                self.joined = true;
                self.spawn(false);
            }
            PacketS::Leave { .. } if self.joined => {
                self.out.push(PacketC::RemovePlayer { id: PlayerID(0) });
                self.joined = false;
            }
            PacketS::Movement {
                player,
                dir,
                boost,
                pos,
            } => {
                let dt = self.last_movement.elapsed();
                self.last_movement += dt;
                self.movement.position = pos.unwrap_or(self.movement.position);
                self.movement.input(dir, boost);
                self.movement.update(&self.walkable, dt.as_secs_f32());
                self.out.push(self.movement.movement_packet_c(player));
            }
            PacketS::Communicate {
                message: Some(Message::Text(t)),
                ..
            } => {
                let t = t.strip_prefix("/").unwrap_or(&t);
                self.handle_command(
                    Command::try_parse_from(
                        shlex::split(t)
                            .ok_or(anyhow!("invalid quoting"))?
                            .into_iter(),
                    )
                    .map_err(|e| anyhow!("{e}"))?,
                )?;
            }
            PacketS::Interact {
                hand: Hand(1),
                pos: Some(_),
                ..
            } => {
                self.tile.0 += 1;
                self.tile.0 %= TILES.len();
                self.out.push(PacketC::ServerMessage {
                    message: Message::Text(format!("tile brush: {}", TILES[self.tile.0].0)),
                    error: false,
                });
            }
            PacketS::Interact {
                hand: Hand(0),
                pos: Some(_),
                ..
            } => {
                let bpos = self.movement.position.floor().as_ivec2();
                self.set_tile(bpos, self.tile);
                for off in [IVec2::NEG_X, IVec2::NEG_Y, IVec2::X, IVec2::Y] {
                    if !self.tiles.contains_key(&(bpos + off)) {
                        self.set_tile(bpos + off, self.tile);
                    }
                }
                self.flush();
            }
            _ => (),
        }
        Ok(())
    }
    pub fn spawn(&mut self, c: bool) {
        self.movement.position = if c {
            self.customer_spawn
        } else {
            self.chef_spawn
        }
        .as_vec2();
        self.out.push(PacketC::Movement {
            player: PlayerID(0),
            pos: self.movement.position,
            rot: 0.,
            dir: Vec2::X,
            boost: false,
        });
        self.out.push(PacketC::MovementSync {
            player: PlayerID(0),
        });
    }

    pub fn save(&mut self, name: &str) -> Result<()> {
        let e = export_state(self);
        File::create(format!("data/maps/{name}.yaml"))?.write_all(e.as_bytes())?;
        self.out.push(PacketC::ServerMessage {
            message: Message::Text("Map saved.".to_string()),
            error: false,
        });
        self.mapname = name.to_owned();
        Ok(())
    }
    pub fn load(&mut self, name: &str) -> Result<()> {
        let e = read_to_string(format!("data/maps/{name}.yaml"))?;
        self.clear();
        import_state(self, &e)?;
        self.out.push(PacketC::ServerMessage {
            message: Message::Text("Map loaded.".to_string()),
            error: false,
        });
        self.mapname = name.to_owned();
        Ok(())
    }

    pub fn handle_command(&mut self, command: Command) -> Result<()> {
        match command {
            Command::Play => {
                self.save(&self.mapname.clone())?;
                let mapname = self.mapname.clone();
                spawn(move || {
                    if let Err(e) =
                        start_map_bot("ws://127.0.0.1:27032", "ws://127.0.0.1:27035", &mapname)
                    {
                        warn!("editor bot: {e}")
                    }
                });
                self.out.push(PacketC::Redirect {
                    uri: vec!["ws://127.0.0.1:27032".to_string()],
                });
            }
            Command::Save { name } => {
                self.save(name.as_ref().unwrap_or(&self.mapname.clone()))?;
            }
            Command::Load { name } => {
                self.load(name.as_ref().unwrap_or(&self.mapname.clone()))?;
            }
            Command::Spawn { customer } => {
                self.spawn(customer);
            }
        }
        Ok(())
    }
}

fn start_map_bot(address: &str, own_addr: &str, mapname: &str) -> Result<()> {
    let mut network = Network::connect(address)?;

    network.queue_out.push_back(PacketS::Join {
        name: "editor-bot".to_owned(),
        character: Character::default(),
        class: PlayerClass::Bot,
        id: None,
        position: None,
    });

    let mut timer = 10.;
    loop {
        let dt = 1. / 50.;

        network.poll()?;

        while let Some(packet) = network.queue_in.pop_front() {
            match &packet {
                PacketC::Joined { id } => {
                    network.queue_out.push_back(PacketS::Communicate {
                        player: *id,
                        message: Some(Message::Text(format!(
                            "/set-editor-address {own_addr}\n/start {mapname}"
                        ))),
                        timeout: None,
                        pin: None,
                    });
                }
                PacketC::ServerMessage {
                    message,
                    error: true,
                } => {
                    warn!("server error message: {message:?}");
                }
                _ => (),
            }
        }

        timer -= dt;
        if timer < 0. {
            break;
        }
        sleep(Duration::from_secs_f32(dt));
    }
    Ok(())
}
