mod utils;
mod byte_ops;
mod chunk;

use std::{io::{self, Read, Write}, net::{TcpListener, TcpStream}};

use base16ct::lower::encode_string;
use chunk::{BlockArray, MapChunk, PreChunk};
use log::{info, warn};
use byteorder::{ReadBytesExt, WriteBytesExt, BE};
use num_derive::FromPrimitive;
use num_traits::FromPrimitive;
use byte_ops::ToBytes;
use utils::{MCString, ReadMCString, WriteMCString};
use rand::random;


fn main() {
    colog::default_builder()
        .filter_level(log::LevelFilter::Debug)
        .init();

    let listener = TcpListener::bind("0.0.0.0:25565").unwrap();

    for mut connection in listener.incoming().filter_map(|c| c.ok()) {
        info!("Connected to client @ {}", connection.peer_addr().unwrap());
        while let Some(cmd) = connection.read_u8().ok() {
            let command = Command::from_u8(cmd);
            info!("COMMAND: {command:?} (0x{cmd:02X?})");
            handle_command(&mut connection, command.unwrap()).unwrap();
        }
        warn!("Lost connection to client");
    }
}

fn handle_command(mut connection: &mut TcpStream, command: Command) -> Result<(), io::Error> {
    match command {
        Command::KeepAlive => todo!(),
        Command::Handshake => {
            let username = connection.read_mcstring()?;
            let random_number = random::<u128>();
            let random_hash = encode_string(md5::compute(random_number.to_le_bytes()).as_slice());

            info!("Handshake with {username} successful. Providing hash: {random_hash:?}");

            connection.write_u8(0x02)?;
            connection.write_mcstring(&MCString::try_from(random_hash).unwrap())?;
        },
        Command::Login => {
            info!("Initiating login");
            let protocol_version = connection.read_u32::<BE>()?;
            let username = connection.read_mcstring()?;
            let password = connection.read_mcstring()?;
            let map_seed = connection.read_i64::<BE>()?;
            let dimension = connection.read_i8()?;

            info!("Protocol Version: {protocol_version}");
            info!("Username: {username}");
            info!("Password: {password}");
            info!("Map Seed: {map_seed}");
            info!("Dimension: {dimension}");

            let login_packet = ServerLoginPacket {
                entity_id: 1200,
                unknown1: MCString::default(),
                unknown2: MCString::default(),
                map_seed: 1715505462032542147,
                dimension: 0,
            };
            connection.write_u8(Command::Login as u8).unwrap();
            connection.write(&login_packet.to_bytes())?;

            info!("Responded to auth request");

            for i in 0..7 {
                for o in 0..7 {
                    let x = (-4 + i) * 16;
                    let z = (-5 + o) * 16;

                    connection.write_u8(Command::PreChunk as u8).unwrap();
                    connection.write_all(&PreChunk::new_load(x, z).to_bytes()).unwrap();

                    connection.write_u8(Command::MapChunk as u8)?;
                    connection.write_all(&MapChunk::new(x, z, BlockArray::new_air()).to_bytes())?;
                }
            }

            connection.write_u8(Command::SpawnPosition as u8)?;
            connection.write_u32::<BE>(0)?;
            connection.write_u32::<BE>(70)?;
            connection.write_u32::<BE>(0)?;

            let playerpos = PlayerPositionAndLook {
                position_x: 0.0,
                stance: 0.0,
                position_y: 70.0,
                position_z: 0.0,
                yaw: 0.0,
                pitch: 0.0,
                on_ground: true,
            };
            connection.write_u8(Command::PlayerPositionAndLook as u8)?;
            connection.write_all(&playerpos.to_bytes())?;
        },
        Command::PlayerPositionAndLook => {
            let _poslook = PlayerPositionAndLook::from_bytes(&mut connection);
        }
        Command::PlayerPosition => {
            let _pos = PlayerPosition::from_bytes(&mut connection);
        }
        c => unimplemented!("This command ({c:?}) is probably `Server -> Client` only")
    }

    Ok(())
}

#[repr(u8)]
#[derive(Debug, Clone, Copy)]
#[derive(FromPrimitive)]
enum Command {
    KeepAlive = 0x00,
    Login = 0x01,
    Handshake = 0x02,
    ChatMessage = 0x03,
    TimeUpdate = 0x04,
    PlayerInventory = 0x05,
    SpawnPosition = 0x06,
    UpdateHealth = 0x08,
    Respawn = 0x09,
    PlayerPositionAndLook = 0x0D,
    PlayerPosition = 0x0B,
    PreChunk = 0x32,
    MapChunk = 0x33,
    Disconnect = 0xFF,
}

#[derive(Debug, Clone, Copy)]
struct CommandError {
    _id: u8,
}

struct ServerLoginPacket {
    entity_id: i32,
    unknown1: MCString,
    unknown2: MCString,
    map_seed: i64,
    dimension: i8,
}

impl ToBytes for ServerLoginPacket {
    type Bytes = Vec<u8>;

    fn to_bytes(self) -> Self::Bytes {
        let mut out_buf = Vec::new();
        out_buf.write_i32::<BE>(self.entity_id).unwrap();
        out_buf.write_mcstring(&self.unknown1).unwrap();
        out_buf.write_mcstring(&self.unknown2).unwrap();
        out_buf.write_i64::<BE>(self.map_seed).unwrap();
        out_buf.write_i8(self.dimension).unwrap();

        out_buf
    }
}

#[derive(Debug, Clone, Copy)]
struct PlayerPositionAndLook {
    position_x: f64,
    stance: f64,
    position_y: f64,
    position_z: f64,
    yaw: f32,
    pitch: f32,
    on_ground: bool,
}

impl ToBytes for PlayerPositionAndLook {
    type Bytes = Vec<u8>;

    fn to_bytes(self) -> Self::Bytes {
        let mut out_buf = Vec::new();
        out_buf.write_f64::<BE>(self.position_x).unwrap();
        out_buf.write_f64::<BE>(self.stance).unwrap();
        out_buf.write_f64::<BE>(self.position_y).unwrap();
        out_buf.write_f64::<BE>(self.position_z).unwrap();
        out_buf.write_f32::<BE>(self.yaw).unwrap();
        out_buf.write_f32::<BE>(self.pitch).unwrap();
        out_buf.write_u8(self.on_ground as u8).unwrap();

        out_buf
    }
}

impl PlayerPositionAndLook {
    fn from_bytes<R: Read>(stream: &mut R) -> Self {
        let position_x = stream.read_f64::<BE>().unwrap();
        let position_y = stream.read_f64::<BE>().unwrap();
        let stance = stream.read_f64::<BE>().unwrap();
        let position_z = stream.read_f64::<BE>().unwrap();

        let yaw = stream.read_f32::<BE>().unwrap();
        let pitch = stream.read_f32::<BE>().unwrap();
        let on_ground = stream.read_u8().unwrap() != 0;

        Self {
            position_x,
            stance,
            position_y,
            position_z,
            yaw,
            pitch,
            on_ground
        }
    }
}

#[derive(Debug, Clone, Copy)]
struct PlayerPosition {
    position_x: f64,
    position_y: f64,
    stance: f64,
    position_z: f64,
    on_ground: bool,
}

impl ToBytes for PlayerPosition {
    type Bytes = Vec<u8>;

    fn to_bytes(self) -> Self::Bytes {
        let mut out_buf = Vec::new();
        out_buf.write_f64::<BE>(self.position_x).unwrap();
        out_buf.write_f64::<BE>(self.position_y).unwrap();
        out_buf.write_f64::<BE>(self.stance).unwrap();
        out_buf.write_f64::<BE>(self.position_z).unwrap();
        out_buf.write_u8(self.on_ground as u8).unwrap();

        out_buf
    }
}

impl PlayerPosition {
    fn from_bytes<R: Read>(stream: &mut R) -> Self {
        let position_x = stream.read_f64::<BE>().unwrap();
        let position_y = stream.read_f64::<BE>().unwrap();
        let stance = stream.read_f64::<BE>().unwrap();
        let position_z = stream.read_f64::<BE>().unwrap();

        let on_ground = stream.read_u8().unwrap() != 0;

        Self {
            position_x,
            stance,
            position_y,
            position_z,
            on_ground
        }
    }
}