use core::fmt::Write as _;

#[derive(Clone)]
struct Frame {
    /// `_main` is usually as the deepest depth.
    depth: usize,
    name: String,
    file_and_line: String,
}

/// Capture a callstack, skipping the frames that are not interesting.
///
/// In particular: slips everything before `egui::Context::run`,
/// and skipping all frames in the `egui::` namespace.
#[inline(never)]
pub fn capture() -> String {
    let mut frames = vec![];
    let mut depth = 0;

    backtrace::trace(|frame| {
        // Resolve this instruction pointer to a symbol name
        backtrace::resolve_frame(frame, |symbol| {
            let mut file_and_line = symbol.filename().map(shorten_source_file_path);

            if let Some(file_and_line) = &mut file_and_line
                && let Some(line_nr) = symbol.lineno()
            {
                write!(file_and_line, ":{line_nr}").ok();
            }
            let file_and_line = file_and_line.unwrap_or_default();

            let name = symbol
                .name()
                .map(|name| clean_symbol_name(name.to_string()))
                .unwrap_or_default();

            frames.push(Frame {
                depth,
                name,
                file_and_line,
            });
        });

        depth += 1; // note: we can resolve multiple symbols on the same frame.

        true // keep going to the next frame
    });

    if frames.is_empty() {
        return
            "Failed to capture a backtrace. A common cause of this is compiling with panic=\"abort\" (https://github.com/rust-lang/backtrace-rs/issues/397)".to_owned();
    }

    // Inclusive:
    let mut min_depth = 0;
    let mut max_depth = usize::MAX;

    for frame in &frames {
        if frame.name.starts_with("egui::callstack::capture") {
            min_depth = frame.depth + 1;
        }
        if frame.name.starts_with("egui::context::Context::run") {
            max_depth = frame.depth;
        }
    }

    /// Is this the name of some sort of useful entry point?
    fn is_start_name(name: &str) -> bool {
        name == "main"
            || name == "_main"
            || name.starts_with("eframe::run_native")
            || name.starts_with("egui::context::Context::run")
    }

    let mut has_kept_any_start_names = false;

    frames.reverse(); // main on top, i.e. chronological order. Same as Python.

    // Remove frames that are uninteresting:
    frames.retain(|frame| {
        // Keep the first "start" frame we can detect (e.g. `main`) to give the user a sense of chronology:
        if is_start_name(&frame.name) && !has_kept_any_start_names {
            has_kept_any_start_names = true;
            return true;
        }

        if frame.depth < min_depth || max_depth < frame.depth {
            return false;
        }

        // Remove frames that are part of egui itself, since they are not useful to the user.
        let skip_crates = [
            "alloc",
            "backtrace",
            "core",
            "eframe",
            "egui_extras",
            "egui_plot",
            "egui",
            "epaint",
            "std",
            "winit",
        ];
        for crate_name in skip_crates {
            let name = frame.name.trim_start_matches('<');
            if name.starts_with(crate_name)
                && name
                    .as_bytes()
                    .get(crate_name.len())
                    .copied()
                    .is_none_or(|b| b.is_ascii_punctuation() && b != b'_')
            {
                return false;
            }
        }

        let skip_prefixes = [
            // "backtrace::", // not needed, since we cut at egui::callstack::capture
            "<F as egui::widgets::Widget>",
            "core::ptr::drop_in_place<egui::ui::Ui>",
            "core::ops::function::FnOnce::call_once",
            "<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once",
        ];
        for prefix in skip_prefixes {
            if frame.name.starts_with(prefix) {
                return false;
            }
        }
        true
    });

    let mut deepest_depth = 0;
    let mut widest_file_line = 0;
    for frame in &frames {
        deepest_depth = frame.depth.max(deepest_depth);
        widest_file_line = frame.file_and_line.len().max(widest_file_line);
    }

    let widest_depth = deepest_depth.to_string().len();

    let mut formatted = String::new();

    if !frames.is_empty() {
        let mut last_depth = frames[0].depth;

        for frame in &frames {
            let Frame {
                depth,
                name,
                file_and_line,
            } = frame;

            if frame.depth + 1 < last_depth || last_depth + 1 < frame.depth {
                // Show that some frames were elided
                writeln!(formatted, "{:widest_depth$}  …", "").ok();
            }

            writeln!(
                formatted,
                "{depth:widest_depth$}: {file_and_line:widest_file_line$}  {name}"
            )
            .ok();

            last_depth = frame.depth;
        }
    }

    formatted
}

fn clean_symbol_name(mut s: String) -> String {
    // We get a hex suffix (at least on macOS) which is quite unhelpful,
    // e.g. `my_crate::my_function::h3bedd97b1e03baa5`.
    // Let's strip that.
    if let Some(h) = s.rfind("::h") {
        let hex = &s[h + 3..];
        if hex.len() == 16 && hex.chars().all(|c| c.is_ascii_hexdigit()) {
            s.truncate(h);
        }
    }

    s
}

#[test]
fn test_clean_symbol_name() {
    assert_eq!(
        clean_symbol_name("my_crate::my_function::h3bedd97b1e03baa5".to_owned()),
        "my_crate::my_function"
    );
}

/// Shorten a path to a Rust source file from a callstack.
///
/// Example input:
/// * `/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs`
/// * `crates/rerun/src/main.rs`
/// * `/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs`
fn shorten_source_file_path(path: &std::path::Path) -> String {
    // Look for `src` and strip everything up to it.

    let components: Vec<_> = path.iter().map(|path| path.to_string_lossy()).collect();

    let mut src_idx = None;
    for (i, c) in components.iter().enumerate() {
        if c == "src" {
            src_idx = Some(i);
        }
    }

    // Look for the last `src`:
    if let Some(src_idx) = src_idx {
        // Before `src` comes the name of the crate - let's include that:
        let first_index = src_idx.saturating_sub(1);

        let mut output = components[first_index].to_string();
        for component in &components[first_index + 1..] {
            output.push('/');
            output.push_str(component);
        }
        output
    } else {
        // No `src` directory found - weird!
        path.display().to_string()
    }
}

#[test]
fn test_shorten_path() {
    for (before, after) in [
        (
            "/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs",
            "tokio-1.24.1/src/runtime/runtime.rs",
        ),
        ("crates/rerun/src/main.rs", "rerun/src/main.rs"),
        (
            "/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs",
            "core/src/ops/function.rs",
        ),
        ("/weird/path/file.rs", "/weird/path/file.rs"),
    ] {
        use core::str::FromStr as _;
        let before = std::path::PathBuf::from_str(before).unwrap();
        assert_eq!(shorten_source_file_path(&before), after);
    }
}
