use super::context::{ConvertContext, TokenType};
use super::math_ir::{
    MathCaseRow, MathCommand, MathEnvironment, MathIr, MathSpacing, MathStyleMode,
};

pub fn emit_math_ir(ir: &MathIr, ctx: &mut ConvertContext) {
    match ir {
        MathIr::Seq(items) => {
            for item in items {
                emit_math_ir(item, ctx);
            }
        }
        MathIr::Symbol(symbol) => emit_symbol(symbol, ctx),
        MathIr::Ident(ident) => ctx.push_with_spacing(ident, TokenType::Letter),
        MathIr::Number(number) => ctx.push_with_spacing(number, TokenType::Number),
        MathIr::Operator(operator) => ctx.push_with_spacing(operator, TokenType::Operator),
        MathIr::Punctuation(ch) => emit_punctuation(*ch, ctx),
        MathIr::Spacing(spacing) => emit_spacing(spacing, ctx),
        MathIr::Delimited {
            open,
            content,
            close,
        } => emit_delimited(open, content, close, ctx),
        MathIr::Apply { callee, args } => emit_apply(callee, args, ctx),
        MathIr::Limits(content) => emit_math_ir(content, ctx),
        MathIr::Style { mode, content } => emit_style(mode, content, ctx),
        MathIr::Attachment { .. } => emit_attachment(ir, ctx),
        MathIr::Script { base, sub, sup } => emit_script(base, sub.as_deref(), sup.as_deref(), ctx),
        MathIr::Command(command) => emit_command(command, ctx),
        MathIr::Environment(environment) => emit_environment(environment, ctx),
        MathIr::RawLiteral(text) => emit_raw_literal(text, ctx),
    }
}

fn emit_apply(callee: &MathIr, args: &[MathIr], ctx: &mut ConvertContext) {
    emit_math_ir(callee, ctx);
    ctx.push("(");
    for (index, arg) in args.iter().enumerate() {
        if index > 0 {
            ctx.push(", ");
        }
        ctx.push(&emit_math_ir_to_string(arg, ctx));
    }
    ctx.push(")");
    ctx.last_token = TokenType::CloseParen;
}

pub fn emit_math_ir_to_string(ir: &MathIr, template: &ConvertContext) -> String {
    let mut nested = ConvertContext::new();
    nested.options = template.options.clone();
    nested.in_math = true;
    emit_math_ir(ir, &mut nested);
    nested.finalize()
}

fn emit_symbol(symbol: &str, ctx: &mut ConvertContext) {
    if symbol.starts_with('\\') {
        ctx.push(symbol);
        ctx.last_token = TokenType::Command;
    } else {
        ctx.push_with_spacing(symbol, TokenType::Letter);
    }
}

fn emit_punctuation(ch: char, ctx: &mut ConvertContext) {
    ctx.trim_trailing_space();
    match ch {
        ',' | ';' => ctx.push(&format!("{} ", ch)),
        ':' => ctx.push(":"),
        other => ctx.push(&other.to_string()),
    }
    ctx.last_token = TokenType::Punctuation;
}

fn emit_spacing(spacing: &MathSpacing, ctx: &mut ConvertContext) {
    match spacing {
        MathSpacing::Soft => {
            if !ctx.output.ends_with(' ') && !ctx.output.ends_with('{') {
                ctx.push(" ");
            }
            ctx.last_token = TokenType::None;
        }
        MathSpacing::Space => {
            ctx.push("\\ ");
            ctx.last_token = TokenType::Command;
        }
        MathSpacing::Thin => {
            ctx.push("\\,");
            ctx.last_token = TokenType::Command;
        }
        MathSpacing::Med => {
            ctx.push("\\:");
            ctx.last_token = TokenType::Command;
        }
        MathSpacing::Thick => {
            ctx.push("\\;");
            ctx.last_token = TokenType::Command;
        }
        MathSpacing::Quad => {
            ctx.push("\\quad");
            ctx.last_token = TokenType::Command;
        }
        MathSpacing::Qquad | MathSpacing::Wide => {
            ctx.push("\\qquad");
            ctx.last_token = TokenType::Command;
        }
    }
}

fn emit_delimited(open: &str, content: &MathIr, close: &str, ctx: &mut ConvertContext) {
    ctx.push(open);
    ctx.last_token = delimiter_token_type(open, true);
    emit_math_ir(content, ctx);
    ctx.push(close);
    ctx.last_token = delimiter_token_type(close, false);
}

fn emit_style(mode: &MathStyleMode, content: &MathIr, ctx: &mut ConvertContext) {
    match mode {
        MathStyleMode::Display => {
            ctx.push(r"\displaystyle ");
            emit_math_ir(content, ctx);
            if !ctx.options.block_math_mode {
                ctx.push(r" \textstyle ");
                ctx.last_token = TokenType::Command;
            }
        }
        MathStyleMode::Inline => {
            ctx.push(r"\textstyle ");
            emit_math_ir(content, ctx);
            if ctx.options.block_math_mode {
                ctx.push(r" \displaystyle ");
                ctx.last_token = TokenType::Command;
            }
        }
    }
    ctx.last_token = TokenType::Command;
}

fn emit_attachment(attachment: &MathIr, ctx: &mut ConvertContext) {
    let MathIr::Attachment {
        base,
        top,
        bottom,
        top_left,
        top_right,
        bottom_left,
        bottom_right,
    } = attachment
    else {
        return;
    };

    if top_left.is_some() || bottom_left.is_some() {
        ctx.push("{}");
        if let Some(bottom_left) = bottom_left.as_deref() {
            ctx.push("_");
            emit_script_content(bottom_left, ctx);
        }
        if let Some(top_left) = top_left.as_deref() {
            ctx.push("^");
            emit_script_content(top_left, ctx);
        }
        ctx.last_token = TokenType::None;
    }

    emit_math_ir(base, ctx);

    if let Some(bottom) = bottom.as_deref() {
        ctx.push("_");
        emit_script_content(bottom, ctx);
    }
    if let Some(top) = top.as_deref() {
        ctx.push("^");
        emit_script_content(top, ctx);
    }

    if top_right.is_some() || bottom_right.is_some() {
        ctx.push("{}");
        if let Some(bottom_right) = bottom_right.as_deref() {
            ctx.push("_");
            emit_script_content(bottom_right, ctx);
        }
        if let Some(top_right) = top_right.as_deref() {
            ctx.push("^");
            emit_script_content(top_right, ctx);
        }
    }

    ctx.last_token = TokenType::Command;
}

fn emit_script(
    base: &MathIr,
    sub: Option<&MathIr>,
    sup: Option<&MathIr>,
    ctx: &mut ConvertContext,
) {
    emit_math_ir(base, ctx);

    if let Some(sub) = sub {
        ctx.push("_");
        ctx.last_token = TokenType::Operator;
        emit_script_content(sub, ctx);
    }

    if let Some(sup) = sup {
        ctx.push("^");
        ctx.last_token = TokenType::Operator;
        emit_script_content(sup, ctx);
    }
}

fn strip_grouping_parentheses_for_script(ir: &MathIr) -> &MathIr {
    match ir {
        MathIr::Delimited {
            open,
            content,
            close,
        } if (open == "(" && close == ")") || (open == r"\left(" && close == r"\right)") => {
            content.as_ref()
        }
        _ => ir,
    }
}

fn strip_grouping_parentheses_from_rendered_script(rendered: String) -> String {
    if can_strip_wrapping_left_right_parens(&rendered) {
        return rendered
            .trim_start_matches(r"\left(")
            .trim_end_matches(r"\right)")
            .trim()
            .to_string();
    }

    if can_strip_wrapping_plain_parens(&rendered) {
        return rendered[1..rendered.len() - 1].trim().to_string();
    }

    rendered
}

fn can_strip_wrapping_plain_parens(rendered: &str) -> bool {
    if !rendered.starts_with('(') || !rendered.ends_with(')') {
        return false;
    }

    let mut depth = 0i32;
    for (index, ch) in rendered.char_indices() {
        match ch {
            '(' => depth += 1,
            ')' => {
                depth -= 1;
                if depth == 0 && index != rendered.len() - 1 {
                    return false;
                }
                if depth < 0 {
                    return false;
                }
            }
            _ => {}
        }
    }

    depth == 0
}

fn can_strip_wrapping_left_right_parens(rendered: &str) -> bool {
    if !rendered.starts_with(r"\left(") || !rendered.ends_with(r"\right)") {
        return false;
    }

    let mut depth = 0i32;
    let mut index = 0usize;
    while index < rendered.len() {
        let rest = &rendered[index..];
        if rest.starts_with(r"\left(") {
            depth += 1;
            index += r"\left(".len();
            continue;
        }
        if rest.starts_with(r"\right)") {
            depth -= 1;
            if depth == 0 && index + r"\right)".len() != rendered.len() {
                return false;
            }
            if depth < 0 {
                return false;
            }
            index += r"\right)".len();
            continue;
        }
        index += rest.chars().next().map(|c| c.len_utf8()).unwrap_or(1);
    }

    depth == 0
}

fn emit_script_content(ir: &MathIr, ctx: &mut ConvertContext) {
    let ir = strip_grouping_parentheses_for_script(ir);
    let mut rendered = emit_math_ir_to_string(ir, ctx).trim().to_string();
    rendered = strip_grouping_parentheses_from_rendered_script(rendered);
    if rendered.is_empty() {
        return;
    }

    if rendered.len() == 1
        && rendered
            .chars()
            .next()
            .map(|ch| ch.is_alphanumeric())
            .unwrap_or(false)
    {
        ctx.push(&rendered);
    } else {
        ctx.push("{");
        ctx.push(&rendered);
        ctx.push("}");
    }
    ctx.last_token = TokenType::Command;
}

fn emit_command(command: &MathCommand, ctx: &mut ConvertContext) {
    ctx.push(&command.latex);
    if let Some(optional_arg) = &command.optional_arg {
        ctx.push("[");
        ctx.push(&emit_math_ir_to_string(optional_arg, ctx));
        ctx.push("]");
    }

    for arg in &command.args {
        ctx.push("{");
        ctx.push(&emit_math_ir_to_string(arg, ctx));
        ctx.push("}");
    }

    ctx.last_token = TokenType::Command;
}

fn emit_environment(environment: &MathEnvironment, ctx: &mut ConvertContext) {
    match environment {
        MathEnvironment::Matrix { name, rows } => {
            ctx.push("\\begin{");
            ctx.push(name);
            ctx.push("}\n");
            for (row_index, row) in rows.iter().enumerate() {
                ctx.push("  ");
                for (cell_index, cell) in row.iter().enumerate() {
                    if cell_index > 0 {
                        ctx.push(" & ");
                    }
                    ctx.push(&emit_math_ir_to_string(cell, ctx));
                }
                if row_index + 1 < rows.len() {
                    ctx.push(" \\\\\n");
                } else {
                    ctx.push("\n");
                }
            }
            ctx.push("\\end{");
            ctx.push(name);
            ctx.push("}");
            ctx.last_token = TokenType::Command;
        }
        MathEnvironment::Cases { rows } => {
            ctx.push("\\begin{cases}\n");
            for (index, row) in rows.iter().enumerate() {
                emit_case_row(row, ctx);
                if index + 1 < rows.len() {
                    ctx.push(" \\\\\n");
                } else {
                    ctx.push("\n");
                }
            }
            ctx.push("\\end{cases}");
            ctx.last_token = TokenType::Command;
        }
    }
}

fn emit_case_row(row: &MathCaseRow, ctx: &mut ConvertContext) {
    ctx.push("  ");
    ctx.push(&emit_math_ir_to_string(&row.value, ctx));
    if let Some(condition) = &row.condition {
        ctx.push(" & ");
        ctx.push(&emit_math_ir_to_string(condition, ctx));
    }
}

fn emit_raw_literal(text: &str, ctx: &mut ConvertContext) {
    ctx.push(text);
    let last_non_space = text.chars().rev().find(|ch| !ch.is_whitespace());
    ctx.last_token = match last_non_space {
        Some('}') | Some(']') => TokenType::Command,
        Some(')') => TokenType::CloseParen,
        Some('(') | Some('[') => TokenType::OpenParen,
        Some(',') | Some(';') | Some(':') => TokenType::Punctuation,
        Some(ch) if ch.is_ascii_digit() => TokenType::Number,
        Some(ch) if ch.is_alphabetic() => TokenType::Letter,
        Some(_) => TokenType::Operator,
        None => TokenType::None,
    };
}

fn delimiter_token_type(delim: &str, is_open: bool) -> TokenType {
    if delim.starts_with("\\left") || delim.starts_with("\\right") || delim.starts_with('\\') {
        if is_open {
            TokenType::OpenParen
        } else {
            TokenType::CloseParen
        }
    } else if is_open {
        TokenType::OpenParen
    } else {
        TokenType::CloseParen
    }
}

#[cfg(test)]
mod tests {
    use super::{can_strip_wrapping_left_right_parens, can_strip_wrapping_plain_parens};

    #[test]
    fn test_strip_plain_grouping_parentheses_requires_outer_pair() {
        assert!(can_strip_wrapping_plain_parens("(i = 1)"));
        assert!(!can_strip_wrapping_plain_parens("(a + b)(c + d)"));
        assert!(!can_strip_wrapping_plain_parens("((a + b)"));
    }

    #[test]
    fn test_strip_left_right_grouping_parentheses_requires_outer_pair() {
        assert!(can_strip_wrapping_left_right_parens(r"\left(i = 1\right)"));
        assert!(!can_strip_wrapping_left_right_parens(
            r"\left(a + b\right)\left(c + d\right)"
        ));
    }
}
