use super::arithmetic::*;
use super::command_substitution::*;
use super::parameter::*;
use super::raw_rewrites::*;
use super::*;

pub(crate) fn word_gap_end_before_trailing_continuation(word: &Word, source: &str) -> usize {
    let span_end = word.span.end.offset();
    let part_end = word
        .parts
        .iter()
        .map(|part| part.span.end.offset())
        .max()
        .unwrap_or(span_end);
    if part_end >= span_end {
        return span_end;
    }
    let Some(last_part) = word.parts.iter().max_by_key(|part| part.span.end.offset()) else {
        return span_end;
    };
    if !matches!(
        last_part.kind,
        WordPart::SingleQuoted { .. } | WordPart::DoubleQuoted { .. }
    ) {
        return span_end;
    }
    let Some(trailing) = source.get(part_end..span_end) else {
        return span_end;
    };
    if source_fragment_is_line_continuation_padding(trailing) {
        part_end
    } else {
        span_end
    }
}

pub(super) fn source_fragment_is_line_continuation_padding(fragment: &str) -> bool {
    let fragment = fragment.trim_start_matches([' ', '\t']);
    let Some(after_backslash) = fragment.strip_prefix('\\') else {
        return false;
    };
    let Some(after_newline) = after_backslash
        .strip_prefix("\r\n")
        .or_else(|| after_backslash.strip_prefix('\n'))
    else {
        return false;
    };
    after_newline.chars().all(|ch| matches!(ch, ' ' | '\t'))
}

pub(super) fn word_part_nodes_any(
    parts: &[WordPartNode],
    predicate: &mut impl FnMut(&WordPartNode) -> bool,
) -> bool {
    parts.iter().any(|part| {
        predicate(part)
            || matches!(
                &part.kind,
                WordPart::DoubleQuoted { parts, .. }
                    if word_part_nodes_any(parts.as_slice(), predicate)
            )
    })
}

pub(crate) fn render_word_syntax_to_buf(
    word: &Word,
    context: RenderContext<'_, '_>,
    rendered: &mut String,
) {
    render_word_syntax_internal(word, context, true, rendered);
}

pub(crate) fn render_escaped_multiline_word_syntax_to_buf(
    word: &Word,
    context: RenderContext<'_, '_>,
    rendered: &mut String,
) {
    render_word_syntax_internal(word, context, false, rendered);
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum WordRenderDecision<'source> {
    NormalizedCommandSubstitutionPadding { normalized: String },
    PreserveEscapedCommandSubstitution { raw: &'source str },
    PreserveSingleLineEscapedQuoteCommandSubstitution { raw: &'source str },
    NormalizedCompoundAssignmentContinuations { normalized: String },
    NormalizedUnquotedContinuations { normalized: String },
    NormalizedEmptyParameterReplacementDelimiters { normalized: String },
    PreserveMultilineDoubleQuotedRaw { raw: &'source str },
    RenderParts { raw_fallback: Option<&'source str> },
    RenderSyntaxWithRawFallback { raw: &'source str },
    RenderSyntax,
}

impl<'source> WordRenderDecision<'source> {
    fn for_word(
        word: &Word,
        context: RenderContext<'source, '_>,
        preserve_escaped_multiline_words: bool,
    ) -> Self {
        let source = context.source;
        let options = context.options;
        let preserve_raw = !options.simplify() && !options.minify();
        let raw = raw_word_source_slice(word, source);

        // Keep these raw-preservation checks ordered from most-specific source
        // repair to broadest syntax fallback.
        if preserve_raw
            && !word_is_single_quoted_only(word)
            && let Some(raw) = raw
            && let Some(normalized) = normalize_raw_command_substitution_padding(raw)
        {
            let normalized = normalize_raw_arithmetic_command_substitution_padding(&normalized)
                .or_else(|| normalize_raw_arithmetic_expansion_padding(&normalized))
                .unwrap_or(normalized);
            if !raw_command_substitution_needs_structural_spacing(&normalized) {
                return Self::NormalizedCommandSubstitutionPadding { normalized };
            }
        }

        if preserve_escaped_multiline_words
            && word_has_escaped_command_substitution(word, source)
            && let Some(raw) = raw
        {
            return Self::PreserveEscapedCommandSubstitution { raw };
        }

        if preserve_raw
            && let Some(raw) = raw
            && raw_single_line_escaped_quote_command_substitution_should_preserve(raw)
        {
            return Self::PreserveSingleLineEscapedQuoteCommandSubstitution { raw };
        }

        if preserve_raw
            && let Some(raw) = raw
            && let Some(normalized) = normalize_raw_compound_assignment_word_continuations(raw)
        {
            return Self::NormalizedCompoundAssignmentContinuations { normalized };
        }

        if preserve_raw
            && !word_needs_special_rendering(word)
            && let Some(raw) = raw
            && let Some(normalized) = normalize_raw_unquoted_word_continuations(raw)
        {
            return Self::NormalizedUnquotedContinuations { normalized };
        }

        if preserve_raw
            && let Some(raw) = raw
            && let Some(normalized) = normalize_raw_empty_parameter_replacement_delimiters(raw)
        {
            return Self::NormalizedEmptyParameterReplacementDelimiters { normalized };
        }

        if preserve_raw
            && let Some(raw) = raw
            && (word_has_multiline_double_quoted_source(word, source)
                || (raw.starts_with('"') && raw.contains("\\\n")))
            && !word_is_quoted_formattable_command_substitution_only_with_facts(word, context.facts)
            && (preserve_escaped_multiline_words || !raw_escaped_multiline_double_quoted_word(raw))
            && could_need_preserve_raw_syntax(raw)
        {
            return Self::PreserveMultilineDoubleQuotedRaw { raw };
        }

        if word_needs_formatter_rendering(word, context) {
            return Self::RenderParts {
                raw_fallback: preserve_raw.then_some(raw).flatten(),
            };
        }

        if preserve_raw
            && let Some(raw) = raw
            && could_need_preserve_raw_syntax(raw)
        {
            return Self::RenderSyntaxWithRawFallback { raw };
        }

        Self::RenderSyntax
    }

    fn render(
        self,
        word: &Word,
        context: RenderContext<'_, '_>,
        preserve_escaped_multiline_words: bool,
        rendered: &mut String,
    ) {
        let source = context.source;
        let options = context.options;

        match self {
            Self::NormalizedCommandSubstitutionPadding { normalized } => {
                push_raw_shell_text_with_normalized_redirect_spacing(rendered, &normalized);
            }
            Self::PreserveEscapedCommandSubstitution { raw }
            | Self::PreserveSingleLineEscapedQuoteCommandSubstitution { raw } => {
                rendered.push_str(raw);
            }
            Self::NormalizedCompoundAssignmentContinuations { normalized }
            | Self::NormalizedUnquotedContinuations { normalized }
            | Self::NormalizedEmptyParameterReplacementDelimiters { normalized } => {
                rendered.push_str(&normalized);
            }
            Self::PreserveMultilineDoubleQuotedRaw { raw } => {
                push_raw_word_with_normalized_command_redirect_spacing(
                    rendered, word, raw, source, options,
                );
            }
            Self::RenderParts { raw_fallback } => {
                let start = rendered.len();
                if render_word_parts(
                    word.parts.as_slice(),
                    context,
                    preserve_escaped_multiline_words,
                    rendered,
                )
                .is_err()
                {
                    unreachable!("writing into a String should not fail");
                }
                if let Some(raw) = raw_fallback
                    && should_preserve_special_rendered_raw_syntax(raw, &rendered[start..])
                {
                    rendered.truncate(start);
                    push_preserved_raw_word_source(rendered, word, raw, source, options);
                }
            }
            Self::RenderSyntaxWithRawFallback { raw } => {
                let start = rendered.len();
                word.render_syntax_to_buf(source, rendered);
                if should_preserve_raw_syntax(raw, &rendered[start..]) {
                    rendered.truncate(start);
                    push_preserved_raw_word_source(rendered, word, raw, source, options);
                }
            }
            Self::RenderSyntax => {
                word.render_syntax_to_buf(source, rendered);
            }
        }
    }
}

pub(super) fn render_word_syntax_internal(
    word: &Word,
    context: RenderContext<'_, '_>,
    preserve_escaped_multiline_words: bool,
    rendered: &mut String,
) {
    WordRenderDecision::for_word(word, context, preserve_escaped_multiline_words).render(
        word,
        context,
        preserve_escaped_multiline_words,
        rendered,
    );
}

/// Returns `true` when a word contains a command-substitution node whose raw
/// source was escaped in the original word, indicating the parser
/// misinterpreted a literal prompt fragment as a command-substitution delimiter.
/// In that case the word's raw source must be preserved verbatim.
pub(super) fn word_has_escaped_command_substitution(word: &Word, source: &str) -> bool {
    if raw_word_source_slice(word, source)
        .is_some_and(|raw| raw.contains("\\$(") || raw.contains("\\`"))
    {
        return true;
    }

    word_part_nodes_any(&word.parts, &mut |part| {
        word_part_has_escaped_command_substitution(&part.kind, part.span, source)
    })
}

pub(super) fn word_part_has_escaped_command_substitution(
    part: &WordPart,
    span: shuck_ast::Span,
    source: &str,
) -> bool {
    match part {
        WordPart::CommandSubstitution { syntax, .. } => match syntax {
            CommandSubstitutionSyntax::Backtick => {
                raw_source_slice(span, source).is_some_and(|raw| raw.starts_with('\\'))
            }
            CommandSubstitutionSyntax::DollarParen => {
                raw_source_slice(span, source).is_some_and(|raw| raw.starts_with("\\$("))
                    || source
                        .get(..span.start.offset())
                        .is_some_and(|prefix| prefix.ends_with('\\'))
            }
        },
        _ => false,
    }
}

pub(super) fn raw_escaped_multiline_double_quoted_word(raw: &str) -> bool {
    raw.strip_prefix('$').unwrap_or(raw).starts_with("\"\\\n")
        || raw.strip_prefix('$').unwrap_or(raw).starts_with("\"\\\r\n")
}

pub(super) fn raw_single_line_escaped_quote_command_substitution_should_preserve(
    raw: &str,
) -> bool {
    let Some(escaped_quote) = raw.find("\\\"") else {
        return false;
    };
    let Some(command_substitution) = raw.find("$(") else {
        return false;
    };

    !raw.contains('\n')
        && raw.starts_with('"')
        && raw.ends_with('"')
        && escaped_quote < command_substitution
}

pub(super) fn word_needs_special_rendering(word: &Word) -> bool {
    word_part_nodes_any(&word.parts, &mut |part| {
        part_needs_special_rendering(&part.kind)
    })
}

pub(super) fn word_needs_formatter_rendering(word: &Word, context: RenderContext<'_, '_>) -> bool {
    word_part_nodes_any(&word.parts, &mut |part| {
        word_part_needs_formatter_rendering(part, context)
    })
}

pub(super) fn word_part_needs_formatter_rendering(
    part: &WordPartNode,
    context: RenderContext<'_, '_>,
) -> bool {
    part_needs_special_rendering(&part.kind)
        || word_part_has_parameter_raw_subscript_needs_compaction(&part.kind, context)
        || word_part_has_parameter_command_redirect_spacing_needs_normalization(
            &part.kind,
            part.span,
            context.source,
        )
        || word_part_has_arithmetic_expansion_source_needs_trim(&part.kind, context.source)
}

pub(super) fn word_part_has_parameter_raw_subscript_needs_compaction(
    part: &WordPart,
    context: RenderContext<'_, '_>,
) -> bool {
    match part {
        WordPart::Parameter(parameter) => {
            parameter_raw_subscript_needs_compaction(parameter, context)
        }
        _ => false,
    }
}

pub(super) fn word_part_has_parameter_command_redirect_spacing_needs_normalization(
    part: &WordPart,
    span: shuck_ast::Span,
    source: &str,
) -> bool {
    match part {
        WordPart::Parameter(_) | WordPart::ParameterExpansion { .. } => {
            raw_source_slice(span, source).is_some_and(raw_parameter_command_spacing_would_change)
        }
        _ => false,
    }
}

pub(super) fn word_part_has_arithmetic_expansion_source_needs_trim(
    part: &WordPart,
    source: &str,
) -> bool {
    match part {
        WordPart::ArithmeticExpansion { expression, .. } => {
            let raw = expression.slice(source);
            raw.trim_matches([' ', '\t', '\r']).len() != raw.len()
        }
        _ => false,
    }
}

pub(super) fn word_has_multiline_double_quoted_source(word: &Word, source: &str) -> bool {
    word_part_nodes_any(&word.parts, &mut |part| {
        matches!(&part.kind, WordPart::DoubleQuoted { .. })
            && raw_source_slice(part.span, source).is_some_and(|raw| raw.contains('\n'))
    })
}

pub(crate) fn word_is_quoted_formattable_command_substitution_only_with_facts(
    word: &Word,
    facts: &FormatterFacts<'_>,
) -> bool {
    quoted_command_substitution_only_body(word)
        .is_some_and(|body| !facts.sequence_contains_multiline_literal_source(body))
}

pub(crate) fn word_is_quoted_command_substitution_only(word: &Word) -> bool {
    quoted_command_substitution_only_body(word).is_some()
}

pub(super) fn quoted_command_substitution_only_body(word: &Word) -> Option<&StmtSeq> {
    let [
        shuck_ast::WordPartNode {
            kind:
                WordPart::DoubleQuoted {
                    parts,
                    dollar: false,
                },
            ..
        },
    ] = word.parts.as_slice()
    else {
        return None;
    };

    let mut substitution_body = None;
    for part in parts {
        match &part.kind {
            WordPart::CommandSubstitution { body, .. } if substitution_body.is_none() => {
                substitution_body = Some(body);
            }
            WordPart::Literal(text) if text.is_empty() => {}
            _ => return None,
        }
    }

    substitution_body
}

pub(super) fn part_needs_special_rendering(part: &WordPart) -> bool {
    match part {
        WordPart::ArithmeticExpansion { expression_ast, .. } => expression_ast.is_some(),
        WordPart::Parameter(parameter) => parameter_needs_special_rendering(parameter),
        WordPart::ParameterExpansion { operator, .. } => matches!(
            operator.as_ref(),
            ParameterOp::ReplaceFirst { .. } | ParameterOp::ReplaceAll { .. }
        ),
        WordPart::SingleQuoted { dollar: true, .. }
        | WordPart::Substring { .. }
        | WordPart::ArraySlice { .. }
        | WordPart::CommandSubstitution { .. }
        | WordPart::ProcessSubstitution { .. } => true,
        _ => false,
    }
}

pub(super) fn render_word_parts(
    parts: &[shuck_ast::WordPartNode],
    env: RenderContext<'_, '_>,
    allow_source_indented_inline_command_substitution: bool,
    rendered: &mut String,
) -> Result<(), std::fmt::Error> {
    for part in parts {
        render_word_part(
            rendered,
            &part.kind,
            part.span,
            env,
            WordPartRenderContext {
                allow_source_indented_inline_command_substitution,
                ..WordPartRenderContext::default()
            },
        )?;
    }
    Ok(())
}

#[derive(Debug, Default, Clone, Copy)]
pub(super) struct WordPartRenderContext {
    allow_source_indented_inline_command_substitution: bool,
    source_indented_inline_command_substitution: bool,
}

pub(super) fn render_word_part(
    rendered: &mut String,
    part: &WordPart,
    span: shuck_ast::Span,
    env: RenderContext<'_, '_>,
    part_context: WordPartRenderContext,
) -> Result<(), std::fmt::Error> {
    let source = env.source;
    let options = env.options;
    let facts = env.facts;

    if let Some(raw) = preferred_raw_word_part_source(part, span, env) {
        rendered.push_str(raw);
        return Ok(());
    }

    match part {
        WordPart::Literal(text) => {
            push_unquoted_literal(rendered, text.syntax_str(source, span));
        }
        WordPart::SingleQuoted { value, dollar } => {
            if *dollar {
                rendered.push('$');
            }
            rendered.push('\'');
            rendered.push_str(value.slice(source));
            rendered.push('\'');
        }
        WordPart::DoubleQuoted { parts, dollar } => {
            if *dollar {
                rendered.push('$');
            }
            rendered.push('"');
            let mut inner = String::new();
            let mut follows_line_indent_literal = false;
            for part in parts {
                match &part.kind {
                    WordPart::Literal(text) => {
                        let literal = if text.is_source_backed() {
                            text.syntax_str(source, part.span)
                        } else {
                            text.as_str(source, part.span)
                        };
                        if text.is_source_backed() {
                            inner.push_str(literal);
                        } else {
                            render_double_quoted_literal(&mut inner, literal);
                        }
                        follows_line_indent_literal =
                            literal_ends_with_line_indent_for_word_part(literal);
                    }
                    other => {
                        render_word_part(
                            &mut inner,
                            other,
                            part.span,
                            env,
                            WordPartRenderContext {
                                allow_source_indented_inline_command_substitution: part_context
                                    .allow_source_indented_inline_command_substitution,
                                source_indented_inline_command_substitution: part_context
                                    .allow_source_indented_inline_command_substitution
                                    && follows_line_indent_literal,
                            },
                        )?;
                        follows_line_indent_literal = false;
                    }
                }
            }
            if let Some(normalized) = normalize_raw_arithmetic_expansion_padding(&inner) {
                rendered.push_str(&normalized);
            } else {
                rendered.push_str(&inner);
            }
            rendered.push('"');
        }
        WordPart::Variable(name) => {
            std::write!(rendered, "${name}")?;
        }
        WordPart::CommandSubstitution { body, syntax } => {
            if let Some(raw) = raw_source_slice(span, source) {
                let raw = raw_dollar_command_substitution_slice(raw).unwrap_or(raw);
                let layout = command_substitution_layout(
                    Some(raw),
                    body,
                    env,
                    false,
                    part_context.source_indented_inline_command_substitution,
                );
                if raw_dollar_command_substitution_body(raw)
                    .is_some_and(raw_body_contains_pipeline_multistatement_brace_group)
                    && let Some(block) =
                        render_inline_raw_command_substitution_as_block(raw, options)
                {
                    rendered.push_str(&block);
                } else if stmt_seq_contains_comments(facts, body) {
                    let fallback = RawCommandSubstitutionCommentFallback {
                        raw,
                        body,
                        span_start: span.start.offset(),
                        context: env,
                    };
                    if commented_command_substitution_can_use_structural_formatter(body) {
                        let rendered_start = rendered.len();
                        if render_command_substitution(
                            rendered,
                            body,
                            span.end.offset(),
                            env,
                            layout,
                            1,
                            Some(raw),
                        )
                        .is_some()
                        {
                            restore_raw_case_terminator_suffix_comments(
                                rendered,
                                rendered_start,
                                raw,
                            );
                        } else {
                            push_raw_command_substitution_comment_fallback(
                                rendered, fallback, false,
                            );
                        }
                    } else {
                        push_raw_command_substitution_comment_fallback(rendered, fallback, true);
                    }
                } else if let Some(block) =
                    render_inline_raw_command_substitution_as_block(raw, options)
                {
                    rendered.push_str(&block);
                } else if render_command_substitution(
                    rendered,
                    body,
                    span.end.offset(),
                    env,
                    layout,
                    1,
                    Some(raw),
                )
                .is_some()
                {
                } else {
                    push_raw_shell_text_with_normalized_redirect_spacing(rendered, raw);
                }
            } else if render_command_substitution(
                rendered,
                body,
                span.end.offset(),
                env,
                command_substitution_layout(
                    None,
                    body,
                    env,
                    *syntax == CommandSubstitutionSyntax::DollarParen,
                    false,
                ),
                1,
                None,
            )
            .is_some()
            {
            } else {
                std::write!(rendered, "$({body:?})")?;
            }
        }
        WordPart::ProcessSubstitution { body, is_input } => {
            if let Some(raw) = raw_source_slice(span, source) {
                if stmt_seq_contains_comments(facts, body) {
                    if process_substitution_source_opens_to_body_line(raw)
                        && !stmt_seq_has_heredoc(facts, body)
                    {
                        push_raw_block_command_substitution_without_outer_indent(
                            rendered,
                            raw,
                            source,
                            span.start.offset(),
                            options,
                        );
                    } else {
                        rendered.push_str(raw);
                    }
                } else if render_process_substitution(
                    rendered,
                    body,
                    *is_input,
                    span,
                    env,
                    raw.contains('\n'),
                    Some(raw),
                )
                .is_some()
                {
                } else {
                    rendered.push_str(raw);
                }
            } else if render_process_substitution(rendered, body, *is_input, span, env, false, None)
                .is_some()
            {
            } else {
                let prefix = if *is_input { "<" } else { ">" };
                std::write!(rendered, "{}({body:?})", prefix)?;
            }
        }
        WordPart::ArithmeticExpansion {
            expression,
            expression_ast,
            syntax,
            ..
        } => push_arithmetic_expansion(
            rendered,
            expression,
            expression_ast.as_deref(),
            *syntax,
            env,
        ),
        WordPart::Parameter(parameter) => {
            push_parameter_word(rendered, parameter, env)?;
        }
        WordPart::ParameterExpansion {
            reference,
            operator,
            operand,
            colon_variant,
            ..
        } => render_parameter_expansion(
            rendered,
            reference,
            operator.as_ref(),
            operand.as_ref().map(|v| &**v),
            *colon_variant,
            Some(span),
            env,
        )?,
        WordPart::Length(reference) | WordPart::ArrayLength(reference) => {
            push_braced_var_ref(rendered, "#", reference, env);
        }
        WordPart::ArrayAccess(reference) => {
            push_braced_var_ref(rendered, "", reference, env);
        }
        WordPart::ArrayIndices(reference) => {
            push_braced_var_ref(rendered, "!", reference, env);
        }
        WordPart::Substring {
            reference,
            offset,
            offset_ast,
            length,
            length_ast,
            ..
        }
        | WordPart::ArraySlice {
            reference,
            offset,
            offset_ast,
            length,
            length_ast,
            ..
        } => {
            rendered.push_str("${");
            push_var_ref(rendered, reference, env);
            rendered.push(':');
            push_parameter_slice_offset(rendered, offset, offset_ast.as_deref(), env);
            if let Some(length) = length {
                rendered.push(':');
                push_arithmetic_source_text(rendered, length, length_ast.as_deref(), env);
            }
            rendered.push('}');
        }
        WordPart::IndirectExpansion {
            reference,
            operator,
            operand,
            colon_variant,
            ..
        } => {
            rendered.push_str("${!");
            push_var_ref(rendered, reference, env);
            if let Some(operator) = operator {
                if *colon_variant {
                    rendered.push(':');
                }
                rendered.push_str(parameter_defaulting_operator(operator.as_ref()));
                if let Some(operand) = operand {
                    push_parameter_operand(rendered, operand, env);
                }
            }
            rendered.push('}');
        }
        WordPart::PrefixMatch { prefix, kind } => {
            std::write!(rendered, "${{!{}{}}}", prefix, kind.as_char())?;
        }
        WordPart::Transformation { .. } | WordPart::ZshQualifiedGlob(_) => {
            rendered.push_str(span.slice(source));
        }
    }

    Ok(())
}

pub(super) fn push_braced_var_ref(
    rendered: &mut String,
    prefix: &str,
    reference: &VarRef,
    context: RenderContext<'_, '_>,
) {
    rendered.push_str("${");
    rendered.push_str(prefix);
    push_var_ref(rendered, reference, context);
    rendered.push('}');
}

pub(super) fn literal_ends_with_line_indent_for_word_part(literal: &str) -> bool {
    let Some((_, suffix)) = literal.rsplit_once('\n') else {
        return false;
    };
    suffix.chars().all(|ch| matches!(ch, ' ' | '\t'))
}

pub(super) fn preferred_raw_word_part_source<'a>(
    part: &WordPart,
    span: shuck_ast::Span,
    context: RenderContext<'a, '_>,
) -> Option<&'a str> {
    let source = context.source;
    let options = context.options;
    if options.simplify() || options.minify() {
        return None;
    }

    match part {
        WordPart::SingleQuoted { .. } => raw_source_slice(span, source),
        WordPart::DoubleQuoted { parts, .. } => {
            let raw = raw_source_slice(span, source)?;
            let has_formattable_parts = word_part_nodes_any(parts, &mut |part| {
                word_part_needs_formatter_rendering(part, context)
            });
            (!has_formattable_parts).then_some(raw)
        }
        WordPart::Parameter(parameter) => {
            let raw = raw_source_slice(span, source)?;
            (parameter_prefers_raw_source(parameter, span, source)
                && !parameter_raw_subscript_needs_compaction(parameter, context)
                && !raw_parameter_command_spacing_would_change(raw))
            .then_some(raw)
        }
        WordPart::ParameterExpansion { .. } => {
            let raw = raw_source_slice(span, source)?;
            (!raw_parameter_command_spacing_would_change(raw)).then_some(raw)
        }
        WordPart::Substring {
            offset_ast,
            length_ast,
            ..
        }
        | WordPart::ArraySlice {
            offset_ast,
            length_ast,
            ..
        } => (!(offset_ast.is_some() || length_ast.is_some()))
            .then(|| raw_source_slice(span, source))
            .flatten(),
        _ => None,
    }
}

pub(super) fn parameter_raw_subscript_needs_compaction(
    parameter: &shuck_ast::ParameterExpansion,
    context: RenderContext<'_, '_>,
) -> bool {
    let source = context.source;
    if parameter_bourne_operand_needs_subscript_compaction(parameter, source) {
        return true;
    }
    if let Some(subscript) = parameter_bourne_subscript(parameter) {
        let syntax = subscript.syntax_text(source);
        if let Some(ast) = subscript.arithmetic_ast.as_ref()
            && arithmetic_subscript_prefers_spaced_expression(syntax)
        {
            let mut rendered = String::new();
            render_arithmetic_subscript_expr_to_buf(&mut rendered, ast, context, false);
            return rendered != syntax;
        }
        return compact_dynamic_arithmetic_subscript(syntax) != syntax;
    }
    if parameter.bourne().is_some() {
        return false;
    }
    let raw = parameter.raw_body.slice(source);
    compact_raw_parameter_subscript(raw) != raw
}

pub(super) fn parameter_bourne_subscript(
    parameter: &shuck_ast::ParameterExpansion,
) -> Option<&shuck_ast::Subscript> {
    let reference = match parameter.bourne()? {
        BourneParameterExpansion::Access { reference }
        | BourneParameterExpansion::Length { reference }
        | BourneParameterExpansion::Indices { reference }
        | BourneParameterExpansion::Indirect { reference, .. }
        | BourneParameterExpansion::Slice { reference, .. }
        | BourneParameterExpansion::Operation { reference, .. }
        | BourneParameterExpansion::Transformation { reference, .. } => reference,
        BourneParameterExpansion::PrefixMatch { .. } => return None,
    };
    reference.subscript.as_deref()
}

pub(super) fn push_unquoted_literal(rendered: &mut String, literal: &str) {
    if !literal.contains("\\\n") && !literal.contains("\\\r\n") {
        rendered.push_str(literal);
        return;
    }

    let mut chars = literal.chars().peekable();
    while let Some(ch) = chars.next() {
        if ch == '\\'
            && let Some(skipped_indent) = consume_escaped_newline_indent(&mut chars)
        {
            if skipped_indent {
                rendered.push(' ');
            }
            continue;
        }
        rendered.push(ch);
    }
}

pub(super) fn render_double_quoted_literal(rendered: &mut String, text: &str) {
    for ch in text.chars() {
        match ch {
            '"' | '\\' | '$' | '`' => {
                rendered.push('\\');
                rendered.push(ch);
            }
            _ => rendered.push(ch),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::options::ShellFormatOptions;
    use shuck_ast::{Command, File};
    use shuck_parser::parser::Parser;

    fn parse_with_facts<'source>(
        source: &'source str,
        options: &ShellFormatOptions,
    ) -> (File, ResolvedShellFormatOptions, FormatterFacts<'source>) {
        let resolved = options.resolve_for_format(source, None);
        let file = Parser::with_dialect(source, resolved.dialect())
            .parse()
            .unwrap()
            .file;
        let facts = FormatterFacts::build(source, &file, &resolved);
        (file, resolved, facts)
    }

    fn first_arg_word(file: &File) -> &Word {
        let Command::Simple(command) = &file.body[0].command else {
            panic!("expected first statement to be a simple command");
        };
        command.args.first().expect("expected an argument word")
    }

    fn decision_for_first_arg<'source>(
        source: &'source str,
        options: &ShellFormatOptions,
        preserve_escaped_multiline_words: bool,
    ) -> WordRenderDecision<'source> {
        let (file, resolved, facts) = parse_with_facts(source, options);
        let context = RenderContext::new(source, &resolved, &facts);
        WordRenderDecision::for_word(
            first_arg_word(&file),
            context,
            preserve_escaped_multiline_words,
        )
    }

    #[test]
    fn decision_normalizes_command_substitution_padding_before_structural_rendering() {
        let decision = decision_for_first_arg(
            "echo \"$( printf x )\"\n",
            &ShellFormatOptions::default(),
            true,
        );

        assert_eq!(
            decision,
            WordRenderDecision::NormalizedCommandSubstitutionPadding {
                normalized: "\"$(printf x)\"".to_string()
            }
        );
    }

    #[test]
    fn decision_preserves_escaped_command_substitutions_when_allowed() {
        let decision =
            decision_for_first_arg("echo \"\\$(git)\"\n", &ShellFormatOptions::default(), true);

        assert_eq!(
            decision,
            WordRenderDecision::PreserveEscapedCommandSubstitution {
                raw: "\"\\$(git)\""
            }
        );
    }

    #[test]
    fn decision_normalizes_unquoted_word_continuations_before_fallbacks() {
        let decision =
            decision_for_first_arg("echo foo\\\nbar\n", &ShellFormatOptions::default(), true);

        assert_eq!(
            decision,
            WordRenderDecision::NormalizedUnquotedContinuations {
                normalized: "foobar".to_string()
            }
        );
    }

    #[test]
    fn decision_routes_special_words_through_part_renderer_with_raw_fallback() {
        let decision =
            decision_for_first_arg("echo $(( 1 + 2 ))\n", &ShellFormatOptions::default(), true);

        assert_eq!(
            decision,
            WordRenderDecision::RenderParts {
                raw_fallback: Some("$(( 1 + 2 ))")
            }
        );
    }

    #[test]
    fn decision_uses_raw_fallback_for_preservable_plain_syntax() {
        let decision =
            decision_for_first_arg("echo foo\\ bar\n", &ShellFormatOptions::default(), true);

        assert_eq!(
            decision,
            WordRenderDecision::RenderSyntaxWithRawFallback { raw: "foo\\ bar" }
        );
    }

    #[test]
    fn decision_skips_raw_fallbacks_when_simplifying() {
        let options = ShellFormatOptions::default().with_simplify(true);
        let decision = decision_for_first_arg("echo foo\\ bar\n", &options, true);

        assert_eq!(decision, WordRenderDecision::RenderSyntax);
    }
}
