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

const PERIOD: &str = "period";
const LEFT_BRACKET: &str = "left_bracket";
const CALLEE: &str = "callee";
const LEFT_PARENTHESIS: &str = "left_parenthesis";
const PREVIOUS_ARGUMENT: &str = "previous_argument";
const ARGUMENT: &str = "argument";

/// P0028
pub(super) fn diagnostic_member_access_missing_member_name<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let period_span = error.related_spans.get(PERIOD).copied();
    let blame_span = error.blame_span;

    let period_context_span = match period_span {
        Some(period_span) if !file.same_line(period_span.start, blame_span.end) => {
            Some(period_span)
        }
        _ => None,
    };

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

    let title = match found {
        FoundAt::Token(token_text) => {
            format!("expected member name after `.`, found `{}`", token_text)
        }
        FoundAt::EndOfFile => "expected member name after `.`, found end of file".to_string(),
        FoundAt::Unknown => "expected member name after `.`".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 member name here".to_string(),
    };

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

    if let Some(period_context_span) = period_context_span {
        builder = builder.secondary_label(
            period_context_span,
            "after this `.`, a member name was expected",
        );
    }

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

/// P0029
pub(super) fn diagnostic_index_missing_expression<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let left_bracket_span = error
        .related_spans
        .get(diagnostic_labels::EXPRESSION_EXPECTED_AFTER)
        .copied();

    let blame_span = error.blame_span;

    let left_bracket_context_span = match left_bracket_span {
        Some(left_bracket_span) if !file.same_line(left_bracket_span.start, blame_span.end) => {
            Some(left_bracket_span)
        }
        _ => None,
    };

    let primary_span = match left_bracket_context_span {
        Some(left_bracket_context_span) => TokenSpan {
            start: left_bracket_context_span.start,
            end: blame_span.end,
        },
        None => blame_span,
    };

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

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

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

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

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

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

/// P0030
pub(super) fn diagnostic_index_missing_closing_bracket<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let left_bracket_span = error.related_spans.get(LEFT_BRACKET).copied();
    let blame_span = error.blame_span;

    let left_bracket_context_span = match left_bracket_span {
        Some(left_bracket_span) if !file.same_line(left_bracket_span.start, blame_span.end) => {
            Some(left_bracket_span)
        }
        _ => None,
    };

    let primary_span = match left_bracket_context_span {
        Some(left_bracket_context_span) => TokenSpan {
            start: left_bracket_context_span.start,
            end: blame_span.end,
        },
        None => 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 index selector");

    if let Some(left_bracket_context_span) = left_bracket_context_span {
        builder = builder.secondary_label(left_bracket_context_span, "index selector starts here");
    }

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

/// P0031
pub(super) fn diagnostic_function_call_argument_missing_comma<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let callee_span = error.related_spans.get(CALLEE).copied();
    let left_parenthesis_span = error.related_spans.get(LEFT_PARENTHESIS).copied();
    let previous_argument_span = error.related_spans.get(PREVIOUS_ARGUMENT).copied();
    let blame_span = error.blame_span;

    let argument_list_context_span = match left_parenthesis_span {
        Some(left_parenthesis_span)
            if !file.same_line(left_parenthesis_span.start, blame_span.end) =>
        {
            Some(left_parenthesis_span)
        }
        _ => None,
    };

    let callee_context_span = match (callee_span, left_parenthesis_span) {
        (Some(callee_span), Some(left_parenthesis_span))
            if !file.same_line(callee_span.end, left_parenthesis_span.start)
                && !file.same_line(callee_span.end, blame_span.end) =>
        {
            Some(callee_span)
        }
        _ => None,
    };

    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 `,` between function call arguments");

    if let Some(callee_context_span) = callee_context_span {
        builder = builder.secondary_label(callee_context_span, "function called here");
    }

    if let Some(argument_list_context_span) = argument_list_context_span {
        builder = builder.secondary_label(
            argument_list_context_span,
            "function call 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(blame_span, primary_text)
        .code("P0031")
        .build()
}

/// P0032
pub(super) fn diagnostic_function_call_missing_closing_parenthesis<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let callee_span = error.related_spans.get(CALLEE).copied();
    let left_parenthesis_span = error.related_spans.get(LEFT_PARENTHESIS).copied();
    let blame_span = error.blame_span;

    let argument_list_context_span = match left_parenthesis_span {
        Some(left_parenthesis_span)
            if !file.same_line(left_parenthesis_span.start, blame_span.end) =>
        {
            Some(left_parenthesis_span)
        }
        _ => None,
    };

    let callee_context_span = match (callee_span, left_parenthesis_span) {
        (Some(callee_span), Some(left_parenthesis_span))
            if !file.same_line(callee_span.end, left_parenthesis_span.start)
                && !file.same_line(callee_span.end, blame_span.end) =>
        {
            Some(callee_span)
        }
        _ => None,
    };

    let primary_span = match argument_list_context_span {
        Some(argument_list_context_span) => TokenSpan {
            start: argument_list_context_span.start,
            end: blame_span.end,
        },
        None => 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 function call argument list");

    if let Some(callee_context_span) = callee_context_span {
        builder = builder.secondary_label(callee_context_span, "function called here");
    }

    if let Some(argument_list_context_span) = argument_list_context_span {
        builder = builder.secondary_label(
            argument_list_context_span,
            "function call argument list starts here",
        );
    }

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

/// P0033
pub(super) fn diagnostic_function_call_unexpected_token_in_argument_list<'src>(
    error: ParseError,
    file: &TokenizedFile<'src>,
) -> Diagnostic {
    let callee_span = error.related_spans.get(CALLEE).copied();
    let left_parenthesis_span = error.related_spans.get(LEFT_PARENTHESIS).copied();
    let argument_span = error
        .related_spans
        .get(ARGUMENT)
        .or_else(|| error.related_spans.get(PREVIOUS_ARGUMENT))
        .copied();
    let blame_span = error.blame_span;

    let argument_or_blame_span = match argument_span {
        Some(argument_span) => argument_span,
        None => blame_span,
    };

    let argument_list_context_span = match left_parenthesis_span {
        Some(left_parenthesis_span)
            if !file.same_line(left_parenthesis_span.start, argument_or_blame_span.end) =>
        {
            Some(left_parenthesis_span)
        }
        _ => None,
    };

    let callee_context_span = match (callee_span, left_parenthesis_span) {
        (Some(callee_span), Some(left_parenthesis_span))
            if !file.same_line(callee_span.end, left_parenthesis_span.start)
                && !file.same_line(callee_span.end, argument_or_blame_span.end) =>
        {
            Some(callee_span)
        }
        _ => None,
    };

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

    let title = match found {
        FoundAt::Token(token_text) => {
            format!("expected `,` or `)` after argument, found `{}`", token_text)
        }
        FoundAt::EndOfFile => {
            "expected `,` or `)` after argument, found end of file".to_string()
        }
        FoundAt::Unknown => "expected `,` or `)` after argument".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 `,` or `)` here".to_string(),
    };

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

    if let Some(callee_context_span) = callee_context_span {
        builder = builder.secondary_label(callee_context_span, "function called here");
    }

    if let Some(argument_list_context_span) = argument_list_context_span {
        builder = builder.secondary_label(
            argument_list_context_span,
            "function call argument list starts here",
        );
    }

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

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