use super::*;

pub(crate) fn stmt_verbatim_span_with_source_map(stmt: &Stmt, source_map: &SourceMap<'_>) -> Span {
    stmt_verbatim_span_impl(stmt, source_map)
}

pub(super) fn stmt_verbatim_span_impl(stmt: &Stmt, source_map: &SourceMap<'_>) -> Span {
    let source = source_map.source();
    let command_span = if let Command::Simple(command) = &stmt.command
        && simple_command_uses_synthetic_words(command, source)
    {
        synthetic_simple_command_verbatim_span(command, source_map)
    } else {
        command_verbatim_span(&stmt.command, source_map)
    };
    let mut span = command_span;
    for redirect in &stmt.redirects {
        span = merge_non_empty_span(span, redirect.span);
        if let Some(heredoc) = redirect.heredoc() {
            span = span.merge(extend_heredoc_body_span(heredoc.body.span, source));
        }
    }
    if stmt.negated {
        span = merge_non_empty_span(stmt.span, span);
    }
    non_empty_or_stmt_span(stmt, merge_stmt_background_span(stmt, span))
}

fn command_verbatim_span(command: &Command, source_map: &SourceMap<'_>) -> Span {
    match command {
        Command::Simple(command) => command.span,
        Command::Builtin(command) => builtin_like_parts(command).0,
        Command::Decl(command) => command.span,
        Command::Binary(command) => stmt_verbatim_span_impl(&command.left, source_map)
            .merge(stmt_verbatim_span_impl(&command.right, source_map)),
        Command::Compound(command) => compound_verbatim_span(command, source_map),
        Command::Function(command) => function_header_span(command)
            .merge(function_body_verbatim_span(&command.body, source_map)),
        Command::AnonymousFunction(command) => anonymous_function_header_span(command)
            .merge(function_body_verbatim_span(&command.body, source_map))
            .merge(words_span(&command.args)),
    }
}

fn function_body_verbatim_span(body: &Stmt, source_map: &SourceMap<'_>) -> Span {
    let mut span = stmt_verbatim_span_impl(body, source_map);
    if let Some(group_span) = command_group_attachment_span(&body.command, source_map) {
        span = merge_non_empty_span(span, group_span);
    }
    span
}

pub(crate) fn stmt_span(stmt: &Stmt) -> Span {
    merge_stmt_background_span(stmt, merge_stmt_redirect_spans(stmt, stmt.span))
}

pub(super) fn complete_stmt_span(stmt: &Stmt, span: Span) -> Span {
    non_empty_or_stmt_span(
        stmt,
        merge_stmt_background_span(stmt, merge_stmt_redirect_spans(stmt, span)),
    )
}

fn merge_stmt_redirect_spans(stmt: &Stmt, mut span: Span) -> Span {
    for redirect in &stmt.redirects {
        span = merge_non_empty_span(span, redirect.span);
    }
    span
}

fn merge_stmt_background_span(stmt: &Stmt, mut span: Span) -> Span {
    if matches!(stmt.terminator, Some(StmtTerminator::Background(_)))
        && let Some(terminator_span) = stmt.terminator_span
    {
        span = merge_non_empty_span(span, terminator_span);
    }
    span
}

fn non_empty_or_stmt_span(stmt: &Stmt, span: Span) -> Span {
    if span == Span::new() {
        stmt_span(stmt)
    } else {
        span
    }
}

fn compound_span(command: &CompoundCommand) -> Span {
    match command {
        CompoundCommand::If(command) => command.span,
        CompoundCommand::For(command) => command.span,
        CompoundCommand::Repeat(command) => command.span,
        CompoundCommand::Foreach(command) => command.span,
        CompoundCommand::ArithmeticFor(command) => command.span,
        CompoundCommand::While(command) => command.span,
        CompoundCommand::Until(command) => command.span,
        CompoundCommand::Case(command) => command.span,
        CompoundCommand::Select(command) => command.span,
        CompoundCommand::Subshell(commands) | CompoundCommand::BraceGroup(commands) => commands
            .iter()
            .map(stmt_span)
            .reduce(Span::merge)
            .unwrap_or_default(),
        CompoundCommand::Arithmetic(command) => command.span,
        CompoundCommand::Time(command) => command.span,
        CompoundCommand::Conditional(command) => command.span,
        CompoundCommand::Coproc(command) => command.span,
        CompoundCommand::Always(command) => command.span,
    }
}

fn compound_verbatim_span(command: &CompoundCommand, source_map: &SourceMap<'_>) -> Span {
    match command {
        CompoundCommand::Subshell(commands) => {
            group_verbatim_span_impl(commands.as_slice(), source_map, '(', ')')
        }
        CompoundCommand::BraceGroup(commands) => {
            group_verbatim_span_impl(commands.as_slice(), source_map, '{', '}')
        }
        _ => compound_verbatim_span_from_children(command, source_map),
    }
}

fn compound_verbatim_span_from_children(
    command: &CompoundCommand,
    source_map: &SourceMap<'_>,
) -> Span {
    let mut span = compound_span(command);
    for_each_compound_child(command, |child| {
        span = match child {
            CompoundChild::Stmt(stmt) => span.merge(stmt_verbatim_span_impl(stmt, source_map)),
            CompoundChild::Sequence(sequence) => {
                merge_stmt_sequence_verbatim_span(span, sequence, source_map)
            }
        };
    });
    span
}

fn merge_stmt_sequence_verbatim_span(
    mut span: Span,
    commands: &StmtSeq,
    source_map: &SourceMap<'_>,
) -> Span {
    for command in commands.iter() {
        span = merge_non_empty_span(span, stmt_verbatim_span_impl(command, source_map));
    }
    span
}

pub(crate) fn command_format_span(command: &Command) -> Span {
    match command {
        Command::Simple(command) => simple_command_format_span(command),
        Command::Builtin(command) => {
            let (span, name, assignments, primary, extra_args) = builtin_like_parts(command);
            builtin_like_span(span.start, name, assignments, primary, extra_args)
        }
        Command::Decl(command) => decl_clause_format_span(command),
        Command::Binary(command) => {
            stmt_format_span(&command.left).merge(stmt_format_span(&command.right))
        }
        Command::Compound(command) => compound_format_span(command),
        Command::Function(command) => function_attachment_span(command),
        Command::AnonymousFunction(command) => anonymous_function_attachment_span(command),
    }
}

fn simple_command_format_span(command: &SimpleCommand) -> Span {
    let mut span = Span::new();
    for assignment in &command.assignments {
        span = merge_non_empty_span(span, assignment.span);
    }
    if !command.name.parts.is_empty() {
        span = merge_non_empty_span(span, command.name.span);
    }
    for argument in &command.args {
        span = merge_non_empty_span(span, argument.span);
    }
    if span == Span::new() {
        command.span
    } else {
        span
    }
}

fn builtin_like_span(
    start: shuck_ast::Position,
    name: &str,
    assignments: &[Assignment],
    primary: Option<&shuck_ast::Word>,
    extra_args: &[shuck_ast::Word],
) -> Span {
    let mut span = Span::from_positions(start, start.advanced_by(name));
    for assignment in assignments {
        span = merge_non_empty_span(span, assignment.span);
    }
    if let Some(primary) = primary {
        span = merge_non_empty_span(span, primary.span);
    }
    for argument in extra_args {
        span = merge_non_empty_span(span, argument.span);
    }
    span
}

pub(crate) fn builtin_like_parts(
    command: &BuiltinCommand,
) -> (
    Span,
    &'static str,
    &[Assignment],
    Option<&shuck_ast::Word>,
    &[shuck_ast::Word],
) {
    match command {
        BuiltinCommand::Break(command) => (
            command.span,
            "break",
            &command.assignments,
            command.depth.as_ref(),
            &command.extra_args,
        ),
        BuiltinCommand::Continue(command) => (
            command.span,
            "continue",
            &command.assignments,
            command.depth.as_ref(),
            &command.extra_args,
        ),
        BuiltinCommand::Return(command) => (
            command.span,
            "return",
            &command.assignments,
            command.code.as_ref(),
            &command.extra_args,
        ),
        BuiltinCommand::Exit(command) => (
            command.span,
            "exit",
            &command.assignments,
            command.code.as_ref(),
            &command.extra_args,
        ),
    }
}

pub(crate) fn builtin_like_parts_mut(
    command: &mut BuiltinCommand,
) -> (&mut [Assignment], Option<&mut Word>, &mut [Word]) {
    match command {
        BuiltinCommand::Break(command) => (
            &mut command.assignments,
            command.depth.as_mut(),
            &mut command.extra_args,
        ),
        BuiltinCommand::Continue(command) => (
            &mut command.assignments,
            command.depth.as_mut(),
            &mut command.extra_args,
        ),
        BuiltinCommand::Return(command) => (
            &mut command.assignments,
            command.code.as_mut(),
            &mut command.extra_args,
        ),
        BuiltinCommand::Exit(command) => (
            &mut command.assignments,
            command.code.as_mut(),
            &mut command.extra_args,
        ),
    }
}

fn decl_clause_format_span(command: &DeclClause) -> Span {
    let mut span = command.variant_span;
    for assignment in &command.assignments {
        span = merge_non_empty_span(span, assignment.span);
    }
    for operand in &command.operands {
        let operand_span = match operand {
            DeclOperand::Flag(word) | DeclOperand::Dynamic(word) => word.span,
            DeclOperand::Name(name) => name.span,
            DeclOperand::Assignment(assignment) => assignment.span,
        };
        span = merge_non_empty_span(span, operand_span);
    }
    span
}

pub(crate) fn function_attachment_span(command: &FunctionDef) -> Span {
    function_header_span(command).merge(stmt_span(&command.body))
}

pub(crate) fn anonymous_function_attachment_span(command: &AnonymousFunctionCommand) -> Span {
    anonymous_function_header_span(command)
        .merge(stmt_span(&command.body))
        .merge(words_span(&command.args))
}

pub(crate) fn function_header_span(command: &FunctionDef) -> Span {
    command.header.span()
}

pub(crate) fn anonymous_function_header_span(command: &AnonymousFunctionCommand) -> Span {
    match command.surface {
        shuck_ast::AnonymousFunctionSurface::FunctionKeyword {
            function_keyword_span,
        } => function_keyword_span,
        shuck_ast::AnonymousFunctionSurface::Parens { parens_span } => parens_span,
    }
}

pub(crate) fn words_span(words: &[shuck_ast::Word]) -> Span {
    words.iter().fold(Span::new(), |span, word| {
        merge_non_empty_span(span, word.span)
    })
}

pub(crate) fn compound_format_span(command: &CompoundCommand) -> Span {
    match command {
        CompoundCommand::Subshell(commands) | CompoundCommand::BraceGroup(commands) => commands
            .iter()
            .map(stmt_format_span)
            .reduce(Span::merge)
            .unwrap_or_default(),
        _ => compound_span(command),
    }
}

pub(crate) fn stmt_format_span(stmt: &Stmt) -> Span {
    let span = if stmt.negated {
        stmt.span
    } else {
        command_format_span(&stmt.command)
    };
    complete_stmt_span(stmt, span)
}

pub(crate) fn merge_non_empty_span(current: Span, next: Span) -> Span {
    if current == Span::new() {
        next
    } else if next == Span::new() {
        current
    } else {
        current.merge(next)
    }
}

pub(super) fn span_for_offsets(source_map: &SourceMap<'_>, start: usize, end: usize) -> Span {
    source_map.span_for_offsets(start, end)
}

pub(crate) fn simple_command_uses_synthetic_words(command: &SimpleCommand, source: &str) -> bool {
    word_uses_synthetic_source(&command.name, source)
}

fn synthetic_simple_command_verbatim_span(
    command: &SimpleCommand,
    source_map: &SourceMap<'_>,
) -> Span {
    let source = source_map.source();
    let start = command.span.start.offset().min(source.len());
    let end = command.span.end.offset().min(source.len());
    let Some(raw) = source.get(start..end) else {
        return command.span;
    };
    let leading_padding = raw.len() - raw.trim_start_matches([' ', '\t']).len();
    let candidate = &raw[leading_padding..];
    let command_start = if let Some(operator_len) = candidate
        .starts_with("&&")
        .then_some(2)
        .or_else(|| candidate.starts_with("||").then_some(2))
    {
        let after_operator = leading_padding + operator_len;
        let rest = &raw[after_operator..];
        let operator_padding = rest.len() - rest.trim_start_matches([' ', '\t']).len();
        start + after_operator + operator_padding
    } else {
        start
    };
    let command_end =
        trim_synthetic_simple_command_trailing_case_terminator(source, command_start, end);
    span_for_offsets(source_map, command_start, command_end)
}

fn trim_synthetic_simple_command_trailing_case_terminator(
    source: &str,
    start: usize,
    end: usize,
) -> usize {
    let Some(raw) = source.get(start..end) else {
        return end;
    };
    let trimmed_end = raw.trim_end_matches([' ', '\t', '\r', '\n']).len();
    let trimmed = &raw[..trimmed_end];
    let terminator_len = [";;&", ";;", ";&", ";|"]
        .into_iter()
        .find_map(|terminator| trimmed.ends_with(terminator).then_some(terminator.len()));
    let Some(terminator_len) = terminator_len else {
        return start + trimmed_end;
    };
    let before_terminator = trimmed[..trimmed.len() - terminator_len]
        .trim_end_matches([' ', '\t'])
        .len();
    start + before_terminator
}

fn word_uses_synthetic_source(word: &Word, source: &str) -> bool {
    if !word
        .parts_with_spans()
        .any(|(part, _)| word_part_uses_synthetic_source(part))
    {
        return false;
    }
    let rendered = word.render_syntax(source);
    let raw = source
        .get(word.span.start.offset().min(source.len())..word.span.end.offset().min(source.len()));
    raw.is_none_or(|raw| raw != rendered)
}

fn word_part_uses_synthetic_source(part: &WordPart) -> bool {
    match part {
        WordPart::Literal(text) => !text.is_source_backed(),
        WordPart::SingleQuoted { value, .. } => !value.is_source_backed(),
        WordPart::DoubleQuoted { parts, .. } => parts
            .iter()
            .any(|part| word_part_uses_synthetic_source(&part.kind)),
        _ => false,
    }
}
