/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use std::fmt::{self, Write};

use log::{Level, LevelFilter, Log, Metadata, Record};
use moz_task::{Task, TaskRunnable, ThreadPtrHandle, ThreadPtrHolder};
use nserror::nsresult;
use nsstring::nsString;
use xpcom::{interfaces::mozIServicesLogSink, RefPtr};

pub struct LogSink {
    pub max_level: LevelFilter,
    logger: Option<ThreadPtrHandle<mozIServicesLogSink>>,
}

impl Default for LogSink {
    fn default() -> Self {
        LogSink {
            max_level: LevelFilter::Off,
            logger: None,
        }
    }
}

impl LogSink {
    /// Creates a log sink that adapts the Rust `log` crate to the Sync
    /// `Log.sys.mjs` logger.
    ///
    /// This is copied from `bookmark_sync::Logger`. It would be nice to share
    /// these, but, for now, we've just duplicated it to make prototyping
    /// easier.
    #[inline]
    pub fn new(max_level: LevelFilter, logger: ThreadPtrHandle<mozIServicesLogSink>) -> LogSink {
        LogSink {
            max_level,
            logger: Some(logger),
        }
    }

    /// Creates a log sink using the given Services `logger` as the
    /// underlying implementation. The `logger` will always be called
    /// asynchronously on its owning thread; it doesn't need to be
    /// thread-safe.
    pub fn with_logger(logger: Option<&mozIServicesLogSink>) -> Result<LogSink, nsresult> {
        Ok(if let Some(logger) = logger {
            // Fetch the maximum log level while we're on the main thread, so
            // that `LogSink::enabled()` can check it while on the background
            // thread. Otherwise, we'd need to dispatch a `LogTask` for every
            // log message, only to discard most of them when the task calls
            // into the logger on the main thread.
            let mut raw_max_level = 0i16;
            let rv = unsafe { logger.GetMaxLevel(&mut raw_max_level) };
            let max_level = if rv.succeeded() {
                match raw_max_level {
                    mozIServicesLogSink::LEVEL_ERROR => LevelFilter::Error,
                    mozIServicesLogSink::LEVEL_WARN => LevelFilter::Warn,
                    mozIServicesLogSink::LEVEL_DEBUG => LevelFilter::Debug,
                    mozIServicesLogSink::LEVEL_TRACE => LevelFilter::Trace,
                    mozIServicesLogSink::LEVEL_INFO => LevelFilter::Info,
                    _ => LevelFilter::Off,
                }
            } else {
                LevelFilter::Off
            };
            LogSink::new(
                max_level,
                ThreadPtrHolder::new(cstr!("mozIServicesLogSink"), RefPtr::new(logger))?,
            )
        } else {
            LogSink::default()
        })
    }

    /// Returns a reference to the underlying `mozIServicesLogSink`.
    pub fn logger(&self) -> Option<&mozIServicesLogSink> {
        self.logger.as_ref().and_then(|l| l.get())
    }

    /// Logs a message to the Sync logger, if one is set. This would be better
    /// implemented as a macro, as Dogear does, so that we can pass variadic
    /// arguments without manually invoking `fmt_args!()` every time we want
    /// to log a message.
    ///
    /// The `log` crate's macros aren't suitable here, because those log to the
    /// global logger. However, we don't want to set the global logger in our
    /// crate, because that will log _everything_ that uses the Rust `log` crate
    /// to the Sync logs, including WebRender and audio logging.
    pub fn debug(&self, args: fmt::Arguments) {
        let meta = Metadata::builder()
            .level(Level::Debug)
            .target(module_path!())
            .build();
        if self.enabled(&meta) {
            self.log(&Record::builder().args(args).metadata(meta).build());
        }
    }
}

impl Log for LogSink {
    #[inline]
    fn enabled(&self, meta: &Metadata) -> bool {
        self.logger.is_some() && meta.level() <= self.max_level
    }

    fn log(&self, record: &Record) {
        if !self.enabled(record.metadata()) {
            return;
        }
        if let Some(logger) = &self.logger {
            let mut message = nsString::new();
            if write!(message, "{}", record.args()).is_ok() {
                let task = LogTask {
                    logger: logger.clone(),
                    level: record.metadata().level(),
                    message,
                };
                let _ = TaskRunnable::new("extension_storage_sync::Logger::log", Box::new(task))
                    .and_then(|r| TaskRunnable::dispatch(r, logger.owning_thread()));
            }
        }
    }

    fn flush(&self) {}
}

/// Logs a message to the mirror logger. This task is created on the background
/// thread queue, and dispatched to the main thread.
struct LogTask {
    logger: ThreadPtrHandle<mozIServicesLogSink>,
    level: Level,
    message: nsString,
}

impl Task for LogTask {
    fn run(&self) {
        let logger = self.logger.get().unwrap();
        match self.level {
            Level::Error => unsafe {
                logger.Error(&*self.message);
            },
            Level::Warn => unsafe {
                logger.Warn(&*self.message);
            },
            Level::Debug => unsafe {
                logger.Debug(&*self.message);
            },
            Level::Trace => unsafe {
                logger.Trace(&*self.message);
            },
            Level::Info => unsafe {
                logger.Info(&*self.message);
            },
        }
    }

    fn done(&self) -> Result<(), nsresult> {
        Ok(())
    }
}
