//! Switch parsing for Fermented `UnrealScript`.
//!
//! Provides routines for parsing `switch (...) { ... }` expressions.
use crate::arena::ArenaVec;
use crate::ast::{ExpressionRef, StatementRef};
use crate::lexer::{Keyword, Token, TokenPosition, TokenSpan};
use crate::parser::{ParseErrorKind, ResultRecoveryExt};

impl<'src, 'arena> crate::parser::Parser<'src, 'arena> {
    /// Parses a `switch` expression after the `switch` keyword has been
    /// consumed.
    ///
    /// Returns an [`crate::ast::Expression::Switch`] whose span covers the
    /// entire construct, from `switch_start_position` to the closing `}`.
    ///
    /// Only one `default` arm is recorded. Duplicate defaults and `case` arms
    /// after a `default` are reported as errors.
    ///
    /// On premature end-of-file, reports an error 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 selector = self.parse_expression();
        let mut cases = self.arena.vec();
        let mut default_arm = None;
        let mut span = TokenSpan::new(switch_start_position);
        if self
            .expect(Token::LeftBrace, ParseErrorKind::SwitchMissingBody)
            .report_error(self)
        {
            return self.alloc_switch_node(selector, cases, default_arm, span);
        }
        while let Some((token, token_position)) = self.peek_token_and_position() {
            match token {
                Token::RightBrace => {
                    self.advance(); // '}'
                    span.extend_to(token_position);
                    return self.alloc_switch_node(selector, cases, default_arm, span);
                }
                Token::Keyword(Keyword::Case) => {
                    if default_arm.is_some() {
                        self.report_error_here(ParseErrorKind::SwitchCasesAfterDefault);
                    }
                    let case_node = self.parse_switch_case_group(token_position);
                    cases.push(case_node);
                }
                Token::Keyword(Keyword::Default) => {
                    if default_arm.is_some() {
                        self.report_error_here(ParseErrorKind::SwitchDuplicateDefault);
                    }
                    // Duplicate `default` is still parsed so that diagnostics
                    // in its body can be reported.
                    self.parse_switch_default_arm(
                        token_position,
                        default_arm.get_or_insert_with(|| self.arena.vec()),
                    );
                }
                // Items before the first arm declaration are not allowed, but
                // are parsed for basic diagnostics and simplicity.
                _ => self.parse_switch_preamble_items(token_position),
            }
            self.ensure_forward_progress(token_position);
        }
        self.report_error_here(ParseErrorKind::SwitchMissingClosingBrace);
        // This can only be `None` in the pathological case of
        // an empty token stream
        span.extend_to(
            self.last_consumed_position()
                .unwrap_or(switch_start_position),
        );
        self.alloc_switch_node(selector, cases, default_arm, span)
    }

    /// Parses a stacked `case` group and its body:
    /// `case <expr>: (case <expr>:)* <arm-body>`.
    ///
    /// Returns the allocated [`crate::ast::CaseRef`] node.
    ///
    /// The returned node span covers the entire group, from
    /// `first_case_position` to the end of the arm body, or to the end of the
    /// last label if the body is empty.
    #[must_use]
    fn parse_switch_case_group(
        &mut self,
        first_case_position: TokenPosition,
    ) -> crate::ast::SwitchCaseRef<'src, 'arena> {
        let mut labels = self.arena.vec();
        while let Some((Keyword::Case, case_position)) = self.peek_keyword_and_position() {
            self.advance(); // 'case'
            labels.push(self.parse_expression());

            // `:` is required after each case label; missing `:` is recovered
            // at statement sync level.
            self.expect(Token::Colon, ParseErrorKind::SwitchCaseMissingColon)
                .widen_error_span_from(case_position)
                .sync_error_until(self, crate::parser::SyncLevel::Statement)
                .report_error(self);
        }
        let mut body = self.arena.vec();
        self.parse_switch_arm_body(&mut body);
        let case_span = compute_case_span(first_case_position, &labels, &body);
        self.arena
            .alloc_node(crate::ast::SwitchCase { labels, body }, case_span)
    }

    /// Parses a `default:` arm and appends its statements to `statements`.
    fn parse_switch_default_arm(
        &mut self,
        default_position: TokenPosition,
        statements: &mut ArenaVec<'arena, StatementRef<'src, 'arena>>,
    ) {
        self.advance(); // 'default'
        self.expect(Token::Colon, ParseErrorKind::SwitchCaseMissingColon)
            .widen_error_span_from(default_position)
            .sync_error_until(self, crate::parser::SyncLevel::Statement)
            .report_error(self);
        self.parse_switch_arm_body(statements);
    }

    /// Parses statements of a single switch arm body.
    fn parse_switch_arm_body(
        &mut self,
        statements: &mut ArenaVec<'arena, StatementRef<'src, 'arena>>,
    ) {
        while let Some((token, token_position)) = self.peek_token_and_position() {
            match token {
                Token::Keyword(Keyword::Case | Keyword::Default) | Token::RightBrace => break,
                _ => {
                    self.parse_next_block_item_into(statements);
                    self.ensure_forward_progress(token_position);
                }
            }
        }
    }

    /// Parses items that appear before any `case` or `default` arm declaration.
    ///
    /// Such items are not allowed, but they are parsed to produce diagnostics
    /// and maintain forward progress.
    ///
    /// Parsed statements are discarded; only error reporting is preserved.
    ///
    /// Parsing stops at a boundary token or end-of-file.
    /// Boundary tokens: `case`, `default`, `}`.
    fn parse_switch_preamble_items(&mut self, preamble_start_position: TokenPosition)
    where
        'src: 'arena,
    {
        // Discard parsed statements into a sink vector.
        // This is a bit "hacky", but I don't want to adapt code to skip
        // production of AST nodes just to report errors in
        // one problematic case.
        let mut sink = self.arena.vec();
        self.parse_switch_arm_body(&mut sink);
        self.make_error_here(ParseErrorKind::SwitchTopLevelItemNotCase)
            .widen_error_span_from(preamble_start_position)
            .report_error(self);
    }

    /// Helper to allocate a `Switch` expression with the given span.
    #[must_use]
    fn alloc_switch_node(
        &self,
        selector: ExpressionRef<'src, 'arena>,
        cases: ArenaVec<'arena, crate::ast::SwitchCaseRef<'src, 'arena>>,
        default_arm: Option<ArenaVec<'arena, StatementRef<'src, 'arena>>>,
        span: TokenSpan,
    ) -> ExpressionRef<'src, 'arena> {
        self.arena.alloc_node(
            crate::ast::Expression::Switch {
                selector,
                cases,
                default_arm,
            },
            span,
        )
    }
}

/// Computes an [`AstSpan`] covering a `case` group.
///
/// The span begins at `labels_start_position` and extends to:
/// - the end of the last statement in `body`, if present; otherwise
/// - the end of the last label in `labels`, if present.
///
/// If both are empty, the span covers only `labels_start_position`.
#[must_use]
fn compute_case_span(
    labels_start_position: TokenPosition,
    labels: &[ExpressionRef],
    body: &[StatementRef],
) -> TokenSpan {
    let mut span = TokenSpan::new(labels_start_position);
    if let Some(last_statement) = body.last() {
        span.extend_to(last_statement.span().end);
    } else if let Some(last_label) = labels.last() {
        span.extend_to(last_label.span().end);
    }
    span
}
