/*
    Hurry Curry! - a game about cooking
    Copyright 2024 metamuffin
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, version 3 of the License only.
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.
    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see .
*/
use anyhow::{anyhow, Result};
use clap::Parser;
use futures_util::{SinkExt, StreamExt};
use hurrycurry_protocol::{PacketC, PacketS, PlayerID};
use hurrycurry_server::{data::DATA_DIR, state::State};
use log::{debug, info, trace, warn, LevelFilter};
use std::{path::PathBuf, process::exit, str::FromStr, sync::Arc, time::Duration};
use tokio::{
    net::TcpListener,
    spawn,
    sync::{broadcast, mpsc::channel, RwLock},
    time::interval,
};
use tokio_tungstenite::tungstenite::Message;
#[derive(Parser)]
struct Args {
    /// Print the version, then exit
    #[arg(short, long)]
    version: bool,
    /// Set the path to the game data directory, autodetect if ommitted
    #[arg(short, long)]
    data_dir: Option,
}
fn main() -> Result<()> {
    env_logger::builder()
        .filter_level(LevelFilter::Info)
        .parse_env("LOG")
        .init();
    let args = Args::parse();
    if args.version {
        println!("{}", env!("CARGO_PKG_VERSION"));
        exit(0);
    }
    let data_dir = if let Some(d) = args.data_dir {
        d
    } else {
        let d = PathBuf::from_str(
            [
                "data",
                "/usr/local/share/hurrycurry/data",
                "/usr/share/hurrycurry/data",
                "/opt/hurrycurry/data",
            ]
            .into_iter()
            .find(|p| PathBuf::from_str(p).unwrap().join("index.yaml").exists())
            .ok_or(anyhow!(
                "Could not find the data directory. Please run the server next to the `data` directory or specify a path to it via arguments."
            ))?,
        )?;
        info!("Detected data dir to be {d:?}");
        d
    };
    *DATA_DIR.lock().unwrap() = Some(data_dir);
    tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()?
        .block_on(run())?;
    Ok(())
}
async fn run() -> anyhow::Result<()> {
    let ws_listener = TcpListener::bind("0.0.0.0:27032").await?;
    info!("Listening for websockets on {}", ws_listener.local_addr()?);
    let (tx, rx) = broadcast::channel::(128 * 1024);
    let state = Arc::new(RwLock::new(State::new(tx).await?));
    {
        let state = state.clone();
        spawn(async move {
            let dt = 1. / 25.;
            let mut tick = interval(Duration::from_secs_f32(dt));
            loop {
                tick.tick().await;
                if let Err(e) = state.write().await.tick(dt).await {
                    warn!("tick failed: {e}");
                }
            }
        });
    }
    for id in (1..).map(PlayerID) {
        let (sock, addr) = ws_listener.accept().await?;
        let Ok(sock) = tokio_tungstenite::accept_async(sock).await else {
            warn!("invalid ws handshake");
            continue;
        };
        let (mut write, mut read) = sock.split();
        let state = state.clone();
        let mut rx = rx.resubscribe();
        let (error_tx, mut error_rx) = channel::(8);
        info!("{addr} connected via websocket");
        let mut init = state.write().await.game.prime_client();
        init.insert(0, PacketC::Init { id });
        spawn(async move {
            for p in init {
                if let Err(e) = write
                    .send(tokio_tungstenite::tungstenite::Message::Text(
                        serde_json::to_string(&p).unwrap(),
                    ))
                    .await
                {
                    warn!("ws error on init: {e}");
                    return;
                }
            }
            loop {
                let packet = match tokio::select!(
                   p = rx.recv() => p.ok(),
                   p = error_rx.recv() => p,
                ) {
                    Some(p) => p,
                    None => {
                        rx = rx.resubscribe();
                        PacketC::ServerMessage {
                            text: "Lagging behind. Some clientbound packets were dropped."
                                .to_string(),
                        }
                    }
                };
                if let Err(e) = write
                    .send(tokio_tungstenite::tungstenite::Message::Text(
                        serde_json::to_string(&packet).unwrap(),
                    ))
                    .await
                {
                    warn!("ws error: {e}");
                    break;
                }
            }
        });
        spawn(async move {
            info!("{id:?} joined");
            while let Some(Ok(message)) = read.next().await {
                match message {
                    Message::Text(line) => {
                        let packet = match serde_json::from_str(&line) {
                            Ok(p) => p,
                            Err(e) => {
                                warn!("invalid packet: {e}");
                                break;
                            }
                        };
                        if matches!(
                            packet,
                            PacketS::Position { .. } | PacketS::ReplayTick { .. }
                        ) {
                            trace!("<- {id:?} {packet:?}");
                        } else {
                            debug!("<- {id:?} {packet:?}");
                        }
                        let packet_out = match state.write().await.packet_in(id, packet).await {
                            Ok(packets) => packets,
                            Err(e) => {
                                warn!("client error: {e}");
                                vec![PacketC::Error {
                                    message: format!("{e}"),
                                }]
                            }
                        };
                        for packet in packet_out {
                            let _ = error_tx.send(packet).await;
                        }
                    }
                    Message::Close(_) => break,
                    _ => (),
                }
            }
            info!("{id:?} left");
            state.write().await.packet_in(id, PacketS::Leave).await.ok();
        });
    }
    Ok(())
}