use super::{Diagnostic, DiagnosticBuilder};
use crate::lexer::{TokenPosition, TokenSpan, TokenizedFile};
use crate::parser::{ParseError, ParseErrorKind};

pub(crate) fn diagnostic_from_parse_error<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    match error.kind {
        ParseErrorKind::ParenthesizedExpressionInvalidStart => {
            diagnostic_parenthesized_expression_invalid_start(error, file)
        }

        ParseErrorKind::ExpressionExpected => diagnostic_expression_expected(error, file),

        ParseErrorKind::ClassTypeMissingTypeArgument {
            left_angle_bracket_position,
        } => diagnostic_class_type_missing_type_argument(error, left_angle_bracket_position),

        ParseErrorKind::ClassTypeMissingClosingAngleBracket {
            left_angle_bracket_position,
        } => {
            diagnostic_class_type_missing_closing_angle_bracket(error, left_angle_bracket_position)
        }

        ParseErrorKind::ParenthesizedExpressionMissingClosingParenthesis => {
            diagnostic_parenthesized_expression_missing_closing_parenthesis(error, file)
        }

        ParseErrorKind::ClassTypeInvalidTypeArgument {
            left_angle_bracket_position,
        } => diagnostic_class_type_invalid_type_argument(error, left_angle_bracket_position),

        ParseErrorKind::NewTooManyArguments {
            left_parenthesis_position,
        } => diagnostic_new_too_many_arguments(error, left_parenthesis_position),

        ParseErrorKind::NewMissingClosingParenthesis {
            left_parenthesis_position,
        } => diagnostic_new_missing_closing_parenthesis(error, left_parenthesis_position),

        ParseErrorKind::NewMissingClassSpecifier {
            new_keyword_position,
        } => diagnostic_new_missing_class_specifier(error, new_keyword_position),

        _ => DiagnosticBuilder::error(format!("error {:?} while parsing", error.kind))
            .primary_label(error.covered_span, "happened here")
            .build(),
    }
}

fn diagnostic_parenthesized_expression_invalid_start<'src>(
    mut error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let (header_text, primary_text) =
        if let Some(token_text) = file.token_text(error.blame_span.end) {
            (
                format!(
                    "expected expression inside parentheses, found `{}`",
                    token_text
                ),
                format!("unexpected `{}`", token_text),
            )
        } else if file.is_eof(&error.blame_span.end) {
            (
                "expected expression, found end of file".to_string(),
                "reached end of file here".to_string(),
            )
        } else {
            (
                "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");
    };
    // It is more clear to see what happened if just the first token is
    // highlighted in case blame span never leaves the line
    if file.same_line(error.blame_span.start, error.blame_span.end) {
        error.blame_span.start = error.blame_span.end;
    }
    builder
        .primary_label(error.blame_span, primary_text)
        .code("P0001")
        .build()
}

fn diagnostic_expression_expected<'src>(
    mut 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, file.token_text(error.blame_span.end)) {
        (Some(operator_text), Some(token_text)) => (
            format!(
                "expected expression after `{}`, found `{}`",
                operator_text, token_text
            ),
            format!("unexpected `{}`", token_text),
        ),
        (Some(operator_text), None) if file.is_eof(&error.blame_span.end) => (
            format!(
                "expected expression after `{}`, found end of file",
                operator_text
            ),
            "reached end of file here".to_string(),
        ),
        (Some(operator_text), None) => (
            format!("expected expression after `{}`", operator_text),
            "expected expression".to_string(),
        ),

        (None, Some(token_text)) => (
            format!("expected expression, found `{}`", token_text),
            format!("unexpected `{}`", token_text),
        ),
        (None, None) if file.is_eof(&error.blame_span.end) => (
            "expected expression, found end of file".to_string(),
            "reached end of file here".to_string(),
        ),
        (None, None) => (
            "expected expression".to_string(),
            "expected expression".to_string(),
        ),
    };

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

    // Only need this hint if lines are different
    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()
}

fn diagnostic_parenthesized_expression_missing_closing_parenthesis<'src>(
    mut error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let left_parenthesis_span = error.related_spans.get("left_parenthesis").copied();

    let primary_text = if let Some(token_text) = file.token_text(error.blame_span.end) {
        format!("expected `)` before `{}`", token_text)
    } else if file.is_eof(&error.blame_span.end) {
        "expected `)` before end of file".to_string()
    } else {
        "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");
    }

    // On a single line, point only at the exact place where `)` was expected.
    // On multiple lines, keep the full span so the renderer can connect the
    // opening `(` to the failure point.
    if file.same_line(error.blame_span.start, error.blame_span.end) {
        error.blame_span.start = error.blame_span.end;
    }

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

fn diagnostic_class_type_missing_type_argument(
    error: ParseError,
    left_angle_bracket_position: TokenPosition,
) -> Diagnostic {
    DiagnosticBuilder::error("missing type argument in `class<...>`")
        .primary_label(error.blame_span, "expected a type name here")
        .secondary_label(
            TokenSpan::new(left_angle_bracket_position),
            "type argument list starts here",
        )
        .help("Write a type name, for example `class<Pawn>`.")
        .build()
}

fn diagnostic_class_type_missing_closing_angle_bracket(
    error: ParseError,
    left_angle_bracket_position: TokenPosition,
) -> Diagnostic {
    DiagnosticBuilder::error("missing closing `>` in `class<...>`")
        .primary_label(error.blame_span, "expected `>` here")
        .secondary_label(
            TokenSpan::new(left_angle_bracket_position),
            "this `<` starts the type argument",
        )
        .help("Add `>` to close the class type expression.")
        .build()
}

fn diagnostic_class_type_invalid_type_argument(
    error: ParseError,
    left_angle_bracket_position: TokenPosition,
) -> Diagnostic {
    DiagnosticBuilder::error("invalid type argument in `class<...>`")
        .primary_label(error.blame_span, "expected a qualified type name here")
        .secondary_label(
            TokenSpan::new(left_angle_bracket_position),
            "type argument list starts here",
        )
        .note("Only a qualified type name is accepted between `<` and `>` here.")
        .build()
}

fn diagnostic_new_too_many_arguments(
    error: ParseError,
    left_parenthesis_position: TokenPosition,
) -> Diagnostic {
    DiagnosticBuilder::error("too many arguments in `new(...)`")
        .primary_label(error.blame_span, "unexpected extra argument")
        .secondary_label(
            TokenSpan::new(left_parenthesis_position),
            "this argument list accepts at most three arguments",
        )
        .note("The three slots are `outer`, `name`, and `flags`.")
        .help("Remove the extra argument.")
        .build()
}

fn diagnostic_new_missing_closing_parenthesis(
    error: ParseError,
    left_parenthesis_position: TokenPosition,
) -> Diagnostic {
    DiagnosticBuilder::error("missing closing `)` in `new(...)`")
        .primary_label(error.blame_span, "expected `)` here")
        .secondary_label(
            TokenSpan::new(left_parenthesis_position),
            "this argument list starts here",
        )
        .help("Add `)` to close the argument list.")
        .build()
}

fn diagnostic_new_missing_class_specifier(
    error: ParseError,
    new_keyword_position: TokenPosition,
) -> Diagnostic {
    let mut builder = DiagnosticBuilder::error("missing class specifier in `new` expression")
        .primary_label(
            error.blame_span,
            "expected the class or expression to instantiate here",
        )
        .secondary_label(
            TokenSpan::new(new_keyword_position),
            "`new` expression starts here",
        )
        .help("Add the class or expression to instantiate after `new` or `new(...)`.");

    if let Some(related_span) = error.related_spans.get("blablabla") {
        builder = builder.secondary_label(*related_span, "optional `new(...)` arguments end here");
    }

    builder.build()
}
