/// Implements [`log::Log`] to log messages to `console.log`, `console.warn`, etc.
pub struct WebLogger {
    filter: log::LevelFilter,
}

impl WebLogger {
    /// Pipe all [`log`] events to the web console.
    pub fn init(filter: log::LevelFilter) -> Result<(), log::SetLoggerError> {
        log::set_max_level(filter);
        log::set_boxed_logger(Box::new(WebLogger::new(filter)))
    }

    pub fn new(filter: log::LevelFilter) -> Self {
        Self { filter }
    }
}

impl log::Log for WebLogger {
    fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
        metadata.level() <= self.filter
    }

    fn log(&self, record: &log::Record<'_>) {
        if !self.enabled(record.metadata()) {
            return;
        }

        let msg = if let (Some(file), Some(line)) = (record.file(), record.line()) {
            let file = shorten_file_path(file);
            format!("[{}] {file}:{line}: {}", record.target(), record.args())
        } else {
            format!("[{}] {}", record.target(), record.args())
        };

        match record.level() {
            log::Level::Trace => console::trace(&msg),
            log::Level::Debug => console::debug(&msg),
            log::Level::Info => console::info(&msg),
            log::Level::Warn => console::warn(&msg),
            log::Level::Error => console::error(&msg),
        }
    }

    fn flush(&self) {}
}

/// js-bindings for console.log, console.warn, etc
mod console {
    use wasm_bindgen::prelude::*;

    #[wasm_bindgen]
    extern "C" {
        /// `console.trace`
        #[wasm_bindgen(js_namespace = console)]
        pub fn trace(s: &str);

        /// `console.debug`
        #[wasm_bindgen(js_namespace = console)]
        pub fn debug(s: &str);

        /// `console.info`
        #[wasm_bindgen(js_namespace = console)]
        pub fn info(s: &str);

        /// `console.warn`
        #[wasm_bindgen(js_namespace = console)]
        pub fn warn(s: &str);

        /// `console.error`
        #[wasm_bindgen(js_namespace = console)]
        pub fn error(s: &str);
    }
}

/// Shorten a path to a Rust source file.
///
/// Example input:
/// * `/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs`
/// * `crates/rerun/src/main.rs`
/// * `/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs`
///
/// Example output:
/// * `tokio-1.24.1/src/runtime/runtime.rs`
/// * `rerun/src/main.rs`
/// * `core/src/ops/function.rs`
#[allow(dead_code)] // only used on web and in tests
fn shorten_file_path(file_path: &str) -> &str {
    if let Some(i) = file_path.rfind("/src/") {
        if let Some(prev_slash) = file_path[..i].rfind('/') {
            &file_path[prev_slash + 1..]
        } else {
            file_path
        }
    } else {
        file_path
    }
}

#[test]
fn test_shorten_file_path() {
    for (before, after) in [
        ("/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs", "tokio-1.24.1/src/runtime/runtime.rs"),
        ("crates/rerun/src/main.rs", "rerun/src/main.rs"),
        ("/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs", "core/src/ops/function.rs"),
        ("/weird/path/file.rs", "/weird/path/file.rs"),
        ]
    {
        assert_eq!(shorten_file_path(before), after);
    }
}
