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

pub(super) fn diagnostic_condition_expected<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let control_keyword_span = error
        .related_spans
        .get(diagnostic_labels::EXPRESSION_REQUIRED_BY)
        .copied();
    let control_keyword_text = control_keyword_span.and_then(|span| file.token_text(span.end));

    let do_keyword_span = error.related_spans.get("do_keyword").copied();

    // Present only for the recovery path where a parsed block expression is
    // treated as the likely branch body, meaning the condition before it is
    // missing.
    let branch_body_span = error.related_spans.get("branch_body").copied();
    let found_branch_body = branch_body_span.is_some();

    let found = found_at(file, error.blame_span.end);

    let (header_text, primary_text) = match (control_keyword_text, found) {
        (Some(keyword_text), FoundAt::Token(token_text)) => {
            let primary_text = if found_branch_body && token_text == "{" {
                "body starts here, but the condition is missing".to_string()
            } else {
                format!("unexpected `{}`", token_text)
            };

            (
                format!(
                    "expected condition after `{}`, found `{}`",
                    keyword_text, token_text
                ),
                primary_text,
            )
        }
        (Some(keyword_text), FoundAt::EndOfFile) => (
            format!(
                "expected condition after `{}`, found end of file",
                keyword_text
            ),
            "reached end of file here".to_string(),
        ),
        (Some(keyword_text), FoundAt::Unknown) => (
            format!("expected condition after `{}`", keyword_text),
            "expected condition here".to_string(),
        ),

        (None, FoundAt::Token(token_text)) => {
            let primary_text = if found_branch_body && token_text == "{" {
                "body starts here, but the condition is missing".to_string()
            } else {
                format!("unexpected `{}`", token_text)
            };

            (
                format!("expected condition, found `{}`", token_text),
                primary_text,
            )
        }
        (None, FoundAt::EndOfFile) => (
            "expected condition, found end of file".to_string(),
            "reached end of file here".to_string(),
        ),
        (None, FoundAt::Unknown) => (
            "expected condition".to_string(),
            "expected condition here".to_string(),
        ),
    };

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

    let spans_are_same =
        |left: TokenSpan, right: TokenSpan| left.start == right.start && left.end == right.end;

    if let Some(do_span) = do_keyword_span {
        let same_as_control_keyword = control_keyword_span
            .map(|control_span| spans_are_same(do_span, control_span))
            .unwrap_or(false);

        let same_line_as_control_keyword = control_keyword_span
            .map(|control_span| file.same_line(do_span.start, control_span.start))
            .unwrap_or(false);

        if !same_as_control_keyword
            && !same_line_as_control_keyword
            && !file.same_line(do_span.start, error.blame_span.end)
        {
            builder = builder.secondary_label(do_span, "`do` expression starts here");
        }
    }

    if let Some(control_keyword_span) = control_keyword_span
        && !file.same_line(control_keyword_span.start, error.blame_span.end)
    {
        let secondary_text = if let Some(keyword_text) = control_keyword_text {
            format!("after this `{}`, a condition was expected", keyword_text)
        } else {
            "after this control-flow keyword, a condition was expected".to_string()
        };

        builder = builder.secondary_label(control_keyword_span, secondary_text);
    }

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

pub(super) fn diagnostic_control_flow_body_expected<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let control_keyword_span = error
        .related_spans
        .get(diagnostic_labels::EXPRESSION_REQUIRED_BY)
        .copied();
    let control_keyword_text = control_keyword_span.and_then(|span| file.token_text(span.end));

    let body_context_span = error
        .related_spans
        .get(diagnostic_labels::EXPRESSION_EXPECTED_AFTER)
        .copied();
    let body_context_text = body_context_span.and_then(|span| file.token_text(span.end));

    let found = found_at(file, error.blame_span.end);

    let (header_text, primary_text) = match (control_keyword_text, found) {
        (Some(keyword_text), FoundAt::Token(token_text)) => (
            format!(
                "expected body for `{}`, found `{}`",
                keyword_text, token_text
            ),
            format!("unexpected `{}`", token_text),
        ),
        (Some(keyword_text), FoundAt::EndOfFile) => (
            format!("expected body for `{}`, found end of file", keyword_text),
            "reached end of file here".to_string(),
        ),
        (Some(keyword_text), FoundAt::Unknown) => (
            format!("expected body for `{}`", keyword_text),
            "expected body here".to_string(),
        ),

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

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

    let spans_are_same =
        |left: TokenSpan, right: TokenSpan| left.start == right.start && left.end == right.end;

    let body_context_is_same_as_keyword = match (control_keyword_span, body_context_span) {
        (Some(control_span), Some(body_span)) => spans_are_same(control_span, body_span),
        _ => false,
    };

    let body_context_is_trivial_same_line_eof = match body_context_span {
        Some(body_span) => {
            matches!(found, FoundAt::EndOfFile)
                && file.same_line(body_span.start, error.blame_span.end)
        }
        None => false,
    };

    let should_fallback_to_control_keyword = match body_context_span {
        None => true,
        Some(_) if body_context_is_same_as_keyword => true,
        Some(_) => false,
    };

    if let Some(body_span) = body_context_span
        && !body_context_is_same_as_keyword
        && !body_context_is_trivial_same_line_eof
    {
        let secondary_text = if let Some(context_text) = body_context_text {
            format!("after this `{}`, a body was expected", context_text)
        } else {
            "after this construct, a body was expected".to_string()
        };

        builder = builder.secondary_label(body_span, secondary_text);
    } else if should_fallback_to_control_keyword
        && let Some(keyword_span) = control_keyword_span
        && !file.same_line(keyword_span.start, error.blame_span.end)
    {
        let secondary_text = if let Some(keyword_text) = control_keyword_text {
            format!("after this `{}`, a body was expected", keyword_text)
        } else {
            "after this control-flow keyword, a body was expected".to_string()
        };

        builder = builder.secondary_label(keyword_span, secondary_text);
    }

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

pub(super) fn diagnostic_do_missing_until<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let do_keyword_span = error.related_spans.get("do_keyword").copied();

    let found = found_at(file, error.blame_span.end);

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

    let mut builder = DiagnosticBuilder::error("missing `until` after `do` body");

    let primary_context_span =
        do_keyword_span.filter(|span| should_show_context_label(file, *span, error.blame_span));

    if let Some(span) = primary_context_span {
        builder = builder.secondary_label(span, "`do` expression starts here");
    }

    let primary_span =
        primary_span_with_optional_multiline_context(file, primary_context_span, error.blame_span);

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

pub(super) fn diagnostic_for_each_iterator_expression_expected<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let control_keyword_span = error
        .related_spans
        .get(diagnostic_labels::EXPRESSION_REQUIRED_BY)
        .copied();
    let control_keyword_text = control_keyword_span.and_then(|span| file.token_text(span.end));

    let found = found_at(file, error.blame_span.end);
    let _ = matches!(found, FoundAt::Token("{"));

    let (header_text, primary_text) = match (control_keyword_text, found) {
        (Some(keyword_text), FoundAt::Token(token_text)) => {
            let primary_text = if token_text == "{" {
                "body starts here, but the iterator expression is missing".to_string()
            } else {
                format!("unexpected `{}`", token_text)
            };

            (
                format!(
                    "expected iterator expression after `{}`, found `{}`",
                    keyword_text, token_text
                ),
                primary_text,
            )
        }
        (Some(keyword_text), FoundAt::EndOfFile) => (
            format!(
                "expected iterator expression after `{}`, found end of file",
                keyword_text
            ),
            "reached end of file here".to_string(),
        ),
        (Some(keyword_text), FoundAt::Unknown) => (
            format!("expected iterator expression after `{}`", keyword_text),
            "expected iterator expression here".to_string(),
        ),

        (None, FoundAt::Token(token_text)) => {
            let primary_text = if token_text == "{" {
                "body starts here, but the iterator expression is missing".to_string()
            } else {
                format!("unexpected `{}`", token_text)
            };

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

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

    if let Some(control_keyword_span) = control_keyword_span
        && !file.same_line(control_keyword_span.start, error.blame_span.end)
    {
        let secondary_text = if let Some(keyword_text) = control_keyword_text {
            format!(
                "after this `{}`, an iterator expression was expected",
                keyword_text
            )
        } else {
            "after this control-flow keyword, an iterator expression was expected".to_string()
        };

        builder = builder.secondary_label(control_keyword_span, secondary_text);
    }

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

pub(super) fn diagnostic_for_loop_header_initializer_invalid_start<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let for_keyword_span = error
        .related_spans
        .get(diagnostic_labels::EXPRESSION_REQUIRED_BY)
        .copied();

    let left_parenthesis_span = error
        .related_spans
        .get(diagnostic_labels::EXPRESSION_EXPECTED_AFTER)
        .copied();

    let found = found_at(file, error.blame_span.end);

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

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

    let left_parenthesis_label_is_shown = if let Some(span) = left_parenthesis_span
        && !file.same_line(span.start, error.blame_span.end)
    {
        builder = builder.secondary_label(
            span,
            "after this `(`, an initializer expression or `;` was expected",
        );
        true
    } else {
        false
    };

    if !left_parenthesis_label_is_shown && let Some(for_span) = for_keyword_span {
        let for_is_separated_from_error = !file.same_line(for_span.start, error.blame_span.end);
        let for_is_separated_from_left_parenthesis = left_parenthesis_span
            .map(|span| !file.same_line(for_span.start, span.start))
            .unwrap_or(true);

        if for_is_separated_from_error && for_is_separated_from_left_parenthesis {
            builder = builder.secondary_label(for_span, "`for` loop starts here");
        }
    }

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

pub(super) fn diagnostic_for_loop_header_missing_semicolon_after_initializer<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let initializer_span = error.related_spans.get("for_header_initializer").copied();

    let found = found_at(file, error.blame_span.end);
    let initializer_is_omitted = initializer_span.is_none();

    let header_text = if initializer_is_omitted {
        "missing first `;` in `for` header".to_string()
    } else {
        "missing `;` after initializer in `for` header".to_string()
    };

    let primary_text = match found {
        FoundAt::Token(token_text) => {
            if initializer_is_omitted {
                format!("expected first `;` before `{}`", token_text)
            } else {
                format!("expected `;` before `{}`", token_text)
            }
        }
        FoundAt::EndOfFile => {
            if initializer_is_omitted {
                "expected first `;` before end of file".to_string()
            } else {
                "expected `;` before end of file".to_string()
            }
        }
        FoundAt::Unknown => {
            if initializer_is_omitted {
                "expected first `;` here".to_string()
            } else {
                "expected `;` here".to_string()
            }
        }
    };

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

    if let Some(span) = initializer_span {
        builder = builder.secondary_label(span, "initializer ends here");
    }

    let primary_span = collapse_span_to_end_on_same_line(file, error.blame_span);

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

pub(super) fn diagnostic_for_loop_header_condition_invalid_start<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let for_keyword_span = error
        .related_spans
        .get(diagnostic_labels::EXPRESSION_REQUIRED_BY)
        .copied();

    let first_semicolon_span = error
        .related_spans
        .get(diagnostic_labels::EXPRESSION_EXPECTED_AFTER)
        .copied();

    let found = found_at(file, error.blame_span.end);

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

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

    let first_semicolon_label_is_shown = if let Some(span) = first_semicolon_span
        && !file.same_line(span.start, error.blame_span.end)
    {
        builder = builder.secondary_label(
            span,
            "after this `;`, a condition expression or another `;` was expected",
        );
        true
    } else {
        false
    };

    if !first_semicolon_label_is_shown && let Some(for_span) = for_keyword_span {
        let for_is_separated_from_error = !file.same_line(for_span.start, error.blame_span.end);
        let for_is_separated_from_first_semicolon = first_semicolon_span
            .map(|span| !file.same_line(for_span.start, span.start))
            .unwrap_or(true);

        if for_is_separated_from_error && for_is_separated_from_first_semicolon {
            builder = builder.secondary_label(for_span, "`for` loop starts here");
        }
    }

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

pub(super) fn diagnostic_for_loop_header_missing_semicolon_after_condition<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let condition_span = error.related_spans.get("for_header_condition").copied();

    let found = found_at(file, error.blame_span.end);

    let condition_is_omitted = condition_span.is_none();
    let found_is_eof = matches!(found, FoundAt::EndOfFile);

    let header_text = if condition_is_omitted && found_is_eof {
        "missing second `;` in `for` header".to_string()
    } else {
        "missing `;` after condition in `for` header".to_string()
    };

    let primary_text = match found {
        FoundAt::Token(token_text) => {
            if condition_is_omitted {
                format!("expected second `;` before `{}`", token_text)
            } else {
                format!("expected `;` before `{}`", token_text)
            }
        }
        FoundAt::EndOfFile => {
            if condition_is_omitted {
                "expected second `;` before end of file".to_string()
            } else {
                "expected `;` before end of file".to_string()
            }
        }
        FoundAt::Unknown => {
            if condition_is_omitted {
                "expected second `;` here".to_string()
            } else {
                "expected `;` here".to_string()
            }
        }
    };

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

    if let Some(span) = condition_span {
        builder = builder.secondary_label(span, "condition ends here");
    }

    let primary_span = collapse_span_to_end_on_same_line(file, error.blame_span);

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

pub(super) fn diagnostic_for_loop_header_step_invalid_start<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let for_keyword_span = error
        .related_spans
        .get(diagnostic_labels::EXPRESSION_REQUIRED_BY)
        .copied();

    let second_semicolon_span = error
        .related_spans
        .get(diagnostic_labels::EXPRESSION_EXPECTED_AFTER)
        .copied();

    let found = found_at(file, error.blame_span.end);

    let (header_text, primary_text) = match found {
        FoundAt::Token(";") => (
            "unexpected third `;` in `for` header".to_string(),
            "expected step expression or `)` after the second `;`".to_string(),
        ),
        FoundAt::Token(token_text) => (
            format!(
                "expected step expression or `)` after the second `;` in `for` header, found `{}`",
                token_text
            ),
            format!("unexpected `{}`", token_text),
        ),
        FoundAt::EndOfFile => (
            "expected step expression or `)` after the second `;` in `for` header, found end of file"
                .to_string(),
            "reached end of file here".to_string(),
        ),
        FoundAt::Unknown => (
            "expected step expression or `)` after the second `;` in `for` header".to_string(),
            "expected step expression or `)` here".to_string(),
        ),
    };

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

    let second_semicolon_label_is_shown = if let Some(span) = second_semicolon_span
        && !file.same_line(span.start, error.blame_span.end)
    {
        builder = builder.secondary_label(
            span,
            "after this `;`, a step expression or `)` was expected",
        );
        true
    } else {
        false
    };

    if !second_semicolon_label_is_shown && let Some(for_span) = for_keyword_span {
        let for_is_separated_from_error = !file.same_line(for_span.start, error.blame_span.end);
        let for_is_separated_from_second_semicolon = second_semicolon_span
            .map(|span| !file.same_line(for_span.start, span.start))
            .unwrap_or(true);

        if for_is_separated_from_error && for_is_separated_from_second_semicolon {
            builder = builder.secondary_label(for_span, "`for` loop starts here");
        }
    }

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

pub(super) fn diagnostic_for_loop_header_missing_closing_parenthesis<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let for_header_start_span = error.related_spans.get("for_header_start").copied();

    let primary_text = match found_at(file, error.blame_span.end) {
        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 `for` header");

    let primary_context_span = for_header_start_span
        .filter(|span| should_show_context_label(file, *span, error.blame_span));

    if let Some(span) = primary_context_span {
        builder = builder.secondary_label(span, "`for` header starts here");
    }

    let primary_span =
        primary_span_with_optional_multiline_context(file, primary_context_span, error.blame_span);

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

pub(super) fn diagnostic_return_value_invalid_start<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let return_keyword_span = error
        .related_spans
        .get(diagnostic_labels::EXPRESSION_REQUIRED_BY)
        .copied();

    let found = found_at(file, error.blame_span.end);

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

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

    if let Some(span) = return_keyword_span
        && !file.same_line(span.start, error.blame_span.end)
    {
        builder = builder.secondary_label(
            span,
            "after this `return`, a value expression or `;` was expected",
        );
    }

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

pub(super) fn diagnostic_break_value_invalid_start<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let break_keyword_span = error
        .related_spans
        .get(diagnostic_labels::EXPRESSION_REQUIRED_BY)
        .copied();

    let found = found_at(file, error.blame_span.end);

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

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

    if let Some(span) = break_keyword_span
        && !file.same_line(span.start, error.blame_span.end)
    {
        builder = builder.secondary_label(
            span,
            "after this `break`, a value expression or `;` was expected",
        );
    }

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

pub(super) fn diagnostic_goto_missing_label<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let goto_keyword_span = error.related_spans.get("goto_keyword").copied();

    let found = found_at(file, error.blame_span.end);

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

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

    if let Some(span) = goto_keyword_span
        && !file.same_line(span.start, error.blame_span.end)
    {
        builder = builder.secondary_label(span, "after this `goto`, a label was expected");
    }

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