use crate::builtins::array;
use crate::builtins::base64::{global_atob, global_btoa};
use crate::builtins::bigint;
use crate::builtins::date;
use crate::builtins::error;
#[cfg(feature = "fetch")]
use crate::builtins::eventsource;
#[cfg(feature = "fetch")]
use crate::builtins::fetch;
use crate::builtins::function;
use crate::builtins::generator;
use crate::builtins::intl;
use crate::builtins::json::{json_parse, json_stringify};
use crate::builtins::map_set;
use crate::builtins::math;
use crate::builtins::number::number_to_string;
use crate::builtins::object;
use crate::builtins::parse::{global_parsefloat, global_parseint};
#[cfg(feature = "process")]
use crate::builtins::process;
use crate::builtins::promise;
use crate::builtins::regexp;
use crate::builtins::string;
use crate::builtins::symbol;
use crate::builtins::typedarray;
use crate::builtins::url::{
    global_decodeuri, global_decodeuricomponent, global_encodeuri, global_encodeuricomponent,
};
use crate::builtins::weakref;
#[cfg(feature = "fetch")]
use crate::builtins::websocket;
use crate::host::HostFunction;
use crate::object::function::JSFunction;
use crate::object::object::JSObject;
use crate::runtime::context::JSContext;
use crate::value::JSValue;
use std::collections::HashSet;

pub fn create_builtin_function(ctx: &mut JSContext, name: &str) -> JSValue {
    let mut func = JSFunction::new_builtin(ctx.intern(name), 1);
    func.set_builtin_marker(ctx, name);
    let ptr = Box::into_raw(Box::new(func)) as usize;
    ctx.runtime_mut().gc_heap_mut().track_function(ptr);
    JSValue::new_function(ptr)
}

pub fn init_globals(ctx: &mut JSContext) {
    ctx.register_builtin("console_log", HostFunction::new("log", 1, console_log));
    ctx.register_builtin(
        "console_error",
        HostFunction::new("error", 1, console_error),
    );
    ctx.register_builtin("console_warn", HostFunction::new("warn", 1, console_warn));
    ctx.register_builtin("console_info", HostFunction::new("info", 1, console_info));
    ctx.register_builtin(
        "setTimeout",
        HostFunction::new("setTimeout", 2, set_timeout),
    );
    ctx.register_builtin(
        "setInterval",
        HostFunction::new("setInterval", 2, set_interval),
    );
    ctx.register_builtin(
        "clearTimeout",
        HostFunction::new("clearTimeout", 1, clear_timeout),
    );
    ctx.register_builtin(
        "clearInterval",
        HostFunction::new("clearInterval", 1, clear_interval),
    );
    ctx.register_builtin(
        "queueMicrotask",
        HostFunction::new("queueMicrotask", 1, queue_microtask),
    );
    ctx.register_builtin(
        "requestAnimationFrame",
        HostFunction::new("requestAnimationFrame", 1, request_animation_frame),
    );
    ctx.register_builtin(
        "cancelAnimationFrame",
        HostFunction::new("cancelAnimationFrame", 1, cancel_animation_frame),
    );
    ctx.register_builtin("import", HostFunction::new("import", 1, js_import));
    ctx.register_builtin(
        "disassemble",
        HostFunction::new("disassemble", 1, js_disassemble),
    );
    math::register_math_builtins(ctx);
    ctx.register_builtin("global_isnan", HostFunction::new("isNaN", 1, global_isnan));
    ctx.register_builtin(
        "global_isfinite",
        HostFunction::new("isFinite", 1, global_isfinite),
    );
    ctx.register_builtin(
        "global_parseint",
        HostFunction::new("parseInt", 1, global_parseint),
    );
    ctx.register_builtin(
        "global_parsefloat",
        HostFunction::new("parseFloat", 1, global_parsefloat),
    );
    ctx.register_builtin("global_eval", HostFunction::new("eval", 1, global_eval));

    ctx.register_builtin("btoa", HostFunction::new("btoa", 1, global_btoa));
    ctx.register_builtin("atob", HostFunction::new("atob", 1, global_atob));

    ctx.register_builtin(
        "encodeURI",
        HostFunction::new("encodeURI", 1, global_encodeuri),
    );
    ctx.register_builtin(
        "decodeURI",
        HostFunction::new("decodeURI", 1, global_decodeuri),
    );
    ctx.register_builtin(
        "encodeURIComponent",
        HostFunction::new("encodeURIComponent", 1, global_encodeuricomponent),
    );
    ctx.register_builtin(
        "decodeURIComponent",
        HostFunction::new("decodeURIComponent", 1, global_decodeuricomponent),
    );

    array::register_builtins(ctx);
    string::register_builtins(ctx);
    object::register_builtins(ctx);
    function::register_builtins(ctx);
    map_set::register_builtins(ctx);
    symbol::register_builtins(ctx);
    error::register_builtins(ctx);
    bigint::register_builtins(ctx);
    weakref::register_builtins(ctx);
    date::register_builtins(ctx);
    typedarray::register_builtins(ctx);
    intl::register_builtins(ctx);

    init_console(ctx);
    init_math(ctx);
    init_reflect(ctx);
    init_json(ctx);
    init_global_funcs(ctx);

    object::init_object(ctx);
    array::init_array(ctx);
    string::init_string(ctx);

    init_number(ctx);
    init_boolean(ctx);
    function::init_function(ctx);
    init_date(ctx);
    init_regexp(ctx);
    init_promise(ctx);
    error::init_error(ctx);
    map_set::init_map_set(ctx);
    symbol::init_symbol(ctx);
    date::init_date_to_primitive(ctx);
    bigint::init_bigint(ctx);
    weakref::init_weakref(ctx);
    generator::init_generator(ctx);
    typedarray::init_typed_array(ctx);
    intl::init_intl(ctx);

    #[cfg(feature = "fetch")]
    {
        fetch::register_fetch(ctx);
        websocket::register_websocket(ctx);
        eventsource::register_eventsource(ctx);

        let reactor =
            crate::runtime::io_reactor::IoReactor::new().expect("failed to create IoReactor");
        ctx.add_extension(Box::new(reactor));
    }

    #[cfg(feature = "process")]
    {
        process::init_process_module(ctx);

        let proc_ext = crate::runtime::process_task::ProcessExtension::new();
        ctx.add_extension(Box::new(proc_ext));
    }

    if let Some(fn_proto_ptr) = ctx.get_function_prototype() {
        let fn_proto = unsafe { &mut *fn_proto_ptr };
        let has_instance_sym = crate::builtins::symbol::get_symbol_has_instance(ctx);
        let hi_func =
            crate::object::function::JSFunction::new_builtin(ctx.intern("[Symbol.hasInstance]"), 1);
        let hi_value = {
            let mut func = hi_func;
            if let Some(fn_proto_ptr2) = ctx.get_function_prototype() {
                func.base.set_prototype_raw(fn_proto_ptr2);
            }
            func.builtin_atom = Some(ctx.intern("function_has_instance"));
            func.builtin_func = ctx.get_builtin_func("function_has_instance");
            let ptr = Box::into_raw(Box::new(func)) as usize;
            ctx.runtime_mut().gc_heap_mut().track_function(ptr);
            JSValue::new_function(ptr)
        };
        if has_instance_sym.is_symbol() {
            let sym_key = crate::runtime::atom::Atom(0x40000000 | has_instance_sym.get_symbol_id());
            fn_proto.set_cached(sym_key, hi_value, ctx.shape_cache_mut());
        }
    }
}

fn console_log(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    for (i, arg) in args.iter().enumerate() {
        if i > 0 {
            print!(" ");
        }
        print!("{}", jsvalue_to_string(arg, _ctx));
    }
    println!();
    JSValue::undefined()
}

fn console_error(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    for (i, arg) in args.iter().enumerate() {
        if i > 0 {
            eprint!(" ");
        }
        eprint!("{}", jsvalue_to_string(arg, _ctx));
    }
    eprintln!();
    JSValue::undefined()
}

fn console_warn(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    for (i, arg) in args.iter().enumerate() {
        if i > 0 {
            print!(" ");
        }
        print!("{}", jsvalue_to_string(arg, _ctx));
    }
    println!();
    JSValue::undefined()
}

fn console_info(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    for (i, arg) in args.iter().enumerate() {
        if i > 0 {
            print!(" ");
        }
        print!("{}", jsvalue_to_string(arg, _ctx));
    }
    println!();
    JSValue::undefined()
}

pub(crate) fn jsvalue_to_string(value: &JSValue, ctx: &JSContext) -> String {
    let mut visited = HashSet::new();
    jsvalue_to_string_internal(value, ctx, &mut visited)
}

fn jsvalue_to_string_internal(
    value: &JSValue,
    ctx: &JSContext,
    visited: &mut HashSet<usize>,
) -> String {
    if value.is_undefined() {
        "undefined".to_string()
    } else if value.is_null() {
        "null".to_string()
    } else if value.is_bool() {
        value.get_bool().to_string()
    } else if value.is_int() {
        value.get_int().to_string()
    } else if value.is_float() {
        value.get_float().to_string()
    } else if value.is_string() {
        let atom = value.get_atom();
        ctx.get_atom_str(atom).to_string()
    } else if value.is_bigint() {
        let obj = value.as_object();
        format!("{}n", obj.get_bigint_value())
    } else if value.is_object() {
        let ptr = value.get_ptr();

        if visited.contains(&ptr) {
            return "[Circular]".to_string();
        }
        visited.insert(ptr);

        let obj = value.as_object();
        let mut result = String::from("{");
        let mut first = true;
        obj.for_each_property(|key, val, _attrs| {
            if !first {
                result.push_str(", ");
            }
            first = false;
            result.push_str(ctx.get_atom_str(key));
            result.push_str(": ");
            result.push_str(&jsvalue_to_string_internal(&val, ctx, visited));
        });
        obj.for_each_accessor(|_key, getter, setter| {
            if !first {
                result.push_str(", ");
            }
            first = false;
            if getter.is_some() {
                result.push_str("[getter]");
            }
            if setter.is_some() {
                result.push_str("[setter]");
            }
        });
        result.push('}');
        result
    } else if value.is_function() {
        "[Function]".to_string()
    } else {
        "".to_string()
    }
}

fn set_timeout(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        return JSValue::new_int(0);
    }

    let callback = args[0].clone();
    let delay_ms = if args.len() > 1 {
        if args[1].is_int() {
            args[1].get_int() as u64
        } else if args[1].is_float() {
            args[1].get_float() as u64
        } else {
            0
        }
    } else {
        0
    };

    let callback_args: Vec<JSValue> = args.iter().skip(2).cloned().collect();

    let timer_id = ctx
        .event_loop_mut()
        .schedule_timer(callback, callback_args, delay_ms, false);

    JSValue::new_int(timer_id as i64)
}

fn set_interval(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        return JSValue::new_int(0);
    }

    let callback = args[0].clone();
    let interval_ms = if args.len() > 1 {
        if args[1].is_int() {
            args[1].get_int() as u64
        } else if args[1].is_float() {
            args[1].get_float() as u64
        } else {
            0
        }
    } else {
        0
    };

    let callback_args: Vec<JSValue> = args.iter().skip(2).cloned().collect();

    let timer_id = ctx
        .event_loop_mut()
        .schedule_timer(callback, callback_args, interval_ms, true);

    JSValue::new_int(timer_id as i64)
}

fn clear_timeout(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        return JSValue::undefined();
    }

    let timer_id = if args[0].is_int() {
        args[0].get_int() as u32
    } else {
        return JSValue::undefined();
    };

    ctx.event_loop_mut().clear_timer(timer_id);

    JSValue::undefined()
}

fn clear_interval(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    clear_timeout(ctx, args)
}

fn queue_microtask(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        return JSValue::undefined();
    }

    let callback = args[0].clone();
    let callback_args: Vec<JSValue> = args.iter().skip(1).cloned().collect();

    ctx.microtask_enqueue(crate::builtins::promise::Microtask::UserCallback(
        callback,
        callback_args,
    ));

    JSValue::undefined()
}

fn request_animation_frame(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        return JSValue::new_int(0);
    }

    let callback = args[0].clone();
    let id = ctx.event_loop_mut().schedule_animation_callback(callback);

    JSValue::new_int(id as i64)
}

fn cancel_animation_frame(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        return JSValue::undefined();
    }

    let id = if args[0].is_int() {
        args[0].get_int() as u32
    } else {
        return JSValue::undefined();
    };

    ctx.event_loop_mut().cancel_animation_callback(id);

    JSValue::undefined()
}

fn js_disassemble(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() || !args[0].is_function() {
        return JSValue::new_string(ctx.intern("disassemble: expected a function argument"));
    }
    let js_func = args[0].as_function();
    let output = match js_func.bytecode.as_ref() {
        Some(bc) => bc.disassemble(),
        None => {
            let name = ctx.get_atom_str(js_func.name).to_string();
            format!(
                "disassemble: function '{}' has no register bytecode (native/builtin?)",
                name
            )
        }
    };
    JSValue::new_string(ctx.intern(&output))
}

fn js_import(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        let err_msg = JSValue::new_string(ctx.intern("Error: import requires a module specifier"));
        return promise::create_rejected_promise(ctx, err_msg);
    }

    let specifier = if args[0].is_string() {
        ctx.get_atom_str(args[0].get_atom()).to_string()
    } else {
        let err_msg = JSValue::new_string(ctx.intern("Error: import requires a string specifier"));
        return promise::create_rejected_promise(ctx, err_msg);
    };

    match crate::runtime::module::load_and_evaluate_module(ctx, &specifier) {
        Ok(ns_ptr) => {
            let ns_value = JSValue::new_object(ns_ptr);
            let result = promise::create_resolved_promise(ctx, ns_value);
            result
        }
        Err(e) => {
            let err_msg = JSValue::new_string(ctx.intern(&e));
            promise::create_rejected_promise(ctx, err_msg)
        }
    }
}

fn global_isnan(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        return JSValue::bool(true);
    }
    JSValue::bool(args[0].get_float().is_nan())
}

fn global_isfinite(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        return JSValue::bool(false);
    }
    let v = args[0].get_float();
    JSValue::bool(!v.is_nan() && v.is_finite())
}

fn global_eval(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        return JSValue::undefined();
    }

    let source = if args[0].is_string() {
        ctx.get_atom_str(args[0].get_atom()).to_string()
    } else if args[0].is_object_like() {
        return args[0].clone();
    } else {
        return args[0].clone();
    };

    if let Some(vm_ptr) = ctx.get_register_vm_ptr() {
        let vm = unsafe { &mut *(vm_ptr as *mut crate::runtime::vm::VM) };
        match vm.direct_eval(ctx, &source) {
            Ok(v) => return v,
            Err(_) => return JSValue::undefined(),
        }
    }

    match crate::eval(ctx, &source) {
        Ok(v) => v,
        Err(_e) => JSValue::undefined(),
    }
}

fn init_reflect(ctx: &mut JSContext) {
    let reflect_obj = JSObject::new();
    let reflect_ptr = Box::into_raw(Box::new(reflect_obj)) as usize;
    ctx.runtime_mut().gc_heap_mut().track(reflect_ptr);
    let global = ctx.global();
    if global.is_object() {
        let global_obj = global.as_object_mut();
        global_obj.set(ctx.intern("Reflect"), JSValue::new_object(reflect_ptr));
    }
}

fn init_console(ctx: &mut JSContext) {
    let console_atom = ctx.intern("console");
    let mut console_obj = JSObject::new();
    console_obj.set(
        ctx.intern("log"),
        create_builtin_function(ctx, "console_log"),
    );
    console_obj.set(
        ctx.intern("error"),
        create_builtin_function(ctx, "console_error"),
    );
    console_obj.set(
        ctx.intern("warn"),
        create_builtin_function(ctx, "console_warn"),
    );
    console_obj.set(
        ctx.intern("info"),
        create_builtin_function(ctx, "console_info"),
    );
    let console_ptr = Box::into_raw(Box::new(console_obj)) as usize;
    ctx.runtime_mut().gc_heap_mut().track(console_ptr);
    let console_value = JSValue::new_object(console_ptr);
    let global = ctx.global();
    if global.is_object() {
        let global_obj = global.as_object_mut();
        global_obj.set(console_atom, console_value);
    }
}

fn set_non_configurable(
    obj: &mut crate::object::object::JSObject,
    key: crate::runtime::atom::Atom,
    value: JSValue,
) {
    obj.define_property(
        key,
        crate::object::object::PropertyDescriptor {
            value: Some(value),
            writable: false,
            enumerable: false,
            configurable: false,
            get: None,
            set: None,
        },
    );
}

fn init_math(ctx: &mut JSContext) {
    let math_atom = ctx.intern("Math");
    let mut math_obj = JSObject::new();
    let constants = [
        (ctx.intern("PI"), std::f64::consts::PI),
        (ctx.intern("E"), std::f64::consts::E),
        (ctx.intern("LN2"), std::f64::consts::LN_2),
        (ctx.intern("LN10"), std::f64::consts::LN_10),
        (ctx.intern("LOG2E"), std::f64::consts::LOG2_E),
        (ctx.intern("LOG10E"), std::f64::consts::LOG10_E),
        (ctx.intern("SQRT1_2"), std::f64::consts::FRAC_1_SQRT_2),
        (ctx.intern("SQRT2"), std::f64::consts::SQRT_2),
    ];
    for (key, val) in &constants {
        set_non_configurable(&mut math_obj, *key, JSValue::new_float(*val));
    }
    math_obj.set(ctx.intern("abs"), create_builtin_function(ctx, "math_abs"));
    math_obj.set(
        ctx.intern("floor"),
        create_builtin_function(ctx, "math_floor"),
    );
    math_obj.set(
        ctx.intern("ceil"),
        create_builtin_function(ctx, "math_ceil"),
    );
    math_obj.set(
        ctx.intern("round"),
        create_builtin_function(ctx, "math_round"),
    );
    math_obj.set(
        ctx.intern("sqrt"),
        create_builtin_function(ctx, "math_sqrt"),
    );
    math_obj.set(ctx.intern("max"), create_builtin_function(ctx, "math_max"));
    math_obj.set(ctx.intern("min"), create_builtin_function(ctx, "math_min"));
    math_obj.set(ctx.intern("pow"), create_builtin_function(ctx, "math_pow"));
    math_obj.set(
        ctx.intern("random"),
        create_builtin_function(ctx, "math_random"),
    );
    math_obj.set(
        ctx.intern("trunc"),
        create_builtin_function(ctx, "math_trunc"),
    );
    math_obj.set(
        ctx.intern("sign"),
        create_builtin_function(ctx, "math_sign"),
    );
    math_obj.set(
        ctx.intern("cbrt"),
        create_builtin_function(ctx, "math_cbrt"),
    );
    math_obj.set(
        ctx.intern("clz32"),
        create_builtin_function(ctx, "math_clz32"),
    );
    math_obj.set(
        ctx.intern("imul"),
        create_builtin_function(ctx, "math_imul"),
    );
    math_obj.set(
        ctx.intern("fround"),
        create_builtin_function(ctx, "math_fround"),
    );
    math_obj.set(
        ctx.intern("hypot"),
        create_builtin_function(ctx, "math_hypot"),
    );
    math_obj.set(
        ctx.intern("expm1"),
        create_builtin_function(ctx, "math_expm1"),
    );
    math_obj.set(ctx.intern("log"), create_builtin_function(ctx, "math_log"));
    math_obj.set(
        ctx.intern("log1p"),
        create_builtin_function(ctx, "math_log1p"),
    );
    math_obj.set(
        ctx.intern("log10"),
        create_builtin_function(ctx, "math_log10"),
    );
    math_obj.set(
        ctx.intern("log2"),
        create_builtin_function(ctx, "math_log2"),
    );
    math_obj.set(
        ctx.intern("sinh"),
        create_builtin_function(ctx, "math_sinh"),
    );
    math_obj.set(
        ctx.intern("cosh"),
        create_builtin_function(ctx, "math_cosh"),
    );
    math_obj.set(
        ctx.intern("tanh"),
        create_builtin_function(ctx, "math_tanh"),
    );
    math_obj.set(
        ctx.intern("asinh"),
        create_builtin_function(ctx, "math_asinh"),
    );
    math_obj.set(
        ctx.intern("acosh"),
        create_builtin_function(ctx, "math_acosh"),
    );
    math_obj.set(
        ctx.intern("atanh"),
        create_builtin_function(ctx, "math_atanh"),
    );
    math_obj.set(ctx.intern("sin"), create_builtin_function(ctx, "math_sin"));
    math_obj.set(ctx.intern("cos"), create_builtin_function(ctx, "math_cos"));
    math_obj.set(ctx.intern("tan"), create_builtin_function(ctx, "math_tan"));
    math_obj.set(ctx.intern("exp"), create_builtin_function(ctx, "math_exp"));
    math_obj.set(
        ctx.intern("asin"),
        create_builtin_function(ctx, "math_asin"),
    );
    math_obj.set(
        ctx.intern("acos"),
        create_builtin_function(ctx, "math_acos"),
    );
    math_obj.set(
        ctx.intern("atan"),
        create_builtin_function(ctx, "math_atan"),
    );
    math_obj.set(
        ctx.intern("atan2"),
        create_builtin_function(ctx, "math_atan2"),
    );
    let math_ptr = Box::into_raw(Box::new(math_obj)) as usize;
    ctx.runtime_mut().gc_heap_mut().track(math_ptr);
    let math_value = JSValue::new_object(math_ptr);
    let global = ctx.global();
    if global.is_object() {
        let global_obj = global.as_object_mut();
        global_obj.set(math_atom, math_value);
    }
}

fn init_json(ctx: &mut JSContext) {
    ctx.register_builtin("json_parse", HostFunction::new("parse", 1, json_parse));
    ctx.register_builtin(
        "json_stringify",
        HostFunction::new("stringify", 1, json_stringify),
    );

    let json_atom = ctx.intern("JSON");
    let mut json_obj = JSObject::new();
    json_obj.set(
        ctx.intern("parse"),
        create_builtin_function(ctx, "json_parse"),
    );
    json_obj.set(
        ctx.intern("stringify"),
        create_builtin_function(ctx, "json_stringify"),
    );
    let json_ptr = Box::into_raw(Box::new(json_obj)) as usize;
    ctx.runtime_mut().gc_heap_mut().track(json_ptr);
    let json_value = JSValue::new_object(json_ptr);
    let global = ctx.global();
    if global.is_object() {
        let global_obj = global.as_object_mut();
        global_obj.set(json_atom, json_value);
    }
}

fn number_constructor(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        return JSValue::new_int(0);
    }
    let v = &args[0];
    if v.is_int() {
        return *v;
    }
    if v.is_float() {
        return *v;
    }
    if v.is_bool() {
        return JSValue::new_int(if v.get_bool() { 1 } else { 0 });
    }
    if v.is_null() {
        return JSValue::new_int(0);
    }
    if v.is_undefined() {
        return JSValue::new_float(f64::NAN);
    }
    if v.is_string() {
        let s = ctx.get_atom_str(v.get_atom());
        if let Ok(n) = s.parse::<i64>() {
            return JSValue::new_int(n);
        }
        if let Ok(f) = s.parse::<f64>() {
            return JSValue::new_float(f);
        }
        return JSValue::new_float(f64::NAN);
    }
    if v.is_symbol() {
        return JSValue::new_float(f64::NAN);
    }
    if v.is_bigint() {
        if v.is_object() {
            return JSValue::new_int(v.as_object().get_bigint_value() as i64);
        }
        return JSValue::new_int(0);
    }
    JSValue::new_float(f64::NAN)
}

fn init_number(ctx: &mut JSContext) {
    ctx.register_builtin(
        "number_constructor",
        HostFunction::new("Number", 1, number_constructor),
    );
    ctx.register_builtin("number_isNaN", HostFunction::new("isNaN", 1, number_is_nan));
    ctx.register_builtin(
        "number_isFinite",
        HostFunction::new("isFinite", 1, number_is_finite),
    );
    ctx.register_builtin(
        "number_isInteger",
        HostFunction::new("isInteger", 1, number_is_integer),
    );
    ctx.register_builtin(
        "number_parseInt",
        HostFunction::new("parseInt", 1, global_parseint),
    );
    ctx.register_builtin(
        "number_parseFloat",
        HostFunction::new("parseFloat", 1, global_parsefloat),
    );
    ctx.register_builtin(
        "number_toFixed",
        HostFunction::new("toFixed", 1, number_to_fixed),
    );
    ctx.register_builtin(
        "number_toPrecision",
        HostFunction::new("toPrecision", 1, number_to_precision),
    );
    ctx.register_builtin(
        "number_toString",
        HostFunction::new("toString", 0, number_to_string),
    );
    ctx.register_builtin(
        "number_valueOf",
        HostFunction::new("valueOf", 0, number_value_of),
    );

    let number_atom = ctx.intern("Number");
    let mut num_func = JSFunction::new_builtin(number_atom, 1);
    num_func.set_builtin_marker(ctx, "number_constructor");
    num_func
        .base
        .set(ctx.intern("MAX_VALUE"), JSValue::new_float(f64::MAX));
    num_func
        .base
        .set(ctx.intern("MIN_VALUE"), JSValue::new_float(f64::MIN));
    num_func.base.set(
        ctx.intern("POSITIVE_INFINITY"),
        JSValue::new_float(f64::INFINITY),
    );
    num_func.base.set(
        ctx.intern("NEGATIVE_INFINITY"),
        JSValue::new_float(f64::NEG_INFINITY),
    );
    num_func
        .base
        .set(ctx.intern("NaN"), JSValue::new_float(f64::NAN));
    num_func.base.set(
        ctx.intern("isNaN"),
        create_builtin_function(ctx, "number_isNaN"),
    );
    num_func.base.set(
        ctx.intern("isFinite"),
        create_builtin_function(ctx, "number_isFinite"),
    );
    num_func.base.set(
        ctx.intern("isInteger"),
        create_builtin_function(ctx, "number_isInteger"),
    );
    num_func.base.set(
        ctx.intern("parseInt"),
        create_builtin_function(ctx, "number_parseInt"),
    );
    num_func.base.set(
        ctx.intern("parseFloat"),
        create_builtin_function(ctx, "number_parseFloat"),
    );
    let num_ptr = Box::into_raw(Box::new(num_func)) as usize;
    ctx.runtime_mut().gc_heap_mut().track_function(num_ptr);
    let num_value = JSValue::new_function(num_ptr);
    let global = ctx.global();
    if global.is_object() {
        let global_obj = global.as_object_mut();
        global_obj.set(number_atom, num_value);
    }

    let mut proto_obj = JSObject::new();
    proto_obj.set(
        ctx.intern("toFixed"),
        create_builtin_function(ctx, "number_toFixed"),
    );
    proto_obj.set(
        ctx.intern("toPrecision"),
        create_builtin_function(ctx, "number_toPrecision"),
    );
    proto_obj.set(
        ctx.intern("toString"),
        create_builtin_function(ctx, "number_toString"),
    );
    proto_obj.set(
        ctx.intern("valueOf"),
        create_builtin_function(ctx, "number_valueOf"),
    );
    proto_obj.set(ctx.common_atoms.constructor, num_value);

    if let Some(obj_proto_ptr) = ctx.get_object_prototype() {
        proto_obj.prototype = Some(obj_proto_ptr);
    }

    let proto_ptr = Box::into_raw(Box::new(proto_obj)) as usize;
    ctx.runtime_mut().gc_heap_mut().track(proto_ptr);
    ctx.set_number_prototype(proto_ptr);

    let num_func_ptr = num_ptr as *mut crate::object::function::JSFunction;
    unsafe {
        (*num_func_ptr)
            .base
            .set(ctx.common_atoms.prototype, JSValue::new_object(proto_ptr));
    }
}

fn number_value_of(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        return JSValue::undefined();
    }
    let this = &args[0];
    if this.is_int() || this.is_float() {
        return *this;
    }
    if this.is_object() {
        let obj = this.as_object();
        if let Some(v) = obj.get(ctx.common_atoms.__value__) {
            return v;
        }
    }
    JSValue::undefined()
}

fn number_is_nan(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        return JSValue::bool(false);
    }
    let v = &args[0];
    if v.is_float() {
        JSValue::bool(v.get_float().is_nan())
    } else {
        JSValue::bool(false)
    }
}

fn number_is_finite(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        return JSValue::bool(false);
    }
    let v = &args[0];
    if v.is_int() {
        JSValue::bool(true)
    } else if v.is_float() {
        JSValue::bool(v.get_float().is_finite())
    } else {
        JSValue::bool(false)
    }
}

fn number_is_integer(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        return JSValue::bool(false);
    }
    let v = &args[0];
    if v.is_int() {
        JSValue::bool(true)
    } else if v.is_float() {
        let f = v.get_float();
        JSValue::bool(f.is_finite() && f.fract() == 0.0)
    } else {
        JSValue::bool(false)
    }
}

fn number_to_fixed(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    let this = if args.is_empty() {
        return JSValue::new_string(_ctx.intern("0"));
    } else {
        &args[0]
    };
    let digits = if args.len() > 1 {
        args[1].get_int() as usize
    } else {
        0
    };

    let val = if this.is_int() {
        this.get_int() as f64
    } else if this.is_float() {
        this.get_float()
    } else {
        return JSValue::new_string(_ctx.intern("NaN"));
    };

    let result = format!("{:.1$}", val, digits);
    JSValue::new_string(_ctx.intern(&result))
}

fn number_to_precision(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    let this = if args.is_empty() {
        return JSValue::new_string(_ctx.intern("0"));
    } else {
        &args[0]
    };
    let precision = if args.len() > 1 {
        args[1].get_int() as usize
    } else {
        0
    };

    let val = if this.is_int() {
        this.get_int() as f64
    } else if this.is_float() {
        this.get_float()
    } else {
        return JSValue::new_string(_ctx.intern("NaN"));
    };

    if val.is_nan() {
        return JSValue::new_string(_ctx.intern("NaN"));
    }
    if val.is_infinite() {
        return JSValue::new_string(_ctx.intern(if val.is_sign_positive() {
            "Infinity"
        } else {
            "-Infinity"
        }));
    }

    let result = format!("{:.1$}", val, precision);
    JSValue::new_string(_ctx.intern(&result))
}

fn boolean_constructor(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        return JSValue::bool(false);
    }
    JSValue::bool(args[0].is_truthy())
}

fn boolean_to_string(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        return JSValue::new_string(ctx.intern("false"));
    }
    let this = &args[0];
    if this.is_bool() {
        return JSValue::new_string(ctx.intern(if this.get_bool() { "true" } else { "false" }));
    }
    if this.is_object() {
        let obj = this.as_object();
        if let Some(v) = obj.get(ctx.common_atoms.__value__) {
            if v.is_bool() {
                return JSValue::new_string(ctx.intern(if v.get_bool() {
                    "true"
                } else {
                    "false"
                }));
            }
        }
    }
    JSValue::new_string(ctx.intern("false"))
}

fn boolean_value_of(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        return JSValue::bool(false);
    }
    let this = &args[0];
    if this.is_bool() {
        return *this;
    }
    if this.is_object() {
        let obj = this.as_object();
        if let Some(v) = obj.get(ctx.common_atoms.__value__) {
            return v;
        }
    }
    JSValue::bool(false)
}

fn init_boolean(ctx: &mut JSContext) {
    ctx.register_builtin(
        "boolean_constructor",
        HostFunction::new("Boolean", 1, boolean_constructor),
    );
    ctx.register_builtin(
        "boolean_toString",
        HostFunction::new("toString", 0, boolean_to_string),
    );
    ctx.register_builtin(
        "boolean_valueOf",
        HostFunction::new("valueOf", 0, boolean_value_of),
    );

    let boolean_atom = ctx.intern("Boolean");
    let mut bool_func = JSFunction::new_builtin(boolean_atom, 1);
    bool_func.set_builtin_marker(ctx, "boolean_constructor");
    let bool_ptr = Box::into_raw(Box::new(bool_func)) as usize;
    ctx.runtime_mut().gc_heap_mut().track_function(bool_ptr);
    let bool_value = JSValue::new_function(bool_ptr);

    let mut proto_obj = JSObject::new();
    proto_obj.set(
        ctx.intern("toString"),
        create_builtin_function(ctx, "boolean_toString"),
    );
    proto_obj.set(
        ctx.intern("valueOf"),
        create_builtin_function(ctx, "boolean_valueOf"),
    );
    proto_obj.set(ctx.common_atoms.constructor, bool_value);
    if let Some(obj_proto_ptr) = ctx.get_object_prototype() {
        proto_obj.prototype = Some(obj_proto_ptr);
    }
    let proto_ptr = Box::into_raw(Box::new(proto_obj)) as usize;
    ctx.runtime_mut().gc_heap_mut().track(proto_ptr);

    let bool_func_ptr = bool_ptr as *mut crate::object::function::JSFunction;
    unsafe {
        (*bool_func_ptr)
            .base
            .set(ctx.common_atoms.prototype, JSValue::new_object(proto_ptr));
    }

    let global = ctx.global();
    if global.is_object() {
        let global_obj = global.as_object_mut();
        global_obj.set(boolean_atom, bool_value);
    }
}

fn init_date(ctx: &mut JSContext) {
    ctx.register_builtin("date_now", HostFunction::new("now", 0, date::date_now));
    ctx.register_builtin(
        "date_parse",
        HostFunction::new("parse", 1, date::date_parse),
    );
    ctx.register_builtin("date_utc", HostFunction::new("UTC", 7, date::date_utc));
    ctx.register_builtin(
        "date_getTime",
        HostFunction::new("getTime", 0, date::date_get_time),
    );
    ctx.register_builtin(
        "date_getFullYear",
        HostFunction::new("getFullYear", 0, date::date_get_full_year),
    );
    ctx.register_builtin(
        "date_getMonth",
        HostFunction::new("getMonth", 0, date::date_get_month),
    );
    ctx.register_builtin(
        "date_getDate",
        HostFunction::new("getDate", 0, date::date_get_date),
    );
    ctx.register_builtin(
        "date_getDay",
        HostFunction::new("getDay", 0, date::date_get_day),
    );
    ctx.register_builtin(
        "date_getHours",
        HostFunction::new("getHours", 0, date::date_get_hours),
    );
    ctx.register_builtin(
        "date_getMinutes",
        HostFunction::new("getMinutes", 0, date::date_get_minutes),
    );
    ctx.register_builtin(
        "date_getSeconds",
        HostFunction::new("getSeconds", 0, date::date_get_seconds),
    );
    ctx.register_builtin(
        "date_getMilliseconds",
        HostFunction::new("getMilliseconds", 0, date::date_get_milliseconds),
    );
    ctx.register_builtin(
        "date_getTimezoneOffset",
        HostFunction::new("getTimezoneOffset", 0, date::date_get_timezone_offset),
    );
    ctx.register_builtin(
        "date_toString",
        HostFunction::new("toString", 0, date::date_to_string),
    );
    ctx.register_builtin(
        "date_toISOString",
        HostFunction::new("toISOString", 0, date::date_to_iso_string),
    );
    ctx.register_builtin(
        "date_toUTCString",
        HostFunction::new("toUTCString", 0, date::date_to_utc_string),
    );
    ctx.register_builtin(
        "date_toDateString",
        HostFunction::new("toDateString", 0, date::date_to_date_string),
    );
    ctx.register_builtin(
        "date_toTimeString",
        HostFunction::new("toTimeString", 0, date::date_to_time_string),
    );
    ctx.register_builtin(
        "date_valueOf",
        HostFunction::new("valueOf", 0, date::date_value_of),
    );
    ctx.register_builtin(
        "date_toPrimitive",
        HostFunction::new("toPrimitive", 1, date::date_to_primitive),
    );

    date::init_date(ctx);
}

fn init_regexp(ctx: &mut JSContext) {
    ctx.register_builtin(
        "regexp_test",
        HostFunction::new("test", 1, regexp::regexp_test),
    );
    ctx.register_builtin(
        "regexp_exec",
        HostFunction::new("exec", 1, regexp::regexp_exec),
    );
    ctx.register_builtin(
        "regexp_toString",
        HostFunction::new("toString", 0, regexp::regexp_to_string),
    );

    regexp::init_regexp(ctx);
}

fn init_promise(ctx: &mut JSContext) {
    promise::init_promise(ctx);
    promise::register_builtins(ctx);
}

fn init_global_funcs(ctx: &mut JSContext) {
    let global = ctx.global();
    if global.is_object() {
        let global_obj = global.as_object_mut();
        global_obj.set(
            ctx.intern("isNaN"),
            create_builtin_function(ctx, "global_isnan"),
        );
        global_obj.set(
            ctx.intern("isFinite"),
            create_builtin_function(ctx, "global_isfinite"),
        );
        global_obj.set(
            ctx.intern("parseInt"),
            create_builtin_function(ctx, "global_parseint"),
        );
        global_obj.set(
            ctx.intern("parseFloat"),
            create_builtin_function(ctx, "global_parsefloat"),
        );
        global_obj.set(
            ctx.intern("eval"),
            create_builtin_function(ctx, "global_eval"),
        );

        global_obj.set(ctx.intern("btoa"), create_builtin_function(ctx, "btoa"));
        global_obj.set(ctx.intern("atob"), create_builtin_function(ctx, "atob"));

        global_obj.set(
            ctx.intern("encodeURI"),
            create_builtin_function(ctx, "encodeURI"),
        );
        global_obj.set(
            ctx.intern("decodeURI"),
            create_builtin_function(ctx, "decodeURI"),
        );
        global_obj.set(
            ctx.intern("encodeURIComponent"),
            create_builtin_function(ctx, "encodeURIComponent"),
        );
        global_obj.set(
            ctx.intern("decodeURIComponent"),
            create_builtin_function(ctx, "decodeURIComponent"),
        );

        global_obj.set(
            ctx.intern("setTimeout"),
            create_builtin_function(ctx, "setTimeout"),
        );
        global_obj.set(
            ctx.intern("setInterval"),
            create_builtin_function(ctx, "setInterval"),
        );
        global_obj.set(
            ctx.intern("clearTimeout"),
            create_builtin_function(ctx, "clearTimeout"),
        );
        global_obj.set(
            ctx.intern("clearInterval"),
            create_builtin_function(ctx, "clearInterval"),
        );

        global_obj.set(
            ctx.intern("queueMicrotask"),
            create_builtin_function(ctx, "queueMicrotask"),
        );

        global_obj.set(
            ctx.intern("requestAnimationFrame"),
            create_builtin_function(ctx, "requestAnimationFrame"),
        );
        global_obj.set(
            ctx.intern("cancelAnimationFrame"),
            create_builtin_function(ctx, "cancelAnimationFrame"),
        );

        global_obj.set(ctx.intern("globalThis"), global);

        global_obj.set(ctx.intern("import"), create_builtin_function(ctx, "import"));
        global_obj.set(
            ctx.intern("disassemble"),
            create_builtin_function(ctx, "disassemble"),
        );

        use crate::object::object::PropertyDescriptor;
        global_obj.define_property(
            ctx.intern("undefined"),
            PropertyDescriptor {
                value: Some(JSValue::undefined()),
                writable: false,
                enumerable: false,
                configurable: false,
                get: None,
                set: None,
            },
        );
        global_obj.define_property(
            ctx.intern("NaN"),
            PropertyDescriptor {
                value: Some(JSValue::new_float(f64::NAN)),
                writable: false,
                enumerable: false,
                configurable: false,
                get: None,
                set: None,
            },
        );
        global_obj.define_property(
            ctx.intern("Infinity"),
            PropertyDescriptor {
                value: Some(JSValue::new_float(f64::INFINITY)),
                writable: false,
                enumerable: false,
                configurable: false,
                get: None,
                set: None,
            },
        );
        #[cfg(feature = "fetch")]
        {
            global_obj.set(ctx.intern("fetch"), create_builtin_function(ctx, "fetch"));
            global_obj.set(
                ctx.intern("WebSocket"),
                create_builtin_function(ctx, "WebSocket"),
            );
            global_obj.set(
                ctx.intern("EventSource"),
                create_builtin_function(ctx, "EventSource"),
            );
        }
    }
}
