use super::*;
use crate::render_plan::CompoundBodySite;

pub(crate) fn line_gap_break_count(current_line: usize, next_line: usize) -> usize {
    next_line.saturating_sub(current_line).clamp(1, 2)
}

pub(crate) fn rendered_stmt_end_line_with_heredoc<F>(
    stmt: &Stmt,
    source: &str,
    source_map: &crate::comments::SourceMap<'_>,
    stmt_contains_heredoc: F,
) -> usize
where
    F: Fn(&Stmt) -> bool + Copy,
{
    match &stmt.command {
        Command::Function(_) | Command::AnonymousFunction(_) => {
            span_render_end_line(stmt_span(stmt), source, source_map)
        }
        _ if stmt_contains_heredoc(stmt) => span_render_end_line(
            stmt_verbatim_span_with_source_map(stmt, source_map),
            source,
            source_map,
        ),
        Command::Binary(command) => rendered_stmt_end_line_with_heredoc(
            &command.right,
            source,
            source_map,
            stmt_contains_heredoc,
        ),
        _ => {
            if let Some((commands, open)) = command_group_commands(&stmt.command) {
                let mut span = stmt_group_base_span_with_heredoc(
                    stmt,
                    commands,
                    source_map,
                    open,
                    stmt_contains_heredoc,
                );
                for redirect in &stmt.redirects {
                    span = merge_non_empty_span(span, redirect.span);
                }
                if matches!(stmt.terminator, Some(StmtTerminator::Background(_)))
                    && let Some(terminator_span) = stmt.terminator_span
                {
                    span = merge_non_empty_span(span, terminator_span);
                }
                span_render_end_line(span, source, source_map)
            } else {
                span_render_end_line(stmt_format_span(stmt), source, source_map)
            }
        }
    }
}

pub(crate) fn span_render_end_line(
    span: Span,
    source: &str,
    source_map: &crate::comments::SourceMap<'_>,
) -> usize {
    let mut end = span.end.offset().min(source.len());
    while end > span.start.offset()
        && source
            .as_bytes()
            .get(end - 1)
            .is_some_and(u8::is_ascii_whitespace)
    {
        end -= 1;
    }

    if end == span.start.offset() {
        span.start.line()
    } else {
        source_map.line_number_for_offset(end - 1)
    }
}

pub(crate) fn stmt_has_trailing_comment(
    stmt: &Stmt,
    source_map: &crate::comments::SourceMap<'_>,
) -> bool {
    let raw = stmt_span(stmt);
    let formatted = stmt_format_span(stmt);
    raw.end.offset() > formatted.end.offset()
        && source_map.contains_comment_between(formatted.end.offset(), raw.end.offset())
}

pub(crate) fn should_render_verbatim_with_heredoc(
    stmt: &Stmt,
    source_map: &crate::comments::SourceMap<'_>,
    options: &crate::options::ResolvedShellFormatOptions,
    contains_heredoc: bool,
) -> bool {
    (!options.simplify()
        && matches!(&stmt.command, Command::Simple(command) if simple_command_uses_synthetic_words(command, source_map.source())))
        || (options.keep_padding() && stmt_has_alignment_sensitive_padding(stmt, source_map))
        || (contains_heredoc
            && !matches!(stmt.command, Command::Binary(_))
            && stmt_has_trailing_comment(stmt, source_map))
}

pub(crate) fn stmt_attachment_span_with_heredoc<F>(
    stmt: &Stmt,
    source: &str,
    source_map: &crate::comments::SourceMap<'_>,
    options: &crate::options::ResolvedShellFormatOptions,
    stmt_contains_heredoc: F,
) -> Span
where
    F: Fn(&Stmt) -> bool + Copy,
{
    stmt_attachment_span_with_heredoc_and_compound_close(
        stmt,
        source,
        source_map,
        options,
        stmt_contains_heredoc,
        stmt_compound_close_span(stmt, source, source_map),
    )
}

pub(crate) fn stmt_attachment_span_with_heredoc_and_compound_close<F>(
    stmt: &Stmt,
    source: &str,
    source_map: &crate::comments::SourceMap<'_>,
    options: &crate::options::ResolvedShellFormatOptions,
    stmt_contains_heredoc: F,
    compound_close_span: Option<Span>,
) -> Span
where
    F: Fn(&Stmt) -> bool + Copy,
{
    let span = if should_render_verbatim_with_heredoc(
        stmt,
        source_map,
        options,
        stmt_contains_heredoc(stmt),
    ) {
        stmt_verbatim_span_with_source_map(stmt, source_map)
    } else if let Command::Function(command) = &stmt.command {
        function_attachment_span(command)
    } else if let Command::AnonymousFunction(command) = &stmt.command {
        anonymous_function_attachment_span(command)
    } else if let Some((commands, open)) = command_group_commands(&stmt.command) {
        stmt.redirects.iter().fold(
            stmt_group_base_span_with_heredoc(
                stmt,
                commands,
                source_map,
                open,
                stmt_contains_heredoc,
            ),
            |span, redirect| span.merge(redirect.span),
        )
    } else {
        complete_stmt_span(
            stmt,
            command_attachment_span_with_heredoc(
                &stmt.command,
                source,
                source_map,
                options,
                stmt_contains_heredoc,
            ),
        )
    };
    extend_compound_close_suffix_attachment_span(span, compound_close_span, source_map)
}

fn extend_compound_close_suffix_attachment_span(
    span: Span,
    close_span: Option<Span>,
    source_map: &crate::comments::SourceMap<'_>,
) -> Span {
    let Some(close_span) = close_span else {
        return span;
    };
    if let Some(comment) = source_map.suffix_comment_after_span(close_span) {
        merge_non_empty_span(span, comment.span())
    } else {
        span
    }
}

pub(crate) fn stmt_compound_close_span(
    stmt: &Stmt,
    source: &str,
    source_map: &crate::comments::SourceMap<'_>,
) -> Option<Span> {
    let Command::Compound(command) = &stmt.command else {
        return None;
    };
    compound_close_span(command, source, source_map)
}

pub(crate) fn compound_close_span(
    command: &CompoundCommand,
    source: &str,
    source_map: &crate::comments::SourceMap<'_>,
) -> Option<Span> {
    match command {
        CompoundCommand::If(command) => Some(if_close_span(command, source, source_map)),
        CompoundCommand::Case(command) => source_map
            .close_delimiter_span(command.span, CloseDelimiterKind::Esac)
            .or_else(|| {
                Some(normalized_close_keyword_span(
                    source,
                    source_map,
                    command.esac_span,
                    "esac",
                ))
            }),
        _ => CompoundBodySite::single_body_command(command, source_map)
            .and_then(|site| site.close_span()),
    }
}

pub(crate) fn if_close_span(
    command: &IfCommand,
    source: &str,
    source_map: &crate::comments::SourceMap<'_>,
) -> Span {
    let (syntax_close, keyword) = match command.syntax {
        IfSyntax::ThenFi { fi_span, .. } => (fi_span, "fi"),
        IfSyntax::Brace {
            right_brace_span, ..
        } => (right_brace_span, "}"),
    };
    let kind = match command.syntax {
        IfSyntax::ThenFi { .. } => CloseDelimiterKind::Fi,
        IfSyntax::Brace { .. } => CloseDelimiterKind::RightBrace,
    };
    if let Some(indexed_close) = source_map.close_delimiter_span(command.span, kind) {
        return indexed_close;
    }
    normalized_close_keyword_span(source, source_map, syntax_close, keyword)
}

pub(crate) fn done_close_span(
    source: &str,
    source_map: &crate::comments::SourceMap<'_>,
    span: Span,
    fallback: Option<Span>,
) -> Option<Span> {
    if let Some(indexed_close) = source_map.close_delimiter_span(span, CloseDelimiterKind::Done) {
        return Some(indexed_close);
    }

    if let Some(fallback) = fallback {
        let normalized = normalized_close_keyword_span(source, source_map, fallback, "done");
        if span_starts_with_keyword(source, normalized, "done") {
            return Some(normalized);
        }
    }

    close_keyword_at_span_end(source, source_map, span, "done").or_else(|| {
        fallback.map(|span| normalized_close_keyword_span(source, source_map, span, "done"))
    })
}

fn close_keyword_at_span_end(
    source: &str,
    source_map: &crate::comments::SourceMap<'_>,
    span: Span,
    keyword: &str,
) -> Option<Span> {
    let span_end = span.end.offset().min(source.len());
    let start = span_end.checked_sub(keyword.len())?;
    (source.get(start..span_end) == Some(keyword))
        .then(|| source_map.span_for_offsets(start, span_end))
}

pub(crate) fn normalized_close_keyword_span(
    source: &str,
    source_map: &crate::comments::SourceMap<'_>,
    span: Span,
    keyword: &str,
) -> Span {
    let start = span.start.offset().min(source.len());
    let end = start.saturating_add(keyword.len()).min(source.len());
    if source.get(start..end) == Some(keyword) {
        source_map.span_for_offsets(start, end)
    } else {
        span
    }
}

fn span_starts_with_keyword(source: &str, span: Span, keyword: &str) -> bool {
    let start = span.start.offset().min(source.len());
    let end = start.saturating_add(keyword.len()).min(source.len());
    source.get(start..end) == Some(keyword)
}

fn command_attachment_span_with_heredoc<F>(
    command: &Command,
    source: &str,
    source_map: &crate::comments::SourceMap<'_>,
    options: &crate::options::ResolvedShellFormatOptions,
    stmt_contains_heredoc: F,
) -> Span
where
    F: Fn(&Stmt) -> bool + Copy,
{
    match command {
        Command::Binary(command) => stmt_attachment_span_with_heredoc(
            &command.left,
            source,
            source_map,
            options,
            stmt_contains_heredoc,
        )
        .merge(stmt_attachment_span_with_heredoc(
            &command.right,
            source,
            source_map,
            options,
            stmt_contains_heredoc,
        )),
        _ => command_format_span(command),
    }
}

pub(crate) fn stmt_render_start_line_with_heredoc<F>(
    stmt: &Stmt,
    source: &str,
    source_map: &crate::comments::SourceMap<'_>,
    options: &crate::options::ResolvedShellFormatOptions,
    stmt_contains_heredoc: F,
) -> usize
where
    F: Fn(&Stmt) -> bool + Copy,
{
    if let Some((commands, open)) = command_group_commands(&stmt.command) {
        group_render_start_line_with_heredoc(
            stmt,
            commands.as_slice(),
            source,
            source_map,
            open,
            options,
            stmt_contains_heredoc,
        )
    } else {
        stmt_attachment_span_with_heredoc(stmt, source, source_map, options, stmt_contains_heredoc)
            .start
            .line()
    }
}

fn group_render_start_line_with_heredoc<F>(
    stmt: &Stmt,
    commands: &[Stmt],
    source: &str,
    source_map: &crate::comments::SourceMap<'_>,
    open: char,
    options: &crate::options::ResolvedShellFormatOptions,
    stmt_contains_heredoc: F,
) -> usize
where
    F: Fn(&Stmt) -> bool + Copy,
{
    group_attachment_span_with_heredoc(
        commands,
        source_map,
        open,
        matching_group_close(open),
        stmt_contains_heredoc,
    )
    .map(|span| span.start.line())
    .or_else(|| {
        find_empty_group_open_offset(source, stmt_span(stmt).start.offset(), open)
            .map(|offset| source_map.line_number_for_offset(offset))
    })
    .unwrap_or_else(|| {
        stmt_attachment_span_with_heredoc(stmt, source, source_map, options, stmt_contains_heredoc)
            .start
            .line()
    })
}

fn stmt_has_alignment_sensitive_padding(
    stmt: &Stmt,
    source_map: &crate::comments::SourceMap<'_>,
) -> bool {
    let mut spans = stmt_token_spans(stmt);
    spans.retain(|span| span != &Span::new() && span.start.offset() < span.end.offset());
    spans.sort_by_key(|span| span.start.offset());
    spans.windows(2).any(|window| {
        let [left, right] = window else {
            return false;
        };
        if right.start.offset() <= left.end.offset() {
            return false;
        }
        source_map.has_alignment_padding_between(left.end.offset(), right.start.offset())
    })
}

pub(crate) fn case_item_was_inline_in_source(item: &CaseItem) -> bool {
    let Some(stmt) = item.body.first() else {
        return false;
    };

    item.patterns
        .last()
        .is_some_and(|pattern| pattern.span.end.line() == stmt_span(stmt).start.line())
        && item
            .terminator_span
            .is_some_and(|span| span.start.line() == stmt_format_span(stmt).end.line())
}

pub(crate) fn case_item_body_upper_bound(item: &CaseItem, fallback: usize) -> Option<usize> {
    Some(
        item.terminator_span
            .map(|span| span.start.offset())
            .unwrap_or(fallback),
    )
}

fn command_token_spans(command: &Command) -> Vec<Span> {
    match command {
        Command::Simple(command) => {
            let mut spans = command
                .assignments
                .iter()
                .map(|assignment| assignment.span)
                .collect::<Vec<_>>();
            if !command.name.parts.is_empty() {
                spans.push(command.name.span);
            }
            spans.extend(command.args.iter().map(|word| word.span));
            spans
        }
        Command::Builtin(command) => {
            let (span, name, assignments, primary, extra_args) = builtin_like_parts(command);
            builtin_like_token_spans(span.start, name, assignments, primary, extra_args)
        }
        Command::Decl(command) => {
            let mut spans = command
                .assignments
                .iter()
                .map(|assignment| assignment.span)
                .collect::<Vec<_>>();
            spans.push(command.variant_span);
            spans.extend(command.operands.iter().map(|operand| match operand {
                DeclOperand::Flag(word) | DeclOperand::Dynamic(word) => word.span,
                DeclOperand::Name(name) => name.span,
                DeclOperand::Assignment(assignment) => assignment.span,
            }));
            spans
        }
        Command::Binary(command) => vec![command.span],
        Command::Compound(command) => vec![compound_format_span(command)],
        Command::Function(command) => vec![
            function_header_span(command),
            stmt_format_span(&command.body),
        ],
        Command::AnonymousFunction(command) => {
            let mut spans = vec![
                anonymous_function_header_span(command),
                stmt_format_span(&command.body),
            ];
            spans.extend(command.args.iter().map(|argument| argument.span));
            spans
        }
    }
}

fn builtin_like_token_spans(
    start: shuck_ast::Position,
    name: &str,
    assignments: &[Assignment],
    primary: Option<&shuck_ast::Word>,
    extra_args: &[shuck_ast::Word],
) -> Vec<Span> {
    let mut spans = assignments
        .iter()
        .map(|assignment| assignment.span)
        .collect::<Vec<_>>();
    spans.push(Span::from_positions(start, start.advanced_by(name)));
    if let Some(primary) = primary {
        spans.push(primary.span);
    }
    spans.extend(extra_args.iter().map(|argument| argument.span));
    spans
}

fn stmt_token_spans(stmt: &Stmt) -> Vec<Span> {
    let mut spans = if stmt.negated {
        vec![Span::from_positions(
            stmt.span.start,
            stmt.span.start.advanced_by("!"),
        )]
    } else {
        Vec::new()
    };
    spans.extend(command_token_spans(&stmt.command));
    spans.extend(stmt.redirects.iter().map(|redirect| redirect.span));
    if matches!(stmt.terminator, Some(StmtTerminator::Background(_)))
        && let Some(terminator_span) = stmt.terminator_span
    {
        spans.push(terminator_span);
    }
    spans
}
