/*-
 * 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
 */

use std::{cell::RefCell, marker::PhantomData};

use crate::
{
    error::SyRes, 
    formatters::{DefaultSyslogFormatter, SyslogFormatter}, 
    sync::{syslog_sync_internal::SyslogSocketLockless, LogItems, SyStream}, 
    LogFacility, 
    LogStat, 
    Priority, 
    SyStreamApi, 
    SyslogApi, 
    SyslogDestination,
};

#[cfg(target_family = "unix")]
use crate::SyslogLocal;

#[cfg(target_family = "windows")]
use crate::WindowsEvent;

#[cfg(target_family = "unix")]
pub type DefaultLocalSyslogDestination = SyslogLocal;
#[cfg(target_family = "windows")]
pub type DefaultLocalSyslogDestination = WindowsEvent;

/// A threal local syslog which is completely lockless! It can be used in signle threaded
/// applications or with the [thread_local] functionality.
/// 
/// ```ignore
/// thread_local! 
/// {
///     // Could add pub to make it public to whatever Foo already is public to.
///     static SYSLOG: RefCell<SingleSyslog> = 
///         RefCell::new(SingleSyslog::openlog_with(Some("test"), LogStat::LOG_PID , 
///             LogFacility::LOG_DAEMON, SyslogLocal::new()).unwrap());
/// }
/// ```
/// 
/// A stream is availble via [SyStreamApi].
/// 
/// ```ignore
/// let _ = write!(SYSLOG.stream(Priority::LOG_DEBUG), "test {} 123 stream test ", d);
/// ```
/// 
/// The instances will be completly separated and have own FD.
/// 
/// # Generics
/// 
/// * `D` - a [SyslogDestination] instance which is either:
///     [SyslogLocal], [crate::syslog_provider::SyslogFile], [crate::syslog_provider::SyslogNet], 
///     [crate::syslog_provider::SyslogTls]. By default a `SyslogLocal` is selected.
/// 
/// * `F` - a [SyslogFormatter] which sets the instance which would 
///     format the message.
/// 
#[derive(Debug)]
pub struct SingleSyslog<F = DefaultSyslogFormatter, D = DefaultLocalSyslogDestination>
where 
    F: SyslogFormatter, 
    D: SyslogDestination,   
{
    /// An identification i.e program name, thread name
    log_items: RefCell<LogItems>,

    /// A stream (unixdatagram, udp, tcp)
    stream: RefCell<SyslogSocketLockless<D>>,

    _p: PhantomData<F>,

    _p_not_ss: PhantomData<*const ()>
}

impl SingleSyslog
{
    /// Opens a default connection to the local syslog server with default formatter.
    /// 
    /// In order to access the syslog API, use the [SyslogApi].
    /// 
    /// # Arguments
    /// 
    /// * `ident` - A program name which will appear on the logs. If none, will be determined
    ///     automatically.
    /// 
    /// * `logstat` - [LogStat] an instance config.
    /// 
    /// * `facility` - [LogFacility] a syslog facility.
    /// 
    /// * `net_tap_prov` - a [SyslogLocal] instance with configuration.
    /// 
    /// # Returns
    /// 
    /// A [SyRes] is returned ([Result]) with: 
    /// 
    /// * [Result::Ok] - with instance
    /// 
    /// * [Result::Err] - with error description.
    pub 
    fn openlog(ident: Option<&str>, logstat: LogStat, facility: LogFacility, net_tap_prov: DefaultLocalSyslogDestination) -> SyRes<Self> 
    {
        let log_items = 
            LogItems::new(ident, 0xff, logstat, facility);

        let stream = 
            SyslogSocketLockless::<DefaultLocalSyslogDestination>::new(logstat, net_tap_prov)?;
        
        return Ok(
            Self
            {
                log_items: RefCell::new(log_items),
                stream: RefCell::new(stream),
                _p: PhantomData,
                _p_not_ss: PhantomData
            }
        );
    }
}

impl<F, D> SingleSyslog<F, D>
where F: SyslogFormatter, D: SyslogDestination
{
    /// Opens a default connection to the local syslog server with default formatter.
    /// 
    /// # Arguments
    /// 
    /// * `ident` - A program name which will appear on the logs. If none, will be determined
    ///     automatically.
    /// 
    /// * `logstat` - [LogStat] an instance config.
    /// 
    /// * `facility` - [LogFacility] a syslog facility.
    /// 
    /// * `net_tap_prov` - a [SyslogLocal] instance with configuration.
    /// 
    /// # Returns
    /// 
    /// A [SyRes] is returned ([Result]) with: 
    /// 
    /// * [Result::Ok] - with instance
    /// 
    /// * [Result::Err] - with error description.
    pub 
    fn openlog_with(ident: Option<&str>, logstat: LogStat, facility: LogFacility, net_tap_prov: D) -> SyRes<Self> 
    {
        let log_items = 
            LogItems::new(ident, 0xff, logstat, facility);

        let stream = 
            SyslogSocketLockless::<D>::new(logstat, net_tap_prov)?;
        
        return Ok(
            Self
            {
                log_items: RefCell::new(log_items),
                stream: RefCell::new(stream),
                _p: PhantomData,
                _p_not_ss: PhantomData
            }
        );
    }
}

impl<F, D> SyslogApi<F, D> for SingleSyslog<F, D>
where F: SyslogFormatter, D: SyslogDestination
{
        /// Connects the current instance to the syslog server (destination).
    #[inline]
    fn connectlog(&self) -> SyRes<()>
    {
        return 
            self
                .stream
                .borrow_mut()
                .connectlog();
    }

    /// Sets the logmask to filter out the syslog calls.
    /// 
    /// See macroses [LOG_MASK] and [LOG_UPTO] to generate mask
    ///
    /// # Example
    ///
    /// LOG_MASK!(Priority::LOG_EMERG) | LOG_MASK!(Priority::LOG_ERROR)
    ///
    /// or
    ///
    /// ~(LOG_MASK!(Priority::LOG_INFO))
    /// LOG_UPTO!(Priority::LOG_ERROR)
    #[inline]
    fn setlogmask(&self, logmask: i32) -> SyRes<i32>
    {
        return Ok(
            self
                .log_items
                .borrow_mut()
                .set_logmask(logmask)
        );
    }

    /// Closes connection to the syslog server (destination).
    #[inline]
    fn closelog(&self) -> SyRes<()> 
    {
        return 
            self
                .stream
                .borrow_mut()
                .disconnectlog();
    }

    /// Similar to libc, syslog() sends data to syslog server.
    /// 
    /// # Arguments
    ///
    /// * `pri` - a priority [Priority]
    ///
    /// * `fmt` - a formatter [SyslogFormatter] message. In C exists a functions with
    ///     variable argumets amount. In Rust you should create your
    ///     own macros like format!() or use format!()]. The [String] and ref `'static` 
    ///     [str] can be passed directly.
    #[inline]
    fn syslog(&self, pri: Priority, fmt: F) 
    {
        let Some((formatted_msg, logstat)) = 
            self.log_items.borrow().vsyslog1_msg::<F, D>(pri, &fmt)
            else { return };

        self.stream.borrow_mut().vsyslog1(logstat, formatted_msg);

        return;
    }

    /// This function can be used to update the facility name, for example
    /// after fork().
    /// 
    /// # Arguments
    /// 
    /// * `ident` - an [Option] optional new identity (up to 48 UTF8 chars)
    ///     If set to [Option::None] would request the program name from OS.
    #[inline]
    fn change_identity(&self, ident: Option<&str>) -> SyRes<()>
    {
        self.log_items.borrow_mut().set_identity(ident);

        return Ok(());
    }

    /// Re-opens the connection to the syslog server. Can be used to 
    /// rotate logs(handle SIGHUP).
    /// 
    /// # Returns
    /// 
    /// A [Result] is retured as [SyRes].
    /// 
    /// * [Result::Ok] - with empty inner type.
    /// 
    /// * [Result::Err] - an error code and description 
    #[inline]
    fn reconnect(&self) -> SyRes<()>
    {
        return
            self
                .stream
                .borrow_mut()
                .reconnectlog();
    }

    /// Updates the instance's socket. `tap_data` [TapTypeData] should be of
    /// the same variant (type) as current.
    #[inline]
    fn update_tap_data(&self, tap_data: D) -> SyRes<()>
    {
        return 
            self
                .stream
                .borrow_mut()
                .update_tap_data(tap_data.clone());
    }
}

impl<'stream, F: SyslogFormatter, D: SyslogDestination> SyStreamApi<'stream, F, D, SingleSyslog<F, D>> 
for SingleSyslog<F, D>
{
    fn stream(&'stream self, pri: Priority) -> SyStream<'stream, D, F, SingleSyslog<F, D>> 
    {
        return 
            SyStream 
            { 
                inner: self, 
                pri: pri, 
                _p: PhantomData, 
                _p1: PhantomData 
            };
    }
}


#[cfg(target_family = "unix")]
#[cfg(test)]
mod tests
{
    use std::{cell::RefCell, time::Instant};

    use crate::{sync::syslog_threadlocal::SingleSyslog, LogFacility, LogStat, Priority, SyStreamApi, SyslogApi, SyslogLocal};

    #[test]
    fn test_thread_single1()
    {
        thread_local! 
        {
            // Could add pub to make it public to whatever Foo already is public to.
            static SYSLOG: RefCell<SingleSyslog> = 
                RefCell::new(SingleSyslog::openlog_with(Some("test"), LogStat::LOG_PID , 
                    LogFacility::LOG_DAEMON, SyslogLocal::new()).unwrap());
        }

        let thread = 
            std::thread::spawn(move ||
                {
                    for i in 0..10
                    {
                        let fmms = format!("test message {} from thread {:?}", i, std::thread::current().name());
                        let s = Instant::now();

                        SYSLOG
                            .with_borrow_mut(|syslog| 
                                syslog
                                    .syslog(
                                        Priority::LOG_ERR, 
                                        fmms.into()
                                    )
                            );

                        let e = s.elapsed();

                        println!("{:?}", e);
                    }
                }
            );


        SYSLOG
            .with_borrow_mut(|syslog| 
                syslog
                    .syslog(
                        Priority::LOG_DEBUG, 
                        format!("main test message from thread {:?}", std::thread::current().name()).into()
                    )
            );


        thread.join().unwrap();

        return;

    }

    #[test]
    fn test_single_stream_test()
    {
        use std::fmt::Write;

        let log = 
                SingleSyslog::openlog(
                    Some("test1"), 
                    LogStat::LOG_CONS | LogStat::LOG_NDELAY | LogStat::LOG_PID, 
                    LogFacility::LOG_DAEMON,
                SyslogLocal::new());

        assert_eq!(log.is_ok(), true, "{}", log.err().unwrap());

        let log = log.unwrap();
        
        write!(log.stream(Priority::LOG_DEBUG), "test stream singlesyslog").unwrap();

        for i in 0..3
        {
            let s = Instant::now();
            write!(log.stream(Priority::LOG_DEBUG), "test stream singlesyslog {}", i).unwrap();
            let e = s.elapsed();

            println!("{:?}", e);
        }

    }
}

#[cfg(target_family = "windows")]
#[cfg(test)]
mod tests
{
    use std::{cell::RefCell, time::Instant};

    use crate::{sync::syslog_threadlocal::SingleSyslog, LogFacility, LogStat, Priority, SyStreamApi, SyslogApi, WindowsEvent};

    #[test]
    fn test_thread_signalling()
    {
        thread_local! 
        {
            // Could add pub to make it public to whatever Foo already is public to.
            static SYSLOG: RefCell<SingleSyslog> = 
                RefCell::new(SingleSyslog::openlog_with(Some("test"), LogStat::LOG_PID , 
                    LogFacility::LOG_DAEMON, WindowsEvent::new()).unwrap());
        }

        let thread = 
            std::thread::spawn(move ||
                {
                    for i in 0..10
                    {
                        let fmms = format!("test message {} from thread {:?}", i, std::thread::current().name());
                        let s = Instant::now();

                        SYSLOG
                            .with_borrow_mut(|syslog| 
                                syslog
                                    .syslog(
                                        Priority::LOG_ERR, 
                                        fmms.into()
                                    )
                            );

                        let e = s.elapsed();

                        println!("{:?}", e);
                    }
                }
            );


        SYSLOG
            .with_borrow_mut(|syslog| 
                syslog
                    .syslog(
                        Priority::LOG_DEBUG, 
                        format!("main test message from thread {:?}", std::thread::current().name()).into()
                    )
            );


        thread.join().unwrap();

        return;

    }

    #[test]
    fn test_single_stream_test()
    {
        use std::fmt::Write;

        let log = 
                SingleSyslog::openlog(
                    Some("test1"), 
                    LogStat::LOG_CONS | LogStat::LOG_NDELAY | LogStat::LOG_PID, 
                    LogFacility::LOG_DAEMON,
                WindowsEvent::new());

        assert_eq!(log.is_ok(), true, "{}", log.err().unwrap());

        let log = log.unwrap();
        
        write!(log.stream(Priority::LOG_DEBUG), "test stream singlesyslog").unwrap();

        for i in 0..3
        {
            let s = Instant::now();
            write!(log.stream(Priority::LOG_DEBUG), "test stream singlesyslog {}", i).unwrap();
            let e = s.elapsed();

            println!("{:?}", e);
        }

    }
}

