use shuck_ast::{
    Command, CompoundCommand, IfCommand, IfSyntax, Span, Stmt, StmtSeq, StmtTerminator,
};

use crate::command::{stmt_format_span, stmt_span};
use crate::context::RenderContext;

pub(crate) fn can_inline_body(
    context: RenderContext<'_, '_>,
    commands: &StmtSeq,
    enclosing_span: Span,
) -> bool {
    can_inline_body_with_upper_bound(
        context,
        commands,
        enclosing_span,
        Some(enclosing_span.end.offset()),
    )
}

pub(crate) fn can_inline_body_with_upper_bound(
    context: RenderContext<'_, '_>,
    commands: &StmtSeq,
    enclosing_span: Span,
    upper_bound: Option<usize>,
) -> bool {
    let [command] = commands.as_slice() else {
        return false;
    };
    if matches!(command.terminator, Some(StmtTerminator::Background(_)))
        || !can_inline_stmt(context, command)
    {
        return false;
    }

    if context.facts.sequence(commands, upper_bound).has_comments() {
        return false;
    }

    context.options.compact_layout()
        || stmt_span(command).start.line() == enclosing_span.start.line()
}

pub(crate) fn can_inline_stmt(context: RenderContext<'_, '_>, stmt: &Stmt) -> bool {
    let stmt_facts = context.facts.stmt(stmt);
    if stmt_facts.preserve_verbatim() || stmt_facts.has_trailing_comment() {
        return false;
    }

    matches!(
        &stmt.command,
        Command::Simple(_)
            | Command::Builtin(_)
            | Command::Decl(_)
            | Command::Function(_)
            | Command::Binary(_)
            | Command::Compound(
                CompoundCommand::Conditional(_)
                    | CompoundCommand::Arithmetic(_)
                    | CompoundCommand::Time(_)
            )
    )
}

pub(crate) fn can_inline_else_branch_close(
    context: RenderContext<'_, '_>,
    command: &IfCommand,
    body: &StmtSeq,
    fi_span: Span,
) -> bool {
    let [stmt] = body.as_slice() else {
        return false;
    };
    if matches!(stmt.terminator, Some(StmtTerminator::Background(_)))
        || !can_inline_stmt(context, stmt)
        || context
            .facts
            .sequence(body, Some(fi_span.start.offset()))
            .has_comments()
    {
        return false;
    };
    let Some((_, else_offset)) = context
        .facts
        .if_next_branch_region(command, command.elif_branches.len())
    else {
        return false;
    };
    let else_line = context.source_map().line_number_for_offset(else_offset);
    let body_line = stmt_span(stmt).start.line();
    else_line == body_line && body_line == fi_span.start.line()
}

pub(crate) fn can_inline_if_chain(
    context: RenderContext<'_, '_>,
    command: &IfCommand,
    fi_span: Span,
) -> bool {
    if command.elif_branches.is_empty() || command.span.start.line() != fi_span.end.line() {
        return false;
    }

    if !can_inline_body_with_upper_bound(
        context,
        &command.then_branch,
        command.span,
        Some(context.facts.if_branch_upper_bound(command, 0)),
    ) {
        return false;
    }

    for (index, (_, body)) in command.elif_branches.iter().enumerate() {
        if !can_inline_body_with_upper_bound(
            context,
            body,
            command.span,
            Some(context.facts.if_branch_upper_bound(command, index + 1)),
        ) {
            return false;
        }
    }

    command.else_branch.as_ref().is_none_or(|body| {
        can_inline_body_with_upper_bound(context, body, command.span, Some(fi_span.start.offset()))
    })
}

pub(crate) fn then_branch_starts_with_inline_if(
    context: RenderContext<'_, '_>,
    command: &IfCommand,
    then_span: Span,
    fi_span: Span,
) -> bool {
    if command.span.start.line() != fi_span.end.line() {
        return false;
    }
    let [stmt] = command.then_branch.as_slice() else {
        return false;
    };
    if stmt.negated || !stmt.redirects.is_empty() || stmt.terminator.is_some() {
        return false;
    }
    let Command::Compound(CompoundCommand::If(inner)) = &stmt.command else {
        return false;
    };
    matches!(inner.syntax, IfSyntax::ThenFi { .. })
        && then_span.end.line() == inner.span.start.line()
        && !context
            .facts
            .sequence(
                &command.then_branch,
                Some(context.facts.if_branch_upper_bound(command, 0)),
            )
            .has_comments()
}

pub(crate) fn can_inline_group(
    context: RenderContext<'_, '_>,
    commands: &StmtSeq,
    open_char: char,
) -> bool {
    let [command] = commands.as_slice() else {
        return false;
    };

    can_inline_stmt(context, command)
        && can_inline_body(context, commands, stmt_span(command))
        && (stmt_span(command).start.line() == stmt_span(command).end.line()
            || group_delimiters_attach_to_wrapped_body(context, commands, open_char))
}

pub(crate) fn group_has_inline_source_shape(
    context: RenderContext<'_, '_>,
    commands: &StmtSeq,
    open_char: char,
) -> bool {
    context.facts.group_was_inline_in_source(commands)
        || group_delimiters_attach_to_wrapped_body(context, commands, open_char)
}

pub(crate) fn group_delimiters_attach_to_wrapped_body(
    context: RenderContext<'_, '_>,
    commands: &StmtSeq,
    _open_char: char,
) -> bool {
    let (Some(first), Some(last)) = (commands.first(), commands.last()) else {
        return false;
    };
    let Some(group_span) = context
        .facts
        .sequence(commands, None)
        .group_attachment_span()
    else {
        return false;
    };

    group_span.start.line() == stmt_format_span(first).start.line()
        && group_span.end.line() == stmt_format_span(last).end.line()
}

pub(crate) fn can_inline_source_line_subshell(
    context: RenderContext<'_, '_>,
    commands: &StmtSeq,
    upper_bound: Option<usize>,
) -> bool {
    let [stmt] = commands.as_slice() else {
        return false;
    };
    if context.facts.sequence(commands, upper_bound).has_comments()
        || context.facts.stmt(stmt).preserve_verbatim()
        || context.facts.stmt(stmt).has_trailing_comment()
    {
        return false;
    }
    if commands.span.start.line() != commands.span.end.line() {
        return false;
    }

    true
}

pub(crate) fn can_format_multiline_subshell_inline(
    context: RenderContext<'_, '_>,
    commands: &StmtSeq,
    upper_bound: Option<usize>,
) -> bool {
    let [stmt] = commands.as_slice() else {
        return false;
    };
    if context
        .facts
        .sequence(commands, upper_bound)
        .group_open_suffix_span()
        .is_some()
        || context.facts.sequence(commands, upper_bound).has_comments()
    {
        return false;
    }
    let Some(group_span) = context
        .facts
        .sequence(commands, upper_bound)
        .group_attachment_span()
    else {
        return false;
    };
    let group_source = group_span.slice(context.source);
    if !group_source.contains('\n')
        || group_source.contains("\\\n")
        || group_source.contains("\\\r\n")
    {
        return false;
    }

    let first_start = stmt_span(stmt).start.offset().min(context.source.len());
    let open_end = group_span.start.offset().saturating_add('('.len_utf8());
    if context
        .source
        .get(open_end..first_start)
        .is_none_or(|between| between.contains('\n'))
    {
        return false;
    }

    let close_offset =
        group_close_offset(context.source, group_span, upper_bound, ')', ')'.len_utf8());
    let stmt_end = stmt_span(stmt)
        .end
        .offset()
        .min(close_offset)
        .min(context.source.len());
    context
        .source
        .get(stmt_end..close_offset)
        .is_some_and(|between| !between.contains('\n'))
}

pub(crate) fn body_starts_with_inline_do_brace_group(
    context: RenderContext<'_, '_>,
    body: &StmtSeq,
) -> bool {
    let Some(CompoundCommand::BraceGroup(commands)) = single_unadorned_compound_stmt(body) else {
        return false;
    };
    let Some(group_span) = context
        .facts
        .sequence(commands, None)
        .group_attachment_span()
    else {
        return false;
    };
    source_line_before_offset_ends_with_do(context.source, group_span.start.offset())
}

pub(crate) fn body_starts_with_inline_do_if(
    context: RenderContext<'_, '_>,
    body: &StmtSeq,
) -> bool {
    let Some(CompoundCommand::If(command)) = single_unadorned_compound_stmt(body) else {
        return false;
    };
    if !matches!(command.syntax, IfSyntax::ThenFi { .. }) {
        return false;
    }
    source_line_before_offset_ends_with_do(context.source, command.span.start.offset())
}

pub(crate) fn inline_do_brace_group_done_separator(
    context: RenderContext<'_, '_>,
    body: &StmtSeq,
    enclosing_span: Span,
) -> &'static str {
    let [stmt] = body.as_slice() else {
        return "; ";
    };
    let Command::Compound(CompoundCommand::BraceGroup(commands)) = &stmt.command else {
        return "; ";
    };
    let Some(group_span) = context
        .facts
        .sequence(commands, None)
        .group_attachment_span()
    else {
        return "; ";
    };
    let between = context
        .source
        .get(group_span.end.offset()..enclosing_span.end.offset())
        .unwrap_or_default()
        .trim_start_matches([' ', '\t', '\r']);
    if between.starts_with(';') {
        return "; ";
    }
    if brace_group_last_stmt_allows_done_without_semicolon(commands) {
        " "
    } else {
        "; "
    }
}

fn single_unadorned_compound_stmt(body: &StmtSeq) -> Option<&CompoundCommand> {
    let [stmt] = body.as_slice() else {
        return None;
    };
    if stmt.negated || !stmt.redirects.is_empty() || stmt.terminator.is_some() {
        return None;
    }
    match &stmt.command {
        Command::Compound(command) => Some(command),
        _ => None,
    }
}

fn source_line_before_offset_ends_with_do(source: &str, offset: usize) -> bool {
    let line_start = source[..offset]
        .rfind('\n')
        .map_or(0, |offset| offset.saturating_add(1));
    source[line_start..offset]
        .trim_end_matches([' ', '\t', '\r'])
        .ends_with("do")
}

fn brace_group_last_stmt_allows_done_without_semicolon(commands: &StmtSeq) -> bool {
    let Some(last) = commands.last() else {
        return false;
    };
    command_allows_done_without_semicolon(&last.command)
}

fn command_allows_done_without_semicolon(command: &Command) -> bool {
    match command {
        Command::Compound(command) => compound_allows_done_without_semicolon(command),
        Command::Binary(binary) => command_allows_done_without_semicolon(&binary.right.command),
        _ => false,
    }
}

fn compound_allows_done_without_semicolon(command: &CompoundCommand) -> bool {
    match command {
        CompoundCommand::Case(_) => true,
        CompoundCommand::BraceGroup(commands) => {
            brace_group_last_stmt_allows_done_without_semicolon(commands)
        }
        CompoundCommand::For(command) => {
            brace_group_last_stmt_allows_done_without_semicolon(&command.body)
        }
        CompoundCommand::Repeat(command) => {
            brace_group_last_stmt_allows_done_without_semicolon(&command.body)
        }
        CompoundCommand::While(command) => {
            brace_group_last_stmt_allows_done_without_semicolon(&command.body)
        }
        CompoundCommand::Until(command) => {
            brace_group_last_stmt_allows_done_without_semicolon(&command.body)
        }
        CompoundCommand::Select(command) => {
            brace_group_last_stmt_allows_done_without_semicolon(&command.body)
        }
        CompoundCommand::ArithmeticFor(command) => {
            brace_group_last_stmt_allows_done_without_semicolon(&command.body)
        }
        _ => false,
    }
}

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)
}
