use crate::host::HostFunction;
use crate::object::function::JSFunction;
use crate::object::object::JSObject;
use crate::runtime::atom::Atom;
use crate::runtime::context::JSContext;
use crate::value::JSValue;

fn throw_type_error(ctx: &mut JSContext, msg: &str) {
    let mut err = JSObject::new();
    if let Some(proto_ptr) = ctx.get_type_error_prototype() {
        err.prototype = Some(proto_ptr);
    }
    err.set(
        ctx.common_atoms.name,
        JSValue::new_string(ctx.intern("TypeError")),
    );
    err.set(
        ctx.common_atoms.message,
        JSValue::new_string(ctx.intern(msg)),
    );
    let ptr = Box::into_raw(Box::new(err)) as usize;
    ctx.runtime_mut().gc_heap_mut().track(ptr);
    ctx.pending_exception = Some(JSValue::new_object(ptr));
}

fn create_builtin_function(ctx: &mut JSContext, name: &str, arity: u32) -> JSValue {
    let mut func = JSFunction::new_builtin(ctx.intern(name), arity);
    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 const NO_DESCRIPTION: Atom = Atom(u32::MAX);

fn create_symbol(ctx: &mut JSContext, description: Option<Atom>) -> JSValue {
    let desc = description.unwrap_or(NO_DESCRIPTION);
    if desc != NO_DESCRIPTION {
        ctx.mark_symbol_atom(desc);
    }
    let id = ctx.runtime_mut().next_symbol_id();
    JSValue::new_symbol_with_id(desc, id)
}

fn symbol_constructor(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    let desc = if args.is_empty() {
        None
    } else {
        let desc_val = args[0];
        if desc_val.is_undefined() {
            None
        } else if desc_val.is_symbol() {
            throw_type_error(ctx, "Cannot convert a Symbol value to a string");
            return JSValue::undefined();
        } else if let Some(atom) = to_string_via_vm(ctx, desc_val) {
            Some(atom)
        } else {
            throw_type_error(ctx, "Cannot convert object to primitive value");
            return JSValue::undefined();
        }
    };
    create_symbol(ctx, desc)
}

fn to_string_via_vm(ctx: &mut JSContext, value: JSValue) -> Option<Atom> {
    if value.is_string() { return Some(value.get_atom()); }
    if value.is_int() { return Some(ctx.intern(&value.get_int().to_string())); }
    if value.is_float() { return Some(ctx.intern(&value.get_float().to_string())); }
    if value.is_bool() { return Some(ctx.intern(if value.get_bool() { "true" } else { "false" })); }
    if value.is_undefined() { return Some(ctx.intern("undefined")); }
    if value.is_null() { return Some(ctx.intern("null")); }
    if value.is_symbol() { return None; }
    if value.is_object() {
        if let Some(vm_ptr) = ctx.get_register_vm_ptr() {
            let vm = unsafe { &mut *(vm_ptr as *mut crate::runtime::vm::VM) };
            let obj = value.as_object();
            let to_string_val = obj.get(ctx.common_atoms.to_string);
            let to_string_is_callable = to_string_val.map_or(false, |v| v.is_function());
            if to_string_is_callable {
                let f = to_string_val.unwrap();
                if let Ok(r) = vm.call_function_with_this(ctx, f, value.clone(), &[]) {
                    if !r.is_object() {
                        return to_string_via_vm(ctx, r);
                    }
                } else {
                    return None;
                }
            }
            let value_of_val = obj.get(ctx.common_atoms.value_of);
            let value_of_is_callable = value_of_val.map_or(false, |v| v.is_function());
            if value_of_is_callable {
                let f = value_of_val.unwrap();
                if let Ok(r) = vm.call_function_with_this(ctx, f, value.clone(), &[]) {
                    if !r.is_object() {
                        return to_string_via_vm(ctx, r);
                    }
                } else {
                    return None;
                }
            }
        }
        return None;
    }
    None
}

fn symbol_for(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    let key_val = if args.is_empty() {
        JSValue::new_string(ctx.intern("undefined"))
    } else {
        let v = args[0];
        if let Some(atom) = to_string_via_vm(ctx, v) {
            JSValue::new_string(atom)
        } else if v.is_symbol() {
            throw_type_error(ctx, "Can't convert symbol to string");
            return JSValue::undefined();
        } else {
            JSValue::new_string(ctx.intern(""))
        }
    };
    let key = key_val.get_atom();

    let reg = if let Some(ptr) = ctx.get_symbol_registry_ptr() {
        unsafe { &mut *(ptr as *mut JSObject) }
    } else {
        let mut new_reg = JSObject::new_array();
        new_reg.set(ctx.common_atoms.length, JSValue::new_int(0));
        let reg_ptr = Box::into_raw(Box::new(new_reg)) as usize;
        ctx.runtime_mut().gc_heap_mut().track(reg_ptr);
        ctx.runtime_mut().gc_heap_mut().extra_roots.push(JSValue::new_object(reg_ptr));
        ctx.set_symbol_registry_ptr(Some(reg_ptr));
        unsafe { &mut *(reg_ptr as *mut JSObject) }
    };

    let len_atom = ctx.common_atoms.length;
    let len = reg.get(len_atom).map(|v| v.get_int() as usize).unwrap_or(0);
    for i in 0..len {
        let idx_atom = ctx.intern(&i.to_string());
        if let Some(entry) = reg.get(idx_atom) {
            if entry.is_object() {
                let entry_obj = entry.as_object_mut();
                let desc_atom = ctx.intern("description");
                if let Some(desc) = entry_obj.get(desc_atom) {
                    if desc.is_string() && desc.get_atom() == key {
                        let sym_atom = ctx.intern("symbol");
                        if let Some(sym) = entry_obj.get(sym_atom) {
                            return sym;
                        }
                    }
                }
            }
        }
    }

    let new_sym = create_symbol(ctx, Some(key));

    let mut entry = JSObject::new();
    entry.set(ctx.intern("description"), JSValue::new_string(key));
    entry.set(ctx.intern("symbol"), new_sym);
    let entry_ptr = Box::into_raw(Box::new(entry)) as usize;
    let entry_val = JSValue::new_object(entry_ptr);
    let idx_atom = ctx.intern(&len.to_string());
    reg.set(idx_atom, entry_val);
    reg.set(len_atom, JSValue::new_int((len + 1) as i64));

    new_sym
}

fn symbol_key_for(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        throw_type_error(ctx, "Symbol.keyFor requires a symbol");
        return JSValue::undefined();
    }
    let sym_val = args[0];
    if !sym_val.is_symbol() {
        throw_type_error(ctx, "Symbol.keyFor requires a symbol");
        return JSValue::undefined();
    }

    if let Some(ptr) = ctx.get_symbol_registry_ptr() {
        let reg = unsafe { &*(ptr as *const JSObject) };
        let len_atom = ctx.common_atoms.length;
        let len = reg
            .get(len_atom)
            .map(|v| v.get_int() as usize)
            .unwrap_or(0);
        for i in 0..len {
            let idx_atom = ctx.intern(&i.to_string());
            if let Some(entry) = reg.get(idx_atom) {
                if entry.is_object() {
                    let entry_obj = entry.as_object();
                    let sym_atom = ctx.intern("symbol");
                    if let Some(sym) = entry_obj.get(sym_atom) {
                        if sym.strict_eq(&sym_val) {
                            let desc_atom = ctx.intern("description");
                            if let Some(desc) = entry_obj.get(desc_atom) {
                                return desc;
                            }
                        }
                    }
                }
            }
        }
    }

    JSValue::undefined()
}

fn symbol_description(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        throw_type_error(
            ctx,
            "Symbol.prototype.description requires that 'this' be a Symbol",
        );
        return JSValue::undefined();
    }
    let this_val = &args[0];
    let sym = if this_val.is_symbol() {
        this_val.clone()
    } else if this_val.is_object() {
        if let Some(v) = this_val.as_object().get(ctx.common_atoms.__value__) {
            if v.is_symbol() {
                v
            } else {
                throw_type_error(
                    ctx,
                    "Symbol.prototype.description requires that 'this' be a Symbol",
                );
                return JSValue::undefined();
            }
        } else {
            throw_type_error(
                ctx,
                "Symbol.prototype.description requires that 'this' be a Symbol",
            );
            return JSValue::undefined();
        }
    } else {
        throw_type_error(
            ctx,
            "Symbol.prototype.description requires that 'this' be a Symbol",
        );
        return JSValue::undefined();
    };

    let desc_atom = sym.get_atom();
    if desc_atom == NO_DESCRIPTION {
        JSValue::undefined()
    } else {
        JSValue::new_string(desc_atom)
    }
}

fn symbol_to_primitive(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        throw_type_error(
            ctx,
            "Symbol.prototype[Symbol.toPrimitive] requires that 'this' be a Symbol",
        );
        return JSValue::undefined();
    }
    let this_val = &args[0];
    if this_val.is_symbol() {
        return this_val.clone();
    }
    if this_val.is_object() {
        if let Some(v) = this_val.as_object().get(ctx.common_atoms.__value__) {
            if v.is_symbol() {
                return v;
            }
        }
    }
    throw_type_error(
        ctx,
        "Symbol.prototype[Symbol.toPrimitive] requires that 'this' be a Symbol",
    );
    JSValue::undefined()
}

fn symbol_value_of(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        throw_type_error(
            ctx,
            "Symbol.prototype.valueOf requires that 'this' be a Symbol",
        );
        return JSValue::undefined();
    }
    let this_val = &args[0];
    if this_val.is_symbol() {
        return this_val.clone();
    }
    if this_val.is_object() {
        if let Some(v) = this_val.as_object().get(ctx.common_atoms.__value__) {
            if v.is_symbol() {
                return v;
            }
        }
    }
    throw_type_error(
        ctx,
        "Symbol.prototype.valueOf requires that 'this' be a Symbol",
    );
    JSValue::undefined()
}

fn symbol_to_string(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    let this_val = if args.is_empty() {
        return JSValue::new_string(ctx.intern("Symbol()"));
    } else {
        &args[0]
    };

    let sym = if this_val.is_symbol() {
        this_val.clone()
    } else if this_val.is_object() {
        let inner = this_val.as_object().get(ctx.common_atoms.__value__);
        if let Some(v) = inner {
            if v.is_symbol() {
                v
            } else {
                throw_type_error(
                    ctx,
                    "Symbol.prototype.toString requires that 'this' be a Symbol",
                );
                return JSValue::undefined();
            }
        } else {
            throw_type_error(
                ctx,
                "Symbol.prototype.toString requires that 'this' be a Symbol",
            );
            return JSValue::undefined();
        }
    } else {
        throw_type_error(
            ctx,
            "Symbol.prototype.toString requires that 'this' be a Symbol",
        );
        return JSValue::undefined();
    };

    let desc_atom = sym.get_atom();
    if desc_atom == NO_DESCRIPTION {
        JSValue::new_string(ctx.intern("Symbol()"))
    } else {
        let desc = ctx.get_atom_str(desc_atom);
        JSValue::new_string(ctx.intern(&format!("Symbol({})", desc)))
    }
}

const SYMBOL_ITERATOR_DESC: &str = "Symbol.iterator";
const SYMBOL_TO_STRING_TAG_DESC: &str = "Symbol.toStringTag";
const SYMBOL_SPECIES_DESC: &str = "Symbol.species";
const SYMBOL_TO_PRIMITIVE_DESC: &str = "Symbol.toPrimitive";
const SYMBOL_IS_CONCAT_SPREADABLE_DESC: &str = "Symbol.isConcatSpreadable";
const SYMBOL_MATCH_DESC: &str = "Symbol.match";
const SYMBOL_REPLACE_DESC: &str = "Symbol.replace";
const SYMBOL_SEARCH_DESC: &str = "Symbol.search";
const SYMBOL_SPLIT_DESC: &str = "Symbol.split";
const SYMBOL_UNSCOPABLES_DESC: &str = "Symbol.unscopables";
const SYMBOL_HAS_INSTANCE_DESC: &str = "Symbol.hasInstance";
const SYMBOL_ASYNC_ITERATOR_DESC: &str = "Symbol.asyncIterator";
const SYMBOL_MATCH_ALL_DESC: &str = "Symbol.matchAll";
const SYMBOL_ASYNC_DISPOSE_DESC: &str = "Symbol.asyncDispose";
const SYMBOL_DISPOSE_DESC: &str = "Symbol.dispose";

pub fn get_or_create_well_known_symbol(ctx: &mut JSContext, description: &str) -> JSValue {
    let desc_atom = ctx.intern(description);
    let global = ctx.global();
    if global.is_object() {
        let global_obj = global.as_object_mut();
        if let Some(sym) = global_obj.get(desc_atom) {
            return sym;
        }
        let sym = create_symbol(ctx, Some(desc_atom));
        global_obj.set(desc_atom, sym);
        return sym;
    }
    create_symbol(ctx, Some(desc_atom))
}

pub fn init_symbol(ctx: &mut JSContext) {
    let symbol_atom = ctx.intern("Symbol");
    let global = ctx.global();
    if global.is_object() {
        if global.as_object().get(symbol_atom).is_some() {
            return;
        }
    }
    let mut symbol_ctor = JSFunction::new_builtin(ctx.intern("Symbol"), 0);
    symbol_ctor.set_builtin_marker(ctx, "symbol_constructor");

    let for_func = create_builtin_function(ctx, "symbol_for", 1);
    let key_for_func = create_builtin_function(ctx, "symbol_keyFor", 1);

    symbol_ctor.base.define_property(
        ctx.intern("for"),
        crate::object::object::PropertyDescriptor {
            value: Some(for_func),
            writable: true,
            enumerable: false,
            configurable: true,
            get: None,
            set: None,
        },
    );
    symbol_ctor.base.define_property(
        ctx.intern("keyFor"),
        crate::object::object::PropertyDescriptor {
            value: Some(key_for_func),
            writable: true,
            enumerable: false,
            configurable: true,
            get: None,
            set: None,
        },
    );

    let symbol_iter = get_or_create_well_known_symbol(ctx, SYMBOL_ITERATOR_DESC);
    symbol_ctor.base.define_property(
        ctx.intern("iterator"),
        crate::object::object::PropertyDescriptor {
            value: Some(symbol_iter),
            writable: false,
            enumerable: false,
            configurable: false,
            get: None,
            set: None,
        },
    );

    let symbol_to_string_tag = get_or_create_well_known_symbol(ctx, SYMBOL_TO_STRING_TAG_DESC);
    symbol_ctor.base.define_property(
        ctx.intern("toStringTag"),
        crate::object::object::PropertyDescriptor {
            value: Some(symbol_to_string_tag),
            writable: false,
            enumerable: false,
            configurable: false,
            get: None,
            set: None,
        },
    );

    let symbol_species = get_or_create_well_known_symbol(ctx, SYMBOL_SPECIES_DESC);
    symbol_ctor.base.define_property(
        ctx.intern("species"),
        crate::object::object::PropertyDescriptor {
            value: Some(symbol_species),
            writable: false,
            enumerable: false,
            configurable: false,
            get: None,
            set: None,
        },
    );

    let symbol_to_primitive = get_or_create_well_known_symbol(ctx, SYMBOL_TO_PRIMITIVE_DESC);
    symbol_ctor.base.define_property(
        ctx.intern("toPrimitive"),
        crate::object::object::PropertyDescriptor {
            value: Some(symbol_to_primitive),
            writable: false,
            enumerable: false,
            configurable: false,
            get: None,
            set: None,
        },
    );

    let symbol_is_concat_spreadable =
        get_or_create_well_known_symbol(ctx, SYMBOL_IS_CONCAT_SPREADABLE_DESC);
    symbol_ctor.base.define_property(
        ctx.intern("isConcatSpreadable"),
        crate::object::object::PropertyDescriptor {
            value: Some(symbol_is_concat_spreadable),
            writable: false,
            enumerable: false,
            configurable: false,
            get: None,
            set: None,
        },
    );

    let symbol_match = get_or_create_well_known_symbol(ctx, SYMBOL_MATCH_DESC);
    symbol_ctor.base.define_property(
        ctx.intern("match"),
        crate::object::object::PropertyDescriptor {
            value: Some(symbol_match),
            writable: false,
            enumerable: false,
            configurable: false,
            get: None,
            set: None,
        },
    );

    let symbol_replace = get_or_create_well_known_symbol(ctx, SYMBOL_REPLACE_DESC);
    symbol_ctor.base.define_property(
        ctx.intern("replace"),
        crate::object::object::PropertyDescriptor {
            value: Some(symbol_replace),
            writable: false,
            enumerable: false,
            configurable: false,
            get: None,
            set: None,
        },
    );

    let symbol_search = get_or_create_well_known_symbol(ctx, SYMBOL_SEARCH_DESC);
    symbol_ctor.base.define_property(
        ctx.intern("search"),
        crate::object::object::PropertyDescriptor {
            value: Some(symbol_search),
            writable: false,
            enumerable: false,
            configurable: false,
            get: None,
            set: None,
        },
    );

    let symbol_split = get_or_create_well_known_symbol(ctx, SYMBOL_SPLIT_DESC);
    symbol_ctor.base.define_property(
        ctx.intern("split"),
        crate::object::object::PropertyDescriptor {
            value: Some(symbol_split),
            writable: false,
            enumerable: false,
            configurable: false,
            get: None,
            set: None,
        },
    );

    let symbol_unscopables = get_or_create_well_known_symbol(ctx, SYMBOL_UNSCOPABLES_DESC);
    symbol_ctor.base.define_property(
        ctx.intern("unscopables"),
        crate::object::object::PropertyDescriptor {
            value: Some(symbol_unscopables),
            writable: false,
            enumerable: false,
            configurable: false,
            get: None,
            set: None,
        },
    );

    let symbol_has_instance = get_or_create_well_known_symbol(ctx, SYMBOL_HAS_INSTANCE_DESC);
    symbol_ctor.base.define_property(
        ctx.intern("hasInstance"),
        crate::object::object::PropertyDescriptor {
            value: Some(symbol_has_instance),
            writable: false,
            enumerable: false,
            configurable: false,
            get: None,
            set: None,
        },
    );

    let symbol_async_iterator = get_or_create_well_known_symbol(ctx, SYMBOL_ASYNC_ITERATOR_DESC);
    symbol_ctor.base.define_property(
        ctx.intern("asyncIterator"),
        crate::object::object::PropertyDescriptor {
            value: Some(symbol_async_iterator),
            writable: false,
            enumerable: false,
            configurable: false,
            get: None,
            set: None,
        },
    );

    let symbol_match_all = get_or_create_well_known_symbol(ctx, SYMBOL_MATCH_ALL_DESC);
    symbol_ctor.base.define_property(
        ctx.intern("matchAll"),
        crate::object::object::PropertyDescriptor {
            value: Some(symbol_match_all),
            writable: false,
            enumerable: false,
            configurable: false,
            get: None,
            set: None,
        },
    );

    let symbol_async_dispose = get_or_create_well_known_symbol(ctx, SYMBOL_ASYNC_DISPOSE_DESC);
    symbol_ctor.base.define_property(
        ctx.intern("asyncDispose"),
        crate::object::object::PropertyDescriptor {
            value: Some(symbol_async_dispose),
            writable: false,
            enumerable: false,
            configurable: false,
            get: None,
            set: None,
        },
    );

    let symbol_dispose = get_or_create_well_known_symbol(ctx, SYMBOL_DISPOSE_DESC);
    symbol_ctor.base.define_property(
        ctx.intern("dispose"),
        crate::object::object::PropertyDescriptor {
            value: Some(symbol_dispose),
            writable: false,
            enumerable: false,
            configurable: false,
            get: None,
            set: None,
        },
    );

    let mut sym_proto = JSObject::new();
    if let Some(obj_proto_ptr) = ctx.get_object_prototype() {
        sym_proto.prototype = Some(obj_proto_ptr);
    }
    let to_string_fn = create_builtin_function(ctx, "symbol_toString", 0);
    let value_of_fn = create_builtin_function(ctx, "symbol_valueOf", 0);
    let description_fn = create_builtin_function(ctx, "symbol_description", 0);
    let to_primitive_fn = create_builtin_function(ctx, "symbol_toPrimitive", 1);
    sym_proto.set(ctx.intern("toString"), to_string_fn.clone());
    sym_proto.set(ctx.intern("valueOf"), value_of_fn.clone());

    let proto_ptr = Box::into_raw(Box::new(sym_proto)) as usize;
    ctx.runtime_mut().gc_heap_mut().track(proto_ptr);
    let proto_value = JSValue::new_object(proto_ptr);
    symbol_ctor.base.set(ctx.intern("prototype"), proto_value);
    ctx.set_symbol_prototype(proto_ptr);

    let symbol_to_string_tag = get_or_create_well_known_symbol(ctx, SYMBOL_TO_STRING_TAG_DESC);
    let tag_key = crate::runtime::atom::Atom(0x40000000 | symbol_to_string_tag.get_symbol_id());
    proto_value.as_object_mut().define_property(
        tag_key,
        crate::object::object::PropertyDescriptor {
            value: Some(JSValue::new_string(ctx.intern("Symbol"))),
            writable: false,
            enumerable: false,
            configurable: true,
            get: None,
            set: None,
        },
    );

    let symbol_ptr = Box::into_raw(Box::new(symbol_ctor)) as usize;
    ctx.runtime_mut().gc_heap_mut().track_function(symbol_ptr);
    let symbol_ctor_val = JSValue::new_function(symbol_ptr);

    proto_value.as_object_mut().define_property(
        ctx.intern("constructor"),
        crate::object::object::PropertyDescriptor {
            value: Some(symbol_ctor_val.clone()),
            writable: true,
            enumerable: false,
            configurable: true,
            get: None,
            set: None,
        },
    );
    proto_value.as_object_mut().define_property(
        ctx.intern("toString"),
        crate::object::object::PropertyDescriptor {
            value: Some(to_string_fn),
            writable: true,
            enumerable: false,
            configurable: true,
            get: None,
            set: None,
        },
    );
    proto_value.as_object_mut().define_property(
        ctx.intern("valueOf"),
        crate::object::object::PropertyDescriptor {
            value: Some(value_of_fn),
            writable: true,
            enumerable: false,
            configurable: true,
            get: None,
            set: None,
        },
    );
    proto_value.as_object_mut().define_property(
        ctx.intern("description"),
        crate::object::object::PropertyDescriptor {
            value: None,
            writable: false,
            enumerable: false,
            configurable: true,
            get: Some(description_fn),
            set: None,
        },
    );

    let symbol_to_primitive_sym = get_or_create_well_known_symbol(ctx, SYMBOL_TO_PRIMITIVE_DESC);
    let to_prim_key =
        crate::runtime::atom::Atom(0x40000000 | symbol_to_primitive_sym.get_symbol_id());
    proto_value.as_object_mut().define_property(
        to_prim_key,
        crate::object::object::PropertyDescriptor {
            value: Some(to_primitive_fn),
            writable: false,
            enumerable: false,
            configurable: true,
            get: None,
            set: None,
        },
    );

    let symbol_atom = ctx.intern("Symbol");
    let global = ctx.global();
    if global.is_object() {
        let global_obj = global.as_object_mut();
        crate::builtins::global::set_non_enumerable(global_obj, symbol_atom, symbol_ctor_val);
    }
}

pub fn register_builtins(ctx: &mut JSContext) {
    ctx.register_builtin(
        "symbol_constructor",
        HostFunction::ctor("Symbol", 0, symbol_constructor),
    );
    ctx.register_builtin("symbol_for", HostFunction::new("for", 1, symbol_for));
    ctx.register_builtin(
        "symbol_keyFor",
        HostFunction::new("keyFor", 1, symbol_key_for),
    );
    ctx.register_builtin(
        "symbol_valueOf",
        HostFunction::method("valueOf", 0, symbol_value_of),
    );
    ctx.register_builtin(
        "symbol_toString",
        HostFunction::method("toString", 0, symbol_to_string),
    );
    ctx.register_builtin(
        "symbol_description",
        HostFunction::method("description", 0, symbol_description),
    );
    ctx.register_builtin(
        "symbol_toPrimitive",
        HostFunction::method("[Symbol.toPrimitive]", 1, symbol_to_primitive),
    );
    ctx.register_builtin(
        "symbol_species_get",
        HostFunction::method("get [Symbol.species]", 0, species_getter),
    );
}

pub fn fix_prototype_chain(ctx: &mut JSContext) {
    if let Some(sym_proto_ptr) = ctx.get_symbol_prototype() {
        if let Some(obj_proto_ptr) = ctx.get_object_prototype() {
            unsafe {
                (*sym_proto_ptr).prototype = Some(obj_proto_ptr);
            }
        }
    }
}

pub fn get_symbol_iterator(ctx: &mut JSContext) -> JSValue {
    get_or_create_well_known_symbol(ctx, SYMBOL_ITERATOR_DESC)
}

pub fn get_symbol_iterator_atom(ctx: &mut JSContext) -> Atom {
    let sym = get_or_create_well_known_symbol(ctx, SYMBOL_ITERATOR_DESC);
    crate::runtime::atom::Atom(0x40000000 | sym.get_symbol_id())
}

pub fn get_symbol_split(ctx: &mut JSContext) -> JSValue {
    get_or_create_well_known_symbol(ctx, SYMBOL_SPLIT_DESC)
}

pub fn get_symbol_split_atom(ctx: &mut JSContext) -> Atom {
    let sym = get_or_create_well_known_symbol(ctx, SYMBOL_SPLIT_DESC);
    crate::runtime::atom::Atom(0x40000000 | sym.get_symbol_id())
}

pub fn get_symbol_unscopables(ctx: &mut JSContext) -> JSValue {
    get_or_create_well_known_symbol(ctx, SYMBOL_UNSCOPABLES_DESC)
}

pub fn get_symbol_unscopables_atom(ctx: &mut JSContext) -> Atom {
    let sym = get_or_create_well_known_symbol(ctx, SYMBOL_UNSCOPABLES_DESC);
    crate::runtime::atom::Atom(0x40000000 | sym.get_symbol_id())
}

pub fn get_symbol_has_instance(ctx: &mut JSContext) -> JSValue {
    get_or_create_well_known_symbol(ctx, SYMBOL_HAS_INSTANCE_DESC)
}

pub fn get_symbol_has_instance_atom(ctx: &mut JSContext) -> Atom {
    let sym = get_or_create_well_known_symbol(ctx, SYMBOL_HAS_INSTANCE_DESC);
    crate::runtime::atom::Atom(0x40000000 | sym.get_symbol_id())
}

pub fn get_symbol_async_iterator(ctx: &mut JSContext) -> JSValue {
    get_or_create_well_known_symbol(ctx, SYMBOL_ASYNC_ITERATOR_DESC)
}

pub fn get_symbol_async_iterator_atom(ctx: &mut JSContext) -> Atom {
    let sym = get_or_create_well_known_symbol(ctx, SYMBOL_ASYNC_ITERATOR_DESC);
    crate::runtime::atom::Atom(0x40000000 | sym.get_symbol_id())
}

pub fn get_symbol_is_concat_spreadable_atom(ctx: &mut JSContext) -> Atom {
    let sym = get_or_create_well_known_symbol(ctx, SYMBOL_IS_CONCAT_SPREADABLE_DESC);
    crate::runtime::atom::Atom(0x40000000 | sym.get_symbol_id())
}

pub fn get_symbol_to_string_tag_prop_key(ctx: &mut JSContext) -> Atom {
    let sym = get_or_create_well_known_symbol(ctx, SYMBOL_TO_STRING_TAG_DESC);
    crate::runtime::atom::Atom(0x40000000 | sym.get_symbol_id())
}

pub fn get_symbol_species(ctx: &mut JSContext) -> JSValue {
    get_or_create_well_known_symbol(ctx, SYMBOL_SPECIES_DESC)
}

fn species_getter(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if let Some(this_val) = args.first() {
        *this_val
    } else {
        JSValue::undefined()
    }
}

pub fn install_species_accessor(ctx: &mut JSContext, ctor_val: &JSValue) {
    if !ctor_val.is_function() {
        return;
    }
    let species_sym = get_symbol_species(ctx);
    if species_sym.is_undefined() {
        return;
    }
    let species_atom = crate::runtime::atom::Atom(0x40000000 | species_sym.get_symbol_id());

    let getter_fn = {
        let name_atom = ctx.intern("get [Symbol.species]");
        let mut f = crate::object::function::JSFunction::new_builtin(
            name_atom,
            0,
        );
        f.builtin_atom = Some(ctx.intern("symbol_species_get"));
        f.builtin_func = ctx.get_builtin_func("symbol_species_get");
        let name_desc = crate::object::object::PropertyDescriptor {
            value: Some(JSValue::new_string(name_atom)),
            writable: false,
            enumerable: false,
            configurable: true,
            get: None,
            set: None,
        };
        f.base.define_property(ctx.common_atoms.name, name_desc);
        let length_desc = crate::object::object::PropertyDescriptor {
            value: Some(JSValue::new_int(0)),
            writable: false,
            enumerable: false,
            configurable: true,
            get: None,
            set: None,
        };
        f.base.define_property(ctx.common_atoms.length, length_desc);
        let ptr = Box::into_raw(Box::new(f)) as usize;
        ctx.runtime_mut().gc_heap_mut().track_function(ptr);
        JSValue::new_function(ptr)
    };

    let func_mut = ctor_val.as_function_mut();
    let entry = crate::object::object::AccessorEntry {
        get: Some(getter_fn),
        set: None,
        enumerable: false,
        configurable: true,
    };
    func_mut.base.ensure_extra().accessors.get_or_insert_with(|| Box::new(crate::util::FxHashMap::default())).insert(species_atom, entry);
}

pub fn get_symbol_to_string_tag(ctx: &mut JSContext) -> JSValue {
    get_or_create_well_known_symbol(ctx, SYMBOL_TO_STRING_TAG_DESC)
}

pub fn is_symbol(val: &JSValue) -> bool {
    val.is_symbol()
}
