use crate::persist::{AppSizeHandler, ZoomHandler};
use crate::{
    Accounts, AppContext, Args, DataPath, DataPathType, Directory, FileKeyStorage, ImageCache,
    KeyStorageType, NoteCache, ThemeHandler, UnknownIds,
};
use egui::ThemePreference;
use enostr::RelayPool;
use nostrdb::{Config, Ndb, Transaction};
use std::cell::RefCell;
use std::path::Path;
use std::rc::Rc;
use tracing::{error, info};

pub trait App {
    fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui);
}

/// Main notedeck app framework
pub struct Notedeck {
    ndb: Ndb,
    img_cache: ImageCache,
    unknown_ids: UnknownIds,
    pool: RelayPool,
    note_cache: NoteCache,
    accounts: Accounts,
    path: DataPath,
    args: Args,
    theme: ThemeHandler,
    app: Option<Rc<RefCell<dyn App>>>,
    zoom: ZoomHandler,
    app_size: AppSizeHandler,
}

fn margin_top(narrow: bool) -> f32 {
    #[cfg(target_os = "android")]
    {
        // FIXME - query the system bar height and adjust more precisely
        let _ = narrow; // suppress compiler warning on android
        40.0
    }
    #[cfg(not(target_os = "android"))]
    {
        if narrow {
            50.0
        } else {
            0.0
        }
    }
}

/// Our chrome, which is basically nothing
fn main_panel(style: &egui::Style, narrow: bool) -> egui::CentralPanel {
    let inner_margin = egui::Margin {
        top: margin_top(narrow),
        left: 0.0,
        right: 0.0,
        bottom: 0.0,
    };
    egui::CentralPanel::default().frame(egui::Frame {
        inner_margin,
        fill: style.visuals.panel_fill,
        ..Default::default()
    })
}

impl eframe::App for Notedeck {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        #[cfg(feature = "profiling")]
        puffin::GlobalProfiler::lock().new_frame();

        // handle account updates
        self.accounts.update(&self.ndb, &mut self.pool, ctx);

        main_panel(&ctx.style(), crate::ui::is_narrow(ctx)).show(ctx, |ui| {
            // render app
            if let Some(app) = &self.app {
                let app = app.clone();
                app.borrow_mut().update(&mut self.app_context(), ui);
            }
        });

        self.zoom.try_save_zoom_factor(ctx);
        self.app_size.try_save_app_size(ctx);

        if self.args.relay_debug && self.pool.debug.is_none() {
            self.pool.use_debug();
        }

        #[cfg(feature = "profiling")]
        puffin_egui::profiler_window(ctx);
    }

    /// Called by the framework to save state before shutdown.
    fn save(&mut self, _storage: &mut dyn eframe::Storage) {
        //eframe::set_value(storage, eframe::APP_KEY, self);
    }
}

#[cfg(feature = "profiling")]
fn setup_profiling() {
    puffin::set_scopes_on(true); // tell puffin to collect data
}

impl Notedeck {
    pub fn new<P: AsRef<Path>>(ctx: &egui::Context, data_path: P, args: &[String]) -> Self {
        #[cfg(feature = "profiling")]
        setup_profiling();

        let parsed_args = Args::parse(args);

        let data_path = parsed_args
            .datapath
            .clone()
            .unwrap_or(data_path.as_ref().to_str().expect("db path ok").to_string());
        let path = DataPath::new(&data_path);
        let dbpath_str = parsed_args
            .dbpath
            .clone()
            .unwrap_or_else(|| path.path(DataPathType::Db).to_str().unwrap().to_string());

        let _ = std::fs::create_dir_all(&dbpath_str);

        let img_cache_dir = path.path(DataPathType::Cache).join(ImageCache::rel_dir());
        let _ = std::fs::create_dir_all(img_cache_dir.clone());

        let map_size = if cfg!(target_os = "windows") {
            // 16 Gib on windows because it actually creates the file
            1024usize * 1024usize * 1024usize * 16usize
        } else {
            // 1 TiB for everything else since its just virtually mapped
            1024usize * 1024usize * 1024usize * 1024usize
        };

        let theme = ThemeHandler::new(&path);
        let config = Config::new().set_ingester_threads(4).set_mapsize(map_size);

        let keystore = if parsed_args.use_keystore {
            let keys_path = path.path(DataPathType::Keys);
            let selected_key_path = path.path(DataPathType::SelectedKey);
            KeyStorageType::FileSystem(FileKeyStorage::new(
                Directory::new(keys_path),
                Directory::new(selected_key_path),
            ))
        } else {
            KeyStorageType::None
        };

        let mut accounts = Accounts::new(keystore, parsed_args.relays.clone());

        let num_keys = parsed_args.keys.len();

        let mut unknown_ids = UnknownIds::default();
        let ndb = Ndb::new(&dbpath_str, &config).expect("ndb");

        {
            let txn = Transaction::new(&ndb).expect("txn");
            for key in &parsed_args.keys {
                info!("adding account: {}", &key.pubkey);
                accounts
                    .add_account(key.clone())
                    .process_action(&mut unknown_ids, &ndb, &txn);
            }
        }

        if num_keys != 0 {
            accounts.select_account(0);
        }

        // AccountManager will setup the pool on first update
        let mut pool = RelayPool::new();
        {
            let ctx = ctx.clone();
            if let Err(err) = pool.add_multicast_relay(move || ctx.request_repaint()) {
                error!("error setting up multicast relay: {err}");
            }
        }

        let img_cache = ImageCache::new(img_cache_dir);
        let note_cache = NoteCache::default();
        let unknown_ids = UnknownIds::default();
        let zoom = ZoomHandler::new(&path);
        let app_size = AppSizeHandler::new(&path);

        if let Some(z) = zoom.get_zoom_factor() {
            ctx.set_zoom_factor(z);
        }

        // migrate
        if let Err(e) = img_cache.migrate_v0() {
            error!("error migrating image cache: {e}");
        }

        Self {
            ndb,
            img_cache,
            unknown_ids,
            pool,
            note_cache,
            accounts,
            path: path.clone(),
            args: parsed_args,
            theme,
            app: None,
            zoom,
            app_size,
        }
    }

    pub fn app<A: App + 'static>(mut self, app: A) -> Self {
        self.set_app(app);
        self
    }

    pub fn app_context(&mut self) -> AppContext<'_> {
        AppContext {
            ndb: &mut self.ndb,
            img_cache: &mut self.img_cache,
            unknown_ids: &mut self.unknown_ids,
            pool: &mut self.pool,
            note_cache: &mut self.note_cache,
            accounts: &mut self.accounts,
            path: &self.path,
            args: &self.args,
            theme: &mut self.theme,
        }
    }

    pub fn set_app<T: App + 'static>(&mut self, app: T) {
        self.app = Some(Rc::new(RefCell::new(app)));
    }

    pub fn args(&self) -> &Args {
        &self.args
    }

    pub fn theme(&self) -> ThemePreference {
        self.theme.load()
    }
}
