/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

//! The UI model, UI implementations, and functions using them.
//!
//! UIs must implement:
//! * a `fn run_loop(&self, app: model::Application)` method which should display the UI and block while
//!   handling events until the application terminates,
//! * a `fn invoke(&self, f: model::InvokeFn)` method which invokes the given function
//!   asynchronously (without blocking) on the UI loop thread.

use crate::std::sync::Arc;
use crate::{
    async_task::AsyncTask, config::Config, data, logic::ReportCrash, settings::Settings, std,
    thread_bound::ThreadBound,
};
use model::{ui, Application};
use ui_impl::UI;

mod model;

#[cfg(all(not(test), any(target_os = "linux", target_os = "windows")))]
mod icon {
    // Must be DWORD-aligned for Win32 CreateIconFromResource.
    #[repr(align(4))]
    struct Aligned<Bytes: ?Sized>(Bytes);
    static PNG_DATA_ALIGNMENT: &'static Aligned<[u8]> =
        &Aligned(*include_bytes!("crashreporter.png"));
    pub static PNG_DATA: &'static [u8] = &PNG_DATA_ALIGNMENT.0;
}

#[cfg(test)]
pub mod test {
    pub mod model {
        pub use crate::ui::model::*;
    }
}

cfg_if::cfg_if! {
    if #[cfg(test)] {
        #[path = "test.rs"]
        pub mod ui_impl;
    } else if #[cfg(target_os = "linux")] {
        #[path = "gtk.rs"]
        mod ui_impl;
    } else if #[cfg(target_os = "windows")] {
        #[path = "windows/mod.rs"]
        mod ui_impl;
    } else if #[cfg(target_os = "macos")] {
        #[path = "macos/mod.rs"]
        mod ui_impl;
    } else {
        mod ui_impl {
            #[derive(Default)]
            pub struct UI;

            impl UI {
                pub fn run_loop(&self, _app: super::model::Application) {
                    unimplemented!();
                }

                pub fn invoke(&self, _f: super::model::InvokeFn) {
                    unimplemented!();
                }
            }
        }
    }
}

/// Display an error dialog with the given message.
#[cfg_attr(mock, allow(unused))]
pub fn error_dialog<M: std::fmt::Debug>(config: Arc<Config>, message: M) {
    let close = data::Event::default();
    // Config may not have localized strings
    let string_or = |name, fallback: &str| {
        if config.strings.is_none() {
            fallback.into()
        } else {
            config.string(name)
        }
    };

    let window = ui! {
        Window title(string_or("crashreporter-branded-title", "Firefox Crash Reporter")) hsize(600) vsize(400)
            close_when(&close) halign(Alignment::Fill) valign(Alignment::Fill) {
            VBox margin(10) spacing(10) halign(Alignment::Fill) valign(Alignment::Fill) {
                Label text(string_or(
                        "crashreporter-error",
                        "The application had a problem and crashed. \
                        Unfortunately, the crash reporter is unable to submit a report for the crash."
                )),
                Label text(string_or("crashreporter-error-details-header", "Details:")),
                Scroll halign(Alignment::Fill) valign(Alignment::Fill) {
                    TextBox content(format!("{message:?}")) halign(Alignment::Fill) valign(Alignment::Fill)
                },
                HBox valign(Alignment::End) halign(Alignment::End) spacing(10) affirmative_order(true)
                {
                    Button["restart"] hsize(160) visible(config.restart_command.is_some())
                        on_click(cc! { (config, close) move || {
                            config.restart_process();
                            close.fire(&());
                        }})
                    {
                        Label text(string_or("crashreporter-button-restart", "Restart"))
                    },
                    Button["quit"] hsize(160) on_click(move || close.fire(&()))
                    {
                        Label text(string_or("crashreporter-button-quit", "Quit"))
                    }
                }
            }
        }
    };

    UI::default().run_loop(Application {
        windows: vec![window],
        rtl: config.is_rtl(),
    });
}

#[derive(Default, Debug, PartialEq, Eq)]
pub enum SubmitState {
    #[default]
    Initial,
    WaitingHardwareTests,
    InProgress,
    Success,
    Failure,
}

/// The UI for the main crash reporter windows.
pub struct ReportCrashUI {
    state: Arc<ThreadBound<ReportCrashUIState>>,
    ui: Arc<UI>,
    config: Arc<Config>,
    logic: AsyncTask<ReportCrash>,
}

/// The state of the creash UI.
pub struct ReportCrashUIState {
    pub send_report: data::Synchronized<bool>,
    pub include_address: data::Synchronized<bool>,
    pub test_hardware: data::Synchronized<bool>,
    pub show_details: data::Synchronized<bool>,
    pub details: data::Synchronized<String>,
    pub comment: data::OnDemand<String>,
    pub submit_state: data::Synchronized<SubmitState>,
    pub close_window: data::Event<()>,
}

impl ReportCrashUI {
    pub fn new(
        initial_settings: &Settings,
        config: Arc<Config>,
        logic: AsyncTask<ReportCrash>,
    ) -> Self {
        let send_report = data::Synchronized::new(initial_settings.submit_report);
        let include_address = data::Synchronized::new(initial_settings.include_url);
        let test_hardware = data::Synchronized::new(initial_settings.test_hardware);

        ReportCrashUI {
            state: Arc::new(ThreadBound::new(ReportCrashUIState {
                send_report,
                include_address,
                test_hardware,
                show_details: Default::default(),
                details: Default::default(),
                comment: Default::default(),
                submit_state: Default::default(),
                close_window: Default::default(),
            })),
            ui: Default::default(),
            config,
            logic,
        }
    }

    pub fn async_task(&self) -> AsyncTask<ReportCrashUIState> {
        let state = self.state.clone();
        let ui = Arc::downgrade(&self.ui);
        AsyncTask::new(move |f| {
            let Some(ui) = ui.upgrade() else { return };
            ui.invoke(Box::new(cc! { (state) move || {
                f(state.borrow());
            }}));
        })
    }

    pub fn run(&self) {
        let ReportCrashUI {
            state,
            ui,
            config,
            logic,
        } = self;
        let ReportCrashUIState {
            send_report,
            include_address,
            show_details,
            test_hardware,
            details,
            comment,
            submit_state,
            close_window,
        } = state.borrow();

        send_report.on_change(cc! { (logic) move |v| {
            let v = *v;
            logic.push(move |s| s.settings.borrow_mut().submit_report = v);
        }});
        include_address.on_change(cc! { (logic) move |v| {
            let v = *v;
            logic.push(move |s| s.settings.borrow_mut().include_url = v);
        }});
        test_hardware.on_change(cc! { (logic) move |v| {
            let v = *v;
            logic.push(move |s| s.settings.borrow_mut().test_hardware = v);
        }});

        let input_enabled = submit_state.mapped(|s| s == &SubmitState::Initial);
        let send_report_and_input_enabled =
            data::Synchronized::join(send_report, &input_enabled, |s, e| *s && *e);

        let submit_status_text = submit_state.mapped(cc! { (config) move |s| {
            config.string(match s {
                SubmitState::Initial => "crashreporter-submit-status",
                SubmitState::WaitingHardwareTests => "crashreporter-submit-waiting-hardware-tests",
                SubmitState::InProgress => "crashreporter-submit-in-progress",
                SubmitState::Success => "crashreporter-submit-success",
                SubmitState::Failure => "crashreporter-submit-failure",
            })
        }});

        let progress_visible = submit_state.mapped(|s| s == &SubmitState::InProgress);
        let hardware_test_checkbox_visible = data::Synchronized::new(self.config.run_memtest);

        let details_window = ui! {
            Window["crash-details-window"] title(config.string("crashreporter-view-report-title"))
                visible(show_details) modal(true) hsize(600) vsize(400)
                halign(Alignment::Fill) valign(Alignment::Fill)
            {
                VBox margin(10) spacing(10) halign(Alignment::Fill) valign(Alignment::Fill) {
                    Scroll halign(Alignment::Fill) valign(Alignment::Fill) {
                        TextBox["details-text"] content(details) halign(Alignment::Fill) valign(Alignment::Fill)
                    },
                    Button["close-details"] halign(Alignment::End) on_click(cc! { (show_details) move || *show_details.borrow_mut() = false }) {
                        Label text(config.string("crashreporter-button-ok"))
                    }
                }
            }
        };

        let main_window = ui! {
            Window title(config.string("crashreporter-branded-title")) hsize(600) vsize(400)
                halign(Alignment::Fill) valign(Alignment::Fill) close_when(close_window)
                child_window(details_window)
            {
                VBox margin(10) spacing(10) halign(Alignment::Fill) valign(Alignment::Fill) {
                    Label text(config.string("crashreporter-apology")) bold(true),
                    Label text(config.string("crashreporter-crashed-and-restore")),
                    Label text(config.string("crashreporter-plea")),
                    Checkbox["test-hardware"] checked(test_hardware)
                        label(config.string("crashreporter-checkbox-test-hardware"))
                        enabled(&input_enabled) visible(&hardware_test_checkbox_visible),
                    Checkbox["send"] checked(send_report) label(config.string("crashreporter-checkbox-send-report"))
                        enabled(&input_enabled),
                    VBox margin_start(20) spacing(5) halign(Alignment::Fill) valign(Alignment::Fill) {
                        Button["details"] enabled(&send_report_and_input_enabled) on_click(cc! { (config, details, show_details, logic) move || {
                                // Immediately display the window to feel responsive, even if forming
                                // the details string takes a little while (it really shouldn't
                                // though).
                                *details.borrow_mut() = config.string("crashreporter-loading-details");
                                logic.push(|s| s.update_details());
                                *show_details.borrow_mut() = true;
                            }})
                        {
                            Label text(config.string("crashreporter-button-details"))
                        },
                        Scroll halign(Alignment::Fill) valign(Alignment::Fill) {
                            TextBox["comment"] placeholder(config.string("crashreporter-comment-prompt"))
                                content(comment)
                                editable(true)
                                enabled(&send_report_and_input_enabled)
                                halign(Alignment::Fill) valign(Alignment::Fill)
                        },
                        Checkbox["include-url"] checked(include_address)
                            label(config.string("crashreporter-checkbox-include-url")) enabled(&send_report_and_input_enabled),
                        Label text(&submit_status_text) margin_top(20),
                        Progress halign(Alignment::Fill) visible(&progress_visible),
                    },
                    HBox valign(Alignment::End) halign(Alignment::End) spacing(10) affirmative_order(true)
                    {
                        Button["restart"] visible(config.restart_command.is_some())
                            on_click(cc! { (logic) move || logic.push_async(|s| Box::pin(s.restart())) })
                            enabled(&input_enabled) hsize(160)
                        {
                            Label text(config.string("crashreporter-button-restart"))
                        },
                        Button["quit"] on_click(cc! { (logic) move || logic.push_async(|s| Box::pin(s.quit())) })
                            enabled(&input_enabled) hsize(160)
                        {
                            Label text(config.string("crashreporter-button-quit"))
                        }
                    }
                }
            }
        };

        ui.run_loop(Application {
            windows: vec![main_window],
            rtl: config.is_rtl(),
        });
    }
}
