use super::*;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct MultilineCompoundAssignmentLayout {
    pub(crate) lines: Vec<String>,
    pub(crate) open_inline: bool,
    pub(crate) close_inline: bool,
}

pub(crate) fn multiline_compound_assignment_layout(
    assignment: &Assignment,
    source: &str,
) -> Option<MultilineCompoundAssignmentLayout> {
    let AssignmentValue::Compound(_) = &assignment.value else {
        return None;
    };

    let slice = assignment.span.slice(source);
    if !slice.contains('\n') {
        return None;
    }

    let open = slice.find('(')?;
    let close = slice.rfind(')')?;
    if close <= open {
        return None;
    }

    let body = &slice[open + 1..close];
    let open_line = body.split_once('\n').map_or(body, |(line, _)| line);
    let close_line = body.rsplit_once('\n').map_or(body, |(_, line)| line);
    let mut raw_lines = body
        .lines()
        .map(|line| line.trim_end_matches([' ', '\t', '\r']))
        .collect::<Vec<_>>();
    if open_line.trim().is_empty() && raw_lines.first().is_some_and(|line| line.is_empty()) {
        raw_lines.remove(0);
    }
    if close_line.trim().is_empty()
        && raw_lines.last().is_some_and(|line| line.is_empty())
        && !body.ends_with('\n')
    {
        raw_lines.pop();
    }
    let common_indent =
        multiline_compound_assignment_common_body_indent(&raw_lines, !open_line.trim().is_empty());
    let residual_space_indent_width = multiline_compound_assignment_residual_space_indent_width(
        &raw_lines,
        &common_indent,
        !open_line.trim().is_empty(),
    );
    let command_substitution_body_lines =
        multiline_compound_assignment_command_substitution_body_lines(&raw_lines);
    let mut lines = raw_lines
        .iter()
        .enumerate()
        .map(|(index, line)| {
            normalize_multiline_compound_assignment_line(
                line,
                &common_indent,
                residual_space_indent_width,
                index == 0 && !open_line.trim().is_empty(),
                command_substitution_body_lines[index],
            )
        })
        .collect::<Vec<_>>();
    for line in &mut lines {
        *line = RawShellText::new(line).trim_command_substitution_open_padding();
    }
    let mut lines = normalize_multiline_compound_assignment_command_substitutions(lines);
    restore_multiline_compound_assignment_heredoc_tails(&mut lines, &raw_lines);

    (!lines.is_empty()).then_some(MultilineCompoundAssignmentLayout {
        lines,
        open_inline: !open_line.trim().is_empty(),
        close_inline: !close_line.trim().is_empty(),
    })
}

fn restore_multiline_compound_assignment_heredoc_tails(
    lines: &mut [String],
    source_lines: &[&str],
) {
    let mut active_heredoc: Option<crate::raw_syntax::RenderedHeredocTail> = None;
    for (line, source_line) in lines.iter_mut().zip(source_lines) {
        if let Some(heredoc) = active_heredoc.as_ref() {
            if !heredoc.strip_tabs {
                *line = (*source_line).to_string();
            }
            if heredoc.closes_source_line(source_line) {
                active_heredoc = None;
            }
        } else {
            active_heredoc = rendered_heredoc_tail_start(line);
        }
    }
}

fn normalize_multiline_compound_assignment_command_substitutions(
    mut lines: Vec<String>,
) -> Vec<String> {
    let mut index = 0;
    while index < lines.len() {
        let Some((close_index, body_end, close_line_is_standalone)) =
            multiline_compound_assignment_command_substitution_range(&lines, index)
        else {
            index += 1;
            continue;
        };

        normalize_multiline_compound_assignment_command_substitution_pipeline_continuations(
            &mut lines,
            index,
            close_index + 1,
        );

        let body_prefix =
            multiline_compound_assignment_command_substitution_body_prefix(&lines[index]);
        if body_prefix.is_empty() {
            index = close_index + 1;
            continue;
        }

        let common_indent =
            common_nonempty_shell_indent(lines[index + 1..body_end].iter().map(String::as_str));
        for line in &mut lines[index + 1..body_end] {
            if line.trim().is_empty() {
                continue;
            }
            let stripped = if common_indent.is_empty() {
                line.trim_start_matches([' ', '\t'])
            } else {
                line.strip_prefix(&common_indent)
                    .unwrap_or_else(|| line.trim_start_matches([' ', '\t']))
            };
            *line = format!("{body_prefix}{stripped}");
        }
        if close_line_is_standalone {
            lines[close_index] = lines[close_index]
                .trim_start_matches([' ', '\t'])
                .to_string();
        }
        index = close_index + 1;
    }
    lines
}

fn multiline_compound_assignment_command_substitution_range<T: AsRef<str>>(
    lines: &[T],
    open_index: usize,
) -> Option<(usize, usize, bool)> {
    let close_index =
        multiline_compound_assignment_command_substitution_close_index(lines, open_index)?;
    let close_line_is_standalone = lines[close_index]
        .as_ref()
        .trim_start_matches([' ', '\t'])
        .starts_with(')');
    let body_end = if close_line_is_standalone {
        close_index
    } else {
        close_index + 1
    };
    Some((close_index, body_end, close_line_is_standalone))
}

fn multiline_compound_assignment_command_substitution_close_index<T: AsRef<str>>(
    lines: &[T],
    open_index: usize,
) -> Option<usize> {
    let open = RawShellText::new(lines.get(open_index)?.as_ref())
        .unclosed_command_substitution_body_start()?;
    let mut tail = String::new();
    for (index, line) in lines.get(open_index..)?.iter().enumerate() {
        if index > 0 {
            tail.push('\n');
        }
        tail.push_str(line.as_ref());
    }
    let close = matching_raw_command_substitution_close(&tail, open)?;
    Some(open_index + tail[..close].bytes().filter(|byte| *byte == b'\n').count())
}

fn multiline_compound_assignment_command_substitution_body_lines(raw_lines: &[&str]) -> Vec<bool> {
    let mut body_lines = vec![false; raw_lines.len()];
    let mut index = 0;
    while index < raw_lines.len() {
        let Some((close_index, body_end, _)) =
            multiline_compound_assignment_command_substitution_range(raw_lines, index)
        else {
            index += 1;
            continue;
        };

        for body_line in &mut body_lines[index + 1..body_end] {
            *body_line = true;
        }
        index = close_index + 1;
    }
    body_lines
}

fn normalize_multiline_compound_assignment_command_substitution_pipeline_continuations(
    lines: &mut [String],
    body_start: usize,
    body_end: usize,
) {
    if body_start >= body_end {
        return;
    }

    let body = lines[body_start..body_end].join("\n");
    let Some(normalized) = normalize_raw_pipeline_continuations(&body) else {
        return;
    };
    let normalized_lines = normalized
        .lines()
        .map(ToString::to_string)
        .collect::<Vec<_>>();
    if normalized_lines.len() != body_end - body_start {
        return;
    }

    for (line, normalized) in lines[body_start..body_end].iter_mut().zip(normalized_lines) {
        *line = normalized;
    }
}

pub(crate) fn multiline_compound_assignment_command_substitution_body_prefix(
    open_line: &str,
) -> &'static str {
    let Some(open) = open_line.find("$(") else {
        return "";
    };
    let prefix = open_line[..open].trim_start_matches([' ', '\t']);
    let inline_body = !open_line[open + 2..]
        .trim_matches([' ', '\t', '\r', '\\'])
        .is_empty();
    if prefix.is_empty() && inline_body {
        return "\t";
    }
    if prefix.is_empty() || prefix.contains("$(") || prefix.contains('(') || prefix.contains('=') {
        ""
    } else {
        "\t"
    }
}

fn normalize_multiline_compound_assignment_line(
    line: &str,
    common_indent: &str,
    residual_space_indent_width: usize,
    open_inline_line: bool,
    preserve_line_continuation: bool,
) -> String {
    let trimmed_start = line.trim_start_matches([' ', '\t']);
    let trimmed = if preserve_line_continuation {
        trimmed_start.trim_end_matches([' ', '\t'])
    } else {
        trim_multiline_compound_assignment_line_continuation(trimmed_start)
    };
    if trimmed.is_empty() {
        return String::new();
    }
    if open_inline_line {
        return normalize_multiline_compound_assignment_spacing(trimmed);
    }
    if trimmed.starts_with(')') {
        return trimmed.to_string();
    }
    if trimmed.starts_with('[') {
        return normalize_multiline_compound_assignment_spacing(trimmed);
    }
    let stripped = line
        .strip_prefix(common_indent)
        .map(|line| {
            if preserve_line_continuation {
                line.trim_end_matches([' ', '\t'])
            } else {
                trim_multiline_compound_assignment_line_continuation(line)
            }
        })
        .unwrap_or(trimmed);
    let normalized = if preserve_line_continuation
        && RawShellText::new(stripped).starts_with_redirect_continuation()
    {
        format!("\t{}", stripped.trim_start_matches([' ', '\t']))
    } else if preserve_line_continuation {
        canonicalize_multiline_compound_assignment_residual_indent(
            stripped,
            residual_space_indent_width,
        )
    } else {
        stripped.trim_start_matches([' ', '\t']).to_string()
    };
    normalize_multiline_compound_assignment_spacing(&normalized)
}

fn trim_multiline_compound_assignment_line_continuation(line: &str) -> &str {
    let trimmed = line.trim_end_matches([' ', '\t']);
    let trailing_backslashes = trimmed
        .as_bytes()
        .iter()
        .rev()
        .take_while(|byte| **byte == b'\\')
        .count();
    if trailing_backslashes % 2 == 1
        && !RawShellText::new(trimmed).continuation_inside_unclosed_substitution()
    {
        trimmed[..trimmed.len().saturating_sub(1)].trim_end_matches([' ', '\t'])
    } else {
        trimmed
    }
}

fn multiline_compound_assignment_common_body_indent(lines: &[&str], open_inline: bool) -> String {
    let mut common: Option<String> = None;
    for line in multiline_compound_assignment_body_lines(lines, open_inline) {
        let indent = leading_shell_indent(line);
        if indent.is_empty() {
            continue;
        }
        if refine_common_indent(&mut common, indent) {
            return String::new();
        }
    }
    common.unwrap_or_default()
}

fn multiline_compound_assignment_residual_space_indent_width(
    lines: &[&str],
    common_indent: &str,
    open_inline: bool,
) -> usize {
    let mut width = None::<usize>;
    for line in multiline_compound_assignment_body_lines(lines, open_inline) {
        let trimmed = line.trim_start_matches([' ', '\t']);
        let stripped = line.strip_prefix(common_indent).unwrap_or(trimmed);
        let indent = leading_shell_indent(stripped);
        if indent.is_empty() || indent.contains('\t') {
            continue;
        }
        width = Some(width.map_or(indent.len(), |current| current.min(indent.len())));
    }
    width.unwrap_or(1).max(1)
}

fn multiline_compound_assignment_body_lines<'a>(
    lines: &'a [&'a str],
    open_inline: bool,
) -> impl Iterator<Item = &'a str> + 'a {
    lines.iter().enumerate().filter_map(move |(index, line)| {
        if index == 0 && open_inline {
            return None;
        }
        let trimmed = line.trim_start_matches([' ', '\t']);
        if trimmed.is_empty() || trimmed.starts_with(')') || trimmed.starts_with('#') {
            return None;
        }
        Some(*line)
    })
}

fn canonicalize_multiline_compound_assignment_residual_indent(
    line: &str,
    residual_space_indent_width: usize,
) -> String {
    let indent = leading_shell_indent(line);
    if indent.is_empty() {
        return line.to_string();
    }
    let body = &line[indent.len()..];
    let mut indent_units = 0usize;
    let mut pending_spaces = 0usize;
    for ch in indent.chars() {
        if ch == '\t' {
            indent_units += 1;
            pending_spaces = 0;
        } else {
            pending_spaces += 1;
            if pending_spaces == residual_space_indent_width {
                indent_units += 1;
                pending_spaces = 0;
            }
        }
    }
    if pending_spaces > 0 {
        indent_units += 1;
    }

    let mut rendered = "\t".repeat(indent_units);
    rendered.push_str(body);
    rendered
}

fn normalize_multiline_compound_assignment_spacing(line: &str) -> String {
    let indent = leading_shell_indent(line);
    let body = &line[indent.len()..];
    if body.is_empty() || body.starts_with('#') {
        return line.to_string();
    }

    let mut rendered = String::with_capacity(line.len());
    rendered.push_str(indent);
    let mut chars = body.chars().peekable();
    let mut in_single_quotes = false;
    let mut in_double_quotes = false;
    let mut escaped = false;
    let mut changed = false;
    let mut previous_was_space = false;

    while let Some(ch) = chars.next() {
        if escaped {
            rendered.push(ch);
            escaped = false;
            previous_was_space = false;
            continue;
        }
        if ch == '\\' && !in_single_quotes {
            rendered.push(ch);
            escaped = true;
            previous_was_space = false;
            continue;
        }
        if ch == '\'' && !in_double_quotes {
            in_single_quotes = !in_single_quotes;
            rendered.push(ch);
            previous_was_space = false;
            continue;
        }
        if ch == '"' && !in_single_quotes {
            in_double_quotes = !in_double_quotes;
            rendered.push(ch);
            previous_was_space = false;
            continue;
        }
        if !in_single_quotes && !in_double_quotes && matches!(ch, ' ' | '\t') {
            while chars.peek().is_some_and(|next| matches!(next, ' ' | '\t')) {
                chars.next();
                changed = true;
            }
            if !previous_was_space && chars.peek().is_some() {
                rendered.push(' ');
                previous_was_space = true;
            } else {
                changed = true;
            }
            continue;
        }
        rendered.push(ch);
        previous_was_space = false;
    }

    if changed { rendered } else { line.to_string() }
}

pub(crate) fn multiline_compound_assignment_lines(
    assignment: &Assignment,
    source: &str,
) -> Option<Vec<String>> {
    multiline_compound_assignment_layout(assignment, source).map(|layout| layout.lines)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn compound_assignment_command_substitution_matches_unclosed_opener() {
        let raw_lines = [") $(closed) $(open", "echo body", ")"];

        let body_lines = multiline_compound_assignment_command_substitution_body_lines(&raw_lines);

        assert_eq!(body_lines, vec![false, true, false]);
    }
}
