mod components;
mod state;

use ruin_app::prelude::*;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::{fmt, EnvFilter};

use components::{Display, Keypad, ModeToggle, ScientificPanel};
use state::{CalcMode, CalcState};

#[ruin_runtime::async_main]
async fn main() -> ruin_app::Result<()> {
    init_tracing();

    App::new()
        .window(
            Window::new()
                .title("Calculator")
                .app_id("dev.ruin.calculator")
                .base_color(Color::rgba(255, 255, 255, 80))
                .resizable(false),
        )
        .mount(view! { CalculatorApp() {} })
        .run()
        .await
}

fn init_tracing() {
    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
        EnvFilter::new(
            "info,ruin_app::resize=trace,ruin_app::interaction=trace,\
             ruin_ui_platform_wayland::resize=trace,ruin_ui_platform_wayland::perf=debug,\
             ruin_ui_platform_wayland::event_bridge=trace",
        )
    });

    let _ = tracing_subscriber::registry()
        .with(fmt::layer().with_target(true).without_time())
        .with(filter)
        .try_init();
}

#[component]
fn CalculatorApp() -> impl IntoView {
    let state = CalcState::new();

    // ---- Keyboard shortcuts (application-scoped) ----

    for ch in "0123456789.+-*/%()".chars() {
        let s = state.clone();
        let display = match ch {
            '*' => "×",
            '/' => "÷",
            _ => "",
        };
        let text = if display.is_empty() {
            ch.to_string()
        } else {
            display.to_string()
        };
        use_shortcut(
            Shortcut::new(Key::Character(ch)),
            ShortcutScope::Application,
            {
                let text = text.clone();
                move || s.input(&text)
            },
        );
    }

    // = key also evaluates
    {
        let s = state.clone();
        use_shortcut(
            Shortcut::new(Key::Character('=')),
            ShortcutScope::Application,
            move || s.evaluate(),
        );
    }
    {
        let s = state.clone();
        use_shortcut(
            Shortcut::new(Key::Enter),
            ShortcutScope::Application,
            move || s.evaluate(),
        );
    }
    {
        let s = state.clone();
        use_shortcut(
            Shortcut::new(Key::Backspace),
            ShortcutScope::Application,
            move || s.backspace(),
        );
    }
    {
        let s = state.clone();
        use_shortcut(
            Shortcut::new(Key::Escape),
            ShortcutScope::Application,
            move || s.clear(),
        );
    }
    {
        let s = state.clone();
        use_shortcut(
            Shortcut::new(Key::Delete),
            ShortcutScope::Application,
            move || s.clear(),
        );
    }

    // ---- Layout ----

    let mode = state.mode.clone();

    let text_style = PartialTextStyle {
        color: Some(Color::BLACK),
        ..Default::default()
    };

    if mode.get() == CalcMode::Scientific {
        view! {
            column(
                gap = 10.0,
                padding = 12.0,
                align_items = AlignItems::Stretch,
                text_style = text_style,
            ) {
                Display(state = state.clone()) {}
                ModeToggle(state = state.clone()) {}
                ScientificPanel(state = state.clone()) {}
                Keypad(state = state.clone()) {}
            }
        }
    } else {
        view! {
            column(
                gap = 10.0,
                padding = 12.0,
                align_items = AlignItems::Stretch,
                text_style = text_style,
            ) {
                Display(state = state.clone()) {}
                ModeToggle(state = state.clone()) {}
                Keypad(state = state.clone()) {}
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use std::cell::RefCell;
    use std::rc::Rc;

    use super::*;

    #[component]
    fn StateProbe(slot: Rc<RefCell<Option<CalcState>>>) -> impl IntoView {
        let state = CalcState::new();
        *slot.borrow_mut() = Some(state);
        block().children(())
    }

    #[test]
    fn successful_evaluation_replaces_expression_and_preserves_history() {
        let slot = Rc::new(RefCell::new(None::<CalcState>));
        let _ = ruin_app::__render_mountable_for_test(&view! {
            StateProbe(slot = Rc::clone(&slot)) {}
        });

        let state = slot.borrow().clone().expect("state should be captured");
        let _ = state.expression.set("2+2".to_string());
        state.evaluate();

        assert_eq!(state.expression.get(), "4");
        assert_eq!(state.result.get(), "");
        assert!(!state.is_error.get());
        assert_eq!(state.history.get().len(), 1);
        assert_eq!(state.history.get()[0].expression, "2+2");
        assert_eq!(state.history.get()[0].result, "4");
    }
}
