//! Identifier parsing for Fermented UnrealScript.
//!
//! Provides shared routines for parsing plain identifiers and dot-qualified
//! identifier paths, e.g. `KFChar.ZombieClot`.

use crate::arena;
use crate::ast::{self, IdentifierToken, QualifiedIdentifierRef};
use crate::lexer::{self, Token, TokenSpan};
use crate::parser::{ParseErrorKind, ParseResult, Parser, ResultRecoveryExt, diagnostic_labels};

impl<'src, 'arena> Parser<'src, 'arena> {
    /// Parses an identifier.
    ///
    /// Consumes one identifier token and returns its position.
    /// Produces `invalid_identifier_error_kind` if the next token is missing or
    /// cannot be used as an identifier without consuming anything.
    pub(crate) fn parse_identifier(
        &mut self,
        invalid_identifier_error_kind: ParseErrorKind,
    ) -> ParseResult<'src, 'arena, IdentifierToken> {
        let (token, token_position) =
            self.require_token_and_position(invalid_identifier_error_kind)?;
        let identifier = Self::identifier_token_from_token(token, token_position)
            .ok_or_else(|| self.make_error_at(invalid_identifier_error_kind, token_position))?;
        self.advance();
        Ok(identifier)
    }

    /// Returns an [`IdentifierToken`] if `token` can be used as
    /// an identifier name.
    pub(crate) fn identifier_token_from_token(
        token: Token,
        token_position: lexer::TokenPosition,
    ) -> Option<IdentifierToken> {
        token
            .is_valid_identifier_name()
            .then_some(IdentifierToken(token_position))
    }

    /// Parses a dot-qualified identifier path.
    ///
    /// Accepts both a single identifier, such as `KFChar`,
    /// and a qualified path, such as `KFChar.ZombieClot`.
    ///
    /// Produces `invalid_identifier_error_kind` when the first identifier or a
    /// segment after `.` is missing or invalid.
    pub(crate) fn parse_qualified_identifier(
        &mut self,
        invalid_identifier_error_kind: ParseErrorKind,
    ) -> ParseResult<'src, 'arena, QualifiedIdentifierRef<'arena>> {
        let head = self.parse_identifier(invalid_identifier_error_kind)?;
        let mut tail = None;
        let mut identifier_span = TokenSpan::new(head.0);
        while let Some((Token::Period, dot_position)) = self.peek_token_and_position() {
            self.advance(); // '.'
            let Some((segment_token, segment_position)) = self.peek_token_and_position() else {
                let next_position = self.peek_position_or_eof();
                return Err(self
                    .make_error_at(invalid_identifier_error_kind, next_position)
                    .widen_error_span_from(identifier_span.start)
                    .related_token(diagnostic_labels::QUALIFIED_IDENTIFIER_DOT, dot_position));
            };
            let Some(segment) = Self::identifier_token_from_token(segment_token, segment_position)
            else {
                let invalid_segment_position = segment_position;
                let consumed_recovery_tokens = self.recover_invalid_qualified_identifier_tail();
                let diagnostic_position = if consumed_recovery_tokens {
                    self.last_consumed_position_or_start()
                } else {
                    invalid_segment_position
                };
                return Err(self
                    .make_error_at(invalid_identifier_error_kind, diagnostic_position)
                    .widen_error_span_from(identifier_span.start)
                    .blame_token(invalid_segment_position)
                    .related_token(diagnostic_labels::QUALIFIED_IDENTIFIER_DOT, dot_position));
            };
            self.advance(); // consume segment
            identifier_span.extend_to(segment.0);
            let remaining_segments_vec =
                tail.get_or_insert_with(|| arena::ArenaVec::new_in(self.arena));
            remaining_segments_vec.push(segment);
        }

        Ok(arena::ArenaNode::new_in(
            ast::QualifiedIdentifier { head, tail },
            identifier_span,
            self.arena,
        ))
    }

    /// Recovers from an invalid segment in a qualified identifier tail.
    ///
    /// Recovery skips only additional `.segment` suffixes after
    /// the invalid token. Returns whether any recovery token was consumed.
    fn recover_invalid_qualified_identifier_tail(&mut self) -> bool {
        let mut consumed_any = false;
        while self.eat(Token::Period) {
            consumed_any = true;
            if self
                .peek_token()
                .is_some_and(|token| token.is_valid_identifier_name())
            {
                self.advance();
                consumed_any = true;
            }
        }
        consumed_any
    }
}
