//! Parser support for expression selectors.
//!
//! Selectors are suffix forms that require an already parsed left-hand side,
//! such as member access, indexing, and calls.

use crate::ast::{self, ExpressionRef};
use crate::lexer::{self, Token, TokenPosition};
use crate::parser::{ParseErrorKind, ParseExpressionResult, Parser, ResultRecoveryExt, SyncLevel};

// Lack of `Copy` is deliberate to avoid accidental reuse of parser state.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct CallArgumentListParseState {
    /// Number of argument slots already yielded, including omitted slots.
    pub(super) parsed_argument_slot_count: usize,

    /// Whether the last yielded argument expression lacked a following
    /// separator (',' or ')' or end-of-file).
    pub(super) last_slot_missing_separator: bool,
}

impl CallArgumentListParseState {
    #[must_use]
    pub(super) fn new() -> Self {
        Self {
            parsed_argument_slot_count: 0,
            last_slot_missing_separator: false,
        }
    }
    #[must_use]
    fn has_parsed_any_argument_slots(&self) -> bool {
        self.parsed_argument_slot_count > 0
    }
}

/// Represents the result of parsing one call argument slot.
#[must_use]
#[derive(Debug, PartialEq)]
pub(super) enum ParsedArgumentSlot<'src, 'arena> {
    /// No further slots should be parsed.
    NoMoreArguments,
    /// A parsed slot. `None` represents an omitted argument.
    Argument(ast::OptionalExpression<'src, 'arena>),
}

impl<'src, 'arena> Parser<'src, 'arena> {
    /// Parses zero or more postfix selectors after `left_hand_side`.
    ///
    /// If the next token is not a selector starter, consumes nothing and
    /// returns `left_hand_side` unchanged.
    ///
    /// Each accepted selector consumes at least its leading token (`.`, `[` or
    /// `(`) before selector-specific parsing or recovery is attempted.
    pub(super) fn parse_selectors_after(
        &mut self,
        mut left_hand_side: ExpressionRef<'src, 'arena>,
    ) -> ParseExpressionResult<'src, 'arena> {
        while let Some((next_token, next_token_position)) = self.peek_token_and_position() {
            left_hand_side = match next_token {
                Token::Period => {
                    self.advance(); // '.'
                    self.parse_member_access_selector_after(left_hand_side, next_token_position)?
                }
                Token::LeftBracket => {
                    self.advance(); // '['
                    self.parse_index_selector_after(left_hand_side, next_token_position)?
                }
                Token::LeftParenthesis => {
                    self.advance(); // '('
                    self.parse_call_selector_after(left_hand_side, next_token_position)
                }
                _ => break,
            };
            self.ensure_forward_progress(next_token_position);
        }
        Ok(left_hand_side)
    }

    /// Parses a member access selector after `left_hand_side`.
    ///
    /// Expects the leading `.` to have already been consumed.
    fn parse_member_access_selector_after(
        &mut self,
        left_hand_side: ExpressionRef<'src, 'arena>,
        period_position: TokenPosition,
    ) -> ParseExpressionResult<'src, 'arena> {
        let member_access_start = left_hand_side.span().start;
        let member_name_position = self.peek_position_or_eof();
        let member_name = self
            .parse_identifier(ParseErrorKind::MemberAccessMissingMemberName)
            .blame_token(member_name_position)
            .related_token("period", period_position)?;
        let member_access_end = member_name.0;
        Ok(self.arena.alloc_node(
            ast::Expression::Member {
                target: left_hand_side,
                name: member_name,
            },
            lexer::TokenSpan::range(member_access_start, member_access_end),
        ))
    }

    /// Parses an index selector after `left_hand_side`.
    ///
    /// Expects the leading `[` to have already been consumed.
    fn parse_index_selector_after(
        &mut self,
        left_hand_side: ExpressionRef<'src, 'arena>,
        left_bracket_position: TokenPosition,
    ) -> ParseExpressionResult<'src, 'arena> {
        let index_expression = self
            .parse_required_expression_with_context(
                ParseErrorKind::IndexMissingExpression,
                left_hand_side.span().end,
                left_bracket_position,
            )
            .sync_error_at_matching_delimiter(self, left_bracket_position)?;
        let right_bracket_position = self
            .expect(
                Token::RightBracket,
                ParseErrorKind::IndexMissingClosingBracket,
            )
            .widen_error_span_from(left_bracket_position)
            .sync_error_at_matching_delimiter(self, left_bracket_position)
            .related_token("left_bracket", left_bracket_position)?;
        let expression_start = left_hand_side.span().start;
        Ok(self.arena.alloc_node_between(
            ast::Expression::Index {
                target: left_hand_side,
                index: index_expression,
            },
            expression_start,
            right_bracket_position,
        ))
    }

    /// Parses a call selector after `left_hand_side`.
    ///
    /// Expects the leading `(` to have already been consumed.
    /// Reports malformed argument lists internally and still returns
    /// a call expression.
    fn parse_call_selector_after(
        &mut self,
        left_hand_side: ExpressionRef<'src, 'arena>,
        left_parenthesis_position: TokenPosition,
    ) -> ExpressionRef<'src, 'arena> {
        let callee_end_position = left_hand_side.span().end;
        let argument_list =
            self.parse_call_argument_list(callee_end_position, left_parenthesis_position);
        let right_parenthesis_position = self
            .expect(
                Token::RightParenthesis,
                ParseErrorKind::FunctionCallMissingClosingParenthesis,
            )
            .widen_error_span_from(left_parenthesis_position)
            .sync_error_at_matching_delimiter(self, left_parenthesis_position)
            .related_token("callee", callee_end_position)
            .related_token("left_parenthesis", left_parenthesis_position)
            .unwrap_or_fallback(self);

        let expression_start = left_hand_side.span().start;
        self.arena.alloc_node_between(
            ast::Expression::Call {
                callee: left_hand_side,
                arguments: argument_list,
            },
            expression_start,
            right_parenthesis_position,
        )
    }

    /// Parses a call argument list after an already-consumed `(`.
    ///
    /// Returns all parsed argument slots, preserving omitted arguments
    /// as `None`. Does not consume the closing `)`.
    fn parse_call_argument_list(
        &mut self,
        callee_end_position: TokenPosition,
        left_parenthesis_position: TokenPosition,
    ) -> ast::ArgumentList<'src, 'arena> {
        let mut argument_list = crate::arena::ArenaVec::new_in(self.arena);
        let mut argument_list_state = CallArgumentListParseState::new();

        let mut progress_checkpoint = None;
        while let ParsedArgumentSlot::Argument(argument) =
            self.parse_next_call_argument_slot(&mut argument_list_state)
        {
            if let Some(progress_checkpoint) = progress_checkpoint {
                self.ensure_forward_progress(progress_checkpoint);
            }
            let parsed_argument_span = argument.as_ref().map(|argument| *argument.span());
            argument_list.push(argument);
            if argument_list_state.last_slot_missing_separator {
                if !self.recover_after_missing_function_call_argument_separator(
                    callee_end_position,
                    left_parenthesis_position,
                    parsed_argument_span,
                ) {
                    break;
                }
            }
            progress_checkpoint = self.peek_position();
        }
        argument_list
    }

    /// Parses the next logical call-argument slot.
    ///
    /// In UnrealScript, commas introduce follow-up argument slots, so `f(x,)`
    /// means `f(x, <omitted>)`, not a call with a tolerated trailing separator.
    ///
    /// Returns [`ParsedArgumentSlot::NoMoreArguments`] when the argument list
    /// has ended or no safe recovery can continue it.
    /// Returns [`ParsedArgumentSlot::Argument`] for a parsed slot, including
    /// omitted slots.
    ///
    /// Repeated calls with the same `state` are guaranteed to eventually return
    /// [`ParsedArgumentSlot::NoMoreArguments`], even for malformed input.
    ///
    /// Records per-slot status in `state`.
    pub(super) fn parse_next_call_argument_slot(
        &mut self,
        state: &mut CallArgumentListParseState,
    ) -> ParsedArgumentSlot<'src, 'arena> {
        state.last_slot_missing_separator = false;

        // A comma belongs to the next slot because a final comma represents an
        // omitted final argument, not a tolerated trailing separator.
        match self.peek_token() {
            None | Some(Token::RightParenthesis) => {
                return ParsedArgumentSlot::NoMoreArguments;
            }
            Some(Token::Comma) => {
                // In `f(,x)`, the leading comma both creates the omitted first
                // slot and separates it from `x`, so the first slot must not
                // consume it.
                if state.has_parsed_any_argument_slots() {
                    self.advance();
                }
                if self.is_at_call_argument_boundary() {
                    state.parsed_argument_slot_count += 1;
                    return ParsedArgumentSlot::Argument(None);
                }
            }
            _ => (),
        }

        let position_before_argument = self.peek_position_or_eof();
        let mut argument = self.parse_expression();

        let expression_recovery_made_no_progress =
            self.peek_position_or_eof() == position_before_argument;
        if expression_recovery_made_no_progress {
            self.recover_until(SyncLevel::ListSeparator);
            let list_level_recovery_made_no_progress =
                self.peek_position_or_eof() == position_before_argument;
            if list_level_recovery_made_no_progress {
                return ParsedArgumentSlot::NoMoreArguments;
            } else {
                argument
                    .span_mut()
                    .extend_to(self.last_consumed_position_or_start());
            }
        }
        state.parsed_argument_slot_count += 1;
        state.last_slot_missing_separator = !self.is_at_call_argument_boundary();
        ParsedArgumentSlot::Argument(Some(argument))
    }

    /// Reports and recovers from a missing call-argument separator.
    ///
    /// Returns whether argument-list parsing can continue at
    /// the recovered position.
    #[must_use]
    fn recover_after_missing_function_call_argument_separator(
        &mut self,
        callee_end_position: TokenPosition,
        left_parenthesis_position: TokenPosition,
        previous_argument_span: Option<lexer::TokenSpan>,
    ) -> bool {
        if self.next_token_definitely_cannot_start_expression() {
            let unexpected_token_position = self.peek_position_or_eof();
            let mut error = self
                .make_error_at(
                    ParseErrorKind::FunctionCallUnexpectedTokenInArgumentList,
                    unexpected_token_position,
                )
                .widen_error_span_from(left_parenthesis_position)
                .sync_error_until(self, SyncLevel::ListSeparator)
                .blame_token(unexpected_token_position)
                .related_token("callee", callee_end_position)
                .related_token("left_parenthesis", left_parenthesis_position);
            if let Some(previous_argument_span) = previous_argument_span {
                error = error.related("argument", previous_argument_span);
            }
            error.report(self);
            self.is_at_call_argument_boundary()
        } else {
            let next_argument_position = self.peek_position_or_eof();
            let mut error = self
                .make_error_at(
                    ParseErrorKind::FunctionCallArgumentMissingComma,
                    next_argument_position,
                )
                .blame_token(next_argument_position)
                .related_token("callee", callee_end_position)
                .related_token("left_parenthesis", left_parenthesis_position);
            debug_assert!(previous_argument_span.is_some());
            if let Some(previous_argument_span) = previous_argument_span {
                error = error.related("previous_argument", previous_argument_span);
            }
            error.report(self);
            true
        }
    }

    /// Returns whether the current token is a call-argument boundary.
    #[must_use]
    fn is_at_call_argument_boundary(&mut self) -> bool {
        matches!(
            self.peek_token(),
            None | Some(Token::Comma | Token::RightParenthesis)
        )
    }
}
