use std::fmt::Write;

use crate::{
    ast::{BinaryExpr, ExprNode, ProgramNode, StmtNode},
    tokens::Token,
};

/* -------------------------------- *
 * Dumper trait and implementations *
 * -------------------------------- */

#[allow(dead_code)]
pub fn dump_program(ast: &ProgramNode) -> String {
    let mut dumper = Dumper::default();
    dump_statement_list(&mut dumper, &ast.0);
    dumper
        .lines
        .iter()
        .map(DumperLine::to_string)
        .collect::<Vec<String>>()
        .join("\n")
}

fn fun_decl_params(params: &[Token]) -> String {
    params
        .iter()
        .map(|token| &token.lexeme as &str)
        .collect::<Vec<&str>>()
        .join(" ")
}

fn dump_statement_list(dumper: &mut Dumper, statements: &[StmtNode]) {
    for stmt in statements {
        dump_statement(dumper, stmt);
    }
}

fn dump_substatement(dumper: &mut Dumper, statement: &StmtNode) {
    let depth_change = match statement {
        StmtNode::Block(_) => 0,
        _ => 1,
    };
    dumper.depth += depth_change;
    dump_statement(dumper, statement);
    dumper.depth -= depth_change;
}

fn dump_statement(dumper: &mut Dumper, stmt: &StmtNode) {
    match stmt {
        StmtNode::VarDecl(name, Some(expr)) => {
            dumper.integrate(
                &format!("var {} = ", name.lexeme),
                dump_expression(expr),
                ";",
            );
        }
        StmtNode::VarDecl(name, None) => {
            dumper.add_line(format!("var {};", name.lexeme));
        }

        StmtNode::FunDecl(fun_decl) => {
            dumper.add_line(format!(
                "fun {} ({}) {{",
                fun_decl.name.lexeme,
                fun_decl_params(&fun_decl.params),
            ));
            if !fun_decl.body.is_empty() {
                dumper.depth += 1;
                dump_statement_list(dumper, &fun_decl.body);
                dumper.depth -= 1;
                dumper.add_line(String::default());
            }
            dumper.current_line().push('}');
        }

        StmtNode::ClassDecl(decl) => {
            dumper.add_line(format!("class {} {{", decl.name.lexeme));
            if !decl.methods.is_empty() {
                dumper.depth += 1;
                for method in decl.methods.iter() {
                    dumper.add_line(format!(
                        "{} ({}) {{",
                        method.name.lexeme,
                        fun_decl_params(&method.params)
                    ));
                    dumper.depth += 1;
                    dump_statement_list(dumper, &method.body);
                    dumper.depth -= 1;
                    dumper.add_line("}".to_owned());
                }
                dumper.depth -= 1;
                dumper.add_line(String::default());
            }
            dumper.current_line().push('}');
        }

        StmtNode::Expression(expr) => {
            dumper.integrate("", dump_expression(expr), ";");
        }

        StmtNode::Print(expr) => {
            dumper.integrate("print ", dump_expression(expr), ";");
        }

        StmtNode::Block(stmts) => {
            dumper.add_line("{".to_owned());
            dumper.depth += 1;
            dump_statement_list(dumper, stmts);
            dumper.depth -= 1;
            dumper.add_line("}".to_owned());
        }

        StmtNode::If {
            condition,
            then_branch,
            else_branch,
        } => {
            dumper.integrate("if (", dump_expression(condition), ")");
            dump_substatement(dumper, then_branch);
            if let Some(else_branch) = else_branch {
                dumper.add_line("else".to_owned());
                dump_substatement(dumper, else_branch);
            }
        }

        StmtNode::Loop {
            label,
            condition,
            body,
            after_body,
        } => {
            if let Some(label) = label {
                dumper.add_line(format!("@{} ", label.lexeme));
            };
            dumper.integrate("while (", dump_expression(condition), ")");
            if let Some(after_body) = after_body {
                let mut statements = match body.as_ref() {
                    StmtNode::Block(block) => block.clone(),
                    other => vec![other.clone()],
                };
                match after_body.as_ref() {
                    StmtNode::Block(block) => statements.extend(block.clone()),
                    other => statements.push(other.clone()),
                };
                if statements.len() == 1 {
                    dump_substatement(dumper, &statements[0]);
                } else {
                    dump_substatement(dumper, &StmtNode::Block(statements));
                }
            } else {
                dump_substatement(dumper, body);
            }
        }

        StmtNode::LoopControl {
            is_break,
            loop_name,
        } => {
            let stmt = if *is_break { "break" } else { "continue" };
            dumper.add_line(match loop_name {
                Some(name) => format!("{} {};", stmt, name.lexeme),
                None => format!("{};", stmt),
            });
        }

        StmtNode::Return { token: _, value } => match value {
            Some(expr) => dumper.integrate("return ", dump_expression(expr), ";"),
            None => dumper.add_line("return;".to_owned()),
        },
    }
}

/* ----------- *
 * Expressions *
 * ----------- */

fn dump_expression(expr: &ExprNode) -> Dumper {
    let mut dumper = Dumper::default();
    dumper.add_line(String::default());
    dump_expr_node(&mut dumper, expr);
    dumper
}

fn dump_binary_expr(dumper: &mut Dumper, binary_expr: &BinaryExpr) {
    dump_expr_node(dumper, &binary_expr.left);
    dumper.current_line().push(' ');
    dumper.current_line().push_str(&binary_expr.operator.lexeme);
    dumper.current_line().push(' ');
    dump_expr_node(dumper, &binary_expr.right);
}

fn dump_expr_node(dumper: &mut Dumper, expr: &ExprNode) {
    match expr {
        ExprNode::Assignment { name, value, id: _ } => {
            dumper
                .current_line()
                .write_fmt(format_args!("{} = ", name.lexeme))
                .unwrap();
            dump_expr_node(dumper, value);
        }

        ExprNode::Logical(binary_expr) => dump_binary_expr(dumper, binary_expr),
        ExprNode::Binary(binary_expr) => dump_binary_expr(dumper, binary_expr),

        ExprNode::Unary { operator, right } => {
            dumper.current_line().push_str(&operator.lexeme);
            dump_expr_node(dumper, right);
        }

        ExprNode::Grouping { expression } => {
            dumper.current_line().push('(');
            dump_expr_node(dumper, expression);
            dumper.current_line().push(')');
        }

        ExprNode::Litteral { value } => {
            dumper.current_line().push_str(&value.lexeme);
        }

        ExprNode::Variable(var) | ExprNode::This(var) => {
            dumper.current_line().push_str(&var.token.lexeme);
        }

        ExprNode::Lambda { params, body } => {
            dumper
                .current_line()
                .write_fmt(format_args!("fun ({}) {{", fun_decl_params(params)))
                .unwrap();
            if !body.is_empty() {
                dumper.depth += 1;
                dump_statement_list(dumper, body);
                dumper.add_line(String::default());
                dumper.depth -= 1;
            }
            dumper.current_line().push('}');
        }

        ExprNode::Call {
            callee,
            right_paren: _,
            arguments,
        } => {
            dump_expr_node(dumper, callee);
            dumper.current_line().push('(');
            for (i, argument) in arguments.iter().enumerate() {
                dump_expr_node(dumper, argument);
                if arguments.len() - 1 > i {
                    dumper.current_line().push_str(", ");
                }
            }
            dumper.current_line().push(')');
        }

        ExprNode::Get(get_expr) => {
            dump_expr_node(dumper, &get_expr.instance);
            dumper.current_line().push('.');
            dumper.current_line().push_str(&get_expr.name.lexeme);
        }

        ExprNode::Set(set_expr) => {
            dump_expr_node(dumper, &set_expr.instance);
            dumper.current_line().push('.');
            dumper.current_line().push_str(&set_expr.name.lexeme);
            dumper.current_line().push_str(" = ");
            dump_expr_node(dumper, &set_expr.value);
        }
    }
}

/* ------------ *
 * Dumper state *
 * ------------ */

/// The state of the program dumper.
#[derive(Debug, Default)]
struct Dumper {
    depth: usize,
    lines: Vec<DumperLine>,
}

/// A line generated while dumping the program.
#[derive(Debug)]
struct DumperLine {
    text: String,
    depth: usize,
}

// Convert dumped lines to strings.
impl ToString for DumperLine {
    fn to_string(&self) -> String {
        "\t".repeat(self.depth) + &self.text
    }
}

impl Dumper {
    fn integrate(&mut self, prefix: &str, other: Dumper, suffix: &str) {
        match other.lines.len() {
            0 => self.add_line(format!("{}{}", prefix, suffix)),
            1 => self.add_line(format!("{}{}{}", prefix, other.lines[0].text, suffix)),
            len => {
                other
                    .lines
                    .into_iter()
                    .enumerate()
                    .for_each(|(lnb, mut line)| match lnb {
                        0 => self.add_line(format!("{}{}", prefix, line.text)),
                        l if l == len - 1 => self.add_line(format!("{}{}", line.text, suffix)),
                        _ => {
                            line.depth += self.depth;
                            self.lines.push(line);
                        }
                    });
            }
        }
    }

    fn add_line(&mut self, line: String) {
        self.lines.push(DumperLine {
            text: line,
            depth: self.depth,
        })
    }

    fn current_line(&mut self) -> &mut String {
        let line = self.lines.len() - 1;
        &mut self.lines[line].text
    }
}