// Copyright 2016-2017 The Perceptia Project Developers
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
// and associated documentation files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

//! `Timber` is simple logger facility. It provides means to write logs to given file in concurrent
//! applications.
//!
//! `timber!` macro takes as argument level number and writes log only if it is greater than zero.
//! If user defines log levels as constants compiler will be able to ignore strings passed to unused
//! logs and make application smaller. This way user can keep set of debugging logs, but compile
//! them out for release.
//!
//! By default `timber` writes logs to `stdout`. To write to a file one have to pass file path with
//! `timber::init(path)`.
//!
//! Example wrapper for `timber` could look like:
//!
//! ```
//! #[macro_use(timber)]
//! use timber;
//!
//! #[cfg(debug)]
//! pub mod level {
//!     pub const ERR: i32 = 1;
//!     pub const DEB: i32 = 2;
//!     pub const INF: i32 = 7;
//! }
//!
//! #[cfg(not(debug))]
//! pub mod level {
//!     pub const ERR: i32 = 1;
//!     pub const DEB: i32 = 0;
//!     pub const INF: i32 = 3;
//! }
//!
//! macro_rules! log_err{($($arg:tt)*) => {timber!($crate::level::ERR, "ERR", $($arg)*)}}
//! macro_rules! log_deb{($($arg:tt)*) => {timber!($crate::level::DEB, "DEB", $($arg)*)}}
//! macro_rules! log_inf{($($arg:tt)*) => {timber!($crate::level::INF, "INF", $($arg)*)}}
//!
//! //log_err!("This is error! I'm visible!");
//! //log_deb!("I'm debug. I'm visible only in debug mode.");
//! ```

// -------------------------------------------------------------------------------------------------

extern crate time;

use std::sync::{Arc, Mutex, MutexGuard, PoisonError, Once, ONCE_INIT};
use std::io::Write;

// -------------------------------------------------------------------------------------------------

/// Prints timber (processed log). Timber prints time (with microseconds), name of current thread,
/// line number and module name + log text.
#[macro_export]
macro_rules! timber {
    ($lnum:expr, $lname:expr, $($arg:tt)*) => {
        if $lnum > 0 {
            $crate::timber($lname, module_path!(), line!(), format_args!($($arg)*))
        }
    };
}

// -------------------------------------------------------------------------------------------------

/// Timber struct - used as singleton.
pub struct Timber {
    log_file: Option<std::fs::File>,
}

// -------------------------------------------------------------------------------------------------

/// Wrapper for `Timber` struct ensuring thread safety.
struct Wrapper {
    inner: Arc<Mutex<Timber>>,
}

// -------------------------------------------------------------------------------------------------

impl Timber {
    /// Print not formated log.
    pub fn log(&mut self, args: std::fmt::Arguments) {
        match self.log_file {
            Some(ref mut log_file) => {
                log_file.write(format!("{}", args).as_bytes()).expect("Failed to log!");
            }
            None => {
                print!("{}", args);
            }
        }
    }

    /// Print formated log.
    pub fn timber(&mut self, level: &str, module: &str, line: u32, args: std::fmt::Arguments) {
        // Get local time
        let tm = time::now().to_local();

        // Get current thread name
        let current_thread = std::thread::current();
        let thread = current_thread.name().unwrap_or("<unknown>");

        // Format log entry
        let entry = format!("{:02}:{:02}:{:02}.{:06} | {} | {:16} | {:4} | {:40} | {}",
                            tm.tm_hour,
                            tm.tm_min,
                            tm.tm_sec,
                            tm.tm_nsec / 1000,
                            level,
                            thread,
                            line,
                            module,
                            args);

        // Write log entry
        match self.log_file {
            Some(ref mut log_file) => {
                log_file.write(entry.as_bytes()).expect("Failed to timber!");
                log_file.write("\n".as_bytes()).expect("Failed to timber!");
            }
            None => {
                println!("{}", entry);
            }
        }
    }

    /// Initialize logger by providing output log file. Before call to this method logs will be
    /// printed to standard output.
    pub fn init(&mut self, path: &std::path::Path) -> Result<(), std::io::Error> {
        self.log_file = Some(std::fs::File::create(path)?);
        Ok(())
    }
}

// -------------------------------------------------------------------------------------------------

/// Get instance of logger singleton.
fn get_instance() -> &'static Wrapper {
    static mut LOGGER: *const Wrapper = 0 as *const Wrapper;
    static ONCE: Once = ONCE_INIT;

    unsafe {
        ONCE.call_once(|| {
            let logger = Wrapper { inner: Arc::new(Mutex::new(Timber { log_file: None })) };

            LOGGER = std::mem::transmute(Box::new(logger));
        });

        &(*LOGGER)
    }
}

// -------------------------------------------------------------------------------------------------

/// Get locked instance of `Timber` for guarded loging.
pub fn lock<'a>() -> Result<MutexGuard<'a, Timber>, PoisonError<MutexGuard<'a, Timber>>> {
    get_instance().inner.lock()
}

// -------------------------------------------------------------------------------------------------

/// Print formated log.
pub fn timber(level: &str, module: &str, line: u32, args: std::fmt::Arguments) {
    let mut timber = get_instance().inner.lock().unwrap();
    timber.timber(level, module, line, args);
}

// -------------------------------------------------------------------------------------------------

/// Initialize logger by providing output log file. Before call to this method logs will be printed
/// to standard output.
pub fn init(path: &std::path::Path) -> Result<(), std::io::Error> {
    let mut timber = get_instance().inner.lock().unwrap();
    timber.init(path)
}

// -------------------------------------------------------------------------------------------------