use super::{Diagnostic, DiagnosticBuilder, FoundAt, found_at, should_show_context_label};
use crate::lexer::{TokenSpan, TokenizedFile};
use crate::parser::ParseError;

const SWITCH_KEYWORD: &str = "switch_keyword";
const LEFT_BRACE: &str = "left_brace";
const CASE_KEYWORD: &str = "case_keyword";
const DEFAULT_KEYWORD: &str = "default_keyword";
const CASE_EXPRESSION: &str = "case_expression";
const FIRST_DEFAULT: &str = "first_default";

const DUPLICATE_DEFAULT_PREFIX: &str = "duplicate_default";
const CASE_AFTER_DEFAULT_PREFIX: &str = "case_after_default";

fn related_span(error: &ParseError, label: &str) -> Option<TokenSpan> {
    error.related_spans.get(label).copied()
}

fn context_span_if_useful(
    file: &TokenizedFile<'_>,
    context_span: Option<TokenSpan>,
    blame_span: TokenSpan,
) -> Option<TokenSpan> {
    context_span.filter(|span| should_show_context_label(file, *span, blame_span))
}

fn primary_span_with_context(
    context_span: Option<TokenSpan>,
    blame_span: TokenSpan,
) -> TokenSpan {
    match context_span {
        Some(context_span) => TokenSpan {
            start: context_span.start,
            end: blame_span.end,
        },
        None => blame_span,
    }
}

fn same_span(left: TokenSpan, right: TokenSpan) -> bool {
    left.start == right.start && left.end == right.end
}

fn switch_keyword_context_span(
    file: &TokenizedFile<'_>,
    error: &ParseError,
    blame_span: TokenSpan,
    left_brace_span: Option<TokenSpan>,
) -> Option<TokenSpan> {
    let switch_keyword_span = related_span(error, SWITCH_KEYWORD)?;

    if file.same_line(switch_keyword_span.start, blame_span.end) {
        return None;
    }

    if let Some(left_brace_span) = left_brace_span
        && file.same_line(switch_keyword_span.start, left_brace_span.start)
    {
        return None;
    }

    Some(switch_keyword_span)
}

fn switch_case_label_context_span(
    file: &TokenizedFile<'_>,
    error: &ParseError,
    blame_span: TokenSpan,
) -> Option<TokenSpan> {
    let case_keyword_span = related_span(error, CASE_KEYWORD)?;

    let case_label_span = match related_span(error, CASE_EXPRESSION) {
        Some(case_expression_span) => TokenSpan {
            start: case_keyword_span.start,
            end: case_expression_span.end,
        },
        None => case_keyword_span,
    };

    context_span_if_useful(file, Some(case_label_span), blame_span)
}

fn numbered_related_count(error: &ParseError, prefix: &str) -> usize {
    let mut count = 0;

    for index in 1.. {
        let label = format!("{}_{}", prefix, index);
        if related_span(error, &label).is_none() {
            break;
        }
        count += 1;
    }

    count
}

fn add_numbered_related_labels(
    mut builder: DiagnosticBuilder,
    error: &ParseError,
    prefix: &str,
    primary_span: TokenSpan,
    first_text: &'static str,
    later_text: &'static str,
) -> DiagnosticBuilder {
    for index in 1.. {
        let label = format!("{}_{}", prefix, index);
        let Some(span) = related_span(error, &label) else {
            break;
        };

        if same_span(span, primary_span) {
            continue;
        }

        let text = if index == 1 { first_text } else { later_text };
        builder = builder.secondary_label(span, text);
    }

    builder
}

/// P0034
pub(super) fn diagnostic_switch_missing_body(
    error: ParseError,
    file: &TokenizedFile<'_>,
) -> Diagnostic {
    let blame_span = error.blame_span;
    let switch_context_span =
        context_span_if_useful(file, related_span(&error, SWITCH_KEYWORD), blame_span);
    let primary_span = primary_span_with_context(switch_context_span, blame_span);

    let primary_text = match found_at(file, blame_span.start) {
        FoundAt::Token(token_text) => format!("expected `{{` before `{}`", token_text),
        FoundAt::EndOfFile => "expected `{` after the switch expression".to_string(),
        FoundAt::Unknown => "expected `{` here".to_string(),
    };

    DiagnosticBuilder::error("missing `{` to start `switch` body")
        .primary_label(primary_span, primary_text)
        .code("P0034")
        .build()
}

/// P0035
pub(super) fn diagnostic_switch_top_level_item_not_case(
    error: ParseError,
    file: &TokenizedFile<'_>,
) -> Diagnostic {
    let blame_span = error.blame_span;
    let left_brace_span = related_span(&error, LEFT_BRACE);

    let left_brace_context_span = context_span_if_useful(file, left_brace_span, blame_span);
    let switch_keyword_context_span =
        switch_keyword_context_span(file, &error, blame_span, left_brace_span);

    let primary_text = if matches!(found_at(file, blame_span.start), FoundAt::Token("{")) {
        "this block must be inside a `case` or `default` section"
    } else if related_span(&error, "multiple_items").is_some() {
        "these statements must be inside a `case` or `default` section"
    } else {
        "this statement must be inside a `case` or `default` section"
    };

    let mut builder =
        DiagnosticBuilder::error("expected `case` or `default` section label in switch body");

    if let Some(switch_keyword_context_span) = switch_keyword_context_span {
        builder = builder.secondary_label(switch_keyword_context_span, "`switch` starts here");
    }

    if let Some(left_brace_context_span) = left_brace_context_span {
        builder = builder.secondary_label(left_brace_context_span, "switch body starts here");
    }

    builder
        .primary_label(blame_span, primary_text)
        .code("P0035")
        .build()
}

/// P0036
pub(super) fn diagnostic_switch_case_missing_colon(
    error: ParseError,
    file: &TokenizedFile<'_>,
) -> Diagnostic {
    let blame_span = error.blame_span;
    let case_label_context_span = switch_case_label_context_span(file, &error, blame_span);

    let primary_text = match found_at(file, blame_span.start) {
        FoundAt::Token(token_text) => format!("expected `:` before `{}`", token_text),
        FoundAt::EndOfFile => "expected `:` before end of file".to_string(),
        FoundAt::Unknown => "expected `:` here".to_string(),
    };

    let mut builder = DiagnosticBuilder::error("missing `:` after `case` label");

    if let Some(case_label_context_span) = case_label_context_span {
        builder = builder.secondary_label(
            case_label_context_span,
            "this `case` label needs a trailing `:`",
        );
    }

    builder
        .primary_label(blame_span, primary_text)
        .code("P0036")
        .build()
}

/// P0037
pub(super) fn diagnostic_switch_default_missing_colon(
    error: ParseError,
    file: &TokenizedFile<'_>,
) -> Diagnostic {
    let blame_span = error.blame_span;
    let default_context_span =
        context_span_if_useful(file, related_span(&error, DEFAULT_KEYWORD), blame_span);

    let primary_text = match found_at(file, blame_span.start) {
        FoundAt::Token(token_text) => format!("expected `:` before `{}`", token_text),
        FoundAt::EndOfFile => "expected `:` before end of file".to_string(),
        FoundAt::Unknown => "expected `:` here".to_string(),
    };

    let mut builder = DiagnosticBuilder::error("missing `:` after `default`");

    if let Some(default_context_span) = default_context_span {
        builder = builder.secondary_label(
            default_context_span,
            "this `default` label needs a trailing `:`",
        );
    }

    builder
        .primary_label(blame_span, primary_text)
        .code("P0037")
        .build()
}

/// P0038
pub(super) fn diagnostic_switch_duplicate_default(
    error: ParseError,
    _file: &TokenizedFile<'_>,
) -> Diagnostic {
    let blame_span = error.blame_span;
    let duplicate_count = numbered_related_count(&error, DUPLICATE_DEFAULT_PREFIX);

    let title = if duplicate_count > 1 {
        "multiple `default` sections in switch"
    } else {
        "duplicate `default` section in switch"
    };

    let mut builder = DiagnosticBuilder::error(title);

    if let Some(first_default_span) = related_span(&error, FIRST_DEFAULT)
        && !same_span(first_default_span, blame_span)
    {
        builder = builder.secondary_label(first_default_span, "first `default` section is here");
    }

    builder = add_numbered_related_labels(
        builder,
        &error,
        DUPLICATE_DEFAULT_PREFIX,
        blame_span,
        "duplicate `default` section",
        "another duplicate `default` section",
    );

    builder
        .primary_label(blame_span, "duplicate `default` section")
        .code("P0038")
        .build()
}

/// P0039
pub(super) fn diagnostic_switch_cases_after_default(
    error: ParseError,
    _file: &TokenizedFile<'_>,
) -> Diagnostic {
    let blame_span = error.blame_span;
    let case_after_default_count = numbered_related_count(&error, CASE_AFTER_DEFAULT_PREFIX);

    let title = if case_after_default_count > 1 {
        "multiple `case` sections appear after `default`"
    } else {
        "`case` section appears after `default`"
    };

    let mut builder = DiagnosticBuilder::error(title);

    if let Some(first_default_span) = related_span(&error, FIRST_DEFAULT)
        && !same_span(first_default_span, blame_span)
    {
        builder = builder.secondary_label(
            first_default_span,
            "`default` must be the last section in this switch",
        );
    }

    builder = add_numbered_related_labels(
        builder,
        &error,
        CASE_AFTER_DEFAULT_PREFIX,
        blame_span,
        "`case` after `default`",
        "another `case` after `default`",
    );

    builder
        .primary_label(blame_span, "`case` after `default`")
        .code("P0039")
        .build()
}

/// P0040
pub(super) fn diagnostic_switch_missing_closing_brace(
    error: ParseError,
    file: &TokenizedFile<'_>,
) -> Diagnostic {
    let blame_span = error.blame_span;
    let left_brace_span = related_span(&error, LEFT_BRACE);

    let left_brace_context_span = context_span_if_useful(file, left_brace_span, blame_span);
    let switch_keyword_context_span =
        switch_keyword_context_span(file, &error, blame_span, left_brace_span);
    let primary_span = primary_span_with_context(left_brace_context_span, blame_span);

    let primary_text = match found_at(file, blame_span.start) {
        FoundAt::Token(token_text) => format!("expected `}}` before `{}`", token_text),
        FoundAt::EndOfFile => "expected `}` before end of file".to_string(),
        FoundAt::Unknown => "expected `}` here".to_string(),
    };

    let mut builder = DiagnosticBuilder::error("missing `}` to close switch body");

    if let Some(switch_keyword_context_span) = switch_keyword_context_span {
        builder = builder.secondary_label(switch_keyword_context_span, "`switch` starts here");
    }

    builder
        .primary_label(primary_span, primary_text)
        .code("P0040")
        .build()
}

/// P0041
pub(super) fn diagnostic_switch_case_missing_expression(
    error: ParseError,
    file: &TokenizedFile<'_>,
) -> Diagnostic {
    let blame_span = error.blame_span;
    let case_context_span =
        context_span_if_useful(file, related_span(&error, CASE_KEYWORD), blame_span);

    let primary_text = match found_at(file, blame_span.start) {
        FoundAt::Token(":") => "expected expression before `:`".to_string(),
        FoundAt::Token(token_text) => format!("expected expression before `{}`", token_text),
        FoundAt::EndOfFile => "expected expression before end of file".to_string(),
        FoundAt::Unknown => "expected expression here".to_string(),
    };

    let mut builder = DiagnosticBuilder::error("missing expression after `case`");

    if let Some(case_context_span) = case_context_span {
        builder = builder.secondary_label(
            case_context_span,
            "after this `case`, an expression was expected",
        );
    }

    builder
        .primary_label(blame_span, primary_text)
        .code("P0041")
        .build()
}

/// P0042
pub(super) fn diagnostic_switch_case_expression_invalid_start(
    error: ParseError,
    file: &TokenizedFile<'_>,
) -> Diagnostic {
    let blame_span = error.blame_span;
    let case_context_span =
        context_span_if_useful(file, related_span(&error, CASE_KEYWORD), blame_span);

    let found = found_at(file, blame_span.start);

    let title = match found {
        FoundAt::Token(token_text) => {
            format!("expected expression after `case`, found `{}`", token_text)
        }
        FoundAt::EndOfFile => "expected expression after `case`, found end of file".to_string(),
        FoundAt::Unknown => "expected expression after `case`".to_string(),
    };

    let primary_text = match found {
        FoundAt::Token(token_text) => format!("unexpected `{}`", token_text),
        FoundAt::EndOfFile => "reached end of file here".to_string(),
        FoundAt::Unknown => "expected expression here".to_string(),
    };

    let mut builder = DiagnosticBuilder::error(title);

    if let Some(case_context_span) = case_context_span {
        builder = builder.secondary_label(
            case_context_span,
            "after this `case`, an expression was expected",
        );
    }

    builder
        .primary_label(blame_span, primary_text)
        .code("P0042")
        .build()
}