use crate::ast::{Ast, StringPart};
use crate::chunk::{Chunk, StrPattern};
use crate::op::Op;
use crate::spans::Spanned;
use crate::value::*;
use chumsky::prelude::SimpleSpan;
use ordered_float::NotNan;
use regex::Regex;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;

#[derive(Clone, Debug, PartialEq)]
pub struct Binding {
    name: &'static str,
    depth: isize,
    stack_pos: usize,
}

impl std::fmt::Display for Binding {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}@{}//{}", self.name, self.stack_pos, self.depth)
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct Upvalue {
    name: &'static str,
    stack_pos: usize,
}

#[derive(Debug, Clone, PartialEq)]
struct LoopInfo {
    start: usize,
    stack_root: usize,
}

impl LoopInfo {
    fn new(start: usize, stack_root: usize) -> LoopInfo {
        LoopInfo { start, stack_root }
    }
}

fn get_builtin(_name: &str, _arity: usize) -> Option<Op> {
    // match (name, arity) {
    //     ("type", 1) => Some(Op::TypeOf),
    //     ("eq?", 2) => Some(Op::Eq),
    //     ("add", 2) => Some(Op::Add),
    //     ("sub", 2) => Some(Op::Sub),
    //     ("mult", 2) => Some(Op::Mult),
    //     ("div", 2) => Some(Op::Div),
    //     ("unbox", 1) => Some(Op::Unbox),
    //     ("store!", 2) => Some(Op::BoxStore),
    //     ("assert!", 1) => Some(Op::Assert),
    //     ("get", 2) => Some(Op::Get),
    //     ("at", 2) => Some(Op::At),
    //     ("not", 1) => Some(Op::Not),
    //     ("print!", 1) => Some(Op::Print),

    //     _ => None,
    // }
    None
}

#[derive(Debug, Clone)]
pub struct Compiler {
    pub chunk: Chunk,
    pub bindings: Vec<Binding>,
    pub scope_depth: isize,
    pub match_depth: usize,
    pub stack_depth: usize,
    pub ast: &'static Ast,
    pub span: SimpleSpan,
    pub line: usize,
    pub lines: Vec<usize>,
    pub src: &'static str,
    pub input: &'static str,
    pub depth: usize,
    pub upvalues: Vec<&'static str>,
    loop_info: Vec<LoopInfo>,
    tail_pos: bool,
    debug: bool,
}

fn is_binding(expr: &Spanned<Ast>) -> bool {
    let (ast, _) = expr;
    use Ast::*;
    match ast {
        Let(..) | LBox(..) | FnDeclaration(..) => true,
        Fn(name, ..) => !name.is_empty(),
        _ => false,
    }
}

fn has_placeholder(args: &[Spanned<Ast>]) -> bool {
    args.iter().any(|arg| matches!(arg, (Ast::Placeholder, _)))
}

impl Compiler {
    pub fn new(
        ast: &'static Spanned<Ast>,
        input: &'static str,
        src: &'static str,
        depth: usize,
        env: imbl::HashMap<Key, Value>,
        debug: bool,
    ) -> Compiler {
        let chunk = Chunk {
            constants: vec![],
            bytecode: vec![],
            keywords: vec![],
            string_patterns: vec![],
            env,
            msgs: vec![],
            src,
            input,
            spans: vec![],
        };
        Compiler {
            chunk,
            bindings: vec![],
            depth,
            scope_depth: -1,
            match_depth: 0,
            stack_depth: 0,
            ast: &ast.0,
            span: ast.1,
            line: 0,
            lines: vec![],
            loop_info: vec![],
            upvalues: vec![],
            src,
            input,
            tail_pos: false,
            debug,
        }
    }

    pub fn visit(&mut self, node: &'static Spanned<Ast>) {
        let root_node = self.ast;
        let root_span = self.span;
        let (ast, span) = node;
        self.ast = ast;
        self.span = *span;
        self.compile();
        self.ast = root_node;
        self.span = root_span;
    }

    fn jump(&mut self, op: Op, len: usize) {
        let low = len as u8;
        let high = (len >> 8) as u8;
        self.emit_op(op);
        self.emit_byte(high as usize);
        self.emit_byte(low as usize);
    }

    fn stub_jump(&mut self, op: Op) -> usize {
        use Op::*;
        match op {
            JumpIfFalse | JumpIfTrue | JumpIfZero => self.stack_depth -= 1,
            _ => (),
        }
        let out = self.chunk.bytecode.len();
        self.emit_op(op);
        self.emit_byte(0xff);
        self.emit_byte(0xff);
        out
    }

    fn patch_jump(&mut self, i: usize, len: usize) {
        let low = len as u8;
        let high = (len >> 8) as u8;
        self.chunk.bytecode[i + 1] = high;
        self.chunk.bytecode[i + 2] = low;
    }

    pub fn emit_constant(&mut self, val: Value) {
        let const_idx = if let Some(idx) = self.chunk.constants.iter().position(|v| *v == val) {
            idx
        } else {
            self.chunk.constants.push(val);
            self.chunk.constants.len() - 1
        };

        if const_idx > u16::MAX as usize {
            panic!(
                "internal Ludus compiler error: too many constants in chunk:{}:: {}",
                self.span, self.ast
            )
        }
        self.emit_op(Op::Constant);
        let low = const_idx as u8;
        let high = (const_idx >> 8) as u8;
        self.emit_byte(high as usize);
        self.emit_byte(low as usize);
        self.stack_depth += 1;
    }

    fn reset_match(&mut self) {
        self.emit_op(Op::ResetMatch);
        self.match_depth = 0;
    }

    fn match_constant(&mut self, val: Value) {
        let const_idx = match self.chunk.constants.iter().position(|v| *v == val) {
            Some(idx) => idx,
            None => {
                self.chunk.constants.push(val);
                self.chunk.constants.len() - 1
            }
        };
        if const_idx > u16::MAX as usize {
            panic!(
                "internal Ludus compiler error: too many constants in chunk:{}:: {}",
                self.span, self.ast
            )
        }
        self.emit_op(Op::MatchConstant);
        let low = const_idx as u8;
        let high = (const_idx >> 8) as u8;
        self.emit_byte(high as usize);
        self.emit_byte(low as usize);
    }

    fn emit_op(&mut self, op: Op) {
        self.chunk.bytecode.push(op as u8);
        self.chunk.spans.push(self.span);
    }

    fn emit_byte(&mut self, byte: usize) {
        self.chunk.bytecode.push(byte as u8);
        self.chunk.spans.push(self.span);
    }

    fn len(&self) -> usize {
        self.chunk.bytecode.len()
    }

    pub fn bind(&mut self, name: &'static str) {
        self.msg(format!("binding `{name}` in {}", self.input));
        self.msg(format!(
            "stack depth: {}; match depth: {}",
            self.stack_depth, self.match_depth
        ));
        self.msg(format!(
            "at stack index: {}",
            self.stack_depth - self.match_depth - 1
        ));
        let binding = Binding {
            name,
            depth: self.scope_depth,
            stack_pos: self.stack_depth - self.match_depth - 1,
        };
        self.bindings.push(binding);
        self.msg(format!(
            "new locals: {}",
            self.bindings
                .iter()
                .map(|binding| format!("{binding}"))
                .collect::<Vec<_>>()
                .join("|")
        ));
    }

    fn resolve_local(&self, name: &'static str) -> Option<usize> {
        for binding in self.bindings.iter() {
            if binding.name == name {
                return Some(binding.stack_pos);
            }
        }
        None
    }

    fn resolve_upvalue(&self, name: &'static str) -> Option<usize> {
        self.upvalues.iter().position(|uv| *uv == name)
    }

    fn resolve_binding(&mut self, name: &'static str) {
        self.msg(format!(
            "resolving binding `{name}` in {}\nlocals: {}",
            self.input,
            self.bindings
                .iter()
                .map(|binding| format!("{binding}"))
                .collect::<Vec<_>>()
                .join("|")
        ));
        if let Some(pos) = self.resolve_local(name) {
            self.msg(format!("at locals position {pos}"));
            self.emit_op(Op::PushBinding);
            self.emit_byte(pos);
            self.stack_depth += 1;
            return;
        }
        if let Some(pos) = self.resolve_upvalue(name) {
            self.msg(format!("as upvalue {pos}"));
            self.emit_op(Op::GetUpvalue);
            self.emit_byte(pos);
            self.stack_depth += 1;
            return;
        }
        if self.depth == 0 {
            self.msg("as global".to_string());
            self.emit_constant(Value::Keyword(name));
            self.emit_op(Op::PushGlobal);
        } else {
            self.msg(format!("as enclosing upvalue {}", self.upvalues.len()));
            self.emit_op(Op::GetUpvalue);
            self.emit_byte(self.upvalues.len());
            self.upvalues.push(name);
            self.stack_depth += 1;
        }
    }

    fn duplicate(&mut self) {
        self.emit_op(Op::Duplicate);
        self.stack_depth += 1;
    }

    fn pop(&mut self) {
        self.emit_op(Op::Pop);
        self.stack_depth -= 1;
    }

    fn pop_n(&mut self, n: usize) {
        match n {
            0 => (),
            1 => self.pop(),
            n => {
                self.emit_op(Op::PopN);
                self.emit_byte(n);
                self.stack_depth -= n;
            }
        }
    }

    fn store(&mut self) {
        self.emit_op(Op::Store);
        self.stack_depth -= 1;
    }

    fn store_n(&mut self, n: usize) {
        self.emit_op(Op::StoreN);
        self.emit_byte(n);
        self.stack_depth -= n;
    }

    fn load(&mut self) {
        self.emit_op(Op::Load);
        self.stack_depth += 1;
    }

    fn load_n(&mut self, n: usize) {
        self.emit_op(Op::LoadN);
        self.emit_byte(n);
        self.stack_depth += n;
    }

    fn enter_scope(&mut self) {
        self.scope_depth += 1;
    }

    fn leave_scope(&mut self) {
        self.msg(format!("leaving scope {}", self.scope_depth));
        while let Some(binding) = self.bindings.last() {
            if binding.depth == self.scope_depth {
                let unbound = self.bindings.pop();
                self.msg(format!("releasing binding {}", unbound.unwrap()));
            } else {
                break;
            }
        }
        self.scope_depth -= 1;
    }

    fn enter_loop(&mut self, arity: usize) {
        self.loop_info
            .push(LoopInfo::new(self.len(), self.stack_depth - arity));
    }

    fn leave_loop(&mut self) {
        self.loop_info.pop();
    }

    fn loop_info(&self) -> LoopInfo {
        self.loop_info.last().unwrap().clone()
    }

    fn loop_idx(&self) -> usize {
        self.loop_info.last().unwrap().start
    }

    fn loop_root(&self) -> usize {
        self.loop_info.last().unwrap().stack_root
    }

    fn msg(&mut self, str: String) {
        if self.debug {
            self.emit_op(Op::Msg);
            self.emit_byte(self.chunk.msgs.len());
            println!("{str}");
            self.chunk.msgs.push(str);
        }
    }

    fn report_depth(&mut self, label: &'static str) {
        self.msg(format!("***{label} stack depth: {}", self.stack_depth));
    }

    fn report_depth_str(&mut self, label: String) {
        self.msg(format!("***{label} stack depth: {}", self.stack_depth));
    }

    fn report_ast(&mut self, label: String, node: &'static Spanned<Ast>) {
        self.msg(format!("***{label}: {}", node.0.show()))
    }

    pub fn compile(&mut self) {
        use Ast::*;
        match self.ast {
            Error => unreachable!(),
            Nil => {
                self.emit_op(Op::Nil);
                self.stack_depth += 1;
            }
            Number(n) => self.emit_constant(Value::Number(NotNan::new(*n).unwrap())),
            Boolean(b) => {
                self.emit_op(if *b { Op::True } else { Op::False });
                self.stack_depth += 1;
            }
            String(s) => {
                self.emit_constant(Value::Interned(s));
            }
            Keyword(s) => self.emit_constant(Value::Keyword(s)),
            Block(lines) => {
                let tail_pos = self.tail_pos;
                self.tail_pos = false;
                // increase the scope
                self.enter_scope();
                // stash the stack depth
                let stack_depth = self.stack_depth;
                // evaluate all the lines but the last
                for expr in lines.iter().take(lines.len() - 1) {
                    // evaluate the expression
                    self.visit(expr);
                    // if it doesn't bind a name, pop the result from the stack
                    if !is_binding(expr) {
                        self.pop();
                    }
                }

                // now, evaluate the last expression in the block
                let last_expr = lines.last().unwrap();
                match last_expr {
                    // if the last expression is a let form,
                    // return the evaluated rhs instead of whatever is last on the stack
                    // we do this by pretending it's a binding
                    (Let(patt, expr), _) => {
                        self.visit(expr);
                        let expr_pos = self.stack_depth - 1;
                        self.report_ast("let binding: matching".to_string(), patt);
                        self.reset_match();
                        self.emit_op(Op::LoadScrutinee);
                        self.visit(patt);
                        self.emit_op(Op::PanicNoLetMatch);
                        self.emit_op(Op::PushBinding);
                        self.emit_byte(expr_pos);
                        self.stack_depth += 1;
                    }
                    // otherwise, just evaluate it and leave the value on the stack
                    _ => {
                        self.tail_pos = tail_pos;
                        self.visit(last_expr);
                    }
                }

                // store the value in the return register
                self.store();

                // reset the scope
                self.leave_scope();
                // reset the stack
                self.report_depth("leaving block before pop");
                self.msg(format!(
                    "popping back from {} to {}",
                    self.stack_depth, stack_depth,
                ));
                self.pop_n(self.stack_depth - stack_depth);
                // load the value from the return register
                self.load();
            }
            If(cond, then, r#else) => {
                let tail_pos = self.tail_pos;
                self.tail_pos = false;
                self.visit(cond);
                self.report_depth("after condition");
                let jif_idx = self.stub_jump(Op::JumpIfFalse);
                // self.stack_depth -= 1;
                self.tail_pos = tail_pos;
                self.visit(then);
                self.report_depth("after consequent");
                let jump_idx = self.stub_jump(Op::Jump);
                self.stack_depth -= 1;
                self.visit(r#else);
                self.report_depth("after alternative");
                // self.stack_depth += 1;
                let end_idx = self.len();
                let jif_offset = jump_idx - jif_idx;
                let jump_offset = end_idx - jump_idx - 3;
                self.patch_jump(jif_idx, jif_offset);
                self.patch_jump(jump_idx, jump_offset);
            }
            Let(patt, expr) => {
                self.report_depth("before let binding");
                self.visit(expr);
                self.report_depth("after let expr");
                self.report_ast("let binding: matching".to_string(), patt);
                self.reset_match();
                self.emit_op(Op::LoadScrutinee);
                self.visit(patt);
                self.emit_op(Op::PanicNoLetMatch);
                self.report_depth("after let binding");
            }
            WordPattern(name) => {
                self.emit_op(Op::UnconditionalMatch);
                self.bind(name);
            }
            Word(name) | Splat(name) => self.resolve_binding(name),
            PlaceholderPattern => {
                self.emit_op(Op::UnconditionalMatch);
            }
            NilPattern => {
                self.emit_op(Op::MatchNil);
            }
            BooleanPattern(b) => {
                if *b {
                    self.emit_op(Op::MatchTrue);
                } else {
                    self.emit_op(Op::MatchFalse);
                }
            }
            NumberPattern(n) => {
                self.match_constant(Value::Number(NotNan::new(*n).unwrap()));
            }
            KeywordPattern(s) => {
                let existing_kw = self.chunk.keywords.iter().position(|kw| kw == s);
                let kw_index = match existing_kw {
                    Some(index) => index,
                    None => self.chunk.keywords.len(),
                };
                if kw_index == self.chunk.keywords.len() {
                    self.chunk.keywords.push(s);
                }
                self.match_constant(Value::Keyword(s));
            }
            AsPattern(word, typ) => {
                self.emit_constant(Value::Keyword(typ));
                self.emit_op(Op::MatchType);
                self.stack_depth -= 1;
                self.bind(word);
            }
            StringPattern(s) => {
                self.match_constant(Value::Interned(s));
            }
            TuplePattern(members) => {
                // first, test the tuple against length
                // check if we're splatted
                // different opcodes w/ splats, but same logic
                let mut is_splatted = false;
                if let Some((Splattern(_), _)) = members.last() {
                    is_splatted = true;
                    self.emit_op(Op::MatchSplattedTuple);
                } else {
                    self.emit_op(Op::MatchTuple);
                }

                self.emit_byte(members.len());
                // skip everything if tuple lengths don't match
                let before_load_tup_idx = self.stub_jump(Op::JumpIfNoMatch);

                // set up the per-member conditional logic
                let mut jump_idxes = vec![];
                // stash match_depth, and set it to the tuple len
                let match_depth = self.match_depth;
                self.match_depth = members.len();

                // load the tuple and update the stack len
                if is_splatted {
                    self.emit_op(Op::LoadSplattedTuple);
                    self.emit_byte(members.len());
                } else {
                    self.emit_op(Op::LoadTuple);
                }
                self.stack_depth += members.len();

                // visit each member
                for member in members {
                    // reduce the match depth to start
                    self.match_depth -= 1;
                    self.emit_op(Op::MatchDepth);
                    self.emit_byte(self.match_depth);
                    // visit the pattern member
                    self.visit(member);
                    // and jump if there's no match
                    jump_idxes.push(self.stub_jump(Op::JumpIfNoMatch));
                }

                // if we get here--not having jumped on no match--we're matched; jump the "no match" code
                let jump_idx = self.stub_jump(Op::Jump);

                // patch up the previous no match jumps to jump to clean-up code
                for idx in jump_idxes {
                    self.patch_jump(idx, self.len() - idx - 3)
                }
                // pop everything that was pushed
                // don't change the compiler stack representation, tho
                // we need this as cleanup code with no matches
                // the compiler should still have access to the bindings in this pattern
                self.emit_op(Op::PopN);
                self.emit_byte(members.len());

                // patch up the tuple length match jump
                self.patch_jump(before_load_tup_idx, self.len() - before_load_tup_idx - 3);

                // patch up the yes-matches unconditional jump
                self.patch_jump(jump_idx, self.len() - jump_idx - 3);

                // finally, for any further matches (e.g. nested lists/tuples)
                // add increase the match depth, since we've added a bunch
                // of bindings to the stack
                self.match_depth = match_depth + members.len();
            }
            ListPattern(members) => {
                let mut is_splatted = false;
                if let Some((Splattern(_), _)) = members.last() {
                    is_splatted = true;
                    self.emit_op(Op::MatchSplattedList)
                } else {
                    self.emit_op(Op::MatchList);
                }
                // TODO: lists must be able to be longer than 256 elements; fix this
                self.emit_byte(members.len());
                let before_load_tup_idx = self.stub_jump(Op::JumpIfNoMatch);

                let mut jump_idxes = vec![];
                let match_depth = self.match_depth;
                self.match_depth = members.len();

                if is_splatted {
                    self.emit_op(Op::LoadSplattedList);
                    self.emit_byte(members.len());
                } else {
                    self.emit_op(Op::LoadList);
                }
                self.stack_depth += members.len();

                for member in members {
                    self.match_depth -= 1;
                    self.emit_op(Op::MatchDepth);
                    self.emit_byte(self.match_depth);
                    self.visit(member);
                    jump_idxes.push(self.stub_jump(Op::JumpIfNoMatch));
                }

                let jump_idx = self.stub_jump(Op::Jump);

                for idx in jump_idxes {
                    self.patch_jump(idx, self.len() - idx - 3)
                }

                self.emit_op(Op::PopN);
                self.emit_byte(members.len());

                self.patch_jump(before_load_tup_idx, self.len() - before_load_tup_idx - 3);

                self.patch_jump(jump_idx, self.len() - jump_idx - 3);

                self.match_depth = match_depth + members.len();
            }
            DictPattern(pairs) => {
                // here's an algorithm for dealing with splatted dicts
                // check len to see it's at least as long as the pattern
                // then, match against all the values
                // then push the dict itself as last value
                // and then emit an opcode and constant/keyword to OMIT that key from the dict
                let mut is_splatted = false;
                if let Some((Splattern(_), _)) = pairs.last() {
                    is_splatted = true;
                    self.emit_op(Op::MatchSplattedDict);
                } else {
                    self.emit_op(Op::MatchDict);
                }
                self.emit_byte(pairs.len());
                let before_load_dict_idx = self.stub_jump(Op::JumpIfNoMatch);

                let mut jump_idxes = vec![];
                let dict_stack_pos = self.stack_depth - self.match_depth - 1;
                let mut splattern = None;
                let mut pairs_len = pairs.len();
                if is_splatted {
                    splattern = pairs.last();
                    pairs_len -= 1;
                }

                let match_depth = self.match_depth;
                self.match_depth = 0;
                for pair in pairs.iter().take(pairs_len) {
                    let (key, pattern) = match &pair.0 {
                        KeyPairPattern(key, pattern) => (Value::Keyword(key), pattern),
                        StrPairPattern(key, pattern) => (Value::Interned(key), pattern),
                        _ => unreachable!("expected key to be keyword or string"),
                    };
                    self.emit_constant(key);
                    self.emit_op(Op::LoadDictValue);
                    self.emit_byte(dict_stack_pos);
                    self.visit(pattern);
                    jump_idxes.push(self.stub_jump(Op::JumpIfNoMatch));
                }

                if is_splatted {
                    // pull the dict out of the stack
                    // drop every value in the pattern
                    self.emit_op(Op::PushBinding);
                    self.emit_byte(dict_stack_pos);
                    self.stack_depth += 1;

                    for pair in pairs.iter().take(pairs_len) {
                        let (KeyPairPattern(key, _), _) = pair else {
                            unreachable!()
                        };
                        self.emit_constant(Value::Keyword(key));
                        self.emit_op(Op::DropDictEntry);
                        self.stack_depth -= 1;
                    }

                    if let Some(splatt) = splattern {
                        self.visit(splatt);
                    }
                }

                self.match_depth = match_depth + pairs.len();

                let jump_idx = self.stub_jump(Op::Jump);

                for idx in jump_idxes {
                    self.patch_jump(idx, self.len() - idx - 3);
                }

                // do this explicitly to keep compiler & vm stacks in sync
                self.emit_op(Op::Pop);
                self.emit_byte(pairs.len());

                self.patch_jump(before_load_dict_idx, self.len() - before_load_dict_idx - 3);
                self.patch_jump(jump_idx, self.len() - jump_idx - 3);
            }
            Splattern(patt) => self.visit(patt),
            InterpolatedPattern(parts) => {
                // println!("An interpolated pattern of {} parts", parts.len());
                let mut pattern = "".to_string();
                let mut words = vec![];
                for (part, _) in parts {
                    match part {
                        StringPart::Word(word) => {
                            // println!("wordpart: {word}");
                            words.push(*word);
                            pattern.push_str("(.*)");
                        }
                        StringPart::Data(data) => {
                            // println!("datapart: {data}");
                            let data = regex::escape(data);
                            pattern.push_str(data.as_str());
                        }
                        StringPart::Inline(..) => unreachable!(),
                    }
                }
                let re = Regex::new(pattern.as_str()).unwrap();
                let moar_words = words.clone();
                let string_pattern = StrPattern { words, re };

                let pattern_idx = self.chunk.string_patterns.len();
                self.chunk.string_patterns.push(string_pattern);

                self.emit_op(Op::MatchString);
                self.emit_byte(pattern_idx);

                let jnm_idx = self.stub_jump(Op::JumpIfNoMatch);

                self.emit_op(Op::PushStringMatches);
                self.emit_byte(pattern_idx);

                for word in moar_words {
                    let binding = Binding {
                        name: word,
                        depth: self.scope_depth,
                        stack_pos: self.stack_depth,
                    };
                    self.bindings.push(binding);
                    self.stack_depth += 1;
                }

                self.patch_jump(jnm_idx, self.len() - jnm_idx - 3);
            }
            KeyPairPattern(..) | StrPairPattern(..) => unreachable!(),
            Tuple(members) => {
                self.tail_pos = false;
                for member in members {
                    self.visit(member);
                }
                self.emit_op(Op::PushTuple);
                self.emit_byte(members.len());
                self.stack_depth = self.stack_depth + 1 - members.len();
            }
            List(members) => {
                self.tail_pos = false;
                self.emit_op(Op::PushList);
                self.stack_depth += 1;
                for member in members {
                    self.visit(member);
                    if matches!(member, (Splat(..), _)) {
                        self.emit_op(Op::ConcatList);
                    } else {
                        self.emit_op(Op::AppendList);
                    }
                    self.stack_depth -= 1;
                }
            }
            LBox(name, expr) => {
                self.tail_pos = false;
                self.visit(expr);
                self.emit_op(Op::PushBox);
                self.bind(name);
            }
            Dict(pairs) => {
                self.tail_pos = false;
                self.emit_op(Op::PushDict);
                self.stack_depth += 1;
                for pair in pairs.iter().rev() {
                    self.visit(pair);
                    if matches!(pair, (Splat(..), _)) {
                        self.emit_op(Op::ConcatDict);
                        self.stack_depth -= 1;
                    } else {
                        self.emit_op(Op::AppendDict);
                        self.stack_depth -= 2;
                    }
                }
            }
            StringPair(key, value) => {
                self.tail_pos = false;
                self.emit_constant(Value::Interned(key));
                self.visit(value);
            }
            KeywordPair(key, value) => {
                self.tail_pos = false;
                self.emit_constant(Value::Keyword(key));
                self.visit(value);
            }
            // TODO: thread tail position through this
            Synthetic(first, second, rest) => {
                let tail_pos = self.tail_pos;
                self.tail_pos = false;
                match (&first.0, &second.0) {
                    (Word(name), Keyword(key)) => {
                        self.report_depth_str(format!("accessing keyword: {name} :{key}"));
                        self.visit(first);
                        self.visit(second);
                        self.emit_op(Op::GetKey);
                        self.stack_depth -= 1;
                        self.report_depth("after keyword access");
                    }
                    (Keyword(_), Method(str, args)) | (Word(_), Method(str, args)) => {
                        self.visit(first);
                        self.emit_constant(Value::Keyword(str));
                        self.visit(args);
                        self.emit_op(Op::SendMethod);
                        // target, method, args -> result
                        self.stack_depth -= 2;
                    }
                    (Keyword(_), Arguments(args)) => {
                        self.visit(&args[0]);
                        self.visit(first);
                        self.emit_op(Op::GetKey);
                        self.stack_depth -= 1;
                    }
                    (Or, Arguments(args)) => {
                        let mut jump_idxes = vec![];
                        if !args.is_empty() {
                            let mut args = args.iter().rev();
                            let last = args.next().unwrap();
                            for arg in args.rev() {
                                self.visit(arg);
                                self.duplicate();
                                jump_idxes.push(self.stub_jump(Op::JumpIfTrue));
                                self.pop();
                            }
                            self.visit(last);
                            for idx in jump_idxes {
                                self.patch_jump(idx, self.len() - idx - 3);
                            }
                        } else {
                            self.emit_op(Op::False);
                            self.stack_depth += 1;
                        }
                    }
                    (And, Arguments(args)) => {
                        let mut jump_idxes = vec![];
                        if !args.is_empty() {
                            let mut args = args.iter().rev();
                            let last = args.next().unwrap();
                            for arg in args.rev() {
                                self.visit(arg);
                                self.duplicate();
                                jump_idxes.push(self.stub_jump(Op::JumpIfFalse));
                                self.pop();
                            }
                            self.visit(last);
                            for idx in jump_idxes {
                                self.patch_jump(idx, self.len() - idx - 3);
                            }
                        } else {
                            self.emit_op(Op::True);
                            self.stack_depth += 1;
                        }
                    }
                    (Word(fn_name), Arguments(args)) => {
                        self.report_depth_str(format!("calling function {fn_name}"));
                        if has_placeholder(args) {
                            let arity = args.len();
                            for arg in args {
                                self.visit(arg);
                            }
                            self.resolve_binding(fn_name);
                            self.emit_op(Op::Partial);
                            self.emit_byte(arity);
                            self.stack_depth -= args.len() - 1;
                        } else {
                            match get_builtin(fn_name, args.len()) {
                                Some(code) => {
                                    for arg in args {
                                        self.visit(arg);
                                    }
                                    self.emit_op(code);
                                    self.stack_depth -= args.len() - 1;
                                }
                                None => {
                                    let arity = args.len();
                                    for arg in args {
                                        self.visit(arg);
                                    }
                                    self.resolve_binding(fn_name);
                                    // if we're in tail position AND there aren't any rest args, this should be a tail call (I think)
                                    self.report_depth_str(format!("after {arity} args"));
                                    if rest.is_empty() && tail_pos {
                                        self.emit_op(Op::TailCall);
                                    } else {
                                        self.emit_op(Op::Call);
                                    }
                                    self.emit_byte(arity);
                                    self.stack_depth -= arity;
                                }
                            }
                        }
                    }
                    _ => unreachable!(),
                }
                // the last term in rest should be in tail position if we are in tail position
                let num_rest_terms = rest.len();
                for (i, (term, _)) in rest.iter().enumerate() {
                    match term {
                        Keyword(str) => {
                            self.emit_constant(Value::Keyword(str));
                            self.emit_op(Op::GetKey);
                            // target, keyword -> value
                            self.stack_depth -= 1;
                        }
                        Method(str, args) => {
                            self.emit_constant(Value::Keyword(str));
                            self.visit(args);
                            self.emit_op(Op::SendMethod);
                            // target, method, args -> result
                            self.stack_depth -= 2;
                        }
                        Arguments(args) => {
                            self.store();
                            let arity = args.len();
                            for arg in args {
                                self.visit(arg);
                            }
                            self.load();
                            if tail_pos && i == num_rest_terms - 1 {
                                self.emit_op(Op::TailCall)
                            } else {
                                self.emit_op(Op::Call);
                            }
                            self.emit_byte(arity);
                            self.stack_depth -= arity;
                        }
                        _ => unreachable!(),
                    }
                }
                self.tail_pos = tail_pos;
            }
            When(clauses) => {
                let tail_pos = self.tail_pos;
                let mut jump_idxes = vec![];
                let mut clauses = clauses.iter();
                while let Some((WhenClause(cond, body), _)) = clauses.next() {
                    self.tail_pos = false;
                    self.visit(cond.as_ref());
                    let jif_jump_idx = self.stub_jump(Op::JumpIfFalse);
                    self.tail_pos = tail_pos;
                    self.visit(body);
                    self.stack_depth -= 1;
                    jump_idxes.push(self.stub_jump(Op::Jump));
                    self.patch_jump(jif_jump_idx, self.len() - jif_jump_idx - 3);
                }
                self.emit_op(Op::PanicWhenFallthrough);
                for idx in jump_idxes {
                    self.patch_jump(idx, self.len() - idx - 3);
                }
                self.stack_depth += 1;
            }
            WhenClause(..) => unreachable!(),
            Match(scrutinee, clauses) => {
                let tail_pos = self.tail_pos;
                self.tail_pos = false;
                self.visit(scrutinee.as_ref());
                self.emit_op(Op::LoadScrutinee);
                let stack_depth = self.stack_depth;
                let mut jump_idxes = vec![];
                let mut clauses = clauses.iter();
                while let Some((MatchClause(pattern, guard, body), _)) = clauses.next() {
                    self.tail_pos = false;
                    let mut no_match_jumps = vec![];
                    self.report_ast("match clause: ".to_string(), pattern);
                    // self.scope_depth += 1;
                    self.enter_scope();
                    self.match_depth = 0;
                    self.visit(pattern);
                    no_match_jumps.push(self.stub_jump(Op::JumpIfNoMatch));
                    if guard.is_some() {
                        let guard_expr: &'static Spanned<Ast> =
                            Box::leak(Box::new(guard.clone().unwrap()));
                        self.visit(guard_expr);
                        no_match_jumps.push(self.stub_jump(Op::JumpIfFalse));
                    }
                    self.tail_pos = tail_pos;
                    self.visit(body);
                    self.store();
                    self.leave_scope();
                    self.pop_n(self.stack_depth - stack_depth);
                    jump_idxes.push(self.stub_jump(Op::Jump));
                    for idx in no_match_jumps {
                        self.patch_jump(idx, self.len() - idx - 3);
                    }
                }
                self.emit_op(Op::PanicNoMatch);

                for idx in jump_idxes {
                    self.patch_jump(idx, self.len() - idx - 3);
                }
                self.pop_n(self.stack_depth - stack_depth);
                self.emit_op(Op::Load);
                self.stack_depth += 1;
            }
            Spawn(body) => {
                // we compile this identically to a function
                // but it's a single, nullary fn
                // no match at the beginning, just go
                let name = "_spawned";
                let mut compiler = Compiler::new(
                    body,
                    self.input,
                    self.src,
                    self.depth + 1,
                    self.chunk.env.clone(),
                    self.debug,
                );
                compiler.tail_pos = true;
                compiler.visit(body);
                compiler.store();
                compiler.scope_depth -= 1;
                while let Some(binding) = compiler.bindings.last() {
                    if binding.depth > compiler.scope_depth {
                        compiler.bindings.pop();
                    } else {
                        break;
                    }
                }
                compiler.pop_n(compiler.stack_depth);
                compiler.stack_depth = 0;
                compiler.emit_op(Op::Return);
                let arities = vec![0];
                let chunks = vec![compiler.chunk];
                let lfn = crate::value::LFn::Defined {
                    name,
                    doc: None,
                    arities,
                    chunks,
                    splat: 0,
                    closed: RefCell::new(vec![]),
                    patterns: vec!["()".to_string()],
                };

                let the_fn = Value::Fn(Rc::new(lfn));

                self.emit_constant(the_fn);
                for upvalue in compiler.upvalues {
                    self.resolve_binding(upvalue);
                    self.emit_op(Op::SetUpvalue);
                    self.stack_depth -= 1;
                }
                self.emit_op(Op::Spawn);
            }
            Receive(clauses) => {
                self.msg("********starting receive".to_string());
                let tail_pos = self.tail_pos;
                self.emit_op(Op::ClearMessage);
                let receive_begin = self.len();
                self.emit_op(Op::LoadMessage);
                self.stack_depth += 1;
                let stack_depth = self.stack_depth;
                let mut jump_idxes = vec![];
                let mut clauses = clauses.iter();
                while let Some((MatchClause(pattern, guard, body), _)) = clauses.next() {
                    self.tail_pos = false;
                    let mut no_match_jumps = vec![];
                    self.enter_scope();
                    self.reset_match();
                    self.visit(pattern);
                    no_match_jumps.push(self.stub_jump(Op::JumpIfNoMatch));
                    if guard.is_some() {
                        let guard_expr: &'static Spanned<Ast> =
                            Box::leak(Box::new(guard.clone().unwrap()));
                        self.visit(guard_expr);
                        no_match_jumps.push(self.stub_jump(Op::JumpIfFalse));
                    }
                    self.emit_op(Op::MatchMessage);
                    self.tail_pos = tail_pos;
                    self.visit(body);
                    self.store();
                    self.leave_scope();
                    self.pop_n(self.stack_depth - stack_depth);
                    jump_idxes.push(self.stub_jump(Op::Jump));
                    for idx in no_match_jumps {
                        self.patch_jump(idx, self.len() - idx - 3);
                    }
                }
                // TODO: get the next message
                self.emit_op(Op::NextMessage);
                // TODO: jump back to the "get a message" instruction
                let jump_back = self.stub_jump(Op::JumpBack);
                self.patch_jump(jump_back, self.len() - receive_begin - 3);

                for idx in jump_idxes {
                    self.patch_jump(idx, self.len() - idx - 3);
                }
                self.pop_n(self.stack_depth - stack_depth);
                self.emit_op(Op::Load);
                self.stack_depth += 1;
                self.msg("********receive completed".to_string());
            }
            MatchClause(..) => unreachable!(),
            Fn(name, body, doc) => {
                let is_anon = name.is_empty();
                let mut name = name;
                let mut patterns = vec![];

                if !is_anon {
                    let declared = self.chunk.constants.iter().any(|val| match val {
                        Value::Fn(lfn) => {
                            if matches!(lfn.as_ref(), LFn::Declared { .. }) {
                                lfn.name() == *name
                            } else {
                                false
                            }
                        }
                        _ => false,
                    });
                    if !declared {
                        let declaration = Value::Fn(Rc::new(LFn::Declared { name }));
                        self.emit_constant(declaration);
                        self.bind(name);
                    }
                } else {
                    name = &"_anon";
                }

                let FnBody(fn_body) = &body.as_ref().0 else {
                    unreachable!()
                };

                let mut compilers: HashMap<u8, Compiler> = HashMap::new();

                let mut upvalues = vec![];

                let mut has_splat = false;

                for clause in fn_body {
                    let MatchClause(pattern, guard, clause_body) = &clause.0 else {
                        unreachable!()
                    };
                    let full_pattern = pattern;
                    patterns.push(full_pattern.as_ref().0.show());
                    let TuplePattern(pattern) = &pattern.0 else {
                        unreachable!()
                    };

                    if matches!(pattern.last(), Some((Splattern(_), _))) {
                        has_splat = true;
                    };

                    let arity = pattern.len() as u8;

                    let compiler = match compilers.get_mut(&arity) {
                        Some(compiler) => compiler,
                        None => {
                            let mut compiler = Compiler::new(
                                clause,
                                self.input,
                                self.src,
                                self.depth + 1,
                                self.chunk.env.clone(),
                                self.debug,
                            );
                            compiler.reset_match();
                            // compiler.emit_op(Op::ResetMatch);
                            compilers.insert(arity, compiler);
                            compilers.get_mut(&arity).unwrap()
                        }
                    };
                    compiler.tail_pos = false;

                    compiler.stack_depth += arity as usize;
                    compiler.scope_depth += 1;
                    compiler.match_depth = arity as usize;

                    std::mem::swap(&mut upvalues, &mut compiler.upvalues);

                    let mut tup_jump_idxes = vec![];
                    compiler.report_ast("function clause matching: ".to_string(), full_pattern);
                    for member in pattern {
                        compiler.match_depth -= 1;
                        compiler.emit_op(Op::MatchDepth);
                        compiler.emit_byte(compiler.match_depth);
                        compiler.visit(member);
                        tup_jump_idxes.push(compiler.stub_jump(Op::JumpIfNoMatch));
                    }
                    if pattern.is_empty() {
                        compiler.emit_op(Op::UnconditionalMatch);
                    }
                    let jump_idx = compiler.stub_jump(Op::Jump);
                    for idx in tup_jump_idxes {
                        compiler.patch_jump(idx, compiler.len() - idx - 3);
                    }
                    // compiler.emit_op(Op::PopN);
                    // compiler.emit_byte(arity as usize);
                    compiler.patch_jump(jump_idx, compiler.len() - jump_idx - 3);
                    let mut no_match_jumps = vec![];
                    no_match_jumps.push(compiler.stub_jump(Op::JumpIfNoMatch));
                    if guard.is_some() {
                        let guard_expr: &'static Spanned<Ast> =
                            Box::leak(Box::new(guard.clone().unwrap()));
                        compiler.visit(guard_expr);
                        no_match_jumps.push(compiler.stub_jump(Op::JumpIfFalse));
                    }
                    compiler.tail_pos = true;
                    compiler.visit(clause_body);
                    compiler.store();
                    compiler.scope_depth -= 1;
                    while let Some(binding) = compiler.bindings.last() {
                        if binding.depth > compiler.scope_depth {
                            compiler.bindings.pop();
                        } else {
                            break;
                        }
                    }
                    compiler.pop_n(compiler.stack_depth);
                    compiler.stack_depth = 0;
                    compiler.emit_op(Op::Return);
                    for idx in no_match_jumps {
                        compiler.patch_jump(idx, compiler.len() - idx - 3);
                    }
                    // compiler.scope_depth -= 1;

                    std::mem::swap(&mut compiler.upvalues, &mut upvalues);
                }

                let mut compilers = compilers.into_iter().collect::<Vec<_>>();
                compilers.sort_by(|(a, _), (b, _)| a.cmp(b));

                let mut arities = vec![];
                let mut chunks = vec![];

                for (arity, mut compiler) in compilers {
                    compiler.emit_op(Op::PanicNoFnMatch);
                    let chunk = compiler.chunk;
                    if self.debug {
                        println!("=== function chuncktion: {name}/{arity} ===");
                        chunk.dissasemble();
                    }

                    arities.push(arity);
                    chunks.push(chunk);
                }

                let splat = if has_splat {
                    arities.iter().fold(0, |max, curr| max.max(*curr))
                } else {
                    0
                };

                let lfn = crate::value::LFn::Defined {
                    name,
                    doc: *doc,
                    arities,
                    chunks,
                    splat,
                    closed: RefCell::new(vec![]),
                    patterns,
                };

                let the_fn = Value::Fn(Rc::new(lfn));
                // self.emit_constant(the_fn);
                // self.bind(name);

                if !is_anon {
                    let declaration_idx = self
                        .chunk
                        .constants
                        .iter()
                        .position(|val| match val {
                            Value::Fn(lfn) => {
                                if matches!(lfn.as_ref(), LFn::Declared { .. }) {
                                    lfn.name() == *name
                                } else {
                                    false
                                }
                            }
                            _ => false,
                        })
                        .unwrap();
                    self.chunk.constants[declaration_idx] = the_fn;
                    // if the function been forward-declared, bring it to the top of the stack
                    if declaration_idx < self.chunk.constants.len() - 1 {
                        self.resolve_binding(name);
                    }
                } else {
                    self.emit_constant(the_fn)
                }

                for upvalue in upvalues {
                    self.resolve_binding(upvalue);
                    self.emit_op(Op::SetUpvalue);
                    self.stack_depth -= 1;
                }
            }
            FnDeclaration(name) => {
                let lfn = Value::Fn(Rc::new(LFn::Declared { name }));
                self.emit_constant(lfn);
                self.bind(name);
            }
            FnBody(_) => unreachable!(),
            Repeat(times, body) => {
                let tail_pos = self.tail_pos;
                self.tail_pos = false;
                self.visit(times);
                self.emit_op(Op::ToInt);
                // skip the decrement the first time
                self.emit_op(Op::Jump);
                self.emit_byte(0);
                self.emit_byte(1);
                // begin repeat
                self.emit_op(Op::Decrement);
                let repeat_begin = self.len();
                self.duplicate();
                let jiz_idx = self.stub_jump(Op::JumpIfZero);
                // compile the body
                self.visit(body);
                // pop whatever value the body returns
                self.pop();
                let jump_back = self.stub_jump(Op::JumpBack);
                // set jump points
                self.patch_jump(jump_back, self.len() - repeat_begin - 2);
                self.patch_jump(jiz_idx, self.len() - jiz_idx - 3);
                self.pop();
                self.emit_constant(Value::Nil);
                self.tail_pos = tail_pos;
            }
            Loop(value, clauses) => {
                self.report_depth("entering loop");
                let tail_pos = self.tail_pos;
                self.tail_pos = false;
                //algo:
                //first, put the values on the stack
                let (Ast::Tuple(members), _) = value.as_ref() else {
                    unreachable!()
                };
                for member in members {
                    self.visit(member);
                }
                self.report_depth("after loop args");
                let arity = members.len();
                // self.emit_op(Op::StoreN);
                // self.emit_byte(members.len());
                self.store_n(arity);
                let stack_depth = self.stack_depth;
                self.report_depth("loop: after store");
                //then, save the beginning of the loop
                // self.emit_op(Op::Load);
                self.load_n(arity);
                self.enter_loop(arity);
                // self.stack_depth += arity;
                //next, compile each clause:
                let mut clauses = clauses.iter();
                let mut jump_idxes = vec![];
                while let Some((Ast::MatchClause(pattern, guard, body), _)) = clauses.next() {
                    self.tail_pos = false;
                    self.report_depth("loop: after load");
                    self.reset_match();
                    // self.emit_op(Op::ResetMatch);
                    self.enter_scope();
                    // self.scope_depth += 1;
                    let (Ast::TuplePattern(members), _) = pattern.as_ref() else {
                        unreachable!()
                    };
                    self.match_depth = arity;
                    let mut jnm_idxes = vec![];
                    self.report_ast("loop clause matching: ".to_string(), pattern);
                    for member in members {
                        self.match_depth -= 1;
                        self.emit_op(Op::MatchDepth);
                        self.emit_byte(self.match_depth);
                        self.visit(member);
                        jnm_idxes.push(self.stub_jump(Op::JumpIfNoMatch));
                    }
                    if guard.is_some() {
                        let guard_expr: &'static Spanned<Ast> =
                            Box::leak(Box::new(guard.clone().unwrap()));
                        self.visit(guard_expr);
                        jnm_idxes.push(self.stub_jump(Op::JumpIfFalse));
                    }
                    self.tail_pos = tail_pos;
                    self.report_depth("loop: before body");
                    self.visit(body);
                    self.report_depth("loop: after body, before store");
                    // self.emit_op(Op::Store);
                    self.store();
                    self.report_depth("loop: after body, after store");
                    self.leave_scope();
                    self.report_depth_str(format!(
                        "resetting the stack after loop from {} to {stack_depth}",
                        self.stack_depth,
                    ));
                    self.pop_n(self.stack_depth - stack_depth);
                    // while self.stack_depth > stack_depth {
                    //     self.pop();
                    // }
                    jump_idxes.push(self.stub_jump(Op::Jump));
                    for idx in jnm_idxes {
                        self.patch_jump(idx, self.len() - idx - 3);
                    }
                    self.stack_depth += arity;
                }
                self.emit_op(Op::PanicNoMatch);
                for idx in jump_idxes {
                    self.patch_jump(idx, self.len() - idx - 3);
                }
                self.report_depth("before loop arity adjustment");
                self.stack_depth -= arity;
                // pop back to the original depth before load
                // i.e. clear loop args
                // self.pop();
                // self.emit_op(Op::Load);
                self.load();
                // self.stack_depth += 1;
                self.leave_loop();
                self.report_depth("at very end of loop after load");
            }
            Recur(args) => {
                // self.emit_op(Op::Nothing);
                self.report_depth("recur: before args");
                let tail_pos = self.tail_pos;
                self.tail_pos = false;
                let mut argnum = 0;
                for arg in args {
                    self.msg(format!("recur arg: {argnum}"));
                    argnum += 1;
                    self.visit(arg);
                }
                self.report_depth("recur: after args");
                self.store_n(args.len());
                self.report_depth("recur: after store");
                self.msg(format!("loop root depth: {}", self.loop_root()));
                self.pop_n(self.stack_depth - self.loop_root());
                self.report_depth("recur: after stack reset");
                self.load_n(args.len());
                self.report_depth("recur: after load, end of compilation");
                self.jump(Op::JumpBack, self.len() - self.loop_idx());
                self.tail_pos = tail_pos;
            }
            Panic(msg) => {
                self.visit(msg);
                self.emit_op(Op::Panic);
            }
            Interpolated(parts) => {
                self.emit_op(Op::EmptyString);
                self.stack_depth += 1;
                for part in parts {
                    let str = &part.0;
                    match str {
                        StringPart::Inline(_) => unreachable!(),
                        StringPart::Data(str) => {
                            let allocated = Value::String(Rc::new(str.clone()));
                            self.emit_constant(allocated);
                            self.emit_op(Op::ConcatStrings);
                        }
                        StringPart::Word(word) => {
                            self.resolve_binding(word);
                            self.emit_op(Op::Stringify);
                            self.emit_op(Op::ConcatStrings);
                        }
                    }
                    self.stack_depth -= 1;
                }
            }
            Do(terms) => {
                let mut terms = terms.iter();
                let first = terms.next().unwrap();
                let mut terms = terms.rev();
                let last = terms.next().unwrap();
                let terms = terms.rev();
                // put the first value on the stack
                let tail_pos = self.tail_pos;
                self.tail_pos = false;
                self.visit(first);
                for term in terms {
                    self.visit(term);
                    self.emit_op(Op::Call);
                    self.emit_byte(1);
                    self.stack_depth -= 1;
                }
                self.visit(last);
                if tail_pos {
                    self.emit_op(Op::TailCall)
                } else {
                    self.emit_op(Op::Call);
                }
                self.emit_byte(1);
                self.tail_pos = tail_pos;
                self.stack_depth -= 1;
            }
            Placeholder => {
                self.emit_op(Op::Nothing);
            }
            And | Or | Arguments(..) | Method(..) => unreachable!(),
        }
    }

    pub fn disassemble(&self) {
        println!("=== chunk: {} ===", self.input);
        self.chunk.dissasemble();
    }
}
