//! Checks that a list of items is in alphabetical order
//!
//! Use the following marker in the code:
//! ```rust
//! // tidy-alphabetical-start
//! fn aaa() {}
//! fn eee() {}
//! fn z() {}
//! // tidy-alphabetical-end
//! ```
//!
//! The following lines are ignored:
//! - Empty lines
//! - Lines that are indented with more or less spaces than the first line
//! - Lines starting with `//`, `#` (except those starting with `#!`), `)`, `]`, `}` if the comment
//!   has the same indentation as the first line
//! - Lines starting with a closing delimiter (`)`, `[`, `}`) are ignored.
//!
//! If a line ends with an opening delimiter, we effectively join the following line to it before
//! checking it. E.g. `foo(\nbar)` is treated like `foo(bar)`.

use std::cmp::Ordering;
use std::fmt::Display;
use std::iter::Peekable;
use std::path::Path;

use crate::diagnostics::{CheckId, RunningCheck, TidyCtx};
use crate::walk::{filter_dirs, walk};

#[cfg(test)]
mod tests;

fn indentation(line: &str) -> usize {
    line.find(|c| c != ' ').unwrap_or(0)
}

fn is_close_bracket(c: char) -> bool {
    matches!(c, ')' | ']' | '}')
}

const START_MARKER: &str = "tidy-alphabetical-start";
const END_MARKER: &str = "tidy-alphabetical-end";

fn check_section<'a>(
    file: impl Display,
    lines: impl Iterator<Item = (usize, &'a str)>,
    check: &mut RunningCheck,
) {
    let mut prev_line = String::new();
    let mut first_indent = None;
    let mut in_split_line = None;

    for (idx, line) in lines {
        if line.is_empty() {
            continue;
        }

        if line.contains(START_MARKER) {
            check.error(format!(
                "{file}:{} found `{START_MARKER}` expecting `{END_MARKER}`",
                idx + 1
            ));
            return;
        }

        if line.contains(END_MARKER) {
            return;
        }

        let indent = first_indent.unwrap_or_else(|| {
            let indent = indentation(line);
            first_indent = Some(indent);
            indent
        });

        let line = if let Some(prev_split_line) = in_split_line {
            // Join the split lines.
            in_split_line = None;
            format!("{prev_split_line}{}", line.trim_start())
        } else {
            line.to_string()
        };

        if indentation(&line) != indent {
            continue;
        }

        let trimmed_line = line.trim_start_matches(' ');

        if trimmed_line.starts_with("//")
            || (trimmed_line.starts_with('#') && !trimmed_line.starts_with("#!"))
            || trimmed_line.starts_with(is_close_bracket)
        {
            continue;
        }

        if line.trim_end().ends_with('(') {
            in_split_line = Some(line);
            continue;
        }

        let prev_line_trimmed_lowercase = prev_line.trim_start_matches(' ');

        if version_sort(trimmed_line, prev_line_trimmed_lowercase).is_lt() {
            check.error(format!("{file}:{}: line not in alphabetical order", idx + 1));
        }

        prev_line = line;
    }

    check.error(format!("{file}: reached end of file expecting `{END_MARKER}`"));
}

fn check_lines<'a>(
    file: &impl Display,
    mut lines: impl Iterator<Item = (usize, &'a str)>,
    check: &mut RunningCheck,
) {
    while let Some((idx, line)) = lines.next() {
        if line.contains(END_MARKER) {
            check.error(format!(
                "{file}:{} found `{END_MARKER}` expecting `{START_MARKER}`",
                idx + 1
            ));
        }

        if line.contains(START_MARKER) {
            check_section(file, &mut lines, check);
        }
    }
}

pub fn check(path: &Path, tidy_ctx: TidyCtx) {
    let mut check = tidy_ctx.start_check(CheckId::new("alphabetical").path(path));

    let skip =
        |path: &_, _is_dir| filter_dirs(path) || path.ends_with("tidy/src/alphabetical/tests.rs");

    walk(path, skip, &mut |entry, contents| {
        let file = &entry.path().display();
        let lines = contents.lines().enumerate();
        check_lines(file, lines, &mut check)
    });
}

fn consume_numeric_prefix<I: Iterator<Item = char>>(it: &mut Peekable<I>) -> String {
    let mut result = String::new();

    while let Some(&c) = it.peek() {
        if !c.is_numeric() {
            break;
        }

        result.push(c);
        it.next();
    }

    result
}

// A sorting function that is case-sensitive, and sorts sequences of digits by their numeric value,
// so that `9` sorts before `12`.
fn version_sort(a: &str, b: &str) -> Ordering {
    let mut it1 = a.chars().peekable();
    let mut it2 = b.chars().peekable();

    while let (Some(x), Some(y)) = (it1.peek(), it2.peek()) {
        match (x.is_numeric(), y.is_numeric()) {
            (true, true) => {
                let num1: String = consume_numeric_prefix(it1.by_ref());
                let num2: String = consume_numeric_prefix(it2.by_ref());

                let int1: u64 = num1.parse().unwrap();
                let int2: u64 = num2.parse().unwrap();

                // Compare strings when the numeric value is equal to handle "00" versus "0".
                match int1.cmp(&int2).then_with(|| num1.cmp(&num2)) {
                    Ordering::Equal => continue,
                    different => return different,
                }
            }
            (false, false) => match x.cmp(y) {
                Ordering::Equal => {
                    it1.next();
                    it2.next();
                    continue;
                }
                different => return different,
            },
            (false, true) | (true, false) => {
                return x.cmp(y);
            }
        }
    }

    it1.next().cmp(&it2.next())
}
