use std::collections::HashSet;
use std::fmt::{self, Display, Formatter};
use std::ops::Range;
use std::str::FromStr;

use ecow::EcoString;
use typst::syntax::package::PackageVersion;
use typst::syntax::Source;
use unscanny::Scanner;

/// Each test and subset may contain metadata.
#[derive(Debug)]
pub struct TestMetadata {
    /// Configures how the test is run.
    pub config: TestConfig,
    /// Declares properties that must hold for a test.
    ///
    /// For instance, `// Warning: 1-3 no text within underscores`
    /// will fail the test if the warning isn't generated by your test.
    pub annotations: HashSet<Annotation>,
}

/// Configuration of a test or subtest.
#[derive(Debug, Default)]
pub struct TestConfig {
    /// Reference images will be generated and compared.
    ///
    /// Defaults to `true`, can be disabled with `Ref: false`.
    pub compare_ref: Option<bool>,
    /// Hint annotations will be compared to compiler hints.
    ///
    /// Defaults to `true`, can be disabled with `Hints: false`.
    pub validate_hints: Option<bool>,
    /// Autocompletion annotations will be validated against autocompletions.
    /// Mutually exclusive with error and hint annotations.
    ///
    /// Defaults to `false`, can be enabled with `Autocomplete: true`.
    pub validate_autocomplete: Option<bool>,
}

/// Parsing error when the metadata is invalid.
pub(crate) enum InvalidMetadata {
    /// An invalid annotation and it's error message.
    InvalidAnnotation(Annotation, String),
    /// Setting metadata can only be done with `true` or `false` as a value.
    InvalidSet(String),
}

impl InvalidMetadata {
    pub(crate) fn write(
        invalid_data: Vec<InvalidMetadata>,
        output: &mut String,
        print_annotation: &mut impl FnMut(&Annotation, &mut String),
    ) {
        use std::fmt::Write;
        for data in invalid_data.into_iter() {
            let (annotation, error) = match data {
                InvalidMetadata::InvalidAnnotation(a, e) => (Some(a), e),
                InvalidMetadata::InvalidSet(e) => (None, e),
            };
            write!(output, "{error}",).unwrap();
            if let Some(annotation) = annotation {
                print_annotation(&annotation, output)
            } else {
                writeln!(output).unwrap();
            }
        }
    }
}

/// Annotation of the form `// KIND: RANGE TEXT`.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Annotation {
    /// Which kind of annotation this is.
    pub kind: AnnotationKind,
    /// May be written as:
    /// - `{line}:{col}-{line}:{col}`, e.g. `0:4-0:6`.
    /// - `{col}-{col}`, e.g. `4-6`:
    ///    The line is assumed to be the line after the annotation.
    /// - `-1`: Produces a range of length zero at the end of the next line.
    ///   Mostly useful for autocompletion tests which require an index.
    pub range: Option<Range<usize>>,
    /// The raw text after the annotation.
    pub text: EcoString,
}

/// The different kinds of in-test annotations.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum AnnotationKind {
    Error,
    Warning,
    Hint,
    AutocompleteContains,
    AutocompleteExcludes,
}

impl AnnotationKind {
    /// Returns the user-facing string for this annotation.
    pub fn as_str(self) -> &'static str {
        match self {
            AnnotationKind::Error => "Error",
            AnnotationKind::Warning => "Warning",
            AnnotationKind::Hint => "Hint",
            AnnotationKind::AutocompleteContains => "Autocomplete contains",
            AnnotationKind::AutocompleteExcludes => "Autocomplete excludes",
        }
    }
}

impl FromStr for AnnotationKind {
    type Err = &'static str;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(match s {
            "Error" => AnnotationKind::Error,
            "Warning" => AnnotationKind::Warning,
            "Hint" => AnnotationKind::Hint,
            "Autocomplete contains" => AnnotationKind::AutocompleteContains,
            "Autocomplete excludes" => AnnotationKind::AutocompleteExcludes,
            _ => return Err("invalid annotatino"),
        })
    }
}

impl Display for AnnotationKind {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        f.pad(self.as_str())
    }
}

/// Parse metadata for a test.
pub fn parse_part_metadata(
    source: &Source,
    is_header: bool,
) -> Result<TestMetadata, Vec<InvalidMetadata>> {
    let mut config = TestConfig::default();
    let mut annotations = HashSet::default();
    let mut invalid_data = vec![];

    let lines = source_to_lines(source);

    for (i, line) in lines.iter().enumerate() {
        if let Some((key, value)) = parse_metadata_line(line) {
            let key = key.trim();
            match key {
                "Ref" => validate_set_annotation(
                    value,
                    &mut config.compare_ref,
                    &mut invalid_data,
                ),
                "Hints" => validate_set_annotation(
                    value,
                    &mut config.validate_hints,
                    &mut invalid_data,
                ),
                "Autocomplete" => validate_set_annotation(
                    value,
                    &mut config.validate_autocomplete,
                    &mut invalid_data,
                ),
                annotation_key => {
                    let Ok(kind) = AnnotationKind::from_str(annotation_key) else {
                        continue;
                    };
                    let mut s = Scanner::new(value);
                    let range = parse_range(&mut s, i, source);
                    let rest = if range.is_some() { s.after() } else { s.string() };
                    let message = rest
                        .trim()
                        .replace("VERSION", &PackageVersion::compiler().to_string())
                        .into();

                    let annotation =
                        Annotation { kind, range: range.clone(), text: message };

                    if is_header {
                        invalid_data.push(InvalidMetadata::InvalidAnnotation(
                            annotation,
                            format!(
                                "Error: header may not contain annotations of type {kind}"
                            ),
                        ));
                        continue;
                    }

                    if matches!(
                        kind,
                        AnnotationKind::AutocompleteContains
                            | AnnotationKind::AutocompleteExcludes
                    ) {
                        if let Some(range) = range {
                            if range.start != range.end {
                                invalid_data.push(InvalidMetadata::InvalidAnnotation(
                                    annotation,
                                    "Error: found range in Autocomplete annotation where range.start != range.end, range.end would be ignored."
                                        .to_string()
                                    ));
                                continue;
                            }
                        } else {
                            invalid_data.push(InvalidMetadata::InvalidAnnotation(
                                annotation,
                                "Error: autocomplete annotation but no range specified"
                                    .to_string(),
                            ));
                            continue;
                        }
                    }
                    annotations.insert(annotation);
                }
            }
        }
    }
    if invalid_data.is_empty() {
        Ok(TestMetadata { config, annotations })
    } else {
        Err(invalid_data)
    }
}

/// Extract key and value for a metadata line of the form: `// KEY: VALUE`.
fn parse_metadata_line(line: &str) -> Option<(&str, &str)> {
    let mut s = Scanner::new(line);
    if !s.eat_if("// ") {
        return None;
    }

    let key = s.eat_until(':').trim();
    if !s.eat_if(':') {
        return None;
    }

    let value = s.eat_until('\n').trim();
    Some((key, value))
}

/// Parse a quoted string.
fn parse_string<'a>(s: &mut Scanner<'a>) -> Option<&'a str> {
    if !s.eat_if('"') {
        return None;
    }
    let sub = s.eat_until('"');
    if !s.eat_if('"') {
        return None;
    }

    Some(sub)
}

/// Parse a number.
fn parse_num(s: &mut Scanner) -> Option<isize> {
    let mut first = true;
    let n = &s.eat_while(|c: char| {
        let valid = first && c == '-' || c.is_numeric();
        first = false;
        valid
    });
    n.parse().ok()
}

/// Parse a comma-separated list of strings.
pub fn parse_string_list(text: &str) -> HashSet<&str> {
    let mut s = Scanner::new(text);
    let mut result = HashSet::new();
    while let Some(sub) = parse_string(&mut s) {
        result.insert(sub);
        s.eat_whitespace();
        if !s.eat_if(',') {
            break;
        }
        s.eat_whitespace();
    }
    result
}

/// Parse a position.
fn parse_pos(s: &mut Scanner, i: usize, source: &Source) -> Option<usize> {
    let first = parse_num(s)? - 1;
    let (delta, column) =
        if s.eat_if(':') { (first, parse_num(s)? - 1) } else { (0, first) };
    let line = (i + comments_until_code(source, i)).checked_add_signed(delta)?;
    source.line_column_to_byte(line, usize::try_from(column).ok()?)
}

/// Parse a range.
fn parse_range(s: &mut Scanner, i: usize, source: &Source) -> Option<Range<usize>> {
    let lines = source_to_lines(source);
    s.eat_whitespace();
    if s.eat_if("-1") {
        let mut add = 1;
        while let Some(line) = lines.get(i + add) {
            if !line.starts_with("//") {
                break;
            }
            add += 1;
        }
        let next_line = lines.get(i + add)?;
        let col = next_line.chars().count();

        let index = source.line_column_to_byte(i + add, col)?;
        s.eat_whitespace();
        return Some(index..index);
    }
    let start = parse_pos(s, i, source)?;
    let end = if s.eat_if('-') { parse_pos(s, i, source)? } else { start };
    s.eat_whitespace();
    Some(start..end)
}

/// Returns the number of lines of comment from line i to next line of code.
fn comments_until_code(source: &Source, i: usize) -> usize {
    source_to_lines(source)[i..]
        .iter()
        .take_while(|line| line.starts_with("//"))
        .count()
}

fn source_to_lines(source: &Source) -> Vec<&str> {
    source.text().lines().map(str::trim).collect()
}

fn validate_set_annotation(
    value: &str,
    flag: &mut Option<bool>,
    invalid_data: &mut Vec<InvalidMetadata>,
) {
    let value = value.trim();
    if value != "false" && value != "true" {
        invalid_data.push(
            InvalidMetadata::InvalidSet(format!("Error: trying to set Ref, Hints, or Autocomplete with value {value:?} != true, != false.")))
    } else {
        *flag = Some(value == "true")
    }
}
