/*-
 * syslog-rs - a syslog client translated from libc to rust
 * 
 * Copyright 2025 Aleksandr Morozov
 * 
 * The syslog-rs crate can be redistributed and/or modified
 * under the terms of either of the following licenses:
 *
 *   1. the Mozilla Public License Version 2.0 (the “MPL”) OR
 *
 *   2. The MIT License (MIT)
 *                     
 *   3. EUROPEAN UNION PUBLIC LICENCE v. 1.2 EUPL © the European Union 2007, 2016
 */

#[cfg(target_os = "linux")]
pub(crate) use self::portable_linux::*;

#[cfg(any(
    target_os = "freebsd",
    target_os = "dragonfly",
    target_os = "openbsd",
    target_os = "netbsd",
    target_os = "macos"
))]
pub(crate) use self::portable_bsd::*;

#[cfg(target_family = "windows")]
pub(crate) use self::portable_windows::*;

/// A portable code for Windows.
#[cfg(target_family = "windows")]
pub mod portable_windows
{
    use std::
    {
        env, ffi::{CStr, c_void}, io::{self, ErrorKind}, mem::transmute, time::Duration
    };

    use windows::
    {
        Win32::
        {
            Foundation::HANDLE, 
            Networking::WinSock::{self, SOCKET_ERROR}, 
            System::
            {
                EventLog::{EVENTLOG_INFORMATION_TYPE, RegisterEventSourceA, ReportEventA}, 
                SystemInformation
            }
        }, 
        core::{Free, PCSTR}
    };

    use crate::{LogFacility, LogMask, NILVALUE, Priority, SyslogMsgPriFac, WindowsEvent, error::SyRes};

    pub const DEFAULT_MAXGRAM_SIZE: u64 = 2048;

    /// A dummy network socket realization for the Windows Event Log.
    #[derive(Debug)]
    pub struct EventLogLocal
    {
        /// windows::Win32::Foundation::HANDLE 
        /// (but it is a *c_void which cannot be sent between threads)
        evs: usize,
    }

    impl Drop for EventLogLocal
    {
        fn drop(&mut self) 
        {
            unsafe { HANDLE(self.evs as *mut c_void).free() };
        }
    }

    impl EventLogLocal
    {
        pub 
        fn new(winnie_ev: &WindowsEvent) -> io::Result<Self>
        {
            /*OpenEventLogA*/

            let evs = 
                unsafe 
                {  
                    RegisterEventSourceA(
                        winnie_ev
                            .get_server_unc()
                            .map_or(
                                PCSTR::null(), 
                                |f| PCSTR::from_raw( transmute(f.as_ptr()) )
                            ), 
                        PCSTR::from_raw(
                                transmute(winnie_ev.get_service_name().as_ptr())
                            ) 
                    )
                }
                .map_err(|e|
                    io::Error::new(ErrorKind::ConnectionRefused, e)
                )?;
            
            return Ok(Self{ evs: evs.0 as usize });
        }

        /// Sends message to log.
        /// 
        /// If `msg` starts with sign `|`, it contains a priority and facility data,
        /// otherwise default values will be set. The `|` will be ommited from message.
        /// It must be `NULL-terminated`!
        /// 
        /// The message may begin from '|'PRI'|', see `windows_10.rs` as example.
        /// 
        /// # Returns
        /// 
        /// * [ErrorKind::InvalidData] - if argument `msg` was not able to wrap into [CStr].
        ///     Probably the `msg` is not NULL-terminated! The error description can be 
        ///     obtained by downcasting the error to [std::ffi::FromBytesWithNulError].
        /// 
        /// * [ErrorKind::ConnectionRefused] - [ReportEventA] failed with error which can be
        ///     obtained by downcasting the error into [windows::core::Error].
        pub 
        fn send(&self, msg: &[u8]) -> io::Result<usize>
        {
            if msg.len() == 0
            {
                return Ok(0);
            }

            let mut event_log_type = EVENTLOG_INFORMATION_TYPE;
            let mut facility = LogFacility::LOG_USER.into_win_facility();
            
            if msg[0] == '|' as u8
            {
                for (idx, c) in msg.iter().enumerate().skip(1)
                {
                    if *c == '|' as u8
                    {
                        let Ok(pri_fac) = 
                            SyslogMsgPriFac::try_from(&msg[1..idx])
                                else { break };

                        event_log_type = pri_fac.get_priority().into();
                        facility = pri_fac.get_log_facility().into_win_facility();
                        break;
                    }

                    if idx == 8 
                    {
                        break;
                    }
                }
            }

            let cmsg = 
                CStr::from_bytes_with_nul(msg)
                    .map_err(|e|
                        io::Error::new(ErrorKind::InvalidData, e)
                    )?;

            let msgs =
                 &[PCSTR::from_raw( unsafe { transmute(/*copied_msg.as_c_str().as_ptr()*/ cmsg.as_ptr()) })];

            unsafe
            {
                ReportEventA(
                    HANDLE(self.evs as *mut c_void), 
                    event_log_type, //EVENTLOG_ERROR_TYPE,
                    1, 
                    0x20010000 | facility, 
                    None, 
                    0, 
                    Some(msgs), 
                    None
                )
                
            }
            .map_err(|e|
                io::Error::new(ErrorKind::ConnectionRefused, e)
            )?;

            return Ok(msg.len());
        }
    }

    /// Reutns the current process name, if available
    pub(crate) 
    fn p_getprogname() -> Option<String>
    {
        return 
            env::current_exe()
                .ok()
                .map_or(None, 
                    |path| 
                    path
                        .file_name()
                        .map(|name| name.to_string_lossy().to_string())
                );
    }

    pub(crate)
    fn get_local_dgram_maxdgram() -> u64
    {
        return DEFAULT_MAXGRAM_SIZE;
    }

    pub(crate)
    fn get_uptime() -> SyRes<Duration>
    {
        let ret: u64 = unsafe {SystemInformation::GetTickCount64() };

        return Ok(Duration::from_millis(ret));
    }

    pub(crate)
    fn portable_gethostname() -> SyRes<String>
    {
        let mut hostname = vec![0_u8; 512];
        
        let res = 
            unsafe
            {
                WinSock::gethostname(&mut hostname)
            };

        if res == SOCKET_ERROR 
        {
            let err = unsafe { WinSock::WSAGetLastError() };

            throw_error!("can bot obtain kern.boottime, err: '{}'", err.0);
        }

        return Ok(String::from_utf8(hostname).map_or(NILVALUE.into(), |f| f));
    }
}

#[cfg(target_os = "linux")]
pub mod portable_linux
{
    use std::ffi::CStr;
    use std::path::Path;
    use std::time::Duration;

    use nix::libc;

    use crate::error::SyRes;
    
    pub const DEFAULT_MAXGRAM_SIZE: u64 = 2048;

    #[link(name = "c")]
    unsafe extern "C" {
        pub static mut program_invocation_name : *mut libc::c_char ;
    }

    /// Returns the current program's name.
    pub(crate) 
    fn p_getprogname() -> Option<String>
    {
        let pn = unsafe{ program_invocation_name };

        let temp = unsafe {CStr::from_ptr(pn)};

        return 
            temp
                .to_str()
                .ok()
                .map_or(None, |r| Path::new(r).file_name().map(|r| r.to_string_lossy().into()));
/*
        match temp.to_str()
        {
            Ok(r) => 
            {
                let path = Path::new(r);
                match path.file_name()
                {
                    Some(r) => return Some(r.to_string_lossy().into()),
                    None => return None,
                }
            },
            Err(_) => 
                return None,
        }
   */
    }

    pub(crate) 
    fn get_local_dgram_maxdgram() -> u64
    {
        return DEFAULT_MAXGRAM_SIZE;
    }

    pub(crate)
    fn get_uptime() -> SyRes<Duration>
    {
        let sysinfo = 
            nix::sys::sysinfo::sysinfo()
                .map_err(|e|
                    map_error!("get_uptime() sysinfo error: {}", e)
                )?;

        return Ok(sysinfo.uptime());
    }

    pub(crate)
    fn portable_gethostname() -> SyRes<String>
    {
        return 
            nix::unistd::gethostname()
                .map_or_else(
                    |e|
                    Err(map_error!("gethostname() error: {}", e)),
                    |hn| 
                    hn.into_string()
                        .map_or(
                            Err(map_error!("gethostname() into_string() error")),
                            |hostname| Ok(hostname)
                        )
                );
    }
}


#[cfg(any(
    target_os = "freebsd",
    target_os = "dragonfly",
    target_os = "openbsd",
    target_os = "netbsd",
    target_os = "macos"
))]
pub mod portable_bsd
{
    use std::{ffi::{CStr, CString}, time::Duration};

    use chrono::Local;
    use nix::{errno::Errno, libc::{self, timeval}};

    use crate::error::SyRes;

    pub const DEFAULT_MAXGRAM_SIZE: u64 = 2048;

    #[link(name = "c")]
    unsafe extern "C" {
        fn getprogname() -> *const libc::c_char;
    }

    /// Reutns the current process name, if available
    pub(crate) 
    fn p_getprogname() -> Option<String>
    {
        let pn = unsafe { getprogname() };

        let temp = unsafe {CStr::from_ptr(pn)};

        match temp.to_str()
        {
            Ok(r) => 
                return Some(r.to_string()),
            Err(_) => 
                return None,
        }
    
    }

    #[cfg(target_os = "openbsd")]
    pub(crate)
    fn get_local_dgram_maxdgram() -> u64
    {
        const UNPCTL_RECVSPACE: libc::c_int = 1;
        const UNPCTL_SENDSPACE: libc::c_int = 2;
        let mibs = [libc::CTL_NET, libc::PF_UNIX, libc::SOCK_DGRAM, UNPCTL_SENDSPACE];
            let mut maxdgram: u64 = 0;
            let mut maxdgram_size: libc::size_t = std::mem::size_of_val(&maxdgram) as libc::size_t;
        
        let res = 
            unsafe
            {
            libc::sysctl(
                mibs.as_ptr(), 
                mibs.len() as libc::c_uint, 
                &mut maxdgram as *mut _ as *mut libc::c_void, 
                        &mut maxdgram_size as *mut _ as *mut libc::size_t,
                        std::ptr::null_mut(), 
                        0
                    )
            };
            
        if res == -1
        {
            if cfg!(feature = "dgram_sysctl_failure_panic")
            {
                let err = Errno::last_raw();
                panic!("can not obtain MAXDGRAM from sysctl \
            	    libc::CTL_NET, libc::PF_UNIX, libc::SOCK_DGRAM, UNPCTL_SENDSPACE '{:?}', err: '{}'", mibs,  err);
            }
            else 
            {
                return DEFAULT_MAXGRAM_SIZE;
            }
        }
        else if res == 0
        {
            return maxdgram;
        }
        else
        {
	    panic!("can not obtain MAXDGRAM from sysctl \
            	libc::CTL_NET, libc::PF_UNIX, libc::SOCK_DGRAM, UNPCTL_SENDSPACE '{:?}', err: '{}'", mibs,  res);
        }
    }
    
    #[cfg(not(target_os = "openbsd"))]
    pub(crate) 
    fn get_local_dgram_maxdgram() -> u64
    {
        let name = CString::new("net.local.dgram.maxdgram").unwrap();
        let mut maxdgram: u64 = 0;
        let mut maxdgram_size: libc::size_t = std::mem::size_of_val(&maxdgram) as libc::size_t;

        let res = 
            unsafe 
            {
                libc::sysctlbyname(
                    name.as_ptr(), 
                    &mut maxdgram as *mut _ as *mut libc::c_void, 
                    &mut maxdgram_size as *mut _ as *mut libc::size_t,
                    std::ptr::null(), 
                    0
                )
            };

        if res == -1
        {
            if cfg!(feature = "dgram_sysctl_failure_panic")
            {
                let err = Errno::last_raw();
                panic!("can not obtain MAXDGRAM from sysctl '{}', err: '{}'", name.to_string_lossy(), err);
            }
            else 
            {
                return DEFAULT_MAXGRAM_SIZE;
            }
        }
        else if res == 0
        {
            return maxdgram;
        }
        else
        {
            panic!("can not obtain MAXDGRAM from sysctl '{}', unknwon res: '{}'", name.to_string_lossy(), res);
        }
    }
    
    #[cfg(target_os = "openbsd")]
    pub(crate)
    fn get_uptime() -> SyRes<Duration>
    {
	let mibs = [libc::CTL_KERN, libc::KERN_BOOTTIME];
	let mut boottime: timeval = unsafe { std::mem::zeroed() };
        let mut size: libc::size_t = std::mem::size_of_val(&boottime) as libc::size_t;
        
	let res = 
	    unsafe
	    {
		libc::sysctl(
		    mibs.as_ptr(), 
		    mibs.len() as libc::c_uint, 
		    &mut boottime as *mut _ as *mut libc::c_void, 
                    &mut size as *mut _ as *mut libc::size_t,
                    std::ptr::null_mut(), 
                    0
                )
	    };

        
        if res == -1
        {
            throw_error!("can bot obtain kern.boottime, err: '{}'", Errno::last_raw());
        }
        else if res == 0
        {
            let c = 
                chrono::Duration::new(boottime.tv_sec, boottime.tv_usec as u32)
                    .ok_or_else(||
                        map_error!("timeval to chrono::Duration converion error")
                    )?;
            let now = Local::now();

            let r = 
                now
                    .checked_sub_signed(c)
                    .ok_or_else(||
                        map_error!("error checked_sub_signed()")
                    )?;
            return Ok(Duration::from_millis(r.timestamp_millis() as u64));
        }
        else
        {
            throw_error!("can not obtain  kern.boottime from sysctl \
        	libc::CTL_KERN, libc::KERN_BOOTTIM {:?}, unknwon res: '{}'", mibs, res);
        }
    }
    

    #[cfg(not(target_os = "openbsd"))]
    pub(crate)
    fn get_uptime() -> SyRes<Duration>
    {
        let name = CString::new("kern.boottime").unwrap();

        let mut boottime: timeval = unsafe { std::mem::zeroed() };
        let mut size: libc::size_t = std::mem::size_of_val(&boottime) as libc::size_t;

        let res = 
            unsafe 
            {
                libc::sysctlbyname(
                    name.as_ptr(), 
                    &mut boottime as *mut _ as *mut libc::c_void, 
                    &mut size as *mut _ as *mut libc::size_t,
                    std::ptr::null(), 
                    0)
            };

        
        if res == -1
        {
            throw_error!("can bot obtain kern.boottime, err: '{}'", Errno::last_raw());
        }
        else if res == 0
        {
            let c = 
                chrono::Duration::new(boottime.tv_sec, boottime.tv_usec as u32)
                    .ok_or_else(||
                        map_error!("timeval to chrono::Duration converion error")
                    )?;
            let now = Local::now();

            let r = 
                now
                    .checked_sub_signed(c)
                    .ok_or_else(||
                        map_error!("error checked_sub_signed()")
                    )?;
            return Ok(Duration::from_millis(r.timestamp_millis() as u64));
        }
        else
        {
            throw_error!("can not obtain  kern.boottime from sysctl '{}', unknwon res: '{}'", name.to_string_lossy(), res);
        }
    }

    pub(crate)
    fn portable_gethostname() -> SyRes<String>
    {
        return 
            nix::unistd::gethostname()
                .map_or_else(
                    |e|
                    Err(map_error!("gethostname() error: {}", e)),
                    |hn| 
                    hn.into_string()
                        .map_or(
                            Err(map_error!("gethostname() into_string() error")),
                            |hostname| Ok(hostname)
                        )
                );
    }

    #[cfg(test)]
    mod tests
    {
        use crate::portable::get_uptime;

        #[test]
        fn test_get_uptime()
        {
            let res = get_uptime();
            assert_eq!(res.is_ok(), true);

            println!("{:?}", res.unwrap());
        }
    }
}


/// Returns pid of current process. Not thread id!
#[inline]
pub 
fn get_pid() -> u32
{
    return std::process::id();
}

#[cfg(test)]
mod test_portables
{
    use super::*;
    
    #[test]
    fn test_get_procname()
    {
        let procname = p_getprogname();

        println!("Processname is: {:?}", procname);

        assert_eq!(procname.is_some(), true);
        assert_eq!(procname.as_ref().unwrap().starts_with("syslog_rs"), true);
    }

    #[test]
    fn test_maxgram()
    {
        let max = get_local_dgram_maxdgram();

        println!("maxdgram: '{}'", max);

        assert_eq!(max > 0, true);
    }
}
