use std::env;
use std::io;
use std::path::PathBuf;
use std::time::Instant;

use ruin_perf::{PerfReportBuilder, RunMetadata};
use ruin_ui::{
    AlignItems, Color, Edges, Element, FlexDirection, LayoutCache, LayoutFrameStats,
    ScrollbarStyle, TextStyle, TextSystem, UiSize, layout_snapshot_with_cache_and_stats,
};

const DEFAULT_FRAMES: u64 = 300;

#[derive(Clone, Debug)]
struct Config {
    scenario: Scenario,
    frames: u64,
    output: Option<PathBuf>,
    text_kb: usize,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Scenario {
    Counter,
    LargeScrollList,
    Calculator,
    ResizeConfigure,
    LargeTextBody,
}

impl Scenario {
    const ALL: &'static [Self] = &[
        Self::Counter,
        Self::LargeScrollList,
        Self::Calculator,
        Self::ResizeConfigure,
        Self::LargeTextBody,
    ];

    const fn name(self) -> &'static str {
        match self {
            Self::Counter => "counter",
            Self::LargeScrollList => "large-scroll-list",
            Self::Calculator => "calculator",
            Self::ResizeConfigure => "resize-configure",
            Self::LargeTextBody => "large-text-body",
        }
    }

    fn parse(value: &str) -> Option<Self> {
        Self::ALL
            .iter()
            .copied()
            .find(|scenario| scenario.name() == value)
    }
}

fn main() -> io::Result<()> {
    let config = Config::parse(env::args().skip(1))?;
    let report = run_scenario(&config);
    let output = config
        .output
        .clone()
        .unwrap_or_else(|| report.default_output_path());
    report.write_json(&output)?;
    report.print_summary();
    println!();
    println!("wrote {}", output.display());
    Ok(())
}

impl Config {
    fn parse(mut args: impl Iterator<Item = String>) -> io::Result<Self> {
        let mut scenario = Scenario::LargeScrollList;
        let mut frames = DEFAULT_FRAMES;
        let mut output = None;
        let mut text_kb = 256usize;

        while let Some(arg) = args.next() {
            match arg.as_str() {
                "--scenario" => {
                    let value = args.next().ok_or_else(|| {
                        io::Error::new(io::ErrorKind::InvalidInput, "--scenario needs a value")
                    })?;
                    scenario = Scenario::parse(&value).ok_or_else(|| {
                        io::Error::new(
                            io::ErrorKind::InvalidInput,
                            format!("unknown scenario '{value}'"),
                        )
                    })?;
                }
                "--frames" => {
                    let value = args.next().ok_or_else(|| {
                        io::Error::new(io::ErrorKind::InvalidInput, "--frames needs a value")
                    })?;
                    frames = value.parse::<u64>().map_err(|error| {
                        io::Error::new(
                            io::ErrorKind::InvalidInput,
                            format!("invalid frame count '{value}': {error}"),
                        )
                    })?;
                }
                "--output" => {
                    let value = args.next().ok_or_else(|| {
                        io::Error::new(io::ErrorKind::InvalidInput, "--output needs a value")
                    })?;
                    output = Some(PathBuf::from(value));
                }
                "--text-kb" => {
                    let value = args.next().ok_or_else(|| {
                        io::Error::new(io::ErrorKind::InvalidInput, "--text-kb needs a value")
                    })?;
                    text_kb = value.parse::<usize>().map_err(|error| {
                        io::Error::new(
                            io::ErrorKind::InvalidInput,
                            format!("invalid text size '{value}': {error}"),
                        )
                    })?;
                }
                "--list" => {
                    for scenario in Scenario::ALL {
                        println!("{}", scenario.name());
                    }
                    std::process::exit(0);
                }
                "--help" | "-h" => {
                    print_help();
                    std::process::exit(0);
                }
                other => {
                    return Err(io::Error::new(
                        io::ErrorKind::InvalidInput,
                        format!("unknown argument '{other}'"),
                    ));
                }
            }
        }

        Ok(Self {
            scenario,
            frames,
            output,
            text_kb,
        })
    }
}

fn print_help() {
    println!(
        "ruin_perf_scenarios --scenario <name> [--frames <n>] [--output <path>] [--text-kb <n>]"
    );
    println!();
    println!("scenarios:");
    for scenario in Scenario::ALL {
        println!("  {}", scenario.name());
    }
}

fn run_scenario(config: &Config) -> ruin_perf::PerfReport {
    let mut report =
        PerfReportBuilder::new(RunMetadata::new(config.scenario.name(), config.frames));
    let mut text_system = TextSystem::new();
    let mut layout_cache = LayoutCache::new();

    report.note("headless layout/app-build scenario; renderer/platform timings are not included");
    if config.scenario == Scenario::LargeTextBody {
        report.set_counter("text_body_kb", config.text_kb as f64);
    }

    for frame in 0..config.frames {
        let build_started = Instant::now();
        let (tree, viewport) = match config.scenario {
            Scenario::Counter => counter_tree(frame),
            Scenario::LargeScrollList => large_scroll_list_tree(frame),
            Scenario::Calculator => calculator_tree(frame),
            Scenario::ResizeConfigure => resize_configure_tree(frame),
            Scenario::LargeTextBody => large_text_body_tree(frame, config.text_kb),
        };
        let build_ms = build_started.elapsed().as_secs_f64() * 1_000.0;
        let (_snapshot, stats) = layout_snapshot_with_cache_and_stats(
            frame + 1,
            viewport,
            &tree,
            &mut text_system,
            &mut layout_cache,
        );

        report.record_ms("app_build", build_ms);
        record_layout_stats(&mut report, stats);
    }

    report.finish()
}

fn record_layout_stats(report: &mut PerfReportBuilder, stats: LayoutFrameStats) {
    report.record_ms("layout_total", stats.total_ms);
    report.record_ms("layout_intrinsic", stats.intrinsic_ms);
    report.record_ms("text_prepare", stats.text_prepare_ms);
    report.record_ms("text_cache_miss", stats.text_miss_ms);
    report.record("layout_nodes", "count", stats.nodes as f64);
    report.record("scene_items", "count", stats.scene_items as f64);
    report.record("text_glyphs", "count", stats.text_output_glyphs as f64);
    report.add_counter("layout_cache_hits", stats.layout_cache_hits as f64);
    report.add_counter("layout_cache_misses", stats.layout_cache_misses as f64);
    report.add_counter("intrinsic_cache_hits", stats.intrinsic_cache_hits as f64);
    report.add_counter("viewport_culled", stats.viewport_culled as f64);
    report.add_counter("text_cache_hits", stats.text_cache_hits as f64);
    report.add_counter("text_cache_misses", stats.text_cache_misses as f64);
    report.add_counter("text_requests", stats.text_requests as f64);
}

fn text_style(size: f32) -> TextStyle {
    TextStyle::new(size, Color::rgb(0x20, 0x20, 0x20))
}

fn counter_tree(frame: u64) -> (Element, UiSize) {
    let count = frame as i64;
    let root = Element::column()
        .gap(12.0)
        .padding(Edges::all(24.0))
        .align_items(AlignItems::Center)
        .child(Element::text("Counter", text_style(28.0)))
        .child(Element::text(format!("count = {count}"), text_style(18.0)))
        .child(Element::text(
            format!("double = {}", count * 2),
            text_style(18.0),
        ))
        .child(button_row(["-1", "Reset", "+1"]));
    (root, UiSize::new(480.0, 320.0))
}

fn large_scroll_list_tree(frame: u64) -> (Element, UiSize) {
    let item_count = 1_000usize;
    let item_height = 28.0;
    let viewport = UiSize::new(480.0, 640.0);
    let max_offset = (item_count as f32 * item_height - viewport.height).max(0.0);
    let offset = ((frame as f32 * 19.0) % max_offset.max(1.0)).round();
    let mut list = Element::column().gap(0.0);
    for index in 0..item_count {
        list = list.child(Element::new().height(item_height).child(Element::text(
            format!("list item {index:04}"),
            text_style(14.0),
        )));
    }
    let root = Element::new().child(
        Element::scroll_box(offset)
            .width(viewport.width)
            .height(viewport.height)
            .scrollbar_style(ScrollbarStyle::default())
            .child(list),
    );
    (root, viewport)
}

fn calculator_tree(frame: u64) -> (Element, UiSize) {
    let expression = match frame % 6 {
        0 => "2",
        1 => "2+",
        2 => "2+2",
        3 => "4",
        4 => "4*8",
        _ => "32",
    };
    let history_len = (frame as usize / 12).min(40);
    let mut history = Element::column().gap(4.0);
    for index in 0..history_len {
        history = history.child(Element::text(
            format!("{} + {} = {}", index, index + 1, index * 2 + 1),
            text_style(12.0),
        ));
    }

    let root = Element::column()
        .gap(10.0)
        .padding(Edges::all(20.0))
        .child(Element::text("Calculator", text_style(28.0)))
        .child(display_panel(expression))
        .child(button_grid())
        .child(Element::scroll_box(10_000.0).height(180.0).child(history));
    (root, UiSize::new(420.0, 720.0))
}

fn resize_configure_tree(frame: u64) -> (Element, UiSize) {
    let width = 360.0 + (frame % 90) as f32 * 4.0;
    let height = 280.0 + (frame % 60) as f32 * 3.0;
    let mut content = Element::column().gap(8.0).padding(Edges::all(16.0));
    for index in 0..40 {
        content = content.child(Element::text(
            format!("resize row {index:02}: {width:.0} x {height:.0}"),
            text_style(14.0),
        ));
    }
    (content, UiSize::new(width, height))
}

fn large_text_body_tree(frame: u64, text_kb: usize) -> (Element, UiSize) {
    let viewport = UiSize::new(720.0, 720.0);
    let text_width = 680.0;
    let line_height = 18.0;
    let text = large_text_body(text_kb);
    let estimated_line_count = (text.len() as f32 / 72.0).ceil();
    let estimated_height = (estimated_line_count * line_height).max(viewport.height);
    let max_offset = (estimated_height - viewport.height).max(0.0);
    let offset = ((frame as f32 * 127.0) % max_offset.max(1.0)).round();
    let style = text_style(14.0)
        .with_line_height(line_height)
        .with_bounds(UiSize::new(text_width, estimated_height));

    let root = Element::new().child(
        Element::scroll_box(offset)
            .width(viewport.width)
            .height(viewport.height)
            .child(
                Element::new()
                    .width(text_width)
                    .child(Element::paragraph(text, style)),
            ),
    );
    (root, viewport)
}

fn large_text_body(text_kb: usize) -> String {
    let target_bytes = text_kb.saturating_mul(1024).max(1);
    let paragraph = "RUIN large text profiling paragraph: the quick brown fox jumps over the lazy dog. This line is intentionally ordinary prose with enough words to exercise wrapping, glyph collection, hit-test geometry, and cache behavior.\n";
    let mut text = String::with_capacity(target_bytes + paragraph.len());
    while text.len() < target_bytes {
        text.push_str(paragraph);
    }
    text.truncate(target_bytes);
    text
}

fn display_panel(expression: &str) -> Element {
    Element::new()
        .padding(Edges::all(12.0))
        .border(1.0, Color::rgb(0x80, 0x80, 0x80))
        .child(Element::text(expression, text_style(24.0)))
}

fn button_grid() -> Element {
    let rows = [
        ["7", "8", "9", "/"],
        ["4", "5", "6", "*"],
        ["1", "2", "3", "-"],
        ["0", ".", "=", "+"],
    ];
    let mut grid = Element::column().gap(6.0);
    for row in rows {
        grid = grid.child(button_row(row));
    }
    grid
}

fn button_row<const N: usize>(labels: [&str; N]) -> Element {
    let mut row = Element::new().direction(FlexDirection::Row).gap(6.0);
    for label in labels {
        row = row.child(
            Element::new()
                .width(76.0)
                .height(36.0)
                .border(1.0, Color::rgb(0x90, 0x90, 0x90))
                .child(Element::text(label, text_style(16.0))),
        );
    }
    row
}
