/* window.rs
 *
 * Copyright 2026 Christian Kruse
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

use std::collections::HashMap;
use std::path::PathBuf;

use adw::prelude::*;
use gettextrs::gettext;
use relm4::gtk::glib;
use relm4::prelude::*;

use crate::app_window::{AppWindow, AppWindowInit, AppWindowMsg, AppWindowOutput};
use crate::config::VERSION;
use crate::release_notes_dialog::present_release_notes_dialog;
use crate::window_content::{WindowContent, WindowContentInit, WindowContentMsg, WindowContentOutput};
use crate::{portal, preferences, release_notes, ui_helpers};

pub static APP_BROKER: relm4::MessageBroker<AppMsg> = relm4::MessageBroker::new();

const MAIN_WINDOW_ID: u64 = 0;

pub enum WindowKind {
    Main,
    Extra(Controller<AppWindow>),
}

pub struct WindowSlot {
    pub kind: WindowKind,
    pub repo_path: Option<PathBuf>,
}

pub struct App {
    content: Controller<WindowContent>,
    windows: HashMap<u64, WindowSlot>,
    next_window_id: u64,
    search_action: gtk::gio::SimpleAction,
    folder_select_supported: bool,
    settings: gtk::gio::Settings,
}

#[derive(Debug)]
pub enum AppMsg {
    Quit,
    About,
    WhatsNew,
    Shortcuts,
    Preferences,
    AddRepository,
    CloneRepository,
    NewWindow,
    ExtraWindowClosed(u64),
    ContentTitleChanged(String),
    MainClosed,
    MainRepoPathChanged(Option<PathBuf>),
    ExtraRepoPathChanged(u64, Option<PathBuf>),
    OpenPath(PathBuf),
    CloseWindow,
    FocusSearch,
    SearchEnabled(bool),
}

#[relm4::component(pub)]
impl Component for App {
    type Init = ();
    type Input = AppMsg;
    type Output = ();
    type CommandOutput = ();

    view! {
        adw::ApplicationWindow {
            set_title: Some("Gitte"),

            #[wrap(Some)]
            set_content = model.content.widget(),
        }
    }

    fn init(_init: Self::Init, root: Self::Root, sender: ComponentSender<Self>) -> ComponentParts<Self> {
        let folder_select_supported = portal::folder_select_supported();

        let content = WindowContent::builder()
            .launch(WindowContentInit {
                folder_select_supported,
            })
            .forward(sender.input_sender(), |output| match output {
                WindowContentOutput::TitleChanged(title) => AppMsg::ContentTitleChanged(title),
                WindowContentOutput::SearchEnabled(enabled) => AppMsg::SearchEnabled(enabled),
                WindowContentOutput::RepoPathChanged(path) => AppMsg::MainRepoPathChanged(path),
            });

        let search_action = gtk::gio::SimpleAction::new("search", None);
        let settings = preferences::app_settings();
        let mut windows = HashMap::new();

        search_action.set_enabled(false);

        windows.insert(
            MAIN_WINDOW_ID,
            WindowSlot {
                kind: WindowKind::Main,
                repo_path: None,
            },
        );

        let model = App {
            content,
            windows,
            next_window_id: 1,
            search_action,
            folder_select_supported,
            settings: settings.clone(),
        };

        let widgets = view_output!();

        let width = settings.int("window-width");
        let height = settings.int("window-height");
        root.set_default_width(width);
        root.set_default_height(height);

        {
            let settings = settings.clone();
            let root = root.clone();
            glib::idle_add_local_once(move || {
                let last = settings.string("last-release-notes-shown").to_string();

                if last.is_empty() {
                    let _ = settings.set_string("last-release-notes-shown", VERSION);
                    return;
                }

                if last == VERSION {
                    return;
                }

                let notes = release_notes::upgrade_path(&last, VERSION);

                if !notes.is_empty() {
                    let parent: gtk::Window = root.clone().upcast();
                    present_release_notes_dialog(Some(&parent), &notes);
                }

                let _ = settings.set_string("last-release-notes-shown", VERSION);
            });
        }

        let close_sender = sender.input_sender().clone();
        root.connect_close_request(move |win| {
            let settings = preferences::app_settings();
            let _ = settings.set_int("window-width", win.width());
            let _ = settings.set_int("window-height", win.height());
            close_sender.emit(AppMsg::MainClosed);
            glib::Propagation::Proceed
        });

        let app = relm4::main_adw_application();
        app.set_resource_base_path(Some("/de/wwwtech/gitte"));

        let css_provider = gtk::CssProvider::new();
        css_provider.load_from_resource("/de/wwwtech/gitte/style.css");
        let display = gtk::gdk::Display::default().expect("Could not get default display");
        gtk::style_context_add_provider_for_display(&display, &css_provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION);

        let diff_font_provider = gtk::CssProvider::new();
        ui_helpers::apply_diff_font_css(&diff_font_provider, &settings.string("diff-font"));
        gtk::style_context_add_provider_for_display(
            &display,
            &diff_font_provider,
            gtk::STYLE_PROVIDER_PRIORITY_APPLICATION + 1,
        );

        let provider_clone = diff_font_provider.clone();
        model.settings.connect_changed(Some("diff-font"), move |s, _| {
            ui_helpers::apply_diff_font_css(&provider_clone, &s.string("diff-font"));
        });

        let compact_provider = gtk::CssProvider::new();
        ui_helpers::apply_compact_css(&compact_provider, settings.boolean("compact-mode"));
        gtk::style_context_add_provider_for_display(
            &display,
            &compact_provider,
            gtk::STYLE_PROVIDER_PRIORITY_APPLICATION + 2,
        );

        let provider_clone = compact_provider.clone();
        model.settings.connect_changed(Some("compact-mode"), move |s, _| {
            ui_helpers::apply_compact_css(&provider_clone, s.boolean("compact-mode"));
        });

        let s = sender.clone();
        let quit_action = gtk::gio::SimpleAction::new("quit", None);
        quit_action.connect_activate(move |_, _| s.input(AppMsg::Quit));
        app.add_action(&quit_action);

        let s = sender.clone();
        let about_action = gtk::gio::SimpleAction::new("about", None);
        about_action.connect_activate(move |_, _| s.input(AppMsg::About));
        app.add_action(&about_action);

        let s = sender.clone();
        let whats_new_action = gtk::gio::SimpleAction::new("whats-new", None);
        whats_new_action.connect_activate(move |_, _| s.input(AppMsg::WhatsNew));
        app.add_action(&whats_new_action);

        let s = sender.clone();
        let shortcuts_action = gtk::gio::SimpleAction::new("shortcuts", None);
        shortcuts_action.connect_activate(move |_, _| s.input(AppMsg::Shortcuts));
        app.add_action(&shortcuts_action);

        let s = sender.clone();
        let preferences_action = gtk::gio::SimpleAction::new("preferences", None);
        preferences_action.connect_activate(move |_, _| s.input(AppMsg::Preferences));
        app.add_action(&preferences_action);

        let s = sender.clone();
        let new_window_action = gtk::gio::SimpleAction::new("new-window", None);
        new_window_action.connect_activate(move |_, _| s.input(AppMsg::NewWindow));
        app.add_action(&new_window_action);

        let s = sender.clone();
        let close_action = gtk::gio::SimpleAction::new("close", None);
        close_action.connect_activate(move |_, _| s.input(AppMsg::CloseWindow));
        root.add_action(&close_action);

        let s = sender.clone();
        let add_repo_action = gtk::gio::SimpleAction::new("add-repository", None);
        add_repo_action.connect_activate(move |_, _| s.input(AppMsg::AddRepository));
        root.add_action(&add_repo_action);

        let s = sender.clone();
        let clone_repo_action = gtk::gio::SimpleAction::new("clone-repository", None);
        clone_repo_action.connect_activate(move |_, _| s.input(AppMsg::CloneRepository));
        root.add_action(&clone_repo_action);

        let s = sender.clone();
        model
            .search_action
            .connect_activate(move |_, _| s.input(AppMsg::FocusSearch));
        root.add_action(&model.search_action);

        app.set_accels_for_action("win.close", &["<Control>w"]);
        app.set_accels_for_action("app.quit", &["<Control>q"]);
        app.set_accels_for_action("app.new-window", &["<Control>n"]);
        app.set_accels_for_action("win.search", &["<Control>f"]);
        app.set_accels_for_action("app.preferences", &["<Control>comma"]);
        app.set_accels_for_action("win.add-repository", &["<Control><Shift>a"]);
        app.set_accels_for_action("repo.fetch", &["<Control><Shift>f"]);
        app.set_accels_for_action("repo.push", &["<Control><Shift>p"]);
        app.set_accels_for_action("repo.stash", &["<Control><Shift>s"]);
        app.set_accels_for_action("repo.stash-untracked", &["<Control><Shift>u"]);
        app.set_accels_for_action("repo.stash-pop", &["<Control><Shift>o"]);
        app.set_accels_for_action("repo.create-tag", &["<Control><Shift>t"]);
        app.set_accels_for_action("repo.create-branch", &["<Control><Shift>b"]);
        app.set_accels_for_action("repo.focus-commit", &["<Control><Shift>c"]);
        app.set_accels_for_action("repo.pull", &["<Control>p"]);
        app.set_accels_for_action("repo.toggle-sidebar", &["F9"]);
        app.set_accels_for_action("app.shortcuts", &["<Control>question"]);

        if let Some(p) = crate::PENDING_OPEN_PATH
            .lock()
            .unwrap_or_else(|e| e.into_inner())
            .take()
        {
            sender.input(AppMsg::OpenPath(p));
        }

        ComponentParts { model, widgets }
    }

    fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>, root: &Self::Root) {
        match msg {
            AppMsg::Quit => relm4::main_adw_application().quit(),
            AppMsg::CloseWindow => root.close(),
            AppMsg::AddRepository => self.content.emit(WindowContentMsg::AddRepository),
            AppMsg::CloneRepository => self.content.emit(WindowContentMsg::CloneRepository),
            AppMsg::ContentTitleChanged(title) => root.set_title(Some(&title)),

            AppMsg::Preferences => {
                let app = relm4::main_adw_application();

                if let Some(win) = app.active_window() {
                    preferences::show_preferences_dialog(&win);
                }
            }

            AppMsg::Shortcuts => {
                let builder = gtk::Builder::from_resource("/de/wwwtech/gitte/ui/shortcuts-dialog.ui");
                let dialog: adw::ShortcutsDialog = builder.object("shortcuts_dialog").expect("shortcuts dialog");

                let app = relm4::main_adw_application();
                if let Some(win) = app.active_window() {
                    dialog.present(Some(&win));
                } else {
                    dialog.present(Some(root));
                }
            }

            AppMsg::WhatsNew => {
                let app = relm4::main_adw_application();
                let parent = app.active_window().or_else(|| Some(root.clone().upcast()));
                let notes: Vec<_> = release_notes::all().iter().collect();
                present_release_notes_dialog(parent.as_ref(), &notes);
            }

            AppMsg::About => {
                let about = adw::AboutDialog::builder()
                    .application_name("Gitte")
                    .application_icon("de.wwwtech.gitte")
                    .developer_name("Christian Kruse")
                    .version(VERSION)
                    .developers(vec![
                        "Christian Kruse https://wwwtech.de",
                        "René de Hesselle (macOS port) https://floss.social/@rdh",
                    ])
                    .artists(vec![
                        "Hylke Bons https://planetpeanut.studio",
                        "Jakub Steiner https://jimmac.eu",
                    ])
                    .translator_credits(gettext("translator-credits"))
                    .copyright("© 2026 Christian Kruse")
                    .comments("GNU Terry Pratchett")
                    .website("https://codeberg.org/ckruse/Gitte")
                    .issue_url("https://codeberg.org/ckruse/Gitte/issues")
                    .license_type(gtk::License::Agpl30)
                    .build();

                let app = relm4::main_adw_application();

                if let Some(win) = app.active_window() {
                    about.present(Some(&win));
                } else {
                    about.present(Some(root));
                }
            }

            AppMsg::NewWindow => {
                self.spawn_extra_window(None, &sender);
            }

            AppMsg::ExtraWindowClosed(id) => {
                self.windows.remove(&id);
            }

            AppMsg::MainClosed => {
                self.windows.remove(&MAIN_WINDOW_ID);
            }

            AppMsg::MainRepoPathChanged(path) => {
                if let Some(slot) = self.windows.get_mut(&MAIN_WINDOW_ID) {
                    slot.repo_path = path;
                }
            }

            AppMsg::ExtraRepoPathChanged(id, path) => {
                if let Some(slot) = self.windows.get_mut(&id) {
                    slot.repo_path = path;
                }
            }

            AppMsg::OpenPath(path) => self.handle_open_path(path, root, &sender),

            AppMsg::FocusSearch => self.content.emit(WindowContentMsg::FocusSearch),

            AppMsg::SearchEnabled(enabled) => {
                self.search_action.set_enabled(enabled);
            }
        }
    }
}

impl App {
    fn spawn_extra_window(&mut self, initial_path: Option<PathBuf>, sender: &ComponentSender<Self>) {
        let id = self.next_window_id;
        self.next_window_id += 1;

        let window = AppWindow::builder()
            .launch(AppWindowInit {
                folder_select_supported: self.folder_select_supported,
                id,
                initial_path,
            })
            .forward(sender.input_sender(), |output| match output {
                AppWindowOutput::Closed(id) => AppMsg::ExtraWindowClosed(id),
                AppWindowOutput::RepoPathChanged { id, path } => AppMsg::ExtraRepoPathChanged(id, path),
            });

        self.windows.insert(
            id,
            WindowSlot {
                kind: WindowKind::Extra(window),
                repo_path: None,
            },
        );
    }

    fn handle_open_path(&mut self, path: PathBuf, root: &<Self as Component>::Root, sender: &ComponentSender<Self>) {
        let path = path.canonicalize().unwrap_or(path);

        let matched = self
            .windows
            .iter()
            .find(|(_, slot)| slot.repo_path.as_deref() == Some(&path))
            .map(|(id, _)| *id);

        if let Some(id) = matched {
            self.present(id, root);
            return;
        }

        let free = self
            .windows
            .iter()
            .find(|(_, slot)| slot.repo_path.is_none())
            .map(|(id, _)| *id);

        if let Some(id) = free {
            self.present(id, root);
            self.send_open_path(id, path);
            return;
        }

        self.spawn_extra_window(Some(path), sender);
    }

    fn present(&self, id: u64, root: &<Self as Component>::Root) {
        match self.windows.get(&id).map(|s| &s.kind) {
            Some(WindowKind::Main) => root.present(),
            Some(WindowKind::Extra(ctrl)) => ctrl.widget().present(),
            None => {}
        }
    }

    fn send_open_path(&self, id: u64, path: PathBuf) {
        match self.windows.get(&id).map(|s| &s.kind) {
            Some(WindowKind::Main) => self.content.emit(WindowContentMsg::OpenPath(path)),
            Some(WindowKind::Extra(ctrl)) => ctrl.emit(AppWindowMsg::OpenPath(path)),
            None => {}
        }
    }
}
