use crate::internal::as2::hir::{
    Affix, BinaryOperator, ConstantKind, Declaration, Expr, ExprKind, ForCondition, Function,
    FunctionSignature, GetUrlMethod, StatementKind, SwitchElement, TryCatch, UnaryOperator,
};
use crate::internal::as2_codegen::access::VariableAccess;
use crate::internal::as2_codegen::builder::CodeBuilder;
use crate::internal::as2_codegen::context::ScriptContext;
use crate::internal::as2_pcode::{Action, CatchTarget, FunctionParam, PCode, PushValue};

pub(crate) fn gen_statements(
    context: &mut ScriptContext,
    builder: &mut CodeBuilder,
    statements: &[StatementKind],
) {
    let mut hoisted = vec![];
    let mut regular = vec![];
    for statement in statements {
        if matches!(
            statement,
            StatementKind::Expr(Expr {
                value: ExprKind::Function(func),
                ..
            }) if matches!(func.as_ref(), Function {
                    signature: FunctionSignature { name: Some(_), .. },
                    ..
                })
        ) {
            hoisted.push(statement);
        } else {
            regular.push(statement);
        }
    }
    for statement in hoisted {
        gen_statement(context, builder, statement);
    }
    for statement in regular {
        gen_statement(context, builder, statement);
    }
}

pub(crate) fn gen_statement(
    context: &mut ScriptContext,
    builder: &mut CodeBuilder,
    statement: &StatementKind,
) {
    match statement {
        StatementKind::Declare(declaration) => {
            gen_declaration(context, builder, declaration);
        }
        StatementKind::Return(exprs) => {
            for expr in exprs {
                gen_expr(context, builder, expr, false);
            }
            if exprs.is_empty() {
                builder.push(PushValue::Undefined);
            }
            builder.action(Action::Return);
        }
        StatementKind::Throw(exprs) => {
            for expr in exprs {
                gen_expr(context, builder, expr, false);
            }
            if exprs.is_empty() {
                builder.push(PushValue::Undefined);
            }
            builder.action(Action::Throw);
        }
        StatementKind::Expr(expr) => {
            let stack_size = builder.stack_size();
            gen_expr(context, builder, expr, true);
            builder.truncate_stack(stack_size);
        }
        StatementKind::Block(statements) => {
            let stack_size = builder.stack_size();
            gen_statements(context, builder, statements);
            builder.truncate_stack(stack_size);
        }
        StatementKind::ForIn { condition, body } => gen_for_loop(context, builder, condition, body),
        StatementKind::If { condition, yes, no } => gen_if(context, builder, condition, yes, no),
        StatementKind::Break => {
            if let Some(label) = context.break_label() {
                builder.action(Action::Jump(label));
            }
        }
        StatementKind::Continue => {
            if let Some(label) = context.continue_label() {
                builder.action(Action::Jump(label));
            }
        }
        StatementKind::Try(try_catch) => {
            gen_try_catch(context, builder, try_catch);
        }
        StatementKind::WaitForFrame {
            frame,
            scene: _, // Scene is effectively not a concept in a standalone compiler, Flash ignores whatever expr is here if it's not a recognised scene name
            if_loaded,
        } => gen_wait_for_frame(context, builder, frame, if_loaded),
        StatementKind::TellTarget { target, body } => {
            gen_tell_target(context, builder, target, body)
        }
        StatementKind::While { condition, body } => {
            gen_while_loop(context, builder, condition, body)
        }
        StatementKind::InlinePCode(pcode) => {
            let source = PCode::new("inline pcode", pcode);
            // TODO raise this better
            let actions = source
                .to_actions()
                .unwrap_or_else(|e| panic!("{}", e.to_string()));
            builder.append(actions);
        }
        StatementKind::With { target, body } => gen_with(context, builder, target, body),
        StatementKind::Switch { target, elements } => {
            gen_switch(context, builder, target, elements)
        }
    }
}

fn gen_tell_target(
    context: &mut ScriptContext,
    builder: &mut CodeBuilder,
    target: &Expr,
    body: &StatementKind,
) {
    let was_in_tell_target = context.is_in_tell_target();
    if was_in_tell_target {
        builder.push(""); // current target path
        builder.push(11); // _target
        builder.action(Action::GetProperty);
    }

    match &target.value {
        ExprKind::Constant(ConstantKind::String(name)) => {
            builder.action(Action::SetTarget(name.to_string()));
        }
        _ => {
            gen_expr(context, builder, target, false);
            builder.action(Action::SetTarget2);
        }
    }
    context.set_is_in_tell_target(true);
    gen_statement(context, builder, body);
    context.set_is_in_tell_target(was_in_tell_target);

    builder.action(Action::SetTarget("".to_string()));
    if was_in_tell_target {
        builder.action(Action::SetTarget2);
    }
}

fn gen_switch(
    context: &mut ScriptContext,
    builder: &mut CodeBuilder,
    target: &Expr,
    elements: &[SwitchElement],
) {
    let break_label = context.create_label();
    let mut conditionals = vec![];
    let mut default_label = break_label.clone();

    // First, generate labels and conditionals
    for element in elements {
        match element {
            SwitchElement::Case(value) => {
                conditionals.push((context.create_label(), value));
            }
            SwitchElement::Default => {
                default_label = context.create_label();
            }
            _ => {}
        }
    }
    if conditionals.is_empty() && default_label == break_label {
        // Nothing found, no code to generate
        return;
    }
    if !conditionals.is_empty() {
        gen_expr(context, builder, target, false);
        builder.action(Action::StoreRegister(0));
        for (i, (label, value)) in conditionals.iter().enumerate() {
            if i > 0 {
                builder.push(PushValue::Register(0));
            }
            gen_expr(context, builder, value, false);
            builder.action(Action::StrictEquals);
            builder.action(Action::If(label.clone()));
        }
    }
    builder.action(Action::Jump(default_label.clone()));

    // Then the body of each case
    let mut i = 0;
    let mut has_case = false;
    let old_break_label = context.set_break_label(Some(break_label.clone()));
    for element in elements {
        match element {
            SwitchElement::Case(_) => {
                builder.mark_label(conditionals[i].0.clone());
                i += 1;
                has_case = true;
            }
            SwitchElement::Default => {
                builder.mark_label(default_label.clone());
                has_case = true;
            }
            SwitchElement::Statement(statement) => {
                if has_case {
                    gen_statement(context, builder, statement);
                }
            }
        }
    }

    context.set_break_label(old_break_label);
    builder.mark_label(break_label);
}

fn gen_with(
    context: &mut ScriptContext,
    builder: &mut CodeBuilder,
    target: &Expr,
    body: &StatementKind,
) {
    let could_use_special_properties = context.can_use_special_properties();
    let mut sub_builder = CodeBuilder::new();
    context.set_can_use_special_properties(false);
    gen_statement(context, &mut sub_builder, body);
    context.set_can_use_special_properties(could_use_special_properties);
    let actions = sub_builder.into_actions();

    gen_expr(context, builder, target, false);
    builder.action(Action::With(actions));
}

fn gen_wait_for_frame(
    context: &mut ScriptContext,
    builder: &mut CodeBuilder,
    frame: &Expr,
    if_loaded: &StatementKind,
) {
    let mut sub_builder = CodeBuilder::new();
    gen_statement(context, &mut sub_builder, if_loaded);
    let actions = sub_builder.into_actions();
    let num_actions = actions.actions().len();

    match frame.value {
        ExprKind::Constant(ConstantKind::Integer(frame)) => {
            let mut frame_number = frame;
            if frame_number != 0 {
                frame_number = frame_number.wrapping_sub(1);
            }
            builder.action(Action::WaitForFrame {
                frame: (frame_number & 0xFFFF) as u16,
                skip_count: num_actions as u8,
            });
        }
        _ => {
            gen_expr(context, builder, frame, false);
            builder.action(Action::WaitForFrame2 {
                skip_count: num_actions as u8,
            });
        }
    }

    builder.append(actions);
}

fn gen_while_loop(
    context: &mut ScriptContext,
    builder: &mut CodeBuilder,
    condition: &Expr,
    body: &StatementKind,
) {
    let start = context.create_label();
    let end = context.create_label();
    let old_break = context.set_break_label(Some(end.clone()));
    let old_continue = context.set_continue_label(Some(start.clone()));

    builder.mark_label(start.clone());
    gen_expr(context, builder, condition, false);
    builder.action(Action::Not);
    builder.action(Action::If(end.clone()));
    gen_statement(context, builder, body);

    builder.action(Action::Jump(start));
    builder.mark_label(end);
    context.set_break_label(old_break);
    context.set_continue_label(old_continue);
}

fn gen_for_loop(
    context: &mut ScriptContext,
    builder: &mut CodeBuilder,
    condition: &ForCondition,
    body: &StatementKind,
) {
    let start_stack_size = builder.stack_size();
    let end_label = context.create_label();
    let continue_label = context.create_label();
    let old_break = context.set_break_label(Some(end_label.clone()));
    let old_continue = context.set_continue_label(Some(continue_label.clone()));

    match condition {
        ForCondition::Enumerate {
            variable,
            declare,
            object,
        } => {
            gen_expr(context, builder, object, false);
            builder.action(Action::Enumerate2);
            builder.mark_label(continue_label.clone());
            builder.action(Action::StoreRegister(0));
            builder.push(PushValue::Null);
            builder.action(Action::Equals2);
            builder.action(Action::If(end_label.clone()));

            if *declare {
                let value = context.constants.add(variable);
                builder.push(value);
                builder.push(PushValue::Register(0));
                builder.action(Action::DefineLocal);
            } else {
                let access = VariableAccess::for_identifier(context, builder, variable);
                builder.push(PushValue::Register(0));
                access.set_value(builder);
            }

            gen_statement(context, builder, body);
            builder.action(Action::Jump(continue_label));
        }
        ForCondition::Classic {
            initialize,
            condition,
            update,
        } => {
            if let Some(initialize) = initialize {
                gen_statement(context, builder, initialize);
            }
            let start_label = context.create_label();
            builder.mark_label(start_label.clone());
            if !condition.is_empty() {
                let last_cond = condition.len() - 1;
                for (i, expr) in condition.iter().enumerate() {
                    let stack_size = builder.stack_size();
                    gen_expr(context, builder, expr, i != last_cond);
                    if i != last_cond {
                        builder.truncate_stack(stack_size);
                    }
                }
                builder.action(Action::Not);
                builder.action(Action::If(end_label.clone()));
            }
            gen_statement(context, builder, body);
            builder.mark_label(continue_label.clone());
            for expr in update {
                let stack_size = builder.stack_size();
                gen_expr(context, builder, expr, true);
                builder.truncate_stack(stack_size);
            }
            builder.action(Action::Jump(start_label));
        }
    }

    builder.truncate_stack(start_stack_size);
    builder.mark_label(end_label);
    context.set_break_label(old_break);
    context.set_continue_label(old_continue);
}

fn gen_if(
    context: &mut ScriptContext,
    builder: &mut CodeBuilder,
    condition: &Expr,
    yes: &StatementKind,
    no: &Option<Box<StatementKind>>,
) {
    let end_label = context.create_label();
    gen_expr(context, builder, condition, false);
    builder.action(Action::Not);

    if let Some(no) = no {
        let no_label = context.create_label();
        builder.action(Action::If(no_label.clone()));
        gen_statement(context, builder, yes);
        builder.action(Action::Jump(end_label.clone()));
        builder.mark_label(no_label);
        gen_statement(context, builder, no);
    } else {
        builder.action(Action::If(end_label.clone()));
        gen_statement(context, builder, yes);
    }

    builder.mark_label(end_label);
}

fn gen_ternary(
    context: &mut ScriptContext,
    builder: &mut CodeBuilder,
    condition: &Expr,
    yes: &Expr,
    no: &Expr,
) {
    let end_label = context.create_label();
    let no_label = context.create_label();

    gen_expr(context, builder, condition, false);
    builder.action(Action::Not);
    builder.action(Action::If(no_label.clone()));
    gen_expr(context, builder, yes, false);
    builder.action(Action::Jump(end_label.clone()));
    builder.mark_label(no_label);
    gen_expr(context, builder, no, false);
    builder.mark_label(end_label);

    // -1 because we'll have assumed the stack grew twice, but the player will skip one of the expressions
    builder.assume_stack_delta(-1);
}

fn gen_declaration(
    context: &mut ScriptContext,
    builder: &mut CodeBuilder,
    declaration: &Declaration,
) {
    let value = context.constants.add(&declaration.name);
    builder.push(value);
    if let Some(value) = &declaration.value {
        gen_expr(context, builder, value, false);
        builder.action(Action::DefineLocal);
    } else {
        builder.action(Action::DefineLocal2);
    }
}

pub fn gen_expr(
    context: &mut ScriptContext,
    builder: &mut CodeBuilder,
    expr: &Expr,
    will_discard_result: bool,
) {
    let (_span, kind) = (expr.span, &expr.value);
    match kind {
        ExprKind::Constant(constant) => {
            VariableAccess::for_constant(context, builder, constant).get_value(builder)
        }
        ExprKind::Call { name, args } => gen_call(context, builder, name, args),
        ExprKind::New { name, args } => gen_new(context, builder, name, args),
        ExprKind::BinaryOperator(op, left, right) => {
            gen_binary_op(context, builder, *op, left, right, will_discard_result)
        }
        ExprKind::UnaryOperator(op, exr) => {
            gen_unary_op(context, builder, *op, exr, will_discard_result)
        }
        ExprKind::Ternary { condition, yes, no } => {
            gen_ternary(context, builder, condition, yes, no)
        }
        ExprKind::InitObject(values) => gen_init_object(context, builder, values),
        ExprKind::InitArray(values) => gen_init_array(context, builder, values),
        ExprKind::Field(_, _) => {
            let access = VariableAccess::for_expr(context, builder, expr);
            access.get_value(builder);
        }
        ExprKind::TypeOf(expr) => {
            gen_expr(context, builder, expr, false);
            builder.action(Action::TypeOf);
        }
        ExprKind::Delete(expr) => {
            VariableAccess::for_expr(context, builder, expr).delete(builder);
        }
        ExprKind::Void(_) => {}
        ExprKind::Function(function) => gen_function(context, builder, function, true),
        ExprKind::GetVariable(name) => {
            gen_expr(context, builder, name, false);
            builder.action(Action::GetVariable);
        }
        ExprKind::SetVariable(name, value) => {
            gen_expr(context, builder, name, false);
            gen_expr(context, builder, value, false);
            builder.action(Action::SetVariable);
        }
        ExprKind::CallFrame(frame) => {
            gen_expr(context, builder, frame, false);
            builder.action(Action::Call);
        }
        ExprKind::AsciiToChar(value) => {
            gen_expr(context, builder, value, false);
            builder.action(Action::AsciiToChar);
        }
        ExprKind::MBAsciiToChar(value) => {
            gen_expr(context, builder, value, false);
            builder.action(Action::MBAsciiToChar);
        }
        ExprKind::DuplicateMovieClip {
            source,
            target,
            depth,
        } => {
            gen_expr(context, builder, source, false);
            gen_expr(context, builder, target, false);
            builder.push(16384);
            gen_expr(context, builder, depth, false);
            builder.action(Action::Add2);
            builder.action(Action::CloneSprite);
        }
        ExprKind::GetTime => {
            builder.action(Action::GetTime);
        }
        ExprKind::GotoFrame(frame, play) => match &frame.value {
            ExprKind::Constant(ConstantKind::String(label)) => {
                builder.action(Action::GotoLabel(label.to_string()));
                if *play {
                    builder.action(Action::Play);
                }
            }
            ExprKind::Constant(ConstantKind::Integer(frame_number)) => {
                let mut frame_number = *frame_number;
                if frame_number != 0 {
                    frame_number = frame_number.wrapping_sub(1);
                }
                builder.action(Action::GotoFrame((frame_number & 0xFFFF) as u16));
                if *play {
                    builder.action(Action::Play);
                }
            }
            _ => {
                gen_expr(context, builder, frame, false);
                builder.action(Action::GotoFrame2 {
                    scene_bias: 0,
                    play: *play,
                });
            }
        },
        ExprKind::GetUrl {
            target,
            url,
            load_variables,
            load_target,
            method,
        } => {
            gen_get_url(
                context,
                builder,
                target,
                url,
                *load_variables,
                *load_target,
                *method,
            );
        }
        ExprKind::CastToInteger(value) => {
            gen_expr(context, builder, value, false);
            builder.action(Action::ToInteger);
        }
        ExprKind::CastToNumber(value) => {
            gen_expr(context, builder, value, false);
            builder.action(Action::ToNumber);
        }
        ExprKind::CastToString(value) => {
            gen_expr(context, builder, value, false);
            builder.action(Action::ToString);
        }
        ExprKind::StringLength(value) => {
            gen_expr(context, builder, value, false);
            builder.action(Action::StringLength);
        }
        ExprKind::MBStringLength(value) => {
            gen_expr(context, builder, value, false);
            builder.action(Action::MBStringLength);
        }
        ExprKind::CharToAscii(value) => {
            gen_expr(context, builder, value, false);
            builder.action(Action::CharToAscii);
        }
        ExprKind::MBCharToAscii(value) => {
            gen_expr(context, builder, value, false);
            builder.action(Action::MBCharToAscii);
        }
        ExprKind::Substring {
            string,
            start,
            length,
        } => {
            gen_expr(context, builder, string, false);
            gen_expr(context, builder, start, false);
            gen_expr(context, builder, length, false);
            builder.action(Action::StringExtract);
        }
        ExprKind::MBSubstring {
            string,
            start,
            length,
        } => {
            gen_expr(context, builder, string, false);
            gen_expr(context, builder, start, false);
            gen_expr(context, builder, length, false);
            builder.action(Action::MBStringExtract);
        }
        ExprKind::NextFrame => builder.action(Action::NextFrame),
        ExprKind::PreviousFrame => builder.action(Action::PrevFrame),
        ExprKind::Play => builder.action(Action::Play),
        ExprKind::Stop => builder.action(Action::Stop),
        ExprKind::StartDrag {
            target,
            lock,
            constraints,
        } => {
            let stack_delta = if let Some((a, b, c, d)) = constraints {
                gen_expr(context, builder, a, false);
                gen_expr(context, builder, b, false);
                gen_expr(context, builder, c, false);
                gen_expr(context, builder, d, false);
                builder.push(1);
                -7
            } else {
                builder.push(0);
                -3
            };
            builder.push(if *lock { 1 } else { 0 });
            gen_expr(context, builder, target, false);
            builder.action_with_stack_delta(Action::StartDrag, stack_delta);
        }
        ExprKind::StopSounds => builder.action(Action::StopSounds),
        ExprKind::EndDrag => builder.action(Action::EndDrag),
        ExprKind::ToggleQuality => builder.action(Action::ToggleQuality),
        ExprKind::GetTargetPath(expr) => {
            gen_expr(context, builder, expr, false);
            builder.action(Action::TargetPath);
        }
        ExprKind::Trace(expr) => {
            gen_expr(context, builder, expr, false);
            builder.action(Action::Trace);
        }
        ExprKind::RemoveSprite(expr) => {
            gen_expr(context, builder, expr, false);
            builder.action(Action::RemoveSprite);
        }
        ExprKind::GetRandomNumber(expr) => {
            gen_expr(context, builder, expr, false);
            builder.action(Action::RandomNumber);
        }
        ExprKind::GetProperty(target, property) => {
            gen_expr(context, builder, target, false);
            builder.push(*property);
            builder.action(Action::GetProperty);
        }
        ExprKind::SetProperty(target, property, value) => {
            gen_expr(context, builder, target, false);
            builder.push(*property);
            gen_expr(context, builder, value, false);
            builder.action(Action::SetProperty);
        }
    }
}

fn gen_get_url(
    context: &mut ScriptContext,
    builder: &mut CodeBuilder,
    target: &Expr,
    url: &Expr,
    load_variables: bool,
    load_target: bool,
    method: GetUrlMethod,
) {
    // First we see if getUrl is viable - it requires constant values and no loading
    if !load_target
        && !load_variables
        && method == GetUrlMethod::None
        && let Expr {
            value: ExprKind::Constant(ConstantKind::String(url)),
            ..
        } = url
        && let Expr {
            value: ExprKind::Constant(ConstantKind::String(target)),
            ..
        } = target
    {
        builder.action(Action::GetUrl {
            url: url.to_string(),
            target: target.to_string(),
        });
        return;
    }

    // We can't use the "classic" getUrl, so let's use the stack
    gen_expr(context, builder, url, false);
    gen_expr(context, builder, target, false);
    builder.action(Action::GetUrl2 {
        load_variables,
        load_target,
        method: match method {
            GetUrlMethod::None => 0,
            GetUrlMethod::Get => 1,
            GetUrlMethod::Post => 2,
        },
    });
}

fn gen_try_catch(context: &mut ScriptContext, builder: &mut CodeBuilder, try_catch: &TryCatch) {
    let mut end_label: Option<String> = Some(context.create_label());
    let mut try_builder = CodeBuilder::new();
    gen_statements(context, &mut try_builder, &try_catch.try_body);
    try_builder.action(Action::Jump(end_label.clone().unwrap()));
    let try_body = try_builder.into_actions();

    let catch_body = if try_catch.typed_catches.is_empty() && try_catch.catch_all.is_none() {
        // Nothing to do
        None
    } else if let Some(catch_all) = &try_catch.catch_all
        && try_catch.typed_catches.is_empty()
    {
        // A single catch-all block
        let mut catch_builder = CodeBuilder::new();
        gen_statements(context, &mut catch_builder, &catch_all.body);
        let catch_body = catch_builder.into_actions();
        Some((
            CatchTarget::Variable(catch_all.name.value.to_owned()),
            catch_body,
        ))
    } else {
        // Any number of typed catch blocks, with an optional catch-all block at the end
        let mut catch_builder = CodeBuilder::new();
        let catch_target = CatchTarget::Register(0);
        let mut first = true;

        for (type_name, catch) in &try_catch.typed_catches {
            let next_label = context.create_label();
            if !first {
                catch_builder.action(Action::Pop);
            }
            first = false;
            catch_builder.push(type_name.value.to_owned());
            catch_builder.action(Action::GetVariable);
            catch_builder.push(PushValue::Register(0));
            catch_builder.action(Action::CastOp);
            catch_builder.action(Action::PushDuplicate);
            catch_builder.push(PushValue::Null);
            catch_builder.action(Action::Equals2);
            catch_builder.action(Action::If(next_label.clone()));
            catch_builder.push(catch.name.value.to_owned());
            catch_builder.action(Action::StackSwap);
            catch_builder.action(Action::DefineLocal);
            gen_statements(context, &mut catch_builder, &catch.body);
            catch_builder.action(Action::Jump(end_label.clone().unwrap()));
            catch_builder.mark_label(next_label);
        }

        if let Some(catch) = &try_catch.catch_all {
            catch_builder.action(Action::Pop);
            catch_builder.push(PushValue::Register(0));
            catch_builder.push(catch.name.value.to_owned());
            catch_builder.action(Action::StackSwap);
            catch_builder.action(Action::DefineLocal);
            gen_statements(context, &mut catch_builder, &catch.body);
        } else {
            catch_builder.action(Action::Pop);
            catch_builder.push(PushValue::Register(0));
            catch_builder.action(Action::Throw);
        }

        let catch_body = catch_builder.into_actions();
        Some((catch_target, catch_body))
    };

    let finally_body = if try_catch.finally.is_empty() {
        None
    } else {
        let mut finally_builder = CodeBuilder::new();
        if let Some(end_label) = end_label.take() {
            finally_builder.mark_label(end_label);
        }
        gen_statements(context, &mut finally_builder, &try_catch.finally);
        let finally_body = finally_builder.into_actions();
        Some(finally_body)
    };

    builder.action(Action::Try {
        try_body,
        catch_body,
        finally_body,
    });
    if let Some(end_label) = end_label.take() {
        builder.mark_label(end_label);
    }
}

pub(crate) fn gen_function(
    context: &mut ScriptContext,
    builder: &mut CodeBuilder,
    function: &Function,
    with_name: bool,
) {
    let mut fun_builder = CodeBuilder::new();
    let old_break = context.set_break_label(None);
    let old_continue = context.set_continue_label(None);
    gen_statements(context, &mut fun_builder, &function.body);
    let actions = fun_builder.into_actions();
    let name = if with_name {
        function.signature.name.clone().map(|s| s.value)
    } else {
        None
    };
    if function.register_count <= 1 {
        builder.action(Action::DefineFunction {
            name: name.unwrap_or_default(),
            params: function
                .signature
                .args
                .iter()
                .map(|arg| arg.name.to_string())
                .collect(),
            actions,
        });
    } else {
        builder.action(Action::DefineFunction2 {
            name: name.unwrap_or_default(),
            params: function
                .signature
                .args
                .iter()
                .map(|arg| FunctionParam {
                    name: arg.name.to_string(),
                    register: arg.register.unwrap_or(0),
                })
                .collect(),
            actions,
            register_count: function.register_count,
            preload_this: function.preload_this,
            suppress_this: function.suppress_this,
            preload_arguments: function.preload_arguments,
            suppress_arguments: function.suppress_arguments,
            preload_super: function.preload_super,
            suppress_super: function.suppress_super,
            preload_root: function.preload_root,
            preload_parent: function.preload_parent,
            preload_global: function.preload_global,
        });
    }
    context.set_break_label(old_break);
    context.set_continue_label(old_continue);
}

fn gen_init_object(
    context: &mut ScriptContext,
    builder: &mut CodeBuilder,
    values: &[(String, Expr)],
) {
    let num_fields = values.len() as i32;
    for (key, value) in values.iter() {
        let push_value = context.constants.add(key);
        builder.push(push_value);
        gen_expr(context, builder, value, false);
    }
    builder.push(num_fields);
    builder.action_with_stack_delta(Action::InitObject, -num_fields * 2);
}

fn gen_init_array(context: &mut ScriptContext, builder: &mut CodeBuilder, values: &[Expr]) {
    let num_values = values.len() as i32;
    for value in values.iter().rev() {
        gen_expr(context, builder, value, false);
    }
    builder.push(num_values);
    builder.action_with_stack_delta(Action::InitArray, -num_values);
}

fn gen_unary_op(
    context: &mut ScriptContext,
    builder: &mut CodeBuilder,
    op: UnaryOperator,
    expr: &Expr,
    will_discard_result: bool,
) {
    let adjust_in_place =
        |context: &mut ScriptContext, builder: &mut CodeBuilder, action, affix| {
            if !will_discard_result && affix == Affix::Postfix {
                // Record the old value onto the stack
                VariableAccess::for_expr(context, builder, expr).get_value(builder);
            }

            // Keep the variable on the stack without retrieving its value
            let _ = VariableAccess::for_expr(context, builder, expr);

            let access = VariableAccess::for_expr(context, builder, expr);
            access.get_value(builder);
            builder.action(action);
            if !will_discard_result && affix == Affix::Prefix {
                access.get_and_set_value(builder);
            } else {
                access.set_value(builder);
            }
        };

    match op {
        UnaryOperator::Sub => match expr {
            Expr {
                value: ExprKind::Constant(ConstantKind::Integer(value)),
                ..
            } => {
                VariableAccess::for_constant(context, builder, &ConstantKind::Integer(-*value));
            }
            Expr {
                value: ExprKind::Constant(ConstantKind::Float(value)),
                ..
            } => {
                VariableAccess::for_constant(context, builder, &ConstantKind::Float(-*value));
            }
            _ => {
                VariableAccess::for_constant(context, builder, &ConstantKind::Integer(0));
                gen_expr(context, builder, expr, false);
                builder.action(Action::Subtract);
            }
        },
        UnaryOperator::BitNot => {
            gen_expr(context, builder, expr, false);
            builder.push(-1);
            builder.action(Action::BitXor);
        }
        UnaryOperator::Increment(affix) => {
            adjust_in_place(context, builder, Action::Increment, affix)
        }
        UnaryOperator::Decrement(affix) => {
            adjust_in_place(context, builder, Action::Decrement, affix)
        }
        UnaryOperator::LogicalNot => {
            gen_expr(context, builder, expr, false);
            builder.action(Action::Not);
        }
    }
}

fn gen_binary_op(
    context: &mut ScriptContext,
    builder: &mut CodeBuilder,
    op: BinaryOperator,
    left: &Expr,
    right: &Expr,
    will_discard_result: bool, // if we can optimise slightly by not deliberately pushing the result onto the stack
) {
    let trivial = |context: &mut ScriptContext, builder: &mut CodeBuilder, action: Action| {
        gen_expr(context, builder, left, false);
        gen_expr(context, builder, right, false);
        builder.action(action);
    };
    let trivial_assign =
        |context: &mut ScriptContext, builder: &mut CodeBuilder, action: Action| {
            let access = VariableAccess::for_expr(context, builder, left);
            trivial(context, builder, action);
            if will_discard_result {
                access.set_value(builder);
            } else {
                access.get_and_set_value(builder);
            }
        };
    match op {
        BinaryOperator::Add => trivial(context, builder, Action::Add2),
        BinaryOperator::Assign => {
            let access = VariableAccess::for_expr(context, builder, left);
            gen_expr(context, builder, right, false);
            if will_discard_result {
                access.set_value(builder);
            } else {
                access.get_and_set_value(builder);
            }
        }
        BinaryOperator::AddAssign => trivial_assign(context, builder, Action::Add2),
        BinaryOperator::Sub => trivial(context, builder, Action::Subtract),
        BinaryOperator::SubAssign => trivial_assign(context, builder, Action::Subtract),
        BinaryOperator::Divide => trivial(context, builder, Action::Divide),
        BinaryOperator::DivideAssign => trivial_assign(context, builder, Action::Divide),
        BinaryOperator::Multiply => trivial(context, builder, Action::Multiply),
        BinaryOperator::MultiplyAssign => trivial_assign(context, builder, Action::Multiply),
        BinaryOperator::Modulo => trivial(context, builder, Action::Modulo),
        BinaryOperator::ModuloAssign => trivial_assign(context, builder, Action::Modulo),
        BinaryOperator::BitAnd => trivial(context, builder, Action::BitAnd),
        BinaryOperator::BitAndAssign => trivial_assign(context, builder, Action::BitAnd),
        BinaryOperator::BitOr => trivial(context, builder, Action::BitOr),
        BinaryOperator::BitOrAssign => trivial_assign(context, builder, Action::BitOr),
        BinaryOperator::BitXor => trivial(context, builder, Action::BitXor),
        BinaryOperator::BitXorAssign => trivial_assign(context, builder, Action::BitXor),
        BinaryOperator::BitShiftLeft => trivial(context, builder, Action::BitLShift),
        BinaryOperator::BitShiftLeftAssign => trivial_assign(context, builder, Action::BitLShift),
        BinaryOperator::BitShiftRight => trivial(context, builder, Action::BitRShift),
        BinaryOperator::BitShiftRightAssign => trivial_assign(context, builder, Action::BitRShift),
        BinaryOperator::BitShiftRightUnsigned => trivial(context, builder, Action::BitURShift),
        BinaryOperator::BitShiftRightUnsignedAssign => {
            trivial_assign(context, builder, Action::BitURShift)
        }
        BinaryOperator::Equal => trivial(context, builder, Action::Equals2),
        BinaryOperator::StrictEqual => trivial(context, builder, Action::StrictEquals),
        BinaryOperator::NotEqual => {
            trivial(context, builder, Action::Equals2);
            builder.action(Action::Not)
        }
        BinaryOperator::StrictNotEqual => {
            trivial(context, builder, Action::StrictEquals);
            builder.action(Action::Not)
        }
        BinaryOperator::LessThan => trivial(context, builder, Action::Less2),
        BinaryOperator::LessThanEqual => {
            trivial(context, builder, Action::Greater);
            builder.action(Action::Not)
        }
        BinaryOperator::GreaterThan => trivial(context, builder, Action::Greater),
        BinaryOperator::GreaterThanEqual => {
            trivial(context, builder, Action::Less2);
            builder.action(Action::Not)
        }
        BinaryOperator::LogicalAnd => {
            let end_label = context.create_label();
            gen_expr(context, builder, left, false);
            builder.action(Action::PushDuplicate);
            builder.action(Action::Not);
            builder.action(Action::If(end_label.clone()));
            builder.action(Action::Pop);
            gen_expr(context, builder, right, false);
            builder.mark_label(end_label);
        }
        BinaryOperator::LogicalOr => {
            let end_label = context.create_label();
            gen_expr(context, builder, left, false);
            builder.action(Action::PushDuplicate);
            builder.action(Action::If(end_label.clone()));
            builder.action(Action::Pop);
            gen_expr(context, builder, right, false);
            builder.mark_label(end_label);
        }
        BinaryOperator::InstanceOf => trivial(context, builder, Action::InstanceOf),
        BinaryOperator::StringEqual => trivial(context, builder, Action::StringEquals),
        BinaryOperator::StringGreaterThan => trivial(context, builder, Action::StringGreater),
        BinaryOperator::StringGreaterThanEqual => {
            trivial(context, builder, Action::StringLess);
            builder.action(Action::Not);
        }
        BinaryOperator::StringLessThan => trivial(context, builder, Action::StringLess),
        BinaryOperator::StringLessThanEqual => {
            trivial(context, builder, Action::StringGreater);
            builder.action(Action::Not);
        }
        BinaryOperator::StringNotEqual => {
            trivial(context, builder, Action::StringEquals);
            builder.action(Action::Not);
        }
        BinaryOperator::BooleanAnd => trivial(context, builder, Action::And),
        BinaryOperator::BooleanOr => trivial(context, builder, Action::Or),
        BinaryOperator::StringAdd => trivial(context, builder, Action::StringAdd),
    }
}

fn gen_call(context: &mut ScriptContext, builder: &mut CodeBuilder, name: &Expr, args: &[Expr]) {
    for arg in args.iter().rev() {
        gen_expr(context, builder, arg, false);
    }
    let num_args = args.len() as i32;
    builder.push(num_args);
    VariableAccess::for_expr(context, builder, name).call(builder, num_args);
}

fn gen_new(context: &mut ScriptContext, builder: &mut CodeBuilder, name: &Expr, args: &[Expr]) {
    for arg in args.iter().rev() {
        gen_expr(context, builder, arg, false);
    }
    let num_args = args.len() as i32;
    builder.push(num_args);
    let access = VariableAccess::for_expr(context, builder, name);
    access.call_new(builder, num_args);
}
