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

const LOCAL_KEYWORD: &str = "local_keyword";
const DECLARATION_KEYWORD: &str = "declaration_keyword";
const VARIABLE_TYPE: &str = "variable_type";
const LAST_DECLARATOR: &str = "last_declarator";
const SEPARATOR: &str = "separator";
const OPEN_BRACKET: &str = "open_bracket";

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

fn should_label_keyword(
    file: &TokenizedFile<'_>,
    keyword_span: Option<TokenSpan>,
    context_span: Option<TokenSpan>,
    blame_span: TokenSpan,
) -> bool {
    let Some(keyword_span) = keyword_span else {
        return false;
    };

    let Some(context_span) = context_span else {
        return !file.same_line(keyword_span.start, blame_span.end);
    };

    !file.same_line(keyword_span.start, context_span.end)
        && !file.same_line(keyword_span.start, blame_span.end)
}

fn expected_before_found(
    file: &TokenizedFile<'_>,
    blame_span: TokenSpan,
    expected: &str,
) -> String {
    match found_at(file, blame_span.start) {
        FoundAt::Token(token_text) => format!("{expected} before `{token_text}`"),
        FoundAt::EndOfFile => format!("{expected} before end of file"),
        FoundAt::Unknown => format!("{expected} here"),
    }
}

fn expected_variable_name_primary_text(file: &TokenizedFile<'_>, blame_span: TokenSpan) -> String {
    expected_before_found(file, blame_span, "expected variable name")
}

/// P0050
pub(super) fn diagnostic_local_variable_declaration_missing_semicolon<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let local_keyword_span = related_span(&error, LOCAL_KEYWORD);
    let last_declarator_span = related_span(&error, LAST_DECLARATOR);
    let blame_span = error.blame_span;

    let primary_text = match found_at(file, blame_span.start) {
        FoundAt::Token("local") => "expected `;` before this declaration".to_string(),
        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 local variable declaration");

    if should_label_keyword(file, local_keyword_span, last_declarator_span, blame_span) {
        if let Some(local_keyword_span) = local_keyword_span {
            builder = builder
                .secondary_label(local_keyword_span, "local variable declaration starts here");
        }
    }

    if let Some(last_declarator_span) = last_declarator_span {
        builder = builder.secondary_label(last_declarator_span, "declaration ends here");
    }

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

/// P0051
pub(super) fn diagnostic_variable_declarator_list_empty<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let declaration_keyword_span = related_span(&error, DECLARATION_KEYWORD);
    let variable_type_span = related_span(&error, VARIABLE_TYPE);
    let blame_span = error.blame_span;

    let primary_text = expected_variable_name_primary_text(file, blame_span);

    let mut builder =
        DiagnosticBuilder::error("expected at least one variable name in variable declaration");

    if should_label_keyword(
        file,
        declaration_keyword_span,
        variable_type_span,
        blame_span,
    ) {
        if let Some(declaration_keyword_span) = declaration_keyword_span {
            builder = builder
                .secondary_label(declaration_keyword_span, "variable declaration starts here");
        }
    }

    if let Some(variable_type_span) = variable_type_span {
        if should_show_context_label(file, variable_type_span, blame_span) {
            builder = builder.secondary_label(
                variable_type_span,
                "after this type, a variable name was expected",
            );
        }
    }

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

/// P0052
pub(super) fn diagnostic_variable_declarator_expected<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let declaration_keyword_span = related_span(&error, DECLARATION_KEYWORD);
    let variable_type_span = related_span(&error, VARIABLE_TYPE);
    let separator_span = related_span(&error, SEPARATOR);
    let blame_span = error.blame_span;

    let first_declarator = error.flags.contains("first_declarator");
    let empty_declarator_run = error.flags.contains("empty_declarator_run");

    let blame_is_multi_token = blame_span.start != blame_span.end;

    let title = if first_declarator && empty_declarator_run {
        "expected variable name before `,`"
    } else if separator_span.is_some() {
        "expected variable name after `,`"
    } else {
        "expected variable name in variable declaration"
    };

    let primary_text = if empty_declarator_run {
        match found_at(file, blame_span.start) {
            FoundAt::Token(",") if blame_is_multi_token => {
                "empty variable declarators are not allowed".to_string()
            }
            FoundAt::Token(",") => "unexpected `,`".to_string(),
            FoundAt::Token(token_text) => {
                format!("expected variable name before `{token_text}`")
            }
            FoundAt::EndOfFile => "expected variable name before end of file".to_string(),
            FoundAt::Unknown => "expected variable name here".to_string(),
        }
    } else if separator_span.is_some() {
        match found_at(file, blame_span.start) {
            FoundAt::Token(token_text) => {
                format!("expected variable name before `{token_text}`")
            }
            FoundAt::EndOfFile => "expected variable name before end of file".to_string(),
            FoundAt::Unknown => "expected variable name here".to_string(),
        }
    } else {
        expected_variable_name_primary_text(file, blame_span)
    };

    let context_span = separator_span.or(variable_type_span);

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

    if should_label_keyword(file, declaration_keyword_span, context_span, blame_span) {
        if let Some(declaration_keyword_span) = declaration_keyword_span {
            builder = builder
                .secondary_label(declaration_keyword_span, "variable declaration starts here");
        }
    }

    if let Some(separator_span) = separator_span {
        if should_show_context_label(file, separator_span, blame_span) {
            builder = builder.secondary_label(
                separator_span,
                "after this `,`, another variable name was expected",
            );
        }
    } else if let Some(variable_type_span) = variable_type_span {
        if should_show_context_label(file, variable_type_span, blame_span) {
            builder = builder.secondary_label(
                variable_type_span,
                "after this type, a variable name was expected",
            );
        }
    }

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

/// P0053
pub(super) fn diagnostic_variable_declarator_list_missing_separator<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let declaration_keyword_span = related_span(&error, DECLARATION_KEYWORD);
    let last_declarator_span = related_span(&error, LAST_DECLARATOR);
    let blame_span = error.blame_span;

    let primary_text = expected_before_found(file, blame_span, "expected `,`");

    let mut builder = DiagnosticBuilder::error("missing `,` between variable declarators");

    if should_label_keyword(
        file,
        declaration_keyword_span,
        last_declarator_span,
        blame_span,
    ) {
        if let Some(declaration_keyword_span) = declaration_keyword_span {
            builder = builder
                .secondary_label(declaration_keyword_span, "variable declaration starts here");
        }
    }

    if let Some(last_declarator_span) = last_declarator_span {
        builder = builder.secondary_label(last_declarator_span, "previous declarator ends here");
    }

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

/// P0054
pub(super) fn diagnostic_variable_declarator_array_size_missing_closing_bracket<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let declaration_keyword_span = related_span(&error, DECLARATION_KEYWORD);
    let open_bracket_span = related_span(&error, OPEN_BRACKET);
    let blame_span = error.blame_span;

    let primary_text = expected_before_found(file, blame_span, "expected `]`");
    let primary_span =
        primary_span_with_optional_multiline_context(file, open_bracket_span, blame_span);

    let mut builder = DiagnosticBuilder::error("missing `]` to close array size expression");

    if should_label_keyword(
        file,
        declaration_keyword_span,
        open_bracket_span,
        blame_span,
    ) {
        if let Some(declaration_keyword_span) = declaration_keyword_span {
            builder = builder
                .secondary_label(declaration_keyword_span, "variable declaration starts here");
        }
    }

    if let Some(open_bracket_span) = open_bracket_span {
        if should_show_context_label(file, open_bracket_span, blame_span) {
            builder =
                builder.secondary_label(open_bracket_span, "array size expression starts here");
        }
    }

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

/// P0055
pub(super) fn diagnostic_variable_declarator_array_size_expected<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let variable_name_span = related_span(
        &error,
        diagnostic_labels::EXPRESSION_REQUIRED_BY,
    );
    let expected_after_span = related_span(
        &error,
        diagnostic_labels::EXPRESSION_EXPECTED_AFTER,
    );
    let blame_span = error.blame_span;

    let variable_name = variable_name_span.and_then(|span| match found_at(file, span.start) {
        FoundAt::Token(token_text) => Some(token_text),
        FoundAt::EndOfFile | FoundAt::Unknown => None,
    });

    let title = match variable_name {
        Some(variable_name) => {
            format!("expected array size expression for `{variable_name}` between `[` and `]`")
        }
        None => "expected array size expression between `[` and `]`".to_string(),
    };

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

    let primary_span =
        primary_span_with_optional_multiline_context(file, expected_after_span, blame_span);

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

    if let Some(expected_after_span) = expected_after_span {
        if should_show_context_label(file, expected_after_span, blame_span) {
            builder = builder.secondary_label(
                expected_after_span,
                "after this `[`, an array size expression was expected",
            );
        }
    }

    if let Some(variable_name_span) = variable_name_span {
        let variable_name_is_already_visible =
            file.same_line(variable_name_span.start, blame_span.start)
                || file.same_line(variable_name_span.start, blame_span.end)
                || expected_after_span.is_some_and(|expected_after_span| {
                    file.same_line(variable_name_span.start, expected_after_span.start)
                        || file.same_line(variable_name_span.start, expected_after_span.end)
                });

        if should_show_context_label(file, variable_name_span, blame_span)
            && !variable_name_is_already_visible
        {
            builder = builder.secondary_label(
                variable_name_span,
                "this variable has an array size suffix",
            );
        }
    }

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

/// P0056
pub(super) fn diagnostic_variable_declarator_initializer_expected<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let variable_name_span = related_span(
        &error,
        diagnostic_labels::EXPRESSION_REQUIRED_BY,
    );
    let expected_after_span = related_span(
        &error,
        diagnostic_labels::EXPRESSION_EXPECTED_AFTER,
    );
    let blame_span = error.blame_span;

    let variable_name = variable_name_span.and_then(|span| match found_at(file, span.start) {
        FoundAt::Token(token_text) => Some(token_text),
        FoundAt::EndOfFile | FoundAt::Unknown => None,
    });

    let title = match (variable_name, found_at(file, blame_span.start)) {
        (Some(variable_name), FoundAt::Token(token_text)) => {
            format!(
                "expected initializer expression for `{variable_name}` after `=`, found `{token_text}`"
            )
        }
        (Some(variable_name), FoundAt::EndOfFile) => {
            format!(
                "expected initializer expression for `{variable_name}` after `=`, found end of file"
            )
        }
        (Some(variable_name), FoundAt::Unknown) => {
            format!("expected initializer expression for `{variable_name}` after `=`")
        }
        (None, FoundAt::Token(token_text)) => {
            format!("expected initializer expression after `=`, found `{token_text}`")
        }
        (None, FoundAt::EndOfFile) => {
            "expected initializer expression after `=`, found end of file".to_string()
        }
        (None, FoundAt::Unknown) => "expected initializer expression after `=`".to_string(),
    };

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

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

    if let Some(expected_after_span) = expected_after_span {
        if should_show_context_label(file, expected_after_span, blame_span) {
            builder = builder.secondary_label(
                expected_after_span,
                "after this `=`, an initializer expression was expected",
            );
        }
    }

    if let Some(variable_name_span) = variable_name_span {
        let variable_name_is_already_visible =
            file.same_line(variable_name_span.start, blame_span.start)
                || file.same_line(variable_name_span.start, blame_span.end)
                || expected_after_span.is_some_and(|expected_after_span| {
                    file.same_line(variable_name_span.start, expected_after_span.start)
                        || file.same_line(variable_name_span.start, expected_after_span.end)
                });

        if should_show_context_label(file, variable_name_span, blame_span)
            && !variable_name_is_already_visible
        {
            builder = builder.secondary_label(
                variable_name_span,
                "initializer is for this variable",
            );
        }
    }

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