use super::*;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum RecoveryBoundary {
    CommandBoundary,
}

impl<'a> Parser<'a> {
    pub(super) fn is_recovery_separator(boundary: RecoveryBoundary, kind: TokenKind) -> bool {
        matches!(
            (boundary, kind),
            (_, TokenKind::Newline)
                | (_, TokenKind::Semicolon)
                | (_, TokenKind::Background)
                | (_, TokenKind::BackgroundPipe)
                | (_, TokenKind::BackgroundBang)
                | (_, TokenKind::And)
                | (_, TokenKind::Or)
                | (_, TokenKind::Pipe)
                | (_, TokenKind::DoubleSemicolon)
                | (_, TokenKind::SemiAmp)
                | (_, TokenKind::SemiPipe)
                | (_, TokenKind::DoubleSemiAmp)
        )
    }

    pub(super) fn recover_to_command_boundary(&mut self, failed_offset: usize) -> bool {
        let mut advanced = false;

        while let Some(kind) = self.current_token_kind {
            if Self::is_recovery_separator(RecoveryBoundary::CommandBoundary, kind) {
                while let Some(kind) = self.current_token_kind {
                    if !Self::is_recovery_separator(RecoveryBoundary::CommandBoundary, kind) {
                        break;
                    }
                    self.advance();
                    advanced = true;
                }
                break;
            }

            let before_offset = self.current_span.start.offset();
            self.advance();
            advanced = true;

            if self.current_token.is_none() {
                break;
            }

            if self.current_span.start.offset() > failed_offset
                && before_offset != self.current_span.start.offset()
            {
                continue;
            }
        }

        advanced
    }

    pub(super) fn parse_impl(&mut self) -> ParseResult {
        let file_span =
            Span::from_positions(Position::new(), Position::new().advanced_by(self.input));
        let mut stmts = Vec::new();
        let mut diagnostics = Vec::new();
        let mut terminal_error = None;

        while self.current_token.is_some() {
            let checkpoint = self.current_span.start.offset();

            if let Err(error) = self.tick() {
                diagnostics.push(self.parse_diagnostic_from_error(error.clone()));
                terminal_error.get_or_insert(error);
                break;
            }
            if let Err(error) = self.skip_newlines() {
                diagnostics.push(self.parse_diagnostic_from_error(error.clone()));
                terminal_error.get_or_insert(error);
                break;
            }
            if let Err(error) = self.check_error_token() {
                diagnostics.push(self.parse_diagnostic_from_error(error.clone()));
                let recovered = self.recover_to_command_boundary(checkpoint);
                if recovered
                    || (self.current_token.is_some()
                        && self.current_span.start.offset() < self.input.len())
                {
                    terminal_error.get_or_insert(error);
                }
                if !recovered && terminal_error.is_some() {
                    break;
                }
                continue;
            }
            if self.current_token.is_none() {
                break;
            }

            let command_start = self.current_span.start.offset();
            match self.parse_command_list_required() {
                Ok(command_stmts) => {
                    self.apply_stmt_list_effects(&command_stmts);
                    stmts.extend(command_stmts);
                }
                Err(error) => {
                    diagnostics.push(self.parse_diagnostic_from_error(error.clone()));
                    let recovered = self.recover_to_command_boundary(command_start);
                    if recovered
                        || (self.current_token.is_some()
                            && self.current_span.start.offset() < self.input.len())
                    {
                        terminal_error.get_or_insert(error);
                    }
                    if !recovered && terminal_error.is_some() {
                        break;
                    }
                }
            }
        }

        let mut file = File {
            body: Self::stmt_seq_with_span(file_span, stmts),
            span: file_span,
        };
        self.attach_comments_to_file(&mut file);

        let status = if terminal_error.is_some() {
            ParseStatus::Fatal
        } else if diagnostics.is_empty() {
            ParseStatus::Clean
        } else {
            ParseStatus::Recovered
        };

        ParseResult {
            file,
            diagnostics,
            status,
            terminal_error,
            syntax_facts: std::mem::take(&mut self.syntax_facts),
        }
    }
}
