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 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)
}

fn create_symbol(ctx: &mut JSContext, description: Option<Atom>) -> JSValue {
    let desc = description.unwrap_or(ctx.intern(""));
    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_string() {
            Some(desc_val.get_atom())
        } else if desc_val.is_undefined() {
            None
        } else {
            let s = if desc_val.is_int() {
                desc_val.get_int().to_string()
            } else if desc_val.is_float() {
                desc_val.get_float().to_string()
            } else if desc_val.is_bool() {
                desc_val.get_bool().to_string()
            } else {
                String::new()
            };
            Some(ctx.intern(&s))
        }
    };
    create_symbol(ctx, desc)
}

fn symbol_for(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        return JSValue::undefined();
    }
    let key_val = args[0];
    let key = if key_val.is_string() {
        key_val.get_atom()
    } else {
        let s = if key_val.is_int() {
            key_val.get_int().to_string()
        } else if key_val.is_float() {
            key_val.get_float().to_string()
        } else {
            String::new()
        };
        ctx.intern(&s)
    };

    let global = ctx.global();
    if global.is_object() {
        let global_obj = global.as_object_mut();
        let reg_atom = ctx.intern("__symbol_registry__");
        if let Some(reg) = global_obj.get(reg_atom) {
            if reg.is_object() {
                let reg_obj = reg.as_object_mut();
                let len_atom = ctx.common_atoms.length;
                let len = reg_obj
                    .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_obj.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 reg = if let Some(r) = global_obj.get(reg_atom) {
            if r.is_object() {
                unsafe { JSValue::object_from_ptr_mut(r.get_ptr()) }
            } 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;
                let val = JSValue::new_object(reg_ptr);
                global_obj.set(reg_atom, val);

                unsafe { JSValue::object_from_ptr_mut(reg_ptr) }
            }
        } 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;
            let val = JSValue::new_object(reg_ptr);
            global_obj.set(reg_atom, val);

            unsafe { JSValue::object_from_ptr_mut(reg_ptr) }
        };

        let len_atom = ctx.common_atoms.length;
        let len = reg.get(len_atom).map(|v| v.get_int() as usize).unwrap_or(0);
        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));

        return new_sym;
    }

    create_symbol(ctx, Some(key))
}

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

    let global = ctx.global();
    if global.is_object() {
        let global_obj = global.as_object();
        let reg_atom = ctx.intern("__symbol_registry__");
        if let Some(reg) = global_obj.get(reg_atom) {
            if reg.is_object() {
                let reg_obj = reg.as_object();
                let len_atom = ctx.common_atoms.length;
                let len = reg_obj
                    .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_obj.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_value_of(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() {
        return JSValue::undefined();
    }
    let this_val = &args[0];
    if this_val.is_symbol() {
        *this_val
    } else {
        JSValue::undefined()
    }
}

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

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";

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 mut symbol_ctor = JSFunction::new_builtin(ctx.intern("Symbol"), 1);
    symbol_ctor.set_builtin_marker(ctx, "symbol_constructor");

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

    symbol_ctor.base.set(ctx.intern("for"), for_func);
    symbol_ctor.base.set(ctx.intern("keyFor"), key_for_func);

    let symbol_iter = get_or_create_well_known_symbol(ctx, SYMBOL_ITERATOR_DESC);
    symbol_ctor.base.set(ctx.intern("iterator"), symbol_iter);

    let symbol_to_string_tag = get_or_create_well_known_symbol(ctx, SYMBOL_TO_STRING_TAG_DESC);
    symbol_ctor
        .base
        .set(ctx.intern("toStringTag"), symbol_to_string_tag);

    let symbol_species = get_or_create_well_known_symbol(ctx, SYMBOL_SPECIES_DESC);
    symbol_ctor.base.set(ctx.intern("species"), symbol_species);

    let symbol_to_primitive = get_or_create_well_known_symbol(ctx, SYMBOL_TO_PRIMITIVE_DESC);
    symbol_ctor
        .base
        .set(ctx.intern("toPrimitive"), symbol_to_primitive);

    let symbol_is_concat_spreadable =
        get_or_create_well_known_symbol(ctx, SYMBOL_IS_CONCAT_SPREADABLE_DESC);
    symbol_ctor.base.set(
        ctx.intern("isConcatSpreadable"),
        symbol_is_concat_spreadable,
    );

    let symbol_match = get_or_create_well_known_symbol(ctx, SYMBOL_MATCH_DESC);
    symbol_ctor.base.set(ctx.intern("match"), symbol_match);

    let symbol_replace = get_or_create_well_known_symbol(ctx, SYMBOL_REPLACE_DESC);
    symbol_ctor.base.set(ctx.intern("replace"), symbol_replace);

    let symbol_search = get_or_create_well_known_symbol(ctx, SYMBOL_SEARCH_DESC);
    symbol_ctor.base.set(ctx.intern("search"), symbol_search);

    let symbol_split = get_or_create_well_known_symbol(ctx, SYMBOL_SPLIT_DESC);
    symbol_ctor.base.set(ctx.intern("split"), symbol_split);

    let symbol_unscopables = get_or_create_well_known_symbol(ctx, SYMBOL_UNSCOPABLES_DESC);
    symbol_ctor
        .base
        .set(ctx.intern("unscopables"), symbol_unscopables);

    let symbol_has_instance = get_or_create_well_known_symbol(ctx, SYMBOL_HAS_INSTANCE_DESC);
    symbol_ctor
        .base
        .set(ctx.intern("hasInstance"), symbol_has_instance);

    let symbol_async_iterator = get_or_create_well_known_symbol(ctx, SYMBOL_ASYNC_ITERATOR_DESC);
    symbol_ctor
        .base
        .set(ctx.intern("asyncIterator"), symbol_async_iterator);

    let mut sym_proto = JSObject::new();
    if let Some(obj_proto_ptr) = ctx.get_object_prototype() {
        sym_proto.prototype = Some(obj_proto_ptr);
    }
    sym_proto.set(
        ctx.intern("toString"),
        create_builtin_function(ctx, "symbol_toString"),
    );
    sym_proto.set(
        ctx.intern("valueOf"),
        create_builtin_function(ctx, "symbol_valueOf"),
    );

    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_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);

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

pub fn register_builtins(ctx: &mut JSContext) {
    ctx.register_builtin(
        "symbol_constructor",
        HostFunction::new("Symbol", 1, 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::new("valueOf", 0, symbol_value_of),
    );
    ctx.register_builtin(
        "symbol_toString",
        HostFunction::new("toString", 0, symbol_to_string),
    );
}

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 {
    ctx.intern("Symbol.iterator")
}

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 {
    ctx.intern("Symbol.split")
}

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 {
    ctx.intern("Symbol.unscopables")
}

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 {
    ctx.intern("Symbol.hasInstance")
}

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 {
    ctx.intern("Symbol.asyncIterator")
}

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