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

pub(super) fn diagnostic_parenthesized_expression_invalid_start<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let (header_text, primary_text) = match found_at(file, error.blame_span.end) {
        FoundAt::Token(token_text) => (
            format!(
                "expected expression inside parentheses, found `{}`",
                token_text
            ),
            format!("unexpected `{}`", token_text),
        ),
        FoundAt::EndOfFile => (
            "expected expression, found end of file".to_string(),
            "reached end of file here".to_string(),
        ),
        FoundAt::Unknown => (
            "expected expression inside parentheses".to_string(),
            "expected expression".to_string(),
        ),
    };

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

    if let Some(related_span) = error.related_spans.get("left_parenthesis")
        && !file.same_line(related_span.start, error.blame_span.end)
    {
        builder = builder.secondary_label(*related_span, "parenthesized expression starts here");
    }

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

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

pub(super) fn diagnostic_expression_expected<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let prefix_operator_span = error.related_spans.get("prefix_operator").copied();
    let infix_operator_span = error.related_spans.get("infix_operator").copied();
    let operator_span = infix_operator_span.or(prefix_operator_span);

    let operator_text = operator_span.and_then(|span| file.token_text(span.end));

    let (header_text, primary_text) = match (operator_text, found_at(file, error.blame_span.end)) {
        (Some(operator_text), FoundAt::Token(token_text)) => (
            format!(
                "expected expression after `{}`, found `{}`",
                operator_text, token_text
            ),
            format!("unexpected `{}`", token_text),
        ),
        (Some(operator_text), FoundAt::EndOfFile) => (
            format!(
                "expected expression after `{}`, found end of file",
                operator_text
            ),
            "reached end of file here".to_string(),
        ),
        (Some(operator_text), FoundAt::Unknown) => (
            format!("expected expression after `{}`", operator_text),
            "expected expression".to_string(),
        ),

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

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

    if let Some(span) = operator_span
        && !file.same_line(span.start, error.blame_span.end)
    {
        let secondary_text = if let Some(operator_text) = operator_text {
            format!("after this `{}`, an expression was expected", operator_text)
        } else {
            "an expression was expected after this operator".to_string()
        };

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

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

pub(super) fn diagnostic_parenthesized_expression_missing_closing_parenthesis<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let left_parenthesis_span = error.related_spans.get("left_parenthesis").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 parenthesized expression");

    if let Some(span) = left_parenthesis_span
        && !file.same_line(span.start, error.blame_span.end)
    {
        builder = builder.secondary_label(span, "parenthesized expression starts here");
    }

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

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

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

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

    let mut builder = DiagnosticBuilder::error("missing type argument in `class<...>`");

    if let Some(span) = left_angle_bracket_span
        && !file.same_line(span.start, error.blame_span.end)
    {
        builder = builder.secondary_label(span, "type argument starts here");
    }

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

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

pub(super) fn diagnostic_class_type_expected_qualified_type_name<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let qualifier_dot_span = error
        .related_spans
        .get(diagnostic_labels::QUALIFIED_IDENTIFIER_DOT)
        .copied();
    let class_span = error.related_spans.get("class_keyword").copied();

    let blame_pos = error.blame_span.end;

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

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

    if let Some(dot_span) = qualifier_dot_span {
        if !file.same_line(dot_span.start, blame_pos) {
            builder = builder.secondary_label(
                dot_span,
                "after this `.`, another type segment was expected",
            );
        }

        if let Some(class_span) = class_span
            && !file.same_line(class_span.end, dot_span.start)
        {
            builder = builder.secondary_label(
                class_span,
                "while parsing this `class<...>` type expression",
            );
        }
    }

    let primary_span = TokenSpan {
        start: blame_pos,
        end: blame_pos,
    };

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

pub(super) fn diagnostic_class_type_invalid_start<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let left_angle_bracket_span = error.related_spans.get("left_angle_bracket").copied();
    let class_keyword_span = error.related_spans.get("class_keyword").copied();

    let (header_text, primary_text) = match found_at(file, error.blame_span.end) {
        FoundAt::Token(token_text) => (
            format!(
                "expected a type argument after `<` in `class<...>`, found `{}`",
                token_text
            ),
            format!("unexpected `{}`", token_text),
        ),
        FoundAt::EndOfFile => (
            "expected a type argument after `<` in `class<...>`, found end of file".to_string(),
            "reached end of file here".to_string(),
        ),
        FoundAt::Unknown => (
            "expected a type argument after `<` in `class<...>`".to_string(),
            "expected a type argument here".to_string(),
        ),
    };

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

    if let Some(left_angle_span) = left_angle_bracket_span {
        if !file.same_line(left_angle_span.start, error.blame_span.end) {
            builder = builder.secondary_label(left_angle_span, "type argument starts here");
        }

        if let Some(class_span) = class_keyword_span
            && !file.same_line(class_span.end, left_angle_span.start)
        {
            builder = builder.secondary_label(
                class_span,
                "while parsing this `class<...>` type expression",
            );
        }
    }

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

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

pub(super) fn diagnostic_class_type_missing_closing_angle_bracket<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let left_angle_bracket_span = error.related_spans.get("left_angle_bracket").copied();
    let class_keyword_span = error.related_spans.get("class_keyword").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 `class<...>`");

    if let Some(left_angle_bracket_span) = left_angle_bracket_span {
        if !file.same_line(left_angle_bracket_span.start, error.blame_span.end) {
            builder = builder.secondary_label(left_angle_bracket_span, "type argument starts here");
        }

        if let Some(class_keyword_span) = class_keyword_span
            && !file.same_line(class_keyword_span.end, left_angle_bracket_span.start)
        {
            builder = builder.secondary_label(
                class_keyword_span,
                "while parsing this `class<...>` type expression",
            );
        }
    }

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

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

pub(super) fn diagnostic_new_missing_class_specifier<'src>(
    mut error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let new_keyword_span = error.related_spans.get("new_keyword").copied();
    let argument_list_end_span = error.related_spans.get("argument_list_end").copied();

    let construct_text = if argument_list_end_span.is_some() {
        "`new(...)`"
    } else {
        "`new`"
    };

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

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

    if let Some(new_keyword_span) = new_keyword_span
        && !file.same_line(new_keyword_span.start, error.blame_span.end)
    {
        builder = builder.secondary_label(new_keyword_span, "`new` expression starts here");
    }

    match argument_list_end_span {
        Some(argument_list_end_span)
            if !file.same_line(argument_list_end_span.start, error.blame_span.end) =>
        {
            builder = builder.secondary_label(
                argument_list_end_span,
                "optional `new(...)` arguments end here",
            );
            error.blame_span.start = argument_list_end_span.start;
        }

        Some(_) | None => {
            error.blame_span.start = error.blame_span.end;
        }
    }

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

pub(super) fn diagnostic_new_too_many_arguments<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let new_keyword_span = error.related_spans.get("new_keyword").copied();
    let left_parenthesis_span = error.related_spans.get("left_parenthesis").copied();
    let last_allowed_argument_span = error.related_spans.get("last_allowed_argument").copied();
    let first_extra_argument_span = error.related_spans.get("first_extra_argument").copied();

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

    let (primary_text, mut primary_span) =
        if let Some(first_extra_argument_span) = first_extra_argument_span {
            (
                "a fourth argument is not allowed in `new(...)`".to_string(),
                first_extra_argument_span,
            )
        } else if matches!(found, FoundAt::Token(",")) {
            (
                "this `,` starts a fourth argument, which is not allowed here".to_string(),
                error.blame_span,
            )
        } else if matches!(found, FoundAt::EndOfFile) {
            (
                "a fourth argument is not allowed here".to_string(),
                error.blame_span,
            )
        } else if let FoundAt::Token(token_text) = found {
            (
                format!("unexpected start of a fourth argument: `{}`", token_text),
                error.blame_span,
            )
        } else {
            (
                "a fourth argument is not allowed here".to_string(),
                error.blame_span,
            )
        };

    let mut builder = DiagnosticBuilder::error("too many arguments in `new(...)`");

    if let Some(new_keyword_span) = new_keyword_span {
        let show_new_label = !file.same_line(new_keyword_span.start, primary_span.end)
            && match left_parenthesis_span {
                Some(left_parenthesis_span) => {
                    !file.same_line(new_keyword_span.start, left_parenthesis_span.start)
                }
                None => true,
            };

        if show_new_label {
            builder = builder.secondary_label(new_keyword_span, "`new` expression starts here");
        }
    }

    if let Some(left_parenthesis_span) = left_parenthesis_span
        && !file.same_line(left_parenthesis_span.start, primary_span.end)
    {
        builder = builder.secondary_label(
            left_parenthesis_span,
            "`new(...)` argument list starts here",
        );
    }

    if let Some(last_allowed_argument_span) = last_allowed_argument_span
        && !file.same_line(last_allowed_argument_span.start, primary_span.end)
    {
        builder = builder.secondary_label(
            last_allowed_argument_span,
            "the third allowed argument ends here",
        );

        primary_span.start = last_allowed_argument_span.start;
    }

    builder
        .primary_label(primary_span, primary_text)
        .note("`new(...)` accepts up to three optional arguments: `outer`, `name`, and `flags`.")
        .code("P0009")
        .build()
}

pub(super) fn diagnostic_new_missing_closing_parenthesis<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let left_parenthesis_span = error.related_spans.get("left_parenthesis").copied();
    let new_keyword_span = error.related_spans.get("new_keyword").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 `new(...)` argument list");

    if let Some(left_parenthesis_span) = left_parenthesis_span {
        if !file.same_line(left_parenthesis_span.start, error.blame_span.end) {
            if let Some(new_keyword_span) = new_keyword_span
                && !file.same_line(new_keyword_span.end, left_parenthesis_span.start)
                && !file.same_line(new_keyword_span.start, error.blame_span.end)
            {
                builder = builder.secondary_label(new_keyword_span, "`new` expression starts here");
            }

            builder = builder.secondary_label(
                left_parenthesis_span,
                "`new(...)` argument list starts here",
            );
        }
    }

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

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

pub(super) fn diagnostic_new_argument_missing_comma<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let new_keyword_span = error.related_spans.get("new_keyword").copied();
    let left_parenthesis_span = error.related_spans.get("left_parenthesis").copied();
    let previous_argument_span = error.related_spans.get("previous_argument").copied();

    let primary_span = TokenSpan::new(error.blame_span.end);

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

    let mut builder = DiagnosticBuilder::error("missing `,` between `new(...)` arguments");

    if let Some(new_keyword_span) = new_keyword_span {
        let show_new_label = !file.same_line(new_keyword_span.start, primary_span.end)
            && match left_parenthesis_span {
                Some(left_parenthesis_span) => {
                    !file.same_line(new_keyword_span.start, left_parenthesis_span.start)
                }
                None => true,
            };

        if show_new_label {
            builder = builder.secondary_label(new_keyword_span, "`new` expression starts here");
        }
    }

    if let Some(left_parenthesis_span) = left_parenthesis_span
        && !file.same_line(left_parenthesis_span.start, primary_span.end)
    {
        builder = builder.secondary_label(
            left_parenthesis_span,
            "`new(...)` argument list starts here",
        );
    }

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

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