//! Parser for `new` expressions in Fermented UnrealScript.

use super::super::selectors::{CallArgumentListParseState, ParsedArgumentSlot};
use crate::ast::{Expression, ExpressionRef, OptionalExpression};
use crate::lexer::{Token, TokenPosition, TokenSpan};
use crate::parser::{ParseErrorKind, Parser, ResultRecoveryExt, SyncLevel};

/// Determines how parsing of the class specifier proceeds after the optional
/// `new(...)` argument list has been parsed or recovered.
#[must_use]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
enum NewClassSpecifierParseAction {
    Parse,
    Skip,
}

/// Stores the parsed `new(...)` arguments and the action to take for
/// class-specifier parsing.
#[derive(Debug)]
struct NewArgumentListParseResult<'src, 'arena> {
    outer_argument: OptionalExpression<'src, 'arena>,
    name_argument: OptionalExpression<'src, 'arena>,
    flags_argument: OptionalExpression<'src, 'arena>,
    class_specifier_parse_action: NewClassSpecifierParseAction,
}

impl<'src, 'arena> NewArgumentListParseResult<'src, 'arena> {
    /// Returns the parse result for `new` without an argument list.
    fn without_argument_list() -> Self {
        Self {
            outer_argument: None,
            name_argument: None,
            flags_argument: None,
            class_specifier_parse_action: NewClassSpecifierParseAction::Parse,
        }
    }
}

/// Holds shared state for parsing the optional `new(...)` argument list.
#[derive(Debug)]
struct NewArgumentListParseState<'src, 'arena> {
    call_argument_list_parse_state: CallArgumentListParseState,

    new_keyword_position: TokenPosition,
    left_parenthesis_position: TokenPosition,

    outer_argument: OptionalExpression<'src, 'arena>,
    name_argument: OptionalExpression<'src, 'arena>,
    flags_argument: OptionalExpression<'src, 'arena>,
}

impl<'src, 'arena> NewArgumentListParseState<'src, 'arena> {
    /// Stores an argument in the current `new` argument slot.
    fn store_argument_in_current_slot(&mut self, argument: OptionalExpression<'src, 'arena>) {
        match self.call_argument_list_parse_state.parsed_argument_slot_count {
            1 => self.outer_argument = argument,
            2 => self.name_argument = argument,
            3 => self.flags_argument = argument,
            _ => unreachable!("this method cannot be called after parsing three arguments"),
        }
    }

    /// Returns the span of the argument in the current parsed slot.
    ///
    /// Assumes the current slot has already been stored.
    #[must_use]
    fn current_argument_span(&self) -> Option<TokenSpan> {
        debug_assert!(
            (1..=3).contains(&self.call_argument_list_parse_state.parsed_argument_slot_count),
            "parsed_slot_count out of range in new-argument parser"
        );
        match self.call_argument_list_parse_state.parsed_argument_slot_count {
            1 => &self.outer_argument,
            2 => &self.name_argument,
            3 => &self.flags_argument,
            // Diagnostics can fall back to a missing span here.
            _ => return None,
        }
        .as_ref()
        .map(|argument| *argument.span())
    }

    /// Returns the span of the last parsed non-empty `new` argument.
    #[must_use]
    fn last_parsed_allowed_argument_span(&self) -> Option<TokenSpan> {
        [
            &self.flags_argument,
            &self.name_argument,
            &self.outer_argument,
        ]
        .into_iter()
        .find_map(|slot| slot.as_ref().map(|argument| *argument.span()))
    }

    /// Finishes argument-list parsing and returns the collected result.
    #[must_use]
    fn into_result(
        self,
        class_specifier_parse_action: NewClassSpecifierParseAction,
    ) -> NewArgumentListParseResult<'src, 'arena> {
        NewArgumentListParseResult {
            outer_argument: self.outer_argument,
            name_argument: self.name_argument,
            flags_argument: self.flags_argument,
            class_specifier_parse_action,
        }
    }
}

impl<'src, 'arena> Parser<'src, 'arena> {
    /// Parses a `new` expression.
    ///
    /// Assumes the `new` keyword has already been consumed. Parses an optional
    /// parenthesized argument list before the required class specifier.
    #[must_use]
    pub(super) fn parse_new_expression_tail(
        &mut self,
        new_keyword_position: TokenPosition,
    ) -> ExpressionRef<'src, 'arena> {
        let mut argument_list_end_position = None;
        let NewArgumentListParseResult {
            outer_argument,
            name_argument,
            flags_argument,
            class_specifier_parse_action,
        } = if let Some(left_parenthesis_position) = self.eat_with_position(Token::LeftParenthesis)
        {
            let parsed_argument_list =
                self.parse_new_argument_list_tail(new_keyword_position, left_parenthesis_position);
            argument_list_end_position = self.last_consumed_position();
            parsed_argument_list
        } else {
            NewArgumentListParseResult::without_argument_list()
        };

        let class_specifier = self.parse_new_class_specifier(
            new_keyword_position,
            argument_list_end_position,
            class_specifier_parse_action,
        );
        let class_specifier_end_position = class_specifier.span().end;
        self.arena.alloc_node_between(
            Expression::New {
                outer_argument,
                name_argument,
                flags_argument,
                class_specifier,
            },
            new_keyword_position,
            class_specifier_end_position,
        )
    }

    /// Parses the parenthesized argument list of a `new` expression.
    ///
    /// Assumes the opening `(` has already been consumed.
    #[must_use]
    fn parse_new_argument_list_tail(
        &mut self,
        new_keyword_position: TokenPosition,
        left_parenthesis_position: TokenPosition,
    ) -> NewArgumentListParseResult<'src, 'arena> {
        let mut state = NewArgumentListParseState {
            new_keyword_position,
            left_parenthesis_position,
            outer_argument: None,
            name_argument: None,
            flags_argument: None,
            call_argument_list_parse_state: CallArgumentListParseState::new(),
        };
        if let Some(class_specifier_parse_action) = self.parse_new_argument_slots(&mut state) {
            return state.into_result(class_specifier_parse_action);
        }
        self.diagnose_extra_new_arguments(&mut state);
        let class_specifier_parse_action = if self.eat(Token::RightParenthesis) {
            NewClassSpecifierParseAction::Parse
        } else {
            self.recover_from_missing_new_closing_parenthesis(&state)
        };
        state.into_result(class_specifier_parse_action)
    }

    /// Parses up to three positional `new` arguments.
    ///
    /// Returns [`Some`] only when recovery determines whether
    /// the class specifier should be parsed or skipped.
    #[must_use]
    fn parse_new_argument_slots(
        &mut self,
        state: &mut NewArgumentListParseState<'src, 'arena>,
    ) -> Option<NewClassSpecifierParseAction> {
        // Only successful slot parses continue the loop,
        // so each iteration makes progress.
        while state.call_argument_list_parse_state.parsed_argument_slot_count < 3
            && let ParsedArgumentSlot::Argument(argument) =
                self.parse_next_call_argument_slot(&mut state.call_argument_list_parse_state)
        {
            // On `ParsedCallArgumentSlot::Argument(_)`,
            // `parse_call_argument_slot` increases `parsed_slot_count` by 1,
            // so it is now in `1`, `2` or `3`, guaranteeing that this call
            // is valid and won't hit `unreachable!`.
            state.store_argument_in_current_slot(argument);

            if state
                .call_argument_list_parse_state
                .last_slot_missing_separator
            {
                if let Some(class_specifier_parse_action) =
                    self.recover_from_missing_new_argument_separator(state)
                {
                    return Some(class_specifier_parse_action);
                }
            }
        }
        None
    }

    /// Recovers from a missing separator between `new` arguments.
    ///
    /// Returns [`Some`] when recovery instead treats the boundary as the end of
    /// the argument list.
    fn recover_from_missing_new_argument_separator(
        &mut self,
        state: &mut NewArgumentListParseState<'src, 'arena>,
    ) -> Option<NewClassSpecifierParseAction> {
        let has_parsed_all_allowed_arguments =
            state.call_argument_list_parse_state.parsed_argument_slot_count >= 3;
        let likely_missing_comma = !self.next_token_definitely_cannot_start_expression()
            && !has_parsed_all_allowed_arguments;
        if likely_missing_comma {
            let next_argument_position = self.peek_position_or_eof();
            let mut error = self
                .make_error_at_last_consumed(ParseErrorKind::NewArgumentMissingComma)
                .widen_error_span_from(state.left_parenthesis_position)
                .blame_token(next_argument_position)
                .related_token("new_keyword", state.new_keyword_position)
                .related_token("left_parenthesis", state.left_parenthesis_position);
            if let Some(argument_span) = state.current_argument_span() {
                error = error.related("previous_argument", argument_span);
            }
            error.report_error(self);
            None
        } else {
            // If this does not look like another argument,
            // treat the boundary as the missing `)` rather than inventing
            // an extra comma diagnostic.
            Some(self.recover_from_missing_new_closing_parenthesis(state))
        }
    }

    /// Diagnoses and skips any arguments beyond the three accepted by `new`.
    fn diagnose_extra_new_arguments(
        &mut self,
        state: &mut NewArgumentListParseState<'src, 'arena>,
    ) {
        // Require an explicit comma before diagnosing extra arguments so this
        // does not mask a missing `)`.
        if let Some((Token::Comma, extra_argument_comma_position)) = self.peek_token_and_position()
        {
            // Preserve the first extra argument span for a more precise
            // diagnostic before we do any syncing.
            let first_extra_argument_span =
                match self.parse_next_call_argument_slot(&mut state.call_argument_list_parse_state) {
                    ParsedArgumentSlot::Argument(Some(argument)) => Some(*argument.span()),
                    ParsedArgumentSlot::Argument(None) => None,
                    ParsedArgumentSlot::NoMoreArguments => None,
                };
            let mut error = self
                .make_error_at_last_consumed(ParseErrorKind::NewTooManyArguments)
                .widen_error_span_from(state.left_parenthesis_position)
                .blame_token(extra_argument_comma_position)
                .related_token("new_keyword", state.new_keyword_position)
                .related_token("left_parenthesis", state.left_parenthesis_position);
            if let Some(span) = state.last_parsed_allowed_argument_span() {
                error = error.related("last_allowed_argument", span);
            }
            if let Some(span) = first_extra_argument_span {
                error = error.related("first_extra_argument", span);
            }
            error
                .sync_error_until_matching_delimiter(self, state.left_parenthesis_position)
                .report_error(self);
        }
    }

    /// Recovers from a missing closing `)` in a `new(...)` argument list.
    ///
    /// Returns whether class-specifier parsing should continue after recovery.
    #[must_use]
    fn recover_from_missing_new_closing_parenthesis(
        &mut self,
        state: &NewArgumentListParseState<'src, 'arena>,
    ) -> NewClassSpecifierParseAction {
        self.make_error_at_last_consumed(ParseErrorKind::NewMissingClosingParenthesis)
            .widen_error_span_from(state.left_parenthesis_position)
            .blame_token(self.peek_position_or_eof())
            .related_token("new_keyword", state.new_keyword_position)
            .related_token("left_parenthesis", state.left_parenthesis_position)
            .report(self);
        // Missing-delimiter recovery normally syncs to the matching `)`.
        // `new(...) ClassName` is an exception: after a missing `)`, the next
        // expression may already be the class specifier, not another argument.
        let matching_right_parenthesis_ahead = self
            .file
            .matching_delimiter(state.left_parenthesis_position)
            .is_some_and(|right_parenthesis_position| {
                self.peek_position_or_eof() <= right_parenthesis_position
            });
        if matching_right_parenthesis_ahead {
            self.recover_at_matching_delimiter_or_sync(state.left_parenthesis_position);
            // After syncing through the matched `)`, the argument-list error is
            // contained, so class-specifier parsing can proceed normally.
            return NewClassSpecifierParseAction::Parse;
        }
        if self.next_token_definitely_cannot_start_expression() {
            // There is no plausible class specifier to parse, so skip it to
            // avoid error cascade.
            NewClassSpecifierParseAction::Skip
        } else {
            NewClassSpecifierParseAction::Parse
        }
    }

    /// Parses the class specifier of a `new` expression after argument-list
    /// parsing and recovery.
    #[must_use]
    fn parse_new_class_specifier(
        &mut self,
        new_keyword_position: TokenPosition,
        argument_list_end: Option<TokenPosition>,
        class_specifier_parse_action: NewClassSpecifierParseAction,
    ) -> ExpressionRef<'src, 'arena> {
        match class_specifier_parse_action {
            NewClassSpecifierParseAction::Parse
                if self.next_token_definitely_cannot_start_expression() =>
            {
                let mut error = self
                    .make_error_at_last_consumed(ParseErrorKind::NewMissingClassSpecifier)
                    .widen_error_span_from(new_keyword_position)
                    .sync_error_until(self, SyncLevel::ExpressionStart)
                    .extend_blame_to_next_token(self)
                    .related_token("new_keyword", new_keyword_position);
                if let Some(argument_list_end) = argument_list_end {
                    error = error.related_token("argument_list_end", argument_list_end);
                }
                return self.report_error_with_fallback(error);
            }
            NewClassSpecifierParseAction::Parse => self.parse_expression(),
            NewClassSpecifierParseAction::Skip => {
                let error_position = self.peek_position_or_eof();
                self.make_error_expression_at(error_position)
            }
        }
    }
}
