//! Thread-safe simulator statistics matching `Simulator.SimStats`.

#![allow(clippy::missing_errors_doc)] // Result shapes are fixed by the compatibility map.

use crate::Error;
use std::fmt;
use std::sync::atomic::{AtomicI32, AtomicI64, AtomicU32, Ordering};

fn csharp_convert_to_i32(value: f32) -> Option<i32> {
    let rounded = value.round_ties_even();
    if !rounded.is_finite() || !(-2_147_483_648.0..2_147_483_648.0).contains(&rounded) {
        return None;
    }
    #[allow(clippy::cast_possible_truncation)]
    Some(rounded as i32)
}

macro_rules! integer_property {
    ($get:ident, $set:ident, $field:ident) => {
        #[must_use]
        pub fn $get(&self) -> i32 {
            self.$field.load(Ordering::Acquire)
        }

        pub fn $set(&mut self, value: i32) {
            self.$field.store(value, Ordering::Release);
        }
    };
}

macro_rules! float_property {
    ($get:ident, $set:ident, $field:ident) => {
        #[must_use]
        pub fn $get(&self) -> f32 {
            f32::from_bits(self.$field.load(Ordering::Acquire))
        }

        pub fn $set(&mut self, value: f32) {
            self.$field.store(value.to_bits(), Ordering::Release);
        }
    };
}

#[derive(Default)]
pub struct SimulatorSimStats {
    sent_packets: AtomicI64,
    recv_packets: AtomicI64,
    sent_bytes: AtomicI64,
    recv_bytes: AtomicI64,
    connect_time: AtomicI32,
    resent_packets: AtomicI32,
    received_resends: AtomicI32,
    dropped_packets: AtomicI32,
    sent_pings: AtomicI32,
    received_pongs: AtomicI32,
    incoming_bps: AtomicI32,
    outgoing_bps: AtomicI32,
    last_ping_sent: AtomicI32,
    last_ping_id: AtomicI32,
    last_lag: AtomicI32,
    missed_pings: AtomicI32,
    dilation: AtomicU32,
    fps: AtomicI32,
    physics_fps: AtomicU32,
    agent_updates: AtomicU32,
    frame_time: AtomicU32,
    net_time: AtomicU32,
    physics_time: AtomicU32,
    image_time: AtomicU32,
    script_time: AtomicU32,
    agent_time: AtomicU32,
    other_time: AtomicU32,
    objects: AtomicI32,
    scripted_objects: AtomicI32,
    agents: AtomicI32,
    child_agents: AtomicI32,
    active_scripts: AtomicI32,
    lslips: AtomicI32,
    inpps: AtomicI32,
    outpps: AtomicI32,
    pending_downloads: AtomicI32,
    pending_uploads: AtomicI32,
    virtual_size: AtomicI32,
    resident_size: AtomicI32,
    pending_local_uploads: AtomicI32,
    unacked_bytes: AtomicI32,
}

impl fmt::Debug for SimulatorSimStats {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("SimulatorSimStats")
            .field("sent_packets", &self.sent_packets.load(Ordering::Relaxed))
            .field("recv_packets", &self.recv_packets.load(Ordering::Relaxed))
            .field("sent_bytes", &self.sent_bytes.load(Ordering::Relaxed))
            .field("recv_bytes", &self.recv_bytes.load(Ordering::Relaxed))
            .field(
                "dropped_packets",
                &self.dropped_packets.load(Ordering::Relaxed),
            )
            .finish_non_exhaustive()
    }
}

impl SimulatorSimStats {
    pub fn new() -> Result<Self, Error> {
        Ok(Self::default())
    }

    pub fn add_recv_bytes(&self, value: i64) -> Result<(), Error> {
        self.recv_bytes.fetch_add(value, Ordering::SeqCst);
        Ok(())
    }

    pub fn add_sent_bytes(&self, value: i64) -> Result<(), Error> {
        self.sent_bytes.fetch_add(value, Ordering::SeqCst);
        Ok(())
    }

    pub fn get_and_increment_last_ping_id(&self) -> Result<i32, Error> {
        Ok(self.last_ping_id.fetch_add(1, Ordering::SeqCst))
    }

    pub fn get_connect_time(&self) -> Result<i32, Error> {
        Ok(self.connect_time.load(Ordering::SeqCst))
    }

    pub fn get_dropped_packets(&self) -> Result<i32, Error> {
        Ok(self.dropped_packets.load(Ordering::SeqCst))
    }

    pub fn get_incoming_bps(&self) -> Result<i32, Error> {
        Ok(self.incoming_bps.load(Ordering::SeqCst))
    }

    pub fn get_last_ping_sent(&self) -> Result<i32, Error> {
        Ok(self.last_ping_sent.load(Ordering::SeqCst))
    }

    pub fn get_outgoing_bps(&self) -> Result<i32, Error> {
        Ok(self.outgoing_bps.load(Ordering::SeqCst))
    }

    pub fn get_received_pongs(&self) -> Result<i32, Error> {
        Ok(self.received_pongs.load(Ordering::SeqCst))
    }

    pub fn get_received_resends(&self) -> Result<i32, Error> {
        Ok(self.received_resends.load(Ordering::SeqCst))
    }

    pub fn get_recv_bytes(&self) -> Result<i64, Error> {
        Ok(self.recv_bytes.load(Ordering::SeqCst))
    }

    pub fn get_recv_packets(&self) -> Result<i64, Error> {
        Ok(self.recv_packets.load(Ordering::SeqCst))
    }

    pub fn get_resent_packets(&self) -> Result<i32, Error> {
        Ok(self.resent_packets.load(Ordering::SeqCst))
    }

    pub fn get_sent_bytes(&self) -> Result<i64, Error> {
        Ok(self.sent_bytes.load(Ordering::SeqCst))
    }

    pub fn get_sent_packets(&self) -> Result<i64, Error> {
        Ok(self.sent_packets.load(Ordering::SeqCst))
    }

    pub fn get_sent_pings(&self) -> Result<i32, Error> {
        Ok(self.sent_pings.load(Ordering::SeqCst))
    }

    pub fn increment_dropped_packets(&self) -> Result<(), Error> {
        self.dropped_packets.fetch_add(1, Ordering::SeqCst);
        Ok(())
    }

    pub fn increment_received_pongs(&self) -> Result<(), Error> {
        self.received_pongs.fetch_add(1, Ordering::SeqCst);
        Ok(())
    }

    pub fn increment_received_resends(&self) -> Result<(), Error> {
        self.received_resends.fetch_add(1, Ordering::SeqCst);
        Ok(())
    }

    pub fn increment_recv_packets(&self) -> Result<(), Error> {
        self.recv_packets.fetch_add(1, Ordering::SeqCst);
        Ok(())
    }

    pub fn increment_resent_packets(&self) -> Result<(), Error> {
        self.resent_packets.fetch_add(1, Ordering::SeqCst);
        Ok(())
    }

    pub fn increment_sent_packets(&self) -> Result<(), Error> {
        self.sent_packets.fetch_add(1, Ordering::SeqCst);
        Ok(())
    }

    pub fn increment_sent_pings(&self) -> Result<(), Error> {
        self.sent_pings.fetch_add(1, Ordering::SeqCst);
        Ok(())
    }

    pub fn set_connect_time(&self, value: i32) -> Result<(), Error> {
        self.connect_time.store(value, Ordering::SeqCst);
        Ok(())
    }

    pub fn set_incoming_bps(&self, value: i32) -> Result<(), Error> {
        self.incoming_bps.store(value, Ordering::SeqCst);
        Ok(())
    }

    pub fn set_last_ping_sent(&self, value: i32) -> Result<(), Error> {
        self.last_ping_sent.store(value, Ordering::SeqCst);
        Ok(())
    }

    pub fn set_outgoing_bps(&self, value: i32) -> Result<(), Error> {
        self.outgoing_bps.store(value, Ordering::SeqCst);
        Ok(())
    }

    pub(crate) fn record_pong(&self, tick_count: i32) {
        let sent = self.last_ping_sent.load(Ordering::Acquire);
        self.last_lag
            .store(tick_count.wrapping_sub(sent), Ordering::Release);
        self.received_pongs.fetch_add(1, Ordering::SeqCst);
    }

    pub(crate) fn apply_server_stat(&self, id: u32, value: f32) {
        let float = |field: &AtomicU32| field.store(value.to_bits(), Ordering::Release);
        let integer = |field: &AtomicI32| {
            if let Some(value) = csharp_convert_to_i32(value) {
                field.store(value, Ordering::Release);
            }
        };
        match id {
            0 => float(&self.dilation),
            1 => integer(&self.fps),
            2 => float(&self.physics_fps),
            3 => float(&self.agent_updates),
            4 => float(&self.frame_time),
            5 => float(&self.net_time),
            6 => float(&self.other_time),
            7 => float(&self.physics_time),
            8 => float(&self.agent_time),
            9 => float(&self.image_time),
            10 => float(&self.script_time),
            11 => integer(&self.objects),
            12 => integer(&self.scripted_objects),
            13 => integer(&self.agents),
            14 => integer(&self.child_agents),
            15 => integer(&self.active_scripts),
            16 => integer(&self.lslips),
            17 => integer(&self.inpps),
            18 => integer(&self.outpps),
            19 => integer(&self.pending_downloads),
            20 => integer(&self.pending_uploads),
            21 => integer(&self.virtual_size),
            22 => integer(&self.resident_size),
            23 => integer(&self.pending_local_uploads),
            24 => integer(&self.unacked_bytes),
            _ => {}
        }
    }

    integer_property!(active_scripts, set_active_scripts, active_scripts);
    float_property!(agent_time, set_agent_time, agent_time);
    float_property!(agent_updates, set_agent_updates, agent_updates);
    integer_property!(agents, set_agents, agents);
    integer_property!(child_agents, set_child_agents, child_agents);
    float_property!(dilation, set_dilation, dilation);
    integer_property!(fps, set_fps, fps);
    float_property!(frame_time, set_frame_time, frame_time);
    integer_property!(inpps, set_inpps, inpps);
    float_property!(image_time, set_image_time, image_time);
    integer_property!(lslips, set_lslips, lslips);
    integer_property!(last_lag, set_last_lag, last_lag);
    integer_property!(missed_pings, set_missed_pings, missed_pings);
    float_property!(net_time, set_net_time, net_time);
    integer_property!(outpps, set_outpps, outpps);
    integer_property!(objects, set_objects, objects);
    float_property!(other_time, set_other_time, other_time);
    integer_property!(pending_downloads, set_pending_downloads, pending_downloads);
    integer_property!(
        pending_local_uploads,
        set_pending_local_uploads,
        pending_local_uploads
    );
    integer_property!(pending_uploads, set_pending_uploads, pending_uploads);
    float_property!(physics_fps, set_physics_fps, physics_fps);
    float_property!(physics_time, set_physics_time, physics_time);
    integer_property!(resident_size, set_resident_size, resident_size);
    float_property!(script_time, set_script_time, script_time);
    integer_property!(scripted_objects, set_scripted_objects, scripted_objects);
    integer_property!(unacked_bytes, set_unacked_bytes, unacked_bytes);
    integer_property!(virtual_size, set_virtual_size, virtual_size);
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Arc;
    use std::thread;

    #[test]
    fn counters_are_atomic_and_properties_preserve_float_bits() {
        let stats = Arc::new(SimulatorSimStats::new().unwrap());
        let workers: Vec<_> = (0..4)
            .map(|_| {
                let stats = Arc::clone(&stats);
                thread::spawn(move || {
                    for _ in 0..1_000 {
                        stats.increment_recv_packets().unwrap();
                        stats.add_recv_bytes(7).unwrap();
                    }
                })
            })
            .collect();
        for worker in workers {
            worker.join().unwrap();
        }
        assert_eq!(stats.get_recv_packets().unwrap(), 4_000);
        assert_eq!(stats.get_recv_bytes().unwrap(), 28_000);
        assert_eq!(stats.get_and_increment_last_ping_id().unwrap(), 0);
        assert_eq!(stats.get_and_increment_last_ping_id().unwrap(), 1);

        let mut stats = Arc::try_unwrap(stats).ok().unwrap();
        stats.set_dilation(f32::from_bits(0x7fc0_1234));
        assert_eq!(stats.dilation().to_bits(), 0x7fc0_1234);

        stats.apply_server_stat(1, 44.5);
        assert_eq!(stats.fps(), 44);
        stats.apply_server_stat(1, 45.5);
        assert_eq!(stats.fps(), 46);
        stats.apply_server_stat(1, f32::NAN);
        assert_eq!(stats.fps(), 46);
    }
}
