use super::*;

fn arithmetic_command_is_followed_by_inline_branch_keyword(span: Span, source: &str) -> bool {
    let Some(after) = source.get(span.end.offset().min(source.len())..) else {
        return false;
    };
    let trimmed = after.trim_start_matches([' ', '\t', '\r']);
    let after_separator = trimmed
        .strip_prefix(';')
        .map(|rest| rest.trim_start_matches([' ', '\t', '\r']))
        .unwrap_or(trimmed);

    shell_keyword_prefix(after_separator, "then") || shell_keyword_prefix(after_separator, "do")
}

fn shell_keyword_prefix(text: &str, keyword: &str) -> bool {
    let Some(rest) = text.strip_prefix(keyword) else {
        return false;
    };
    rest.chars()
        .next()
        .is_none_or(|ch| matches!(ch, ' ' | '\t' | '\r' | '\n' | ';' | '&' | '|'))
}

fn trim_final_line_ending(text: &mut String) {
    if text.ends_with("\r\n") {
        text.truncate(text.len().saturating_sub(2));
    } else if text.ends_with('\n') {
        text.truncate(text.len().saturating_sub(1));
    }
}

fn loop_condition_has_multiple_commands(condition: &StmtSeq) -> bool {
    condition.len() > 1
}

fn time_inner_stmt_needs_trailing_comment(stmt: &Stmt) -> bool {
    match &stmt.command {
        Command::Simple(_)
        | Command::Builtin(_)
        | Command::Decl(_)
        | Command::Compound(CompoundCommand::Arithmetic(_) | CompoundCommand::Conditional(_)) => {
            true
        }
        Command::Binary(command) => time_inner_stmt_needs_trailing_comment(&command.right),
        _ => false,
    }
}

impl<'source, 'facts, S> ShellRenderer<'source, 'facts, S>
where
    S: StreamSink,
{
    pub(super) fn format_if(&mut self, command: &IfCommand) -> Result<()> {
        match command.syntax {
            IfSyntax::ThenFi { .. } => self.format_then_fi_if(command),
            IfSyntax::Brace { .. } => self.format_brace_if(command),
        }
    }

    pub(super) fn format_then_fi_if(&mut self, command: &IfCommand) -> Result<()> {
        let plan = IfLayoutPlan::then_fi(command, self.render_context());
        let IfLayoutPlan {
            then_span,
            fi_span,
            style,
        } = plan;

        match style {
            IfLayoutStyle::RawGroupedCondition { raw_condition } => {
                self.format_raw_grouped_then_fi_if(command, fi_span, &raw_condition)
            }
            IfLayoutStyle::SplitCondition => {
                self.format_split_condition_then_fi_if(command, then_span, fi_span)
            }
            layout => self.format_inline_condition_then_fi_if(command, then_span, fi_span, layout),
        }
    }

    pub(super) fn format_raw_grouped_then_fi_if(
        &mut self,
        command: &IfCommand,
        fi_span: Span,
        raw_condition: &str,
    ) -> Result<()> {
        let source = self.source();
        let fi_upper_bound = fi_span.start.offset();
        self.write_text("if");
        self.write_text(raw_condition);
        self.write_text("then");
        let then_upper_bound =
            if_branch_upper_bound(command, 0, source, self.source_map(), self.facts());
        let then_site =
            CompoundBodySite::if_then_branch(command, &command.then_branch, then_upper_bound);
        self.format_if_branch_body_site(then_site)?;
        if let Some(body) = &command.else_branch {
            if self.if_next_branch_has_blank_line_before_keyword(command, 0, source) {
                self.newline();
            }
            self.newline();
            self.write_text("else");
            let site = CompoundBodySite::if_else_branch(command, body, fi_upper_bound);
            self.format_if_branch_body_site(site)?;
        }
        self.finish_multiline_if_close(command, then_upper_bound, fi_span);
        Ok(())
    }

    pub(super) fn format_split_condition_then_fi_if(
        &mut self,
        command: &IfCommand,
        then_span: Span,
        fi_span: Span,
    ) -> Result<()> {
        let source = self.source();
        let fi_upper_bound = fi_span.start.offset();
        self.write_text("if");
        self.newline();
        self.with_indent(|formatter| {
            formatter.format_stmt_sequence(&command.condition, Some(then_span.start.offset()))
        })?;
        self.newline();
        self.write_text("then");
        let then_upper_bound =
            if_branch_upper_bound(command, 0, source, self.source_map(), self.facts());
        let then_site =
            CompoundBodySite::if_then_branch(command, &command.then_branch, then_upper_bound);
        self.format_if_branch_body_site(then_site)?;
        for (index, (condition, body)) in command.elif_branches.iter().enumerate() {
            self.write_if_branch_prefix(command, index, source);
            self.write_elif_header(command, index, condition, body, source)?;
            let body_upper_bound =
                if_branch_upper_bound(command, index + 1, source, self.source_map(), self.facts());
            let site = CompoundBodySite::if_then_branch(command, body, body_upper_bound);
            self.format_if_branch_body_site(site)?;
        }
        if let Some(body) = &command.else_branch {
            self.write_if_branch_prefix(command, command.elif_branches.len(), source);
            self.write_text("else");
            let site = CompoundBodySite::if_else_branch(command, body, fi_upper_bound);
            self.format_if_branch_body_site(site)?;
        }
        self.finish_multiline_if_close(command, then_upper_bound, fi_span);
        Ok(())
    }

    pub(super) fn format_inline_condition_then_fi_if(
        &mut self,
        command: &IfCommand,
        then_span: Span,
        fi_span: Span,
        layout: IfLayoutStyle,
    ) -> Result<()> {
        self.write_text("if ");
        self.format_inline_stmts(&command.condition)?;
        let then_separator = self.then_separator_for_condition(&command.condition);

        match layout {
            IfLayoutStyle::Inline(InlineIfLayout::Then) => {
                self.write_text(then_separator);
                self.write_space();
                self.format_inline_stmts(&command.then_branch)?;
                self.write_if_close("; fi", fi_span);
                Ok(())
            }
            IfLayoutStyle::Inline(InlineIfLayout::ThenElse) => {
                let Some(else_branch) = command.else_branch.as_ref() else {
                    unreachable!("inline then/else layout requires an else branch");
                };
                self.write_text(then_separator);
                self.write_space();
                self.format_inline_stmts(&command.then_branch)?;
                self.write_text("; else ");
                self.format_inline_stmts(else_branch)?;
                self.write_if_close("; fi", fi_span);
                Ok(())
            }
            IfLayoutStyle::Inline(InlineIfLayout::ThenMultilineElse) => {
                let Some(else_branch) = command.else_branch.as_ref() else {
                    unreachable!("inline then/multiline else layout requires an else branch");
                };
                self.write_text(then_separator);
                self.write_space();
                self.format_inline_stmts(&command.then_branch)?;
                self.write_text("; else");
                let site =
                    CompoundBodySite::if_else_branch(command, else_branch, fi_span.start.offset());
                self.format_if_branch_body_site(site)?;
                let then_upper_bound = if_branch_upper_bound(
                    command,
                    0,
                    self.source(),
                    self.source_map(),
                    self.facts(),
                );
                self.finish_multiline_if_close(command, then_upper_bound, fi_span);
                Ok(())
            }
            IfLayoutStyle::Inline(InlineIfLayout::ThenNestedIf) => {
                self.write_text(then_separator);
                self.write_space();
                self.format_stmt(&command.then_branch[0])?;
                self.write_if_close("; fi", fi_span);
                Ok(())
            }
            IfLayoutStyle::Inline(InlineIfLayout::Chain) => {
                self.write_text(then_separator);
                self.write_space();
                self.format_inline_stmts(&command.then_branch)?;
                for (condition, body) in &command.elif_branches {
                    self.write_text("; elif ");
                    self.format_inline_stmts(condition)?;
                    self.write_text(self.then_separator_for_condition(condition));
                    self.write_space();
                    self.format_inline_stmts(body)?;
                }
                if let Some(else_branch) = &command.else_branch {
                    self.write_text("; else ");
                    self.format_inline_stmts(else_branch)?;
                }
                self.write_if_close("; fi", fi_span);
                Ok(())
            }
            IfLayoutStyle::Expanded(layout) => {
                self.format_expanded_then_fi_if(command, then_span, fi_span, then_separator, layout)
            }
            IfLayoutStyle::RawGroupedCondition { .. } | IfLayoutStyle::SplitCondition => {
                unreachable!("non-inline if layout routed to inline emitter")
            }
        }
    }

    pub(super) fn format_expanded_then_fi_if(
        &mut self,
        command: &IfCommand,
        then_span: Span,
        fi_span: Span,
        then_separator: &'static str,
        layout: ExpandedIfLayout,
    ) -> Result<()> {
        let source = self.source();
        let fi_upper_bound = fi_span.start.offset();
        self.write_text(then_separator);
        let then_upper_bound =
            if_branch_upper_bound(command, 0, source, self.source_map(), self.facts());
        let then_site =
            CompoundBodySite::if_then_branch(command, &command.then_branch, then_upper_bound);
        if !self.write_condition_separator_suffix_comment(then_span) {
            self.write_compound_body_open_suffix(then_site);
        }
        self.format_if_branch_body_site_with_open_suffix(then_site, false)?;
        for (index, (condition, body)) in command.elif_branches.iter().enumerate() {
            if matches!(layout, ExpandedIfLayout::Compact) {
                self.write_text("; elif ");
                self.format_inline_stmts(condition)?;
                self.write_text(self.then_separator_for_condition(condition));
            } else {
                self.write_if_branch_prefix(command, index, source);
                self.write_elif_header(command, index, condition, body, source)?;
            }
            let body_upper_bound =
                if_branch_upper_bound(command, index + 1, source, self.source_map(), self.facts());
            let site = CompoundBodySite::if_then_branch(command, body, body_upper_bound);
            self.format_if_branch_body_site(site)?;
        }
        if let Some(body) = &command.else_branch {
            match layout {
                ExpandedIfLayout::Compact => self.write_text("; else"),
                ExpandedIfLayout::Multiline { inline_else_close } => {
                    self.write_if_branch_prefix(command, command.elif_branches.len(), source);
                    if inline_else_close {
                        self.write_text("else ");
                        self.format_inline_stmts(body)?;
                        self.write_if_close("; fi", fi_span);
                        return Ok(());
                    }
                    self.write_text("else");
                }
            }
            let site = CompoundBodySite::if_else_branch(command, body, fi_upper_bound);
            self.format_if_branch_body_site(site)?;
        }
        match layout {
            ExpandedIfLayout::Compact => self.write_if_close("; fi", fi_span),
            ExpandedIfLayout::Multiline { .. } => {
                self.finish_multiline_if_close(command, then_upper_bound, fi_span);
            }
        }
        Ok(())
    }

    pub(super) fn finish_multiline_if_close(
        &mut self,
        command: &IfCommand,
        then_upper_bound: usize,
        fi_span: Span,
    ) {
        if self.if_final_branch_has_blank_line_before_fi(command, then_upper_bound) {
            self.newline();
        }
        self.newline();
        self.write_if_close("fi", fi_span);
    }

    pub(super) fn write_if_close(&mut self, close_text: &str, fi_span: Span) {
        self.write_text(close_text);
        self.write_close_suffix_after_span(Some(fi_span));
    }

    pub(super) fn if_final_branch_has_blank_line_before_fi(
        &self,
        command: &IfCommand,
        _then_upper_bound: usize,
    ) -> bool {
        let (branch_index, body) = if let Some(body) = command.else_branch.as_ref() {
            (command.elif_branches.len(), body)
        } else if let Some((index, (_, body))) =
            command.elif_branches.iter().enumerate().next_back()
        {
            (index + 1, body)
        } else {
            (0, &command.then_branch)
        };
        let upper_bound = self.facts().if_branch_upper_bound(command, branch_index);
        !body.is_empty()
            && self
                .facts()
                .sequence(body, Some(upper_bound))
                .has_blank_line_before_close()
    }

    pub(super) fn write_if_branch_prefix(
        &mut self,
        command: &IfCommand,
        branch_index: usize,
        source: &str,
    ) {
        if self.if_next_branch_has_blank_line_before_keyword(command, branch_index, source) {
            self.newline();
        }
        let preserve_blank_after_prefix = self
            .if_branch_prefix_comments_have_blank_line_before_keyword(
                command,
                branch_index,
                source,
            );
        self.emit_branch_prefix_comments(command, branch_index);
        self.newline();
        if preserve_blank_after_prefix {
            self.newline();
        }
    }

    pub(super) fn write_elif_header(
        &mut self,
        command: &IfCommand,
        branch_index: usize,
        condition: &StmtSeq,
        body: &StmtSeq,
        source: &str,
    ) -> Result<()> {
        if condition_keyword_on_previous_non_empty_line(
            condition,
            source,
            self.source_map(),
            "elif",
        ) || elif_condition_has_explicit_statement_break(
            condition,
            body,
            source,
            self.source_map(),
        ) || !self
            .elif_condition_prefix_comments(command, branch_index, condition)
            .is_empty()
        {
            self.write_text("elif");
            self.newline();
            self.with_indent(|formatter| {
                formatter.format_stmt_sequence(condition, Some(body.span.start.offset()))
            })?;
            self.newline();
            self.write_text("then");
            Ok(())
        } else {
            self.write_text("elif ");
            self.format_inline_stmts(condition)?;
            self.write_text(self.then_separator_for_condition(condition));
            Ok(())
        }
    }

    pub(super) fn emit_branch_prefix_comments(&mut self, command: &IfCommand, branch_index: usize) {
        let Some((start, end)) = self.facts().if_next_branch_region(command, branch_index) else {
            return;
        };
        let branch_facts = self.facts().branch_prefix_facts(start, end);
        let comments = branch_facts.comments();
        if comments.is_empty() {
            return;
        }
        let disabled_branch_block = branch_prefix_comments_use_disabled_body_indent(comments);
        self.newline();
        for (index, comment) in comments.iter().enumerate() {
            if disabled_branch_block {
                self.with_indent(|formatter| formatter.write_text(&comment.text));
            } else {
                self.write_text(&comment.text);
            }
            if let Some(next) = comments.get(index + 1) {
                let line = self.source_map().line_number_for_offset(comment.offset);
                let next_line = self.source_map().line_number_for_offset(next.offset);
                self.write_line_breaks(line_gap_break_count(line, next_line));
            }
        }
    }

    pub(super) fn elif_condition_prefix_comments(
        &self,
        command: &IfCommand,
        branch_index: usize,
        condition: &StmtSeq,
    ) -> Vec<BranchPrefixComment> {
        let Some((_, keyword_offset)) = self.facts().if_next_branch_region(command, branch_index)
        else {
            return Vec::new();
        };
        let Some(first) = condition.first() else {
            return Vec::new();
        };
        let condition_start = stmt_span(first).start.offset();
        if keyword_offset >= condition_start {
            return Vec::new();
        }

        self.own_line_comments_in_region(keyword_offset, condition_start)
    }

    pub(super) fn if_next_branch_has_blank_line_before_keyword(
        &self,
        command: &IfCommand,
        branch_index: usize,
        _source: &str,
    ) -> bool {
        self.facts()
            .if_next_branch_region(command, branch_index)
            .is_some_and(|(start, end)| {
                self.facts()
                    .branch_prefix_facts(start, end)
                    .has_blank_line_before_keyword()
            })
    }

    pub(super) fn if_branch_prefix_comments_have_blank_line_before_keyword(
        &self,
        command: &IfCommand,
        branch_index: usize,
        _source: &str,
    ) -> bool {
        self.facts()
            .if_next_branch_region(command, branch_index)
            .is_some_and(|(start, end)| {
                self.facts()
                    .branch_prefix_facts(start, end)
                    .has_blank_line_after_comments()
            })
    }

    pub(super) fn write_compound_body_open_suffix(&mut self, site: CompoundBodySite<'_>) {
        let Some(span) = self
            .facts()
            .sequence(site.body(), site.bounds().render_limit())
            .group_open_suffix_span()
        else {
            return;
        };
        self.write_suffix_comment_after_span(span, false);
    }

    pub(super) fn write_condition_separator_suffix_comment(&mut self, then_span: Span) -> bool {
        let Some(plan) = self.facts().suffix_comment_plan_for_span(then_span) else {
            return false;
        };
        self.write_inline_comment_plan(plan, false);
        true
    }

    pub(super) fn write_unmodeled_branch_background_terminator(
        &mut self,
        body: &StmtSeq,
        upper_bound: usize,
    ) {
        let Some(operator) = unmodeled_branch_background_operator(body, upper_bound, self.source())
        else {
            return;
        };
        self.write_space();
        self.write_text(operator);
    }

    pub(super) fn format_if_branch_body_site(&mut self, site: CompoundBodySite<'_>) -> Result<()> {
        self.format_if_branch_body_site_with_open_suffix(site, true)
    }

    pub(super) fn format_if_branch_body_site_with_open_suffix(
        &mut self,
        site: CompoundBodySite<'_>,
        write_open_suffix: bool,
    ) -> Result<()> {
        let body = site.body();
        let upper_bound = site.bounds().render_end();
        let preserve_open_blank = self
            .facts()
            .sequence(body, Some(upper_bound))
            .has_blank_line_after_open();
        if write_open_suffix {
            self.write_compound_body_open_suffix(site);
        }
        self.format_body_with_upper_bound_and_open_blank(
            body,
            Some(upper_bound),
            preserve_open_blank,
        )?;
        self.write_unmodeled_branch_background_terminator(body, upper_bound);
        Ok(())
    }

    pub(super) fn format_brace_if(&mut self, command: &IfCommand) -> Result<()> {
        let source = self.source();
        self.write_text("if ");
        self.format_inline_stmts(&command.condition)?;
        self.write_space();
        let then_upper_bound =
            if_branch_upper_bound(command, 0, source, self.source_map(), self.facts());
        let then_site =
            CompoundBodySite::if_then_branch(command, &command.then_branch, then_upper_bound);
        self.format_brace_group(then_site.body(), then_site.bounds().render_limit())?;
        for (index, (condition, body)) in command.elif_branches.iter().enumerate() {
            self.write_text(" elif ");
            self.format_inline_stmts(condition)?;
            self.write_space();
            let upper_bound =
                if_branch_upper_bound(command, index + 1, source, self.source_map(), self.facts());
            let site = CompoundBodySite::if_then_branch(command, body, upper_bound);
            self.format_brace_group(site.body(), site.bounds().render_limit())?;
        }
        if let Some(body) = &command.else_branch {
            self.write_text(" else ");
            let site = CompoundBodySite::if_else_branch(command, body, command.span.end.offset());
            self.format_brace_group(site.body(), site.bounds().render_limit())?;
        }
        Ok(())
    }

    pub(super) fn format_for(&mut self, command: &ForCommand) -> Result<()> {
        self.write_text("for ");
        for (index, target) in command.targets.iter().enumerate() {
            if index > 0 {
                self.write_space();
            }
            self.write_word(&target.word);
        }

        match command.syntax {
            ForSyntax::InDoDone { in_span, .. }
            | ForSyntax::InDirect { in_span }
            | ForSyntax::InBrace { in_span, .. } => {
                self.write_for_in_words(command.words.as_deref(), in_span);
            }
            ForSyntax::ParenDoDone { .. }
            | ForSyntax::ParenDirect { .. }
            | ForSyntax::ParenBrace { .. } => {
                self.write_parenthesized_word_list(command.words.as_deref());
            }
        }

        let site = CompoundBodySite::for_command(
            command,
            self.source_map(),
            self.facts().compound_close_span_for_span(command.span),
        );
        let plan = CompoundBodyPlan::loop_body(site, "; ", self.render_context());
        self.format_compound_body_plan(plan)
    }

    pub(super) fn write_for_in_words(&mut self, words: Option<&[Word]>, in_span: Option<Span>) {
        if let Some(words) = words {
            self.write_text(" in");
            self.write_word_list_preserving_breaks_after(
                words,
                in_span.map(|span| span.end.offset()),
            );
        }
    }

    pub(super) fn write_parenthesized_word_list(&mut self, words: Option<&[Word]>) {
        self.write_text(" (");
        if let Some(words) = words {
            self.write_space_separated_words(words);
        }
        self.write_text(")");
    }

    pub(super) fn write_space_separated_words(&mut self, words: &[Word]) {
        for (index, word) in words.iter().enumerate() {
            if index > 0 {
                self.write_space();
            }
            self.write_word(word);
        }
    }

    pub(super) fn format_repeat(&mut self, command: &RepeatCommand) -> Result<()> {
        self.write_text("repeat ");
        self.write_word(&command.count);
        let site = CompoundBodySite::repeat_command(
            command,
            self.source_map(),
            self.facts().compound_close_span_for_span(command.span),
        );
        let plan = CompoundBodyPlan::loop_body(site, " ", self.render_context());
        self.format_compound_body_plan(plan)
    }

    pub(super) fn format_compound_body_plan(&mut self, plan: CompoundBodyPlan<'_>) -> Result<()> {
        match plan.layout() {
            CompoundBodyLayout::DoDone(DoDoneBodyLayout::Inline) => {
                self.write_text("; do ");
                self.format_inline_stmts(plan.body())?;
                self.write_text("; done");
                self.write_close_suffix_after_span(plan.close_span());
            }
            CompoundBodyLayout::DoDone(DoDoneBodyLayout::LegacyInline { close_separator }) => {
                self.write_text("; do ");
                self.format_stmt(&plan.body()[0])?;
                self.write_text(close_separator);
                self.write_text("done");
                self.write_close_suffix_after_span(plan.close_span());
            }
            CompoundBodyLayout::DoDone(DoDoneBodyLayout::Multiline { open }) => {
                self.format_multiline_compound_body_plan(plan, "done", open)?;
            }
            CompoundBodyLayout::DirectInline => {
                self.write_space();
                self.format_inline_stmts(plan.body())?;
            }
            CompoundBodyLayout::BraceGroup { prefix } => {
                self.write_text(prefix);
                self.format_brace_group(plan.body(), plan.upper_bound())?;
            }
        }
        Ok(())
    }

    fn format_multiline_compound_body_plan(
        &mut self,
        plan: CompoundBodyPlan<'_>,
        close: &'static str,
        open: &'static str,
    ) -> Result<()> {
        let body = plan.body();
        let body_upper_bound = plan.body_upper_bound();

        self.write_text(open);
        if let Some(span) = plan.open_suffix_span() {
            self.write_suffix_comment_after_span(span, false);
        }
        self.format_body_with_upper_bound_and_open_blank(
            body,
            Some(body_upper_bound),
            plan.preserve_open_blank(),
        )?;
        self.write_unmodeled_branch_background_terminator(body, body_upper_bound);
        if plan.preserve_close_blank() {
            self.newline();
        }
        self.finish_block_with_close_suffix(close, plan.close_span());
        Ok(())
    }

    pub(super) fn format_foreach(&mut self, command: &ForeachCommand) -> Result<()> {
        self.write_text("foreach ");
        self.write_text(command.variable.as_ref());
        let site = CompoundBodySite::foreach_command(
            command,
            self.source_map(),
            self.facts().compound_close_span_for_span(command.span),
        );
        match command.syntax {
            ForeachSyntax::ParenBrace { .. } => {
                self.write_parenthesized_word_list(Some(&command.words));
                let plan = CompoundBodyPlan::brace_group(site, " ", self.render_context());
                self.format_compound_body_plan(plan)?;
            }
            ForeachSyntax::InDoDone { .. } => {
                self.write_for_in_words(Some(&command.words), None);
                let plan = CompoundBodyPlan::do_done(site, self.render_context());
                self.format_compound_body_plan(plan)?;
            }
        }
        Ok(())
    }

    pub(super) fn format_select(&mut self, command: &SelectCommand) -> Result<()> {
        self.write_text("select ");
        self.write_text(command.variable.as_ref());
        self.write_for_in_words(Some(&command.words), None);
        let site = CompoundBodySite::select_command(
            command,
            self.source_map(),
            self.facts().compound_close_span_for_span(command.span),
        );
        let plan = CompoundBodyPlan::do_done(site, self.render_context());
        self.format_compound_body_plan(plan)?;
        Ok(())
    }

    pub(super) fn format_while(&mut self, command: &WhileCommand) -> Result<()> {
        let site = CompoundBodySite::while_command(
            command,
            self.source_map(),
            self.facts().compound_close_span_for_span(command.span),
        );
        self.format_loop("while", &command.condition, site)
    }

    pub(super) fn format_until(&mut self, command: &UntilCommand) -> Result<()> {
        let site = CompoundBodySite::until_command(
            command,
            self.source_map(),
            self.facts().compound_close_span_for_span(command.span),
        );
        self.format_loop("until", &command.condition, site)
    }

    pub(super) fn format_loop(
        &mut self,
        keyword: &'static str,
        condition: &StmtSeq,
        site: CompoundBodySite<'_>,
    ) -> Result<()> {
        if loop_condition_starts_after_keyword(condition, site.enclosing_span())
            || loop_condition_has_multiple_commands(condition)
        {
            self.write_text(keyword);
            self.newline();
            let condition_upper_bound = site.open_keyword_start(self.source());
            self.with_indent(|formatter| {
                formatter.format_stmt_sequence(condition, condition_upper_bound)
            })?;
            self.newline();
            let plan = CompoundBodyPlan::split_do_done(site, self.render_context());
            return self.format_compound_body_plan(plan);
        }

        self.write_text(keyword);
        self.write_space();
        self.format_inline_stmts(condition)?;
        let plan = CompoundBodyPlan::do_done(site, self.render_context());
        self.format_compound_body_plan(plan)
    }

    pub(super) fn format_case(&mut self, command: &CaseCommand) -> Result<()> {
        let plan = CaseLayoutPlan::for_command(command, self.render_context());
        let CaseLayoutPlan {
            style,
            body_fallback_upper_bound,
            esac_span,
        } = plan;
        if matches!(&style, CaseLayoutStyle::Inline) {
            return self.format_inline_case(command);
        }

        self.write_text("case ");
        self.write_word(&command.word);
        self.write_text(" in");
        self.write_case_open_suffix(command);
        if matches!(&style, CaseLayoutStyle::Compact) {
            for item in &command.cases {
                self.write_space();
                self.format_case_item(
                    item,
                    case_item_body_upper_bound(item, body_fallback_upper_bound),
                )?;
            }
            self.write_text(" esac");
            self.write_close_suffix_after_span(esac_span);
        } else if let CaseLayoutStyle::Multiline {
            header_item_count,
            blank_line_after_in,
            close,
        } = style
        {
            self.format_case_items_on_header_line(
                command,
                body_fallback_upper_bound,
                header_item_count,
            )?;
            for (offset, item) in command.cases[header_item_count..].iter().enumerate() {
                let index = header_item_count + offset;
                self.newline();
                if header_item_count == 0 && index == 0 && blank_line_after_in {
                    self.newline();
                }
                if index > 0 && self.facts().case_item(item).has_blank_line_before() {
                    self.newline();
                }
                self.format_case_item(
                    item,
                    case_item_body_upper_bound(item, body_fallback_upper_bound),
                )?;
            }
            match close {
                CaseCloseLayout::SameLine => self.write_space(),
                CaseCloseLayout::NextLine { blank_before } => {
                    if blank_before {
                        self.newline();
                    }
                    self.newline();
                }
                CaseCloseLayout::SuffixComments(comments) => {
                    self.emit_case_suffix_comments_before_esac(command, &comments, esac_span);
                }
            }
            self.write_text("esac");
            self.write_close_suffix_after_span(esac_span);
        }
        Ok(())
    }

    pub(super) fn format_case_items_on_header_line(
        &mut self,
        command: &CaseCommand,
        case_body_fallback: usize,
        item_count: usize,
    ) -> Result<()> {
        for item in command.cases.iter().take(item_count) {
            let upper_bound = case_item_body_upper_bound(item, case_body_fallback);
            self.write_space();
            self.format_case_item(item, upper_bound)?;
        }
        Ok(())
    }

    pub(super) fn write_case_open_suffix(&mut self, command: &CaseCommand) {
        let Some(first_item) = command.cases.first() else {
            return;
        };
        let Some(first_pattern) = first_item.patterns.first() else {
            return;
        };
        let source = self.source();
        let start = command.word.span.end.offset().min(source.len());
        let end = first_pattern.span.start.offset().min(source.len());
        let Some(between) = source.get(start..end) else {
            return;
        };
        let line_end = between.find('\n').unwrap_or(between.len());
        let header = &between[..line_end];
        let header = header.trim_start_matches([' ', '\t']);
        let Some(suffix) = header.strip_prefix("in") else {
            return;
        };
        if suffix.trim_start().starts_with('#') {
            self.write_space();
            self.write_text(suffix.trim_start().trim_end_matches([' ', '\t', '\r']));
        }
    }

    pub(super) fn format_case_item(
        &mut self,
        item: &CaseItem,
        upper_bound: Option<usize>,
    ) -> Result<()> {
        let base_indent =
            usize::from(!self.options().compact_layout() && self.options().switch_case_indent());
        let first_pattern_start = item
            .patterns
            .first()
            .map(|pattern| pattern.span.start.offset());
        let item_facts = self.facts().case_item(item);
        let prefix_comments = item_facts.prefix_comments().to_vec();
        let pattern_suffix_comment = item_facts.pattern_suffix_comment();
        let has_blank_line_after_pattern = item_facts.has_blank_line_after_pattern();
        let has_blank_line_before_terminator = item_facts.has_blank_line_before_terminator();
        if let Some(first_pattern) = item.patterns.first()
            && !prefix_comments.is_empty()
        {
            self.emit_case_item_prefix_comments(&prefix_comments, first_pattern, base_indent);
        }

        if base_indent > 0 {
            self.write_case_prefix(base_indent);
        }
        for (index, word) in item.patterns.iter().enumerate() {
            if index > 0 {
                let previous = &item.patterns[index - 1];
                if self
                    .facts()
                    .contains_newline_between(previous.span.end.offset(), word.span.start.offset())
                {
                    self.write_text(" |");
                    self.line_continuation();
                    self.write_indent_units(1);
                } else {
                    self.write_text(" | ");
                }
            }
            self.write_case_pattern(item, word);
        }
        self.write_text(")");
        if let Some(comment) = &pattern_suffix_comment {
            self.write_trailing_comment(comment, true);
        }

        if item.body.is_empty() {
            let body_has_comments = self
                .facts()
                .sequence(&item.body, upper_bound)
                .has_comments();
            let comments = self.empty_case_item_body_comments(item);
            if pattern_suffix_comment.is_some() && !self.options().compact_layout() {
                self.newline();
                self.write_case_prefix(base_indent + 1);
                self.write_case_terminator(item);
                return Ok(());
            }
            if (body_has_comments || !comments.is_empty()) && !self.options().compact_layout() {
                self.newline();
                if comments.is_empty() {
                    self.with_extra_prefix_indent(base_indent + 1, |formatter| {
                        formatter.format_stmt_sequence(&item.body, upper_bound)
                    })?;
                } else {
                    self.with_extra_prefix_indent(base_indent + 1, |formatter| {
                        for (index, comment) in comments.iter().enumerate() {
                            if index > 0 {
                                formatter.newline();
                            }
                            formatter.write_text(comment);
                        }
                    });
                }
                self.newline();
                self.write_case_prefix(base_indent + 1);
                self.write_case_terminator(item);
                return Ok(());
            }
            self.write_space();
            self.write_case_terminator(item);
        } else if self.options().compact_layout() {
            self.write_space();
            self.format_stmt_sequence_with_leading_filter(
                &item.body,
                upper_bound,
                first_pattern_start,
            )?;
            self.write_text("; ");
            self.write_case_terminator(item);
        } else {
            let body_sequence = self.facts().sequence(&item.body, upper_bound);
            let pattern_line = item.patterns.last().map(|pattern| pattern.span.end.line());
            let body_has_later_comments = pattern_line.is_some_and(|pattern_line| {
                (0..item.body.len()).any(|index| {
                    body_sequence
                        .leading_for(index)
                        .iter()
                        .chain(body_sequence.trailing_for(index))
                        .any(|comment| comment.line() > pattern_line)
                }) || body_sequence
                    .dangling()
                    .iter()
                    .any(|comment| comment.line() > pattern_line)
            });
            let pattern_body_terminator_was_inline =
                case_item_pattern_body_terminator_was_inline_in_source(item, self.source());
            let item_was_inline_in_source = self.facts().case_item_was_inline_in_source(item)
                || pattern_body_terminator_was_inline;
            if base_indent == 0
                && item.body.len() == 1
                && case_item_single_body_stmt_can_inline(
                    item,
                    self.source(),
                    self.source_map(),
                    pattern_body_terminator_was_inline,
                    self.facts(),
                )
                && (item_was_inline_in_source
                    || (pattern_suffix_comment.is_some()
                        && !body_has_later_comments
                        && case_item_body_can_share_terminator(item)
                        && case_item_body_terminator_was_inline_in_source(item))
                    || (!body_has_later_comments
                        && case_item_body_was_inline_without_terminator(item))
                    || (!body_has_later_comments
                        && case_item_started_inline_without_terminator(item)))
            {
                if pattern_suffix_comment.is_some() && !item_was_inline_in_source {
                    self.newline();
                    self.with_extra_prefix_indent(base_indent + 1, |formatter| {
                        formatter.format_stmt(&item.body[0])
                    })?;
                } else {
                    self.write_space();
                    self.format_stmt(&item.body[0])?;
                }
                self.write_space();
                self.write_case_terminator(item);
                return Ok(());
            }
            self.newline();
            if (case_item_pattern_close_paren_on_own_line(item, self.source(), self.source_map())
                && !case_item_close_paren_shares_line_with_body(
                    item,
                    self.source(),
                    self.source_map(),
                ))
                || has_blank_line_after_pattern
            {
                self.newline();
            }
            self.with_extra_prefix_indent(base_indent + 1, |formatter| {
                formatter.format_stmt_sequence_with_leading_filter(
                    &item.body,
                    upper_bound,
                    first_pattern_start,
                )
            })?;
            if has_blank_line_before_terminator {
                self.newline();
            }
            self.newline();
            self.write_case_prefix(base_indent + 1);
            self.write_case_terminator(item);
        }
        Ok(())
    }

    pub(super) fn write_case_terminator(&mut self, item: &CaseItem) {
        self.write_text(case_terminator(item.terminator));
        if let Some(comment) = self.facts().case_item(item).terminator_suffix_comment() {
            self.write_trailing_comment(&comment, false);
        }
    }

    pub(super) fn empty_case_item_body_comments(&self, item: &CaseItem) -> Vec<String> {
        if !item.body.is_empty() {
            return Vec::new();
        }
        let Some(end) = item.terminator_span.map(|span| span.start.offset()) else {
            return Vec::new();
        };
        let start = item
            .patterns
            .last()
            .map(|pattern| pattern.span.end.offset())
            .unwrap_or(item.body.span.start.offset());
        let Some(slice) = self.source().get(start..end) else {
            return Vec::new();
        };

        slice
            .lines()
            .filter_map(|line| {
                let trimmed = line.trim_start_matches([' ', '\t']);
                trimmed
                    .starts_with('#')
                    .then(|| trimmed.trim_end_matches([' ', '\t', '\r']).to_string())
            })
            .collect()
    }

    pub(super) fn emit_case_suffix_comments_before_esac(
        &mut self,
        command: &CaseCommand,
        comments: &[BranchPrefixComment],
        esac_span: Option<Span>,
    ) {
        let Some(mut previous_line) = command
            .cases
            .last()
            .and_then(|item| self.facts().case_item(item).suffix_comment_start_line())
        else {
            return;
        };
        let comment_indent = usize::from(self.options().switch_case_indent()) + 1;
        for comment in comments {
            let comment_line = self.source_map().line_number_for_offset(comment.offset);
            self.write_line_breaks(line_gap_break_count(previous_line, comment_line));
            self.with_extra_prefix_indent(comment_indent, |formatter| {
                formatter.write_text(&comment.text);
            });
            previous_line = comment_line;
        }
        let esac_line = esac_span
            .map(|span| span.start.line())
            .unwrap_or(command.span.end.line());
        self.write_line_breaks(line_gap_break_count(previous_line, esac_line));
    }

    pub(super) fn emit_case_item_prefix_comments(
        &mut self,
        comments: &[SourceComment<'_>],
        first_pattern: &Pattern,
        base_indent: usize,
    ) {
        let mut body_indent_context = false;
        let mut disabled_case_pattern_context = false;
        for (index, comment) in comments.iter().enumerate() {
            let uses_body_indent = case_prefix_comment_uses_body_indent(
                self.source(),
                self.source_map(),
                comment,
                first_pattern.span.start.offset(),
                disabled_case_pattern_context,
                body_indent_context,
            );
            let extra_indent = base_indent + usize::from(uses_body_indent);
            self.with_extra_prefix_indent(extra_indent, |formatter| {
                formatter.write_comment(comment);
            });
            if uses_body_indent {
                body_indent_context = true;
            }
            if comment_looks_like_disabled_case_pattern(comment) {
                disabled_case_pattern_context = true;
            }
            let target_line = comments
                .get(index + 1)
                .map(SourceComment::line)
                .unwrap_or(first_pattern.span.start.line());
            self.write_line_breaks(line_gap_break_count(comment.line(), target_line));
        }
    }

    pub(super) fn with_extra_prefix_indent<T>(
        &mut self,
        levels: usize,
        f: impl FnOnce(&mut Self) -> T,
    ) -> T {
        self.writer.push_indent(levels);
        let result = f(self);
        self.writer.pop_indent(levels);
        result
    }

    pub(super) fn with_pipeline_continuation_indent<T>(
        &mut self,
        levels: usize,
        f: impl FnOnce(&mut Self) -> T,
    ) -> T {
        let previous = self.pipeline_continuation_indent;
        self.pipeline_continuation_indent = levels;
        let result = f(self);
        self.pipeline_continuation_indent = previous;
        result
    }

    pub(super) fn with_group_body_leading_filter<T>(
        &mut self,
        enabled: bool,
        f: impl FnOnce(&mut Self) -> T,
    ) -> T {
        if !enabled {
            return f(self);
        }
        let previous = self.filter_next_group_body_leading_before_open;
        self.filter_next_group_body_leading_before_open = true;
        let result = f(self);
        self.filter_next_group_body_leading_before_open = previous;
        result
    }

    pub(super) fn format_brace_group(
        &mut self,
        commands: &StmtSeq,
        upper_bound: Option<usize>,
    ) -> Result<()> {
        let sequence_facts = self.facts().sequence(commands, upper_bound);
        let should_inline = sequence_facts.group_open_suffix_span().is_none()
            && group_has_inline_source_shape(self.render_context(), commands, '{')
            && can_inline_group(self.render_context(), commands, '{');
        if should_inline {
            self.write_text("{ ");
            self.format_inline_stmts(commands)?;
            self.write_text("; }");
            return Ok(());
        }
        self.format_group_with_upper_bound("{", "}", '{', commands, false, upper_bound)
    }

    pub(super) fn format_subshell(
        &mut self,
        commands: &StmtSeq,
        upper_bound: Option<usize>,
    ) -> Result<()> {
        let sequence_facts = self.facts().sequence(commands, upper_bound);
        let should_inline = sequence_facts.group_open_suffix_span().is_none()
            && ((group_has_inline_source_shape(self.render_context(), commands, '(')
                && can_inline_group(self.render_context(), commands, '('))
                || can_inline_source_line_subshell(self.render_context(), commands, upper_bound));
        if should_inline {
            self.write_text("(");
            if stmt_sequence_renders_with_subshell_open(commands) {
                self.write_space();
            }
            self.format_inline_stmts(commands)?;
            self.write_text(")");
            return Ok(());
        }
        if can_format_multiline_subshell_inline(self.render_context(), commands, upper_bound) {
            self.write_text("(");
            if stmt_sequence_renders_with_subshell_open(commands) {
                self.write_space();
            }
            self.format_stmt_sequence(commands, upper_bound)?;
            self.write_text(")");
            return Ok(());
        }
        self.format_group_with_upper_bound("(", ")", '(', commands, false, upper_bound)
    }

    pub(super) fn format_arithmetic(&mut self, command: &ArithmeticCommand) -> Result<()> {
        let expression_source = command
            .expr_span
            .and_then(|span| self.source().get(span.start.offset()..span.end.offset()));
        if let Some(expr) = command.expr_ast.as_ref()
            && !expression_source.is_some_and(|source| source.contains('\n'))
        {
            let mut body = self.take_scratch_buffer();
            render_arithmetic_expr_to_buf(&mut body, expr, self.render_context());
            self.write_text("((");
            self.write_text(&body);
            self.write_text("))");
            self.restore_scratch_buffer(body);
            return Ok(());
        }
        let rendered = self
            .source()
            .get(command.span.start.offset()..command.span.end.offset())
            .unwrap_or_default();
        let mut formatted = format_arithmetic_command_source(rendered);
        if arithmetic_command_is_followed_by_inline_branch_keyword(command.span, self.source()) {
            trim_final_line_ending(&mut formatted);
        }
        self.write_text(&formatted);
        Ok(())
    }

    pub(super) fn format_arithmetic_for(&mut self, command: &ArithmeticForCommand) -> Result<()> {
        let source = self.source();
        let init = slice_span(source, command.init_span);
        let condition = command
            .condition_span
            .map(|span| span.slice(source))
            .unwrap_or("");
        let step = command
            .step_span
            .map(|span| span.slice(source))
            .unwrap_or("");
        let init = format_arithmetic_for_clause_source(
            init,
            command.init_ast.as_ref(),
            self.render_context(),
        );
        let condition = format_arithmetic_for_clause_source(
            condition,
            command.condition_ast.as_ref(),
            self.render_context(),
        );
        let step = format_arithmetic_for_clause_source(
            step,
            command.step_ast.as_ref(),
            self.render_context(),
        );
        self.write_text("for ((");
        self.write_text(&init);
        self.write_text("; ");
        self.write_text(&condition);
        self.write_text("; ");
        self.write_text(&step);
        self.write_text("))");
        let site = CompoundBodySite::arithmetic_for_command(
            command,
            self.source_map(),
            self.facts().compound_close_span_for_span(command.span),
        );
        let plan = CompoundBodyPlan::do_done(site, self.render_context());
        self.format_compound_body_plan(plan)
    }

    pub(super) fn format_time(&mut self, command: &TimeCommand) -> Result<()> {
        if command.posix_format {
            self.write_text("time -p");
        } else {
            self.write_text("time");
        }
        if let Some(command) = &command.command {
            self.write_space();
            self.format_stmt(command)?;
            self.write_time_inner_trailing_comment(command);
        }
        Ok(())
    }

    pub(super) fn write_time_inner_trailing_comment(&mut self, stmt: &Stmt) {
        if !time_inner_stmt_needs_trailing_comment(stmt) {
            return;
        }
        let Some(comment) = self
            .facts()
            .close_suffix_comment_plan_after_span(stmt_format_span(stmt))
            .map(|plan| plan.comment())
        else {
            return;
        };
        self.emit_trailing_comments_for_stmt(&[comment]);
    }

    pub(super) fn format_conditional(&mut self, command: &ConditionalCommand) -> Result<()> {
        self.write_text("[[ ");
        self.format_conditional_expr(&command.expression)?;
        let tight_close = self.conditional_needs_tight_close(&command.expression);
        self.write_text(if tight_close { "]]" } else { " ]]" });
        Ok(())
    }

    pub(super) fn format_coproc(&mut self, command: &CoprocCommand) -> Result<()> {
        self.write_text("coproc");
        if command.name.as_str() != "COPROC" || command.name_span.is_some() {
            self.write_space();
            self.write_text(command.name.as_str());
        }
        self.write_space();
        self.format_stmt(&command.body)
    }

    pub(super) fn format_always(&mut self, command: &AlwaysCommand) -> Result<()> {
        self.format_brace_group(&command.body, Some(command.span.end.offset()))?;
        self.write_text(" always ");
        self.format_brace_group(&command.always_body, Some(command.span.end.offset()))
    }

    pub(super) fn format_function(&mut self, function: &FunctionDef) -> Result<()> {
        let header_comment = self.function_header_trailing_comment(function);
        self.format_named_function_header(function);
        if self.options().function_next_line() {
            self.newline();
            self.format_function_body(function.body.as_ref(), function.span.end.offset())
        } else {
            self.write_space();
            self.format_function_body_with_header_comment(
                function.body.as_ref(),
                function.span.end.offset(),
                header_comment,
            )
        }
    }

    pub(super) fn format_anonymous_function(
        &mut self,
        function: &AnonymousFunctionCommand,
    ) -> Result<()> {
        self.write_text(match function.surface {
            shuck_ast::AnonymousFunctionSurface::FunctionKeyword { .. } => "function",
            shuck_ast::AnonymousFunctionSurface::Parens { .. } => "()",
        });
        if self.options().function_next_line() {
            self.newline();
        } else {
            self.write_space();
        }
        self.format_function_body(function.body.as_ref(), function.span.end.offset())?;
        if !function.args.is_empty() {
            for argument in &function.args {
                self.write_space();
                self.write_word(argument);
            }
        }
        Ok(())
    }

    pub(super) fn format_named_function_header(&mut self, function: &FunctionDef) {
        if function.header.entries.len() == 1
            && let Some(name) = function.header.entries[0].static_name.as_ref()
        {
            let mut rendered_entry = self.take_scratch_buffer();
            self.render_word_to_buffer(&function.header.entries[0].word, &mut rendered_entry);
            let classic_single_name = name.as_str() == rendered_entry;
            self.restore_scratch_buffer(rendered_entry);

            if classic_single_name {
                if function.uses_function_keyword() {
                    self.write_text("function ");
                }
                self.write_text(name.as_str());
                if function.has_trailing_parens() {
                    self.write_text("()");
                }
                return;
            }
        }

        if function.uses_function_keyword() {
            self.write_text("function");
            if !function.header.entries.is_empty() {
                self.write_space();
            }
        }
        for (index, entry) in function.header.entries.iter().enumerate() {
            if index > 0 {
                self.write_space();
            }
            self.write_word(&entry.word);
        }
        if function.has_trailing_parens() {
            self.write_text("()");
        }
    }

    pub(super) fn format_function_body(&mut self, body: &Stmt, upper_bound: usize) -> Result<()> {
        self.format_function_body_with_header_comment(body, upper_bound, None)
    }

    pub(super) fn format_function_body_with_header_comment(
        &mut self,
        body: &Stmt,
        upper_bound: usize,
        header_comment: Option<(Span, String)>,
    ) -> Result<()> {
        let plan = FunctionBodyPlan::for_body(
            body,
            upper_bound,
            header_comment.is_some(),
            self.render_context(),
        );
        self.format_function_body_plan(plan, header_comment.as_ref().map(|(_, comment)| &**comment))
    }

    fn format_function_body_plan(
        &mut self,
        plan: FunctionBodyPlan<'_>,
        header_comment: Option<&str>,
    ) -> Result<()> {
        match plan.layout() {
            FunctionBodyLayout::FallbackStmt => self.format_stmt(plan.body()),
            FunctionBodyLayout::BraceGroup { site, layout } => {
                self.format_function_brace_body_plan(site, layout, header_comment)
            }
            FunctionBodyLayout::Subshell { site, layout } => {
                self.format_function_subshell_body_plan(site, layout)
            }
        }
    }

    fn format_function_brace_body_plan(
        &mut self,
        site: CompoundBodySite<'_>,
        layout: FunctionBodyGroupLayout,
        header_comment: Option<&str>,
    ) -> Result<()> {
        match layout {
            FunctionBodyGroupLayout::Inline => {
                self.write_text("{ ");
                self.format_inline_stmts(site.body())?;
                self.write_text("; }");
                Ok(())
            }
            FunctionBodyGroupLayout::Multiline => self.format_group_with_upper_bound(
                "{",
                "}",
                '{',
                site.body(),
                false,
                site.bounds().render_limit(),
            ),
            FunctionBodyGroupLayout::HeaderComment => {
                let comment = header_comment
                    .expect("header-comment function body plan requires a header comment");
                self.format_function_brace_group_with_header_comment(
                    site.body(),
                    site.bounds().render_end(),
                    comment,
                )
            }
        }
    }

    fn format_function_subshell_body_plan(
        &mut self,
        site: CompoundBodySite<'_>,
        layout: FunctionSubshellLayout,
    ) -> Result<()> {
        match layout {
            FunctionSubshellLayout::Inline { .. } => {
                self.write_text("(");
                if stmt_sequence_renders_with_subshell_open(site.body()) {
                    self.write_space();
                }
                self.format_inline_stmts(site.body())?;
                self.write_text(")");
                Ok(())
            }
            FunctionSubshellLayout::MultilineInline => {
                self.write_text("(");
                if stmt_sequence_renders_with_subshell_open(site.body()) {
                    self.write_space();
                }
                self.format_stmt_sequence(site.body(), site.bounds().render_limit())?;
                self.write_text(")");
                Ok(())
            }
            FunctionSubshellLayout::Multiline => self.format_group_with_upper_bound(
                "(",
                ")",
                '(',
                site.body(),
                false,
                site.bounds().render_limit(),
            ),
        }
    }

    pub(super) fn format_function_brace_group_with_header_comment(
        &mut self,
        commands: &StmtSeq,
        upper_bound: usize,
        header_comment: &str,
    ) -> Result<()> {
        self.write_text("{");
        let padding =
            self.function_header_comment_padding(commands, Some(upper_bound), self.column());
        self.write_spaces(padding);
        self.write_text(header_comment.trim_start());

        let open_suffix = self
            .facts()
            .sequence(commands, Some(upper_bound))
            .group_open_suffix_span()
            .map(|span| (span, span.slice(self.source()).trim_start().to_string()));

        if self.options().compact_layout() {
            if let Some((_, suffix)) = open_suffix {
                self.write_space();
                self.write_text(&suffix);
                self.write_text("; ");
            } else {
                self.write_space();
            }
            self.format_stmt_sequence(commands, Some(upper_bound))?;
        } else if commands.is_empty() {
            if let Some((_, suffix)) = open_suffix {
                self.newline();
                self.with_indent(|formatter| formatter.write_text(&suffix));
            }
        } else {
            self.newline();
            self.with_indent(|formatter| {
                if let Some((_, suffix)) = open_suffix {
                    formatter.write_text(&suffix);
                    formatter.newline();
                }
                formatter.format_stmt_sequence(commands, Some(upper_bound))
            })?;
        }

        self.finish_block("}");
        Ok(())
    }

    pub(super) fn function_header_comment_padding(
        &self,
        commands: &StmtSeq,
        upper_bound: Option<usize>,
        header_column: usize,
    ) -> usize {
        if self
            .facts()
            .sequence(commands, upper_bound)
            .group_open_suffix_span()
            .is_some()
        {
            return 1;
        }
        if self
            .facts()
            .sequence(commands, upper_bound)
            .leading_for(0)
            .iter()
            .any(|comment| !comment.inline())
        {
            return 1;
        }
        let Some(target_code_column) = self.first_body_inline_comment_target_column(commands)
        else {
            return 1;
        };
        let body_indent_column = self.indent_column_for_level(self.indent_level() + 1);
        body_indent_column
            .saturating_add(target_code_column)
            .saturating_sub(header_column)
            .max(1)
    }

    pub(super) fn first_body_inline_comment_target_column(
        &self,
        commands: &StmtSeq,
    ) -> Option<usize> {
        let first = commands.first()?;
        let source = self.source();
        let start = stmt_span(first).start.offset().min(source.len());
        let (line_start, line_end) = self.source_map().line_bounds_for_offset(start)?;
        let width = inline_comment_code_width(source, line_start, line_end, None)?;
        Some(width + 1)
    }

    pub(super) fn function_header_trailing_comment(
        &self,
        function: &FunctionDef,
    ) -> Option<(Span, String)> {
        if self.options().function_next_line() {
            return None;
        }

        let source = self.source();
        let header_end = function.header.span().end.offset();
        let body_start = stmt_span(function.body.as_ref()).start.offset();
        if header_end >= body_start || header_end >= source.len() {
            return None;
        }

        let source_map = self.source_map();
        let line_end = source_map
            .slice_between(header_end, body_start)?
            .find('\n')
            .map(|offset| header_end + offset)
            .unwrap_or(body_start);
        let comment = source_map.first_source_comment_between(header_end, line_end)?;
        let before_comment = source_map.slice_between(header_end, comment.span().start.offset())?;
        if before_comment.contains('{') {
            return None;
        }
        let suffix_start = header_end;
        let comment_text = source_map
            .slice_between(suffix_start, comment.span().end.offset())?
            .trim_end_matches([' ', '\t', '\r'])
            .to_string();
        (!comment_text.is_empty()).then(|| (comment.span(), comment_text))
    }

    pub(super) fn then_separator_for_condition(&self, commands: &StmtSeq) -> &'static str {
        if self.inline_condition_ends_with_case(commands) {
            " then"
        } else {
            "; then"
        }
    }

    pub(super) fn inline_condition_ends_with_case(&self, commands: &StmtSeq) -> bool {
        let [stmt] = commands.as_slice() else {
            return false;
        };
        matches!(
            stmt,
            Stmt {
                command: Command::Compound(CompoundCommand::Case(_)),
                negated: false,
                redirects,
                terminator: None,
                ..
            } if redirects.is_empty()
        )
    }

    pub(super) fn format_body_with_upper_bound_and_open_blank(
        &mut self,
        commands: &StmtSeq,
        upper_bound: Option<usize>,
        preserve_open_blank: bool,
    ) -> Result<()> {
        self.format_body_with_upper_bound_open_blank_and_leading_filter(
            commands,
            upper_bound,
            preserve_open_blank,
            None,
        )
    }

    pub(super) fn format_body_with_upper_bound_open_blank_and_leading_filter(
        &mut self,
        commands: &StmtSeq,
        upper_bound: Option<usize>,
        preserve_open_blank: bool,
        first_leading_min_offset: Option<usize>,
    ) -> Result<()> {
        if commands.is_empty() {
            return Ok(());
        }

        if self.options().compact_layout() {
            self.write_space();
            self.format_stmt_sequence_with_leading_filter(
                commands,
                upper_bound,
                first_leading_min_offset,
            )
        } else {
            self.newline();
            if preserve_open_blank {
                self.newline();
            }
            self.with_indent(|formatter| {
                formatter.format_stmt_sequence_with_leading_filter(
                    commands,
                    upper_bound,
                    first_leading_min_offset,
                )
            })
        }
    }

    pub(super) fn finish_block(&mut self, close: &'static str) {
        if self.options().compact_layout() {
            self.write_text("; ");
            self.write_text(close);
        } else {
            self.newline();
            self.write_text(close);
        }
    }

    pub(super) fn finish_block_with_close_suffix(
        &mut self,
        close: &'static str,
        close_span: Option<Span>,
    ) {
        self.finish_block(close);
        self.write_close_suffix_after_span(close_span);
    }

    pub(super) fn format_group_with_upper_bound(
        &mut self,
        open: &'static str,
        close: &'static str,
        _open_char: char,
        commands: &StmtSeq,
        leading_space: bool,
        upper_bound: Option<usize>,
    ) -> Result<()> {
        let filter_leading_before_open = self.filter_next_group_body_leading_before_open;
        self.filter_next_group_body_leading_before_open = false;
        if leading_space {
            self.write_space();
        }
        self.write_text(open);
        let sequence_facts = self.facts().sequence(commands, upper_bound);
        let open_suffix_span = sequence_facts.group_open_suffix_span();
        let open_end_offset = sequence_facts.open_end_offset();
        let preserve_open_blank = sequence_facts.has_blank_line_after_open();
        let preserve_close_blank = sequence_facts.has_blank_line_before_close();
        if let Some(span) = open_suffix_span {
            self.write_suffix_comment_after_span(span, true);
        }

        self.format_body_with_upper_bound_open_blank_and_leading_filter(
            commands,
            upper_bound,
            preserve_open_blank,
            filter_leading_before_open
                .then_some(open_end_offset)
                .flatten(),
        )?;
        if preserve_close_blank {
            self.newline();
        }
        self.finish_block(close);
        Ok(())
    }

    pub(super) fn format_conditional_expr(&mut self, expression: &ConditionalExpr) -> Result<()> {
        match expression {
            ConditionalExpr::Binary(expr) => self.format_conditional_binary(expr),
            ConditionalExpr::Unary(expr) => self.format_conditional_unary(expr),
            ConditionalExpr::Parenthesized(expr) => self.format_conditional_paren(expr),
            ConditionalExpr::Word(word) | ConditionalExpr::Regex(word) => {
                self.write_word(word);
                Ok(())
            }
            ConditionalExpr::Pattern(pattern) => {
                self.write_pattern(pattern);
                Ok(())
            }
            ConditionalExpr::VarRef(reference) => {
                self.write_var_ref(reference);
                Ok(())
            }
        }
    }

    pub(super) fn format_conditional_binary(
        &mut self,
        expression: &ConditionalBinaryExpr,
    ) -> Result<()> {
        self.format_conditional_expr(&expression.left)?;
        self.write_space();
        self.write_text(expression.op.as_str());
        if conditional_binary_has_explicit_rhs_break(expression, self.source_map()) {
            self.newline();
            if !conditional_expr_contains_command_substitution(&expression.left) {
                self.write_indent_units(1);
            }
            self.format_conditional_expr(&expression.right)?;
            return Ok(());
        }
        self.write_space();
        if matches!(expression.op, ConditionalBinaryOp::RegexMatch) {
            self.write_conditional_regex_rhs(&expression.right)
        } else {
            self.format_conditional_expr(&expression.right)
        }
    }

    pub(super) fn write_conditional_regex_rhs(
        &mut self,
        expression: &ConditionalExpr,
    ) -> Result<()> {
        let raw = expression.span().slice(self.source());
        if raw.contains('\n') {
            self.format_conditional_expr(expression)
        } else {
            let raw = trim_unescaped_trailing_whitespace(raw.trim_start_matches([' ', '\t', '\r']));
            self.write_text(raw);
            Ok(())
        }
    }

    pub(super) fn format_conditional_unary(
        &mut self,
        expression: &ConditionalUnaryExpr,
    ) -> Result<()> {
        self.write_text(expression.op.as_str());
        self.write_space();
        self.format_conditional_expr(&expression.expr)
    }

    pub(super) fn format_conditional_paren(
        &mut self,
        expression: &ConditionalParenExpr,
    ) -> Result<()> {
        self.write_text("(");
        self.format_conditional_expr(&expression.expr)?;
        self.write_text(")");
        Ok(())
    }

    pub(super) fn conditional_needs_tight_close(&mut self, expression: &ConditionalExpr) -> bool {
        match expression {
            ConditionalExpr::Word(word) => self.conditional_word_needs_tight_close(word),
            ConditionalExpr::Unary(expression)
                if matches!(expression.op, ConditionalUnaryOp::Not) =>
            {
                self.conditional_needs_tight_close(&expression.expr)
            }
            _ => false,
        }
    }

    pub(super) fn conditional_word_needs_tight_close(&mut self, word: &Word) -> bool {
        let mut rendered = self.take_scratch_buffer();
        self.render_word_to_buffer(word, &mut rendered);
        let needs_tight_close = matches!(
            rendered.as_str(),
            "!" | "-a"
                | "-b"
                | "-c"
                | "-d"
                | "-e"
                | "-f"
                | "-g"
                | "-G"
                | "-h"
                | "-k"
                | "-L"
                | "-N"
                | "-n"
                | "-o"
                | "-O"
                | "-p"
                | "-r"
                | "-R"
                | "-s"
                | "-S"
                | "-t"
                | "-u"
                | "-v"
                | "-w"
                | "-x"
                | "-z"
        );
        self.restore_scratch_buffer(rendered);
        needs_tight_close
    }

    pub(super) fn write_case_prefix(&mut self, levels: usize) {
        if levels == 0 {
            return;
        }
        self.write_indent_units(levels);
    }
}
