#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
// hide console window on Windows in release

#[cfg(feature = "memory")]
use re_memory::AccountingAllocator;

#[cfg(feature = "memory")]
#[global_allocator]
static GLOBAL: AccountingAllocator<std::alloc::System> =
    AccountingAllocator::new(std::alloc::System);

use notedeck::{Args, DataPath, DataPathType, Notedeck, NotedeckOptions};
use notedeck_chrome::{setup::generate_native_options, Chrome};
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::EnvFilter;

fn setup_logging(path: &DataPath) -> Option<WorkerGuard> {
    #[allow(unused_variables)] // need guard to live for lifetime of program
    let (maybe_non_blocking, maybe_guard) = {
        let log_path = path.path(DataPathType::Log);
        // Setup logging to file

        use tracing_appender::{
            non_blocking,
            rolling::{RollingFileAppender, Rotation},
        };

        let file_appender = RollingFileAppender::builder()
            .rotation(Rotation::DAILY)
            .filename_prefix(format!("notedeck-{}", env!("CARGO_PKG_VERSION")))
            .filename_suffix("log")
            .max_log_files(3)
            .build(log_path)
            .expect("failed to initialize rolling file appender");

        let (non_blocking, _guard) = non_blocking(file_appender);

        (Some(non_blocking), Some(_guard))
    };

    // Log to stdout (if you run with `RUST_LOG=debug`).
    if let Some(non_blocking_writer) = maybe_non_blocking {
        use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt};

        let console_layer = fmt::layer().with_target(true).with_writer(std::io::stdout);

        // Create the file layer (writes to the file)
        let file_layer = fmt::layer()
            .with_ansi(false)
            .with_writer(non_blocking_writer);

        let env_filter =
            EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("notedeck=info"));

        // Set up the subscriber to combine both layers
        tracing_subscriber::registry()
            .with(console_layer)
            .with(file_layer)
            .with(env_filter)
            .init();
    } else {
        tracing_subscriber::fmt()
            .with_env_filter(EnvFilter::from_default_env())
            .init();
    }

    maybe_guard
}

fn resolve_native_title(args_raw: &[String]) -> (String, bool) {
    let (args, _) = Args::parse(args_raw);
    let show_title = args.options.contains(NotedeckOptions::ShowTitle);
    let title = args.title.unwrap_or_else(|| "Damus Notedeck".to_string());
    (title, show_title)
}

// Desktop
#[cfg(not(target_arch = "wasm32"))]
#[tokio::main]
async fn main() {
    #[cfg(feature = "memory")]
    re_memory::accounting_allocator::set_tracking_callstacks(true);

    let base_path = DataPath::default_base_or_cwd();
    let path = DataPath::new(base_path.clone());

    // This guard must be scoped for the duration of the entire program so all logs will be written
    let _guard = setup_logging(&path);

    // Pre-scan for --title so we can set the window title and show the
    // titlebar before eframe creates the window.
    let args_raw: Vec<String> = std::env::args().collect();
    let (title, show_title) = resolve_native_title(&args_raw);

    let _res = eframe::run_native(
        &title,
        generate_native_options(path, show_title),
        Box::new(|cc| {
            let args: Vec<String> = std::env::args().collect();
            let ctx = &cc.egui_ctx;

            let mut notedeck = Notedeck::init(ctx, base_path, &args);
            notedeck.setup(ctx);
            let chrome = Chrome::new_with_apps(cc, &args, &mut notedeck)?;
            notedeck.set_app(chrome);

            Ok(Box::new(notedeck))
        }),
    );
}

/*
 * TODO: nostrdb not supported on web
 *
#[cfg(target_arch = "wasm32")]
pub fn main() {
    // Make sure panics are logged using `console.error`.
    console_error_panic_hook::set_once();

    // Redirect tracing to console.log and friends:
    tracing_wasm::set_as_global_default();

    wasm_bindgen_futures::spawn_local(async {
        let web_options = eframe::WebOptions::default();
        eframe::start_web(
            "the_canvas_id", // hardcode it
            web_options,
            Box::new(|cc| Box::new(Damus::new(cc, "."))),
        )
        .await
        .expect("failed to start eframe");
    });
}
*/

#[cfg(test)]
mod tests {
    use super::{resolve_native_title, Notedeck};
    use notedeck::{Args, DataPath, NotedeckOptions};
    use notedeck_columns::Damus;
    use std::path::{Path, PathBuf};
    use tempfile::TempDir;

    fn create_tmp_dir() -> PathBuf {
        tempfile::TempDir::new()
            .expect("tmp path")
            .path()
            .to_path_buf()
    }

    fn rmrf(path: impl AsRef<Path>) {
        let _ = std::fs::remove_dir_all(path);
    }

    fn apply_window_builder_for_args(args: &[String]) -> (String, bool, egui::ViewportBuilder) {
        let (title, show_title) = resolve_native_title(args);
        let tempdir = TempDir::new().expect("tmp path");
        let options = notedeck_chrome::setup::generate_native_options(
            DataPath::new(tempdir.path()),
            show_title,
        );
        let builder = (options.window_builder.expect("window builder should exist"))(
            egui::ViewportBuilder::default(),
        );
        (title, show_title, builder)
    }

    /// Ensure dbpath actually sets the dbpath correctly.
    #[tokio::test]
    async fn test_dbpath() {
        let datapath = create_tmp_dir();
        let dbpath = create_tmp_dir();
        let args: Vec<String> = [
            "notedeck-test",
            "--testrunner",
            "--datapath",
            &datapath.to_str().unwrap(),
            "--dbpath",
            &dbpath.to_str().unwrap(),
        ]
        .iter()
        .map(|s| s.to_string())
        .collect();

        let ctx = egui::Context::default();
        let _app = Notedeck::init(&ctx, &datapath, &args);

        assert!(Path::new(&dbpath.join("data.mdb")).exists());
        assert!(Path::new(&dbpath.join("lock.mdb")).exists());
        assert!(!Path::new(&datapath.join("db")).exists());

        rmrf(datapath);
        rmrf(dbpath);
    }

    #[tokio::test]
    async fn test_column_args() {
        let tmpdir = create_tmp_dir();
        let npub = "npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s";
        let args: Vec<String> = [
            "notedeck-test",
            "--testrunner",
            "--pub",
            npub,
            "-c",
            "notifications",
            "-c",
            "contacts",
        ]
        .iter()
        .map(|s| s.to_string())
        .collect();

        let ctx = egui::Context::default();
        let mut notedeck = Notedeck::init(&ctx, &tmpdir, &args);
        let mut app_ctx = notedeck.app_context(&ctx);
        let app = Damus::new(&mut app_ctx, &args);

        assert_eq!(app.columns(app_ctx.accounts).columns().len(), 2);

        let tl1 = app
            .columns(app_ctx.accounts)
            .column(0)
            .router()
            .top()
            .timeline_id()
            .unwrap();

        let tl2 = app
            .columns(app_ctx.accounts)
            .column(1)
            .router()
            .top()
            .timeline_id()
            .unwrap();

        assert_eq!(app.timeline_cache.num_timelines(), 2);
        assert!(app.timeline_cache.get(&tl1).is_some());
        assert!(app.timeline_cache.get(&tl2).is_some());

        rmrf(tmpdir);
    }

    #[tokio::test]
    async fn test_unknown_args() {
        let tmpdir = create_tmp_dir();
        let npub = "npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s";
        let args: Vec<String> = [
            "notedeck-test",
            "--testrunner",
            "--unknown-arg", // <-- UNKNOWN
            "--pub",
            npub,
            "-c",
            "notifications",
            "-c",
            "contacts",
        ]
        .iter()
        .map(|s| s.to_string())
        .collect();

        let ctx = egui::Context::default();
        let mut notedeck = Notedeck::init(&ctx, &tmpdir, &args);
        let app = Damus::new(&mut notedeck.app_context(&ctx), &args);

        // ensure we recognized all the arguments
        let completely_unrecognized: Vec<String> = notedeck
            .unrecognized_args()
            .intersection(app.unrecognized_args())
            .cloned()
            .collect();
        assert_eq!(completely_unrecognized, ["--unknown-arg"]);

        rmrf(tmpdir);
    }

    #[test]
    fn title_flag_startup_path_matches_args_parse_and_shows_native_titlebar() {
        let args = vec![
            "notedeck".to_string(),
            "--title".to_string(),
            "first".to_string(),
            "--title".to_string(),
            "second".to_string(),
        ];

        let (parsed, unrecognized) = Args::parse(&args[1..]);
        assert!(unrecognized.is_empty());

        let (title, show_title, builder) = apply_window_builder_for_args(&args);

        assert_eq!(title, parsed.title.unwrap());
        assert_eq!(
            show_title,
            parsed.options.contains(NotedeckOptions::ShowTitle)
        );
        assert_eq!(builder.fullsize_content_view, None);
        assert_eq!(builder.titlebar_shown, None);
        assert_eq!(builder.title_shown, None);
    }

    #[test]
    fn missing_or_absent_title_hides_native_titlebar() {
        let cases = [
            vec!["notedeck".to_string()],
            vec!["notedeck".to_string(), "--title".to_string()],
        ];

        for args in cases {
            let (parsed, unrecognized) = Args::parse(&args[1..]);
            assert!(unrecognized.is_empty());

            let (title, show_title, builder) = apply_window_builder_for_args(&args);

            assert_eq!(
                title,
                parsed.title.unwrap_or_else(|| "Damus Notedeck".to_string())
            );
            assert_eq!(
                show_title,
                parsed.options.contains(NotedeckOptions::ShowTitle)
            );
            assert_eq!(builder.fullsize_content_view, Some(true));
            assert_eq!(builder.titlebar_shown, Some(false));
            assert_eq!(builder.title_shown, Some(false));
        }
    }
}
