use super::*;

pub(super) fn stmt_semicolon_terminator_starts_on_continuation_line(
    stmt: &Stmt,
    source_map: &SourceMap<'_>,
) -> bool {
    let Some(terminator_span) = stmt.terminator_span else {
        return false;
    };
    let render_end = stmt
        .redirects
        .last()
        .map(|redirect| redirect.span.end.offset())
        .unwrap_or_else(|| command_format_span(&stmt.command).end.offset());
    source_map.contains_newline_between(render_end, terminator_span.start.offset())
}

pub(super) fn stmt_rendered_end_line_after_format(
    stmt: &Stmt,
    source: &str,
    source_map: &SourceMap<'_>,
    facts: &FormatterFacts,
    fallback: usize,
) -> usize {
    if matches!(stmt.terminator, Some(StmtTerminator::Semicolon))
        && stmt_semicolon_terminator_starts_on_continuation_line(stmt, source_map)
        && let Some(terminator_span) = stmt.terminator_span
    {
        return terminator_span.start.line();
    }
    match &stmt.command {
        Command::Binary(command) => {
            return stmt_rendered_end_line_after_format(
                command.right.as_ref(),
                source,
                source_map,
                facts,
                fallback,
            );
        }
        _ if stmt.redirects.is_empty() && stmt.terminator.is_none() => {
            if let Some((commands, open)) = command_group_commands(&stmt.command)
                && let Some(span) = facts
                    .sequence(commands, Some(stmt_span(stmt).end.offset()))
                    .group_attachment_span()
            {
                let close = matching_group_close(open);
                let close_offset = group_close_offset(
                    source,
                    span,
                    Some(stmt_span(stmt).end.offset()),
                    close,
                    close.len_utf8(),
                );
                return source_map.line_number_for_offset(close_offset);
            }
        }
        _ => {}
    }
    fallback
}

pub(super) fn gap_has_blank_line(source: &str, start: usize, end: usize) -> bool {
    SourceView::new(source)
        .slice_between(start, end)
        .is_some_and(|gap| gap.bytes().filter(|byte| *byte == b'\n').count() >= 2)
}

pub(super) fn group_close_offset(
    source: &str,
    span: Span,
    upper_bound: Option<usize>,
    close_char: char,
    close_len: usize,
) -> usize {
    let fallback = span.end.offset().saturating_sub(close_len);
    let search_end = upper_bound
        .map(|offset| offset.saturating_add(close_len))
        .unwrap_or(span.end.offset())
        .min(source.len())
        .max(span.start.offset());
    source
        .get(span.start.offset()..search_end)
        .and_then(|text| text.rfind(close_char))
        .map_or(fallback, |offset| span.start.offset() + offset)
}

pub(super) fn trim_trailing_gap_before_offset(source: &str, mut offset: usize) -> usize {
    let bytes = source.as_bytes();
    while offset > 0 && matches!(bytes[offset - 1], b' ' | b'\t' | b'\r' | b'\n') {
        offset -= 1;
    }
    offset
}
