#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

mod about;
mod backup_view;
mod config_view;
mod egui_widgets;
mod keyring_view;
mod layout;
mod msg_log_views;
mod password_ids;
mod restore_view;
mod task_progress;
mod util;

use std::{
    collections::HashMap,
    path::Path,
    sync::{Arc, RwLock},
};

use crate::{
    about::show_about,
    backup_view::BackupView,
    config_view::ConfigView,
    keyring_view::KeyringView,
    layout::Layout,
    msg_log_views::{MsgLogLevel, MsgLogView},
    password_ids::PasswordIDs,
    restore_view::RestoreView,
};

use crossbeam_channel::{Sender, unbounded};
use cuba_lib::{
    core::cuba::{Cuba, RunHandle},
    send_error,
    shared::{
        config::{AccessMode, CONFIG_FILE, config_path, load_config_from_file},
        config_writer::ConfigWriter,
        message::Message,
        msg_dispatcher::MsgDispatcher,
    },
};
use eframe::egui;
use egui::{FontData, FontDefinitions, FontFamily};
use egui_dock::{DockArea, DockState, Style, TabViewer};
use serde::{Deserialize, Serialize};

/// The layout file.
const LAYOUT_FILE: &str = "cuba-gui-layout.json";

/// Sets up the fonts for egui.
fn setup_fonts(ctx: &egui::Context) {
    let mut fonts = FontDefinitions::default();

    // Load monospace font.
    fonts.font_data.insert(
        "jetbrains_mono".to_owned(),
        Arc::new(FontData::from_static(include_bytes!(
            "../assets/fonts/JetBrainsMono-Regular.ttf"
        ))),
    );

    // Use it for monospace text.
    fonts
        .families
        .entry(FontFamily::Monospace)
        .or_default()
        .insert(0, "jetbrains_mono".to_owned());

    ctx.set_fonts(fonts);
}

fn main() -> eframe::Result<()> {
    let mut layout = Layout::default();

    if let Ok(config_path) = config_path(AccessMode::Read) {
        let layout_file = config_path.join(Path::new(LAYOUT_FILE));

        // Load layout - if it exists.
        if layout_file.exists()
            && let Some(file_layout) = Layout::load(&layout_file)
        {
            layout = file_layout;
        }
    }

    // Build viewport.
    let mut viewport_builder = egui::ViewportBuilder::default()
        .with_icon(
            eframe::icon_data::from_png_bytes(include_bytes!("../assets/icons/icon128.png"))
                .unwrap(),
        )
        .with_app_id("cuba");

    // Set layout.
    viewport_builder = layout.apply_to_viewport_builder(viewport_builder);

    // Set options.
    let options = eframe::NativeOptions {
        viewport: viewport_builder,
        ..Default::default()
    };

    // Run app.
    eframe::run_native(
        "Cuba GUI",
        options,
        Box::new(|creation_ctx| Ok(Box::new(CubaGui::new(creation_ctx, layout)))),
    )
}

/// Defines a `UpdateHandler`.
///
/// This is for view models to signal that data has been updated.
#[derive(Clone)]
struct UpdateHandler {
    egui_context: egui::Context,
}

/// Methods of `UpdateHandler`.
impl UpdateHandler {
    /// Creates a new `UpdateHandler`.
    pub fn new(egui_context: egui::Context) -> Self {
        Self { egui_context }
    }

    /// Signal that data has been updated.
    pub fn update(&self) {
        self.egui_context.request_repaint();
    }
}

// Defines the different views in the app.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
enum ViewId {
    Backup,
    Restore,
    Config,
    Keyring,
    InfoLog,
    WarningLog,
    ErrorLog,
}

/// Defines the trait `AppView`.
trait AppView {
    /// Returns the name of the view.
    fn name(&self) -> &str;

    /// Renders the view for egui.
    fn ui(&mut self, ui: &mut egui::Ui);

    /// Returns the view id.
    fn view_id(&self) -> ViewId;

    /// Post init.
    fn post_init(&mut self) {}
}

/// Defines a `AppViewer`.
struct AppViewer<'a> {
    app_views: &'a HashMap<ViewId, Arc<RwLock<dyn AppView>>>,
}

/// Impl of `TabViewer` for `AppViewer`.
impl<'a> TabViewer for AppViewer<'a> {
    type Tab = ViewId;

    /// Returns the title of the `AppView` as a `egui::WidgetText`.
    fn title(&mut self, view_id: &mut ViewId) -> egui::WidgetText {
        egui::RichText::new(self.app_views[view_id].read().unwrap().name())
            .font(egui::FontId::proportional(16.0))
            .into()
    }

    /// Renders each view.
    fn ui(&mut self, ui: &mut egui::Ui, view_id: &mut ViewId) {
        self.app_views[view_id].write().unwrap().ui(ui);
    }
}

/// Defines a `CubaGui`.
struct CubaGui {
    sender: Sender<Arc<dyn Message>>,
    cuba: Arc<RwLock<Cuba>>,
    _msg_dispatcher: Arc<MsgDispatcher<Arc<dyn Message>>>,
    app_views: HashMap<ViewId, Arc<RwLock<dyn AppView>>>,
    dock_state: DockState<ViewId>,
    post_init_done: bool,
    show_about: bool,
    icon_64_texture: egui::TextureHandle,
    egui_ctx: egui::Context,
}

/// Methods of `CubaGui`.
impl CubaGui {
    /// Creates a new `CubaGui`.
    fn new(creation_ctx: &eframe::CreationContext<'_>, layout: Layout) -> Self {
        // Load icon.
        let icon_64_image = image::load_from_memory(include_bytes!("../assets/icons/icon64.png"))
            .expect("Failed to load image");

        let size = [
            icon_64_image.width() as usize,
            icon_64_image.height() as usize,
        ];
        let rgba = icon_64_image.to_rgba8();
        let pixels = rgba.as_flat_samples();

        let color_image = egui::ColorImage::from_rgba_unmultiplied(size, pixels.as_slice());

        let icon_64_texture =
            creation_ctx
                .egui_ctx
                .load_texture("icon64", color_image, Default::default());

        // Setup fonts.
        setup_fonts(&creation_ctx.egui_ctx);

        // Set fonts.
        let mut style = (*creation_ctx.egui_ctx.style()).clone();

        style.text_styles = [
            (
                egui::TextStyle::Heading,
                egui::FontId::new(20.0, egui::FontFamily::Proportional),
            ),
            (
                egui::TextStyle::Body,
                egui::FontId::new(16.0, egui::FontFamily::Proportional),
            ),
            (
                egui::TextStyle::Button,
                egui::FontId::new(16.0, egui::FontFamily::Proportional),
            ),
            (
                egui::TextStyle::Small,
                egui::FontId::new(13.0, egui::FontFamily::Proportional),
            ),
            (
                egui::TextStyle::Monospace,
                egui::FontId::new(14.0, egui::FontFamily::Monospace),
            ),
        ]
        .into();

        // Set style.
        creation_ctx.egui_ctx.set_style(style);

        // Sender and receiver for messages between the GUI and the Cuba instance.
        let (sender, receiver) = unbounded::<Arc<dyn Message>>();

        // The message dispatcher.
        let mut msg_dispatcher = MsgDispatcher::new(receiver.clone());
        msg_dispatcher.start();
        let arc_msg_dispatcher = Arc::new(msg_dispatcher);

        // The Cuba instance.
        let cuba = Arc::new(RwLock::new(Cuba::new(sender.clone())));

        // The password ids.
        let password_ids = Arc::new(PasswordIDs::new(cuba.clone()));
        password_ids.update();

        // The run handle.
        let run_handle = RunHandle::default();

        // The backup view.
        let backup_view = Arc::new(RwLock::new(BackupView::new(
            creation_ctx.egui_ctx.clone(),
            sender.clone(),
            cuba.clone(),
            arc_msg_dispatcher.clone(),
            run_handle.clone(),
        )));

        // The restore view.
        let restore_view = Arc::new(RwLock::new(RestoreView::new(
            creation_ctx.egui_ctx.clone(),
            sender.clone(),
            cuba.clone(),
            arc_msg_dispatcher.clone(),
            run_handle.clone(),
        )));

        // The config view.
        let config_view = Arc::new(RwLock::new(ConfigView::new(
            cuba.clone(),
            sender.clone(),
            password_ids.clone(),
        )));

        // The keyring view.
        let keyring_view = Arc::new(RwLock::new(KeyringView::new(
            cuba.clone(),
            password_ids.clone(),
        )));

        // The infos view.
        let infos_view = Arc::new(RwLock::new(MsgLogView::new(
            creation_ctx.egui_ctx.clone(),
            MsgLogLevel::Info,
            arc_msg_dispatcher.clone(),
        )));

        // Currently, cuba produces no warnings.
        // The warnings view.
        /*
        let warnings_view = Arc::new(RwLock::new(MsgLogView::new(
            creation_ctx.egui_ctx.clone(),
            MsgLogLevel::Warning,
            arc_msg_dispatcher.clone(),
        )));
        */

        // The errors view.
        let errors_view = Arc::new(RwLock::new(MsgLogView::new(
            creation_ctx.egui_ctx.clone(),
            MsgLogLevel::Error,
            arc_msg_dispatcher.clone(),
        )));

        let mut app_views = HashMap::<ViewId, Arc<RwLock<dyn AppView>>>::new();
        app_views.insert(ViewId::Backup, backup_view);
        app_views.insert(ViewId::Restore, restore_view);
        app_views.insert(ViewId::Config, config_view);
        app_views.insert(ViewId::Keyring, keyring_view);
        app_views.insert(ViewId::InfoLog, infos_view);
        //app_views.insert(ViewId::WarningLog, warnings_view);
        app_views.insert(ViewId::ErrorLog, errors_view);

        // Init dockstate.
        let mut dock_state: DockState<ViewId> = DockState::new(Vec::new());
        layout.apply_to_dockstate(&mut dock_state);

        Self {
            sender: sender.clone(),
            cuba: cuba.clone(),
            _msg_dispatcher: arc_msg_dispatcher,
            app_views,
            dock_state,
            post_init_done: false,
            show_about: false,
            icon_64_texture,
            egui_ctx: creation_ctx.egui_ctx.clone(),
        }
    }

    // Adds a view button.
    fn add_view_button(&mut self, app_view: &Arc<RwLock<dyn AppView>>, ui: &mut egui::Ui) {
        let view_location = self.dock_state.find_tab_from(|existing_view_id: &ViewId| {
            *existing_view_id == app_view.read().unwrap().view_id()
        });

        if ui.button(app_view.read().unwrap().name()).clicked() {
            if let Some(view_location) = view_location {
                self.dock_state.set_active_tab(view_location);
            } else {
                self.dock_state
                    .push_to_focused_leaf(app_view.read().unwrap().view_id());
            }
        }
    }

    // Set the active tab.
    fn set_active_view(&mut self, view_id: &ViewId) {
        let view_location = self
            .dock_state
            .find_tab_from(|existing_view_id: &ViewId| *existing_view_id == *view_id);

        if let Some(view_location) = view_location {
            self.dock_state.set_active_tab(view_location);
        }
    }

    /// Reset to default layout.
    pub fn reset_to_default_layout(&mut self) {
        let default_layout = Layout::default();
        self.dock_state = egui_dock::DockState::new(Vec::new());
        default_layout.apply_to_context(&self.egui_ctx);
        default_layout.apply_to_dockstate(&mut self.dock_state);
    }

    /// Post initialization.
    fn post_init(&mut self) {
        match config_path(AccessMode::Write) {
            Ok(config_path) => {
                let config_file = config_path.join(Path::new(CONFIG_FILE));

                // Write the example config, if it doesn't exist.
                if !config_file.exists() {
                    ConfigWriter::example(self.sender.clone(), &config_file);
                }

                // Load config.
                if let Some(config) = load_config_from_file(self.sender.clone(), &config_file) {
                    self.cuba.write().unwrap().set_config(config);
                }
            }
            Err(error) => {
                send_error!(self.sender, error);
            }
        }

        // Call post_init for app views.
        for app_view in self.app_views.values() {
            app_view.write().unwrap().post_init();
        }

        // Set active view.
        self.set_active_view(&ViewId::Backup);
    }
}

/// Impl of `eframe::App` for `CubaGui`.
impl eframe::App for CubaGui {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        self.egui_ctx = ctx.clone();

        if !self.post_init_done {
            self.post_init();
            self.post_init_done = true;
        }

        egui::TopBottomPanel::top("Menu").show(ctx, |ui| {
            egui::MenuBar::new().ui(ui, |ui| {
                if ui.button("Quit").clicked() {
                    ctx.send_viewport_cmd(egui::ViewportCommand::Close);
                };

                ui.menu_button("Views", |ui| {
                    for app_view in self.app_views.clone().values() {
                        self.add_view_button(app_view, ui);
                    }
                });

                ui.menu_button("Layout", |ui| {
                    if ui.button("Reset Layout").clicked() {
                        self.reset_to_default_layout();
                    }
                });

                if ui.button("About").clicked() {
                    self.show_about = true;
                };
            });
        });

        // The about dialog.
        if self.show_about {
            show_about(ctx, &mut self.show_about, &self.icon_64_texture);
        }

        DockArea::new(&mut self.dock_state)
            .style(Style::from_egui(ctx.style().as_ref()))
            .show(
                ctx,
                &mut AppViewer {
                    app_views: &self.app_views,
                },
            );
    }

    /// Save layout on exit.
    fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
        let layout = Layout::default()
            .with_ctx(&self.egui_ctx)
            .with_dock_state(self.dock_state.clone());

        match config_path(AccessMode::Write) {
            Ok(config_path) => {
                let layout_file = config_path.join(Path::new(LAYOUT_FILE));

                layout.save(&layout_file);
            }
            Err(error) => {
                send_error!(self.sender, error);
            }
        }
    }
}
