//! Parsing for `switch (...) { ... }` expressions in Fermented UnrealScript.
//!
//! Dispatch into this module happens only after `primary.rs` has committed to
//! keyword-led `switch` syntax. That commitment is purely syntactic: `switch`
//! followed by `(` is treated as a switch expression; otherwise `switch` may
//! still be parsed as an identifier-like primary.
//!
//! This module owns parsing of the selector, body braces, `case` labels,
//! `default`, duplicate-default diagnostics, cases-after-default diagnostics,
//! and recovery for invalid top-level switch items.

use crate::arena::ArenaVec;
use crate::ast::{self, ExpressionRef, StatementList, StatementRef, SwitchCaseRef};
use crate::lexer::{Keyword, Token, TokenPosition, TokenSpan};
use crate::parser::{
    ParseError, ParseErrorKind, ParseResult, Parser, ResultRecoveryExt, SyncLevel,
};

/*
Parser structure:

switch body     = everything between `{` and `}`
switch section  = one labeled part: `case ...:` body or `default:` body
case labels     = stacked `case <expr>:` labels
section body    = statements until `case`, `default`, or `}`
preamble        = invalid statements before the first section label

parse_switch_tail
    parse_switch_header_tail
    parse_switch_sections_tail
        parse_case_section_into_state
            parse_case_labels
                expect_case_label_colon
            parse_switch_section_body
        parse_default_section_into_state
            parse_switch_section_body
        parse_invalid_switch_preamble
            parse_switch_section_body
*/

#[derive(Debug)]
struct SwitchParseState<'src, 'arena> {
    switch_keyword_position: TokenPosition,
    left_brace_position: TokenPosition,

    selector: ExpressionRef<'src, 'arena>,
    cases: ArenaVec<'arena, SwitchCaseRef<'src, 'arena>>,
    default_arm: Option<StatementList<'src, 'arena>>,

    // Retained until the full switch is parsed so diagnostics can report all
    // duplicate `default`s and all `case`s that follow the first `default`.
    default_keyword_positions: Vec<TokenPosition>,
    case_keyword_positions_after_default: Vec<TokenPosition>,

    span: TokenSpan,
}

impl<'src, 'arena> SwitchParseState<'src, 'arena> {
    #[must_use]
    fn new(
        switch_keyword_position: TokenPosition,
        left_brace_position: TokenPosition,
        selector: ExpressionRef<'src, 'arena>,
        cases: ArenaVec<'arena, SwitchCaseRef<'src, 'arena>>,
    ) -> Self {
        Self {
            switch_keyword_position,
            left_brace_position,
            selector,
            cases,
            default_arm: None,
            default_keyword_positions: Vec::new(),
            case_keyword_positions_after_default: Vec::new(),
            span: TokenSpan::new(switch_keyword_position),
        }
    }

    #[must_use]
    fn has_default(&self) -> bool {
        !self.default_keyword_positions.is_empty()
    }

    #[must_use]
    fn first_default_keyword_position(&self) -> Option<TokenPosition> {
        self.default_keyword_positions.first().copied()
    }

    #[must_use]
    fn diagnostic_context(&self) -> SwitchDiagnosticContext {
        SwitchDiagnosticContext {
            switch_keyword_position: self.switch_keyword_position,
            selector_span: *self.selector.span(),
            left_brace_position: self.left_brace_position,
        }
    }
}

/// Carries source locations reused by switch diagnostics.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
struct SwitchDiagnosticContext {
    switch_keyword_position: TokenPosition,
    selector_span: TokenSpan,
    left_brace_position: TokenPosition,
}

impl SwitchDiagnosticContext {
    #[must_use]
    fn attach_to_error(self, error: ParseError) -> ParseError {
        error
            .related_token("switch_keyword", self.switch_keyword_position)
            .related("selector", self.selector_span)
            .related_token("left_brace", self.left_brace_position)
    }

    #[must_use]
    fn attach_to_result<'src, 'arena, T>(
        self,
        result: ParseResult<'src, 'arena, T>,
    ) -> ParseResult<'src, 'arena, T> {
        result.map_err(|error| self.attach_to_error(error))
    }
}

#[must_use]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
enum SwitchSectionBodyExit {
    AtSectionBoundary,
    RecoveredAtSwitchBoundary,
}

#[must_use]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
enum SwitchSectionsExit {
    ClosedByRightBrace,
    ClosedByRecovery,
    EndOfFile,
}

impl<'src, 'arena> Parser<'src, 'arena> {
    /// Parses a `switch` expression after consuming the `switch` keyword.
    ///
    /// If the switch is closed normally, returns an [`ast::Expression::Switch`]
    /// whose span covers the construct from `switch_start_position` through the
    /// closing `}`.
    ///
    /// On errors, reports diagnostics and returns a best-effort switch node.
    #[must_use]
    pub(crate) fn parse_switch_tail(
        &mut self,
        switch_start_position: TokenPosition,
    ) -> ExpressionRef<'src, 'arena> {
        let mut state = match self.parse_switch_header_tail(switch_start_position) {
            Ok(state) => state,
            Err(switch_node) => return switch_node,
        };
        match self.parse_switch_sections_tail(&mut state) {
            SwitchSectionsExit::ClosedByRightBrace | SwitchSectionsExit::ClosedByRecovery => {
                self.report_delayed_switch_errors(&state);
                self.alloc_switch_node_from_state(state)
            }
            SwitchSectionsExit::EndOfFile => {
                let eof_position = self.peek_position_or_eof();
                state
                    .diagnostic_context()
                    .attach_to_error(
                        self.make_error_at(ParseErrorKind::SwitchMissingClosingBrace, eof_position)
                            .sync_error_at_matching_delimiter(self, state.left_brace_position),
                    )
                    .report(self);

                state.span.extend_to(self.last_consumed_position_or_start());
                self.report_delayed_switch_errors(&state);
                self.alloc_switch_node_from_state(state)
            }
        }
    }

    fn parse_switch_header_tail(
        &mut self,
        switch_start_position: TokenPosition,
    ) -> Result<SwitchParseState<'src, 'arena>, ExpressionRef<'src, 'arena>> {
        // The caller has already accepted `switch` as expression syntax,
        // so selector parsing can rely on normal expression recovery instead of
        // revalidating the legacy `switch (` disambiguation.
        let selector = self.parse_expression();
        let span = TokenSpan::new(switch_start_position).extended(selector.span().end);
        let Some(left_brace_position) = self
            .expect(Token::LeftBrace, ParseErrorKind::SwitchMissingBody)
            .related("selector", *selector.span())
            .related_token("switch_keyword", switch_start_position)
            .ok_or_report(self)
        else {
            return Err(self.alloc_switch_node(selector, self.arena.vec(), None, span));
        };

        Ok(SwitchParseState::new(
            switch_start_position,
            left_brace_position,
            selector,
            self.arena.vec(),
        ))
    }

    fn parse_switch_sections_tail(
        &mut self,
        state: &mut SwitchParseState<'src, 'arena>,
    ) -> SwitchSectionsExit {
        while let Some((token, token_position)) = self.peek_token_and_position() {
            let body_exit = match token {
                Token::RightBrace => {
                    self.advance(); // '}'
                    state.span.extend_to(token_position);
                    return SwitchSectionsExit::ClosedByRightBrace;
                }
                Token::Keyword(Keyword::Case) => {
                    self.parse_case_section_into_state(state, token_position)
                }
                Token::Keyword(Keyword::Default) => {
                    self.parse_default_section_into_state(state, token_position)
                }
                // Invalid switch-level items are parsed before being discarded
                // so ordinary statement diagnostics still run.
                _ => self.parse_invalid_switch_preamble(state.diagnostic_context(), token_position),
            };
            if body_exit == SwitchSectionBodyExit::RecoveredAtSwitchBoundary {
                state.span.extend_to(self.last_consumed_position_or_start());
                return SwitchSectionsExit::ClosedByRecovery;
            }
            // Guard against parser bugs that would otherwise leave
            // block parsing stuck on the same token.
            self.ensure_forward_progress(token_position);
        }
        SwitchSectionsExit::EndOfFile
    }

    /// Parses a `case` section and appends it to `state`.
    ///
    /// A section may have stacked `case <expr>:` labels and contains statements
    /// until the next section boundary.
    ///
    /// Returns the boundary that stopped body parsing.
    fn parse_case_section_into_state(
        &mut self,
        state: &mut SwitchParseState<'src, 'arena>,
        first_case_position: TokenPosition,
    ) -> SwitchSectionBodyExit {
        if state.has_default() {
            state
                .case_keyword_positions_after_default
                .push(first_case_position);
        }
        let switch_context = state.diagnostic_context();
        let labels = self.parse_case_labels(switch_context);
        let mut statements = self.arena.vec();
        let body_exit =
            self.parse_switch_section_body(switch_context.left_brace_position, &mut statements);
        let case_span = compute_switch_case_span(first_case_position, &labels, &statements);
        let case_node = self.arena.alloc_node(
            ast::SwitchCase {
                labels,
                body: statements,
            },
            case_span,
        );
        state.cases.push(case_node);

        body_exit
    }

    /// Parses a `default:` section into `state`.
    ///
    /// Duplicate `default` sections contribute statements to the first one so
    /// recovery preserves their bodies while diagnostics are delayed.
    ///
    /// Returns the boundary that stopped body parsing.
    fn parse_default_section_into_state(
        &mut self,
        state: &mut SwitchParseState<'src, 'arena>,
        default_position: TokenPosition,
    ) -> SwitchSectionBodyExit {
        self.advance(); // 'default'
        state.default_keyword_positions.push(default_position);
        let switch_context = state.diagnostic_context();
        let default_arm = state.default_arm.get_or_insert_with(|| self.arena.vec());
        switch_context
            .attach_to_result(
                self.expect(Token::Colon, ParseErrorKind::SwitchDefaultMissingColon)
                    .widen_error_span_from(default_position)
                    .related_token("default_keyword", default_position),
            )
            .sync_error_until(self, SyncLevel::StatementStart)
            .report_error(self);
        self.parse_switch_section_body(switch_context.left_brace_position, default_arm)
    }

    /// Parses invalid switch-level items up to the next section boundary.
    ///
    /// Parsed statements are discarded after diagnostics; they are not part of
    /// any switch arm.
    fn parse_invalid_switch_preamble(
        &mut self,
        switch_context: SwitchDiagnosticContext,
        preamble_start_position: TokenPosition,
    ) -> SwitchSectionBodyExit
    where
        'src: 'arena,
    {
        // Build the statements only to reuse normal statement diagnostics and
        // recovery; switch-level items cannot be represented in the switch AST.
        let mut discarded_statements = self.arena.vec();
        let body_exit = self.parse_switch_section_body(
            switch_context.left_brace_position,
            &mut discarded_statements,
        );
        let preamble_span = TokenSpan::range(
            preamble_start_position,
            self.last_consumed_position_or_start(),
        );
        let error = switch_context.attach_to_error(
            self.make_error_at_last_consumed(ParseErrorKind::SwitchTopLevelItemNotCase)
                .widen_error_span_from(preamble_start_position)
                .blame(preamble_span),
        );
        if discarded_statements.len() > 1 {
            error.related("multiple_items", preamble_span)
        } else {
            error.related("single_item", preamble_span)
        }
        .report(self);
        body_exit
    }

    fn parse_case_labels(
        &mut self,
        switch_context: SwitchDiagnosticContext,
    ) -> ArenaVec<'arena, ExpressionRef<'src, 'arena>> {
        let mut labels = self.arena.vec();
        while let Some((Keyword::Case, case_position)) = self.peek_keyword_and_position() {
            self.advance(); // 'case'
            let mut case_expression_span = None;
            let mut should_expect_colon = true;
            if let Some((Token::Colon, colon_position)) = self.peek_token_and_position() {
                switch_context
                    .attach_to_error(
                        self.make_error_at(
                            ParseErrorKind::SwitchCaseMissingExpression,
                            colon_position,
                        )
                        .blame_token(colon_position)
                        .related_token("case_keyword", case_position),
                    )
                    .report(self);
            } else {
                // Recover only to the label delimiter here;
                // `expect_case_label_colon` will consume it and avoid
                // a duplicate missing-colon diagnostic.
                should_expect_colon = !self.next_token_definitely_cannot_start_expression();
                let expression = switch_context
                    .attach_to_result(self.parse_required_expression(
                        ParseErrorKind::SwitchCaseExpressionInvalidStart,
                        case_position,
                    ))
                    .related_token("case_keyword", case_position)
                    .sync_error_at(self, SyncLevel::ColonDelimiter)
                    .unwrap_or_fallback(self);
                case_expression_span = Some(*expression.span());
                labels.push(expression);
            }
            // Expression recovery may still leave a valid colon to consume
            if self.peek_token() == Some(Token::Colon) {
                should_expect_colon = true;
            }
            if should_expect_colon {
                self.expect_case_label_colon(switch_context, case_position, case_expression_span);
            }
        }
        labels
    }

    fn expect_case_label_colon(
        &mut self,
        switch_context: SwitchDiagnosticContext,
        case_position: TokenPosition,
        case_expression_span: Option<TokenSpan>,
    ) {
        // If the colon is missing, skip to a statement-or-stronger boundary so
        // the damaged label is not parsed as arm body.
        let missing_colon_error = self
            .expect(Token::Colon, ParseErrorKind::SwitchCaseMissingColon)
            .widen_error_span_from(case_position)
            .related_token("case_keyword", case_position);
        let missing_colon_error = if let Some(case_expression_span) = case_expression_span {
            missing_colon_error.related("case_expression", case_expression_span)
        } else {
            missing_colon_error
        };
        switch_context
            .attach_to_result(missing_colon_error)
            .sync_error_until(self, SyncLevel::StatementStart)
            .report_error(self);
    }

    /// Parses the statements belonging to the current switch section.
    ///
    /// Returns [`SwitchSectionBodyExit::AtSectionBoundary`] when parsing stops
    /// at `case`, `default`, `}`, or end-of-file. Returns
    /// [`SwitchSectionBodyExit::RecoveredAtSwitchBoundary`] when statement
    /// recovery has to synchronize to the switch's closing delimiter.
    fn parse_switch_section_body(
        &mut self,
        left_brace_position: TokenPosition,
        statements: &mut ArenaVec<'arena, StatementRef<'src, 'arena>>,
    ) -> SwitchSectionBodyExit {
        while let Some((token, token_position)) = self.peek_token_and_position() {
            match token {
                Token::Keyword(Keyword::Case | Keyword::Default) | Token::RightBrace => {
                    return SwitchSectionBodyExit::AtSectionBoundary;
                }
                // Boundaries outside this switch are left to block-item parsing
                // so it can attach the more specific item-level diagnostic.
                _ => match self.parse_and_append_next_block_item(statements) {
                    Ok(()) => {
                        // Guard against parser bugs that would otherwise leave
                        // switch parsing stuck on the same token.
                        self.ensure_forward_progress(token_position);
                    }
                    Err(error) => {
                        // Item recovery could not find a local boundary, so
                        // recover at the switch's matching closing brace.
                        let error =
                            error.sync_error_at_matching_delimiter(self, left_brace_position);
                        let error_statement = error.fallback(self);
                        statements.push(error_statement);
                        return SwitchSectionBodyExit::RecoveredAtSwitchBoundary;
                    }
                },
            }
        }

        SwitchSectionBodyExit::AtSectionBoundary
    }

    fn report_delayed_switch_errors(&mut self, state: &SwitchParseState<'src, 'arena>) {
        self.report_duplicate_switch_defaults(state);
        self.report_switch_cases_after_default(state);
    }

    fn report_duplicate_switch_defaults(&mut self, state: &SwitchParseState<'src, 'arena>) {
        let Some((first_default_position, duplicate_positions)) =
            state.default_keyword_positions.split_first()
        else {
            return;
        };
        let Some(first_duplicate_position) = duplicate_positions.first().copied() else {
            return;
        };
        let mut error = state.diagnostic_context().attach_to_error(
            self.make_error_at(
                ParseErrorKind::SwitchDuplicateDefault,
                first_duplicate_position,
            )
            .related_token("first_default", *first_default_position),
        );
        for (index, duplicate_position) in duplicate_positions.iter().copied().enumerate() {
            error = error.related_token(
                format!("duplicate_default_{}", index + 1),
                duplicate_position,
            );
        }
        error.report(self);
    }

    fn report_switch_cases_after_default(&mut self, state: &SwitchParseState<'src, 'arena>) {
        let Some(first_default_position) = state.first_default_keyword_position() else {
            return;
        };
        let Some(first_case_position) = state.case_keyword_positions_after_default.first().copied()
        else {
            return;
        };
        let mut error = state.diagnostic_context().attach_to_error(
            self.make_error_at(ParseErrorKind::SwitchCasesAfterDefault, first_case_position)
                .related_token("first_default", first_default_position),
        );
        for (index, case_position) in state
            .case_keyword_positions_after_default
            .iter()
            .copied()
            .enumerate()
        {
            error = error.related_token(format!("case_after_default_{}", index + 1), case_position);
        }
        error.report(self);
    }

    #[must_use]
    fn alloc_switch_node(
        &self,
        selector: ExpressionRef<'src, 'arena>,
        cases: ArenaVec<'arena, SwitchCaseRef<'src, 'arena>>,
        default_arm: Option<StatementList<'src, 'arena>>,
        span: TokenSpan,
    ) -> ExpressionRef<'src, 'arena> {
        self.arena.alloc_node(
            ast::Expression::Switch {
                selector,
                cases,
                default_arm,
            },
            span,
        )
    }

    #[must_use]
    fn alloc_switch_node_from_state(
        &self,
        state: SwitchParseState<'src, 'arena>,
    ) -> ExpressionRef<'src, 'arena> {
        self.alloc_switch_node(state.selector, state.cases, state.default_arm, state.span)
    }
}

/// Computes the span of a `case` section.
///
/// The span starts at the first `case` label and extends through the section
/// body, or through the last label for an empty section.
#[must_use]
fn compute_switch_case_span(
    first_case_position: TokenPosition,
    labels: &[ExpressionRef],
    statements: &[StatementRef],
) -> TokenSpan {
    let mut span = TokenSpan::new(first_case_position);
    if let Some(last_statement) = statements.last() {
        span.extend_to(last_statement.span().end);
    } else if let Some(last_label) = labels.last() {
        span.extend_to(last_label.span().end);
    }
    span
}
