use crate::host::HostFunction;
use crate::object::object::{JSObject, ObjectType, TypedArrayKind};
use crate::runtime::context::JSContext;
use crate::value::JSValue;

#[derive(Debug)]
pub struct ArrayBufferData {
    pub data: Vec<u8>,
}

fn create_array_buffer(ctx: &mut JSContext, length: usize) -> JSValue {
    let mut obj = JSObject::new_typed(ObjectType::ArrayBuffer);
    let data = ArrayBufferData {
        data: vec![0; length],
    };

    obj.set(ctx.intern("byteLength"), JSValue::new_int(length as i64));

    let data_ptr = Box::into_raw(Box::new(data));
    obj.set_array_buffer_data(data_ptr as usize);
    let ptr = Box::into_raw(Box::new(obj)) as usize;
    JSValue::new_object(ptr)
}

fn get_array_buffer_data(obj: &JSObject) -> Option<&mut ArrayBufferData> {
    obj.get_array_buffer_data()
        .map(|ptr| unsafe { &mut *(ptr as *mut ArrayBufferData) })
}

fn create_typed_array(
    ctx: &mut JSContext,
    kind: TypedArrayKind,
    buffer: JSValue,
    byte_offset: usize,
    length: Option<usize>,
) -> Result<JSValue, String> {
    if !buffer.is_object() {
        return Err("TypedArray constructor requires ArrayBuffer".to_string());
    }

    let buffer_obj = buffer.as_object();
    let buffer_data = get_array_buffer_data(&buffer_obj).ok_or("Invalid ArrayBuffer")?;

    let bytes_per_element = kind.bytes_per_element();

    if byte_offset % bytes_per_element != 0 {
        return Err(format!(
            "byteOffset must be a multiple of {}",
            bytes_per_element
        ));
    }

    let byte_length = buffer_data.data.len();
    let remaining_bytes = byte_length.saturating_sub(byte_offset);

    let element_length = match length {
        Some(len) => len,
        None => remaining_bytes / bytes_per_element,
    };

    let required_bytes = byte_offset + element_length * bytes_per_element;
    if required_bytes > byte_length {
        return Err("TypedArray extends beyond ArrayBuffer bounds".to_string());
    }

    let mut obj = JSObject::new_typed(ObjectType::TypedArray);
    obj.set_typed_array_kind(kind);

    obj.set(ctx.intern("buffer"), buffer);
    obj.set(
        ctx.intern("byteOffset"),
        JSValue::new_int(byte_offset as i64),
    );
    obj.set(
        ctx.intern("byteLength"),
        JSValue::new_int((element_length * bytes_per_element) as i64),
    );
    obj.set(
        ctx.intern("length"),
        JSValue::new_int(element_length as i64),
    );

    let ptr = Box::into_raw(Box::new(obj)) as usize;
    Ok(JSValue::new_object(ptr))
}

fn typed_array_from_args(
    ctx: &mut JSContext,
    kind: TypedArrayKind,
    args: &[JSValue],
) -> Result<JSValue, String> {
    if args.is_empty() {
        let buffer = create_array_buffer(ctx, 0);
        return create_typed_array(ctx, kind, buffer, 0, Some(0));
    }

    let first_arg = &args[0];

    if first_arg.is_object() {
        let obj = first_arg.as_object();

        if obj.obj_type() == ObjectType::ArrayBuffer {
            let byte_offset = if args.len() > 1 {
                args[1].get_int() as usize
            } else {
                0
            };
            let length = if args.len() > 2 {
                Some(args[2].get_int() as usize)
            } else {
                None
            };
            return create_typed_array(ctx, kind, *first_arg, byte_offset, length);
        }

        if obj.obj_type() == ObjectType::TypedArray {
            let src_len = obj
                .get(ctx.intern("length"))
                .map(|v| v.get_int() as usize)
                .unwrap_or(0);

            let bytes_per_element = kind.bytes_per_element();
            let buffer = create_array_buffer(ctx, src_len * bytes_per_element);
            let result = create_typed_array(ctx, kind, buffer, 0, Some(src_len))?;

            return Ok(result);
        }

        let len = obj.get_array_elements().map(|e| e.len()).unwrap_or(0);

        let bytes_per_element = kind.bytes_per_element();
        let buffer = create_array_buffer(ctx, len * bytes_per_element);
        let result = create_typed_array(ctx, kind, buffer, 0, Some(len))?;

        return Ok(result);
    }

    if first_arg.is_int() || first_arg.is_float() {
        let length = first_arg.get_int() as usize;
        let bytes_per_element = kind.bytes_per_element();
        let buffer = create_array_buffer(ctx, length * bytes_per_element);
        return create_typed_array(ctx, kind, buffer, 0, Some(length));
    }

    Err("Invalid TypedArray constructor argument".to_string())
}

fn array_buffer_constructor(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    let length = if args.is_empty() {
        0
    } else {
        args[0].get_int() as usize
    };
    create_array_buffer(ctx, length)
}

fn data_view_constructor(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() || !args[0].is_object() {
        return JSValue::undefined();
    }

    let buffer = &args[0];
    let byte_offset = if args.len() > 1 {
        args[1].get_int() as usize
    } else {
        0
    };

    let mut obj = JSObject::new_typed(ObjectType::DataView);
    obj.set(ctx.intern("buffer"), *buffer);
    obj.set(
        ctx.intern("byteOffset"),
        JSValue::new_int(byte_offset as i64),
    );

    if let Some(buffer_obj) = if buffer.is_object() {
        Some(buffer.as_object())
    } else {
        None
    } {
        if let Some(data) = get_array_buffer_data(&buffer_obj) {
            let byte_length = if args.len() > 2 {
                args[2].get_int() as usize
            } else {
                data.data.len().saturating_sub(byte_offset)
            };
            obj.set(
                ctx.intern("byteLength"),
                JSValue::new_int(byte_length as i64),
            );
        }
    }

    let ptr = Box::into_raw(Box::new(obj)) as usize;
    JSValue::new_object(ptr)
}

fn int8_array_constructor(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    typed_array_from_args(ctx, TypedArrayKind::Int8, args).unwrap_or(JSValue::undefined())
}

fn uint8_array_constructor(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    typed_array_from_args(ctx, TypedArrayKind::Uint8, args).unwrap_or(JSValue::undefined())
}

fn uint8_clamped_array_constructor(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    typed_array_from_args(ctx, TypedArrayKind::Uint8Clamped, args).unwrap_or(JSValue::undefined())
}

fn int16_array_constructor(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    typed_array_from_args(ctx, TypedArrayKind::Int16, args).unwrap_or(JSValue::undefined())
}

fn uint16_array_constructor(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    typed_array_from_args(ctx, TypedArrayKind::Uint16, args).unwrap_or(JSValue::undefined())
}

fn int32_array_constructor(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    typed_array_from_args(ctx, TypedArrayKind::Int32, args).unwrap_or(JSValue::undefined())
}

fn uint32_array_constructor(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    typed_array_from_args(ctx, TypedArrayKind::Uint32, args).unwrap_or(JSValue::undefined())
}

fn float32_array_constructor(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    typed_array_from_args(ctx, TypedArrayKind::Float32, args).unwrap_or(JSValue::undefined())
}

fn float64_array_constructor(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    typed_array_from_args(ctx, TypedArrayKind::Float64, args).unwrap_or(JSValue::undefined())
}

fn bigint64_array_constructor(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    typed_array_from_args(ctx, TypedArrayKind::BigInt64, args).unwrap_or(JSValue::undefined())
}

fn biguint64_array_constructor(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    typed_array_from_args(ctx, TypedArrayKind::BigUint64, args).unwrap_or(JSValue::undefined())
}

fn typed_array_get_buffer(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() || !args[0].is_object() {
        return JSValue::undefined();
    }
    let obj = args[0].as_object();
    obj.get(ctx.intern("buffer"))
        .unwrap_or(JSValue::undefined())
}

fn typed_array_get_byte_length(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() || !args[0].is_object() {
        return JSValue::undefined();
    }
    let obj = args[0].as_object();
    obj.get(ctx.intern("byteLength"))
        .unwrap_or(JSValue::undefined())
}

fn typed_array_get_byte_offset(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() || !args[0].is_object() {
        return JSValue::undefined();
    }
    let obj = args[0].as_object();
    obj.get(ctx.intern("byteOffset"))
        .unwrap_or(JSValue::undefined())
}

fn typed_array_get_length(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() || !args[0].is_object() {
        return JSValue::undefined();
    }
    let obj = args[0].as_object();
    obj.get(ctx.intern("length"))
        .unwrap_or(JSValue::undefined())
}

fn array_buffer_get_byte_length(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() || !args[0].is_object() {
        return JSValue::undefined();
    }
    let obj = args[0].as_object();
    obj.get(ctx.intern("byteLength"))
        .unwrap_or(JSValue::undefined())
}

fn data_view_get_buffer(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() || !args[0].is_object() {
        return JSValue::undefined();
    }
    let obj = args[0].as_object();
    obj.get(ctx.intern("buffer"))
        .unwrap_or(JSValue::undefined())
}

fn data_view_get_byte_length(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() || !args[0].is_object() {
        return JSValue::undefined();
    }
    let obj = args[0].as_object();
    obj.get(ctx.intern("byteLength"))
        .unwrap_or(JSValue::undefined())
}

fn data_view_get_byte_offset(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.is_empty() || !args[0].is_object() {
        return JSValue::undefined();
    }
    let obj = args[0].as_object();
    obj.get(ctx.intern("byteOffset"))
        .unwrap_or(JSValue::undefined())
}

fn data_view_get_int8(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.len() < 2 {
        return JSValue::undefined();
    }
    let view = &args[0];
    let offset = args[1].get_int() as usize;

    if !view.is_object() {
        return JSValue::undefined();
    }

    let view_obj = view.as_object();
    let buffer = view_obj.get(ctx.intern("buffer"));
    let byte_offset = view_obj
        .get(ctx.intern("byteOffset"))
        .map(|v| v.get_int() as usize)
        .unwrap_or(0);

    if let Some(buf) = buffer {
        if let Some(buf_obj) = if buf.is_object() {
            Some(buf.as_object())
        } else {
            None
        } {
            if let Some(data) = get_array_buffer_data(&buf_obj) {
                let idx = byte_offset + offset;
                if idx < data.data.len() {
                    return JSValue::new_int(data.data[idx] as i8 as i64);
                }
            }
        }
    }
    JSValue::undefined()
}

fn data_view_get_uint8(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.len() < 2 {
        return JSValue::undefined();
    }
    let view = &args[0];
    let offset = args[1].get_int() as usize;

    if !view.is_object() {
        return JSValue::undefined();
    }

    let view_obj = view.as_object();
    let buffer = view_obj.get(ctx.intern("buffer"));
    let byte_offset = view_obj
        .get(ctx.intern("byteOffset"))
        .map(|v| v.get_int() as usize)
        .unwrap_or(0);

    if let Some(buf) = buffer {
        if let Some(buf_obj) = if buf.is_object() {
            Some(buf.as_object())
        } else {
            None
        } {
            if let Some(data) = get_array_buffer_data(&buf_obj) {
                let idx = byte_offset + offset;
                if idx < data.data.len() {
                    return JSValue::new_int(data.data[idx] as i64);
                }
            }
        }
    }
    JSValue::undefined()
}

fn data_view_set_int8(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.len() < 3 {
        return JSValue::undefined();
    }
    let view = &args[0];
    let offset = args[1].get_int() as usize;
    let value = args[2].get_int() as i8 as u8;

    if !view.is_object() {
        return JSValue::undefined();
    }

    let view_obj = view.as_object();
    let buffer = view_obj.get(ctx.intern("buffer"));
    let byte_offset = view_obj
        .get(ctx.intern("byteOffset"))
        .map(|v| v.get_int() as usize)
        .unwrap_or(0);

    if let Some(buf) = buffer {
        if let Some(buf_obj) = if buf.is_object() {
            Some(buf.as_object())
        } else {
            None
        } {
            if let Some(data) = get_array_buffer_data(&buf_obj) {
                let idx = byte_offset + offset;
                if idx < data.data.len() {
                    data.data[idx] = value;
                }
            }
        }
    }
    JSValue::undefined()
}

fn data_view_set_uint8(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
    if args.len() < 3 {
        return JSValue::undefined();
    }
    let view = &args[0];
    let offset = args[1].get_int() as usize;
    let value = args[2].get_int() as u8;

    if !view.is_object() {
        return JSValue::undefined();
    }

    let view_obj = view.as_object();
    let buffer = view_obj.get(ctx.intern("buffer"));
    let byte_offset = view_obj
        .get(ctx.intern("byteOffset"))
        .map(|v| v.get_int() as usize)
        .unwrap_or(0);

    if let Some(buf) = buffer {
        if let Some(buf_obj) = if buf.is_object() {
            Some(buf.as_object())
        } else {
            None
        } {
            if let Some(data) = get_array_buffer_data(&buf_obj) {
                let idx = byte_offset + offset;
                if idx < data.data.len() {
                    data.data[idx] = value;
                }
            }
        }
    }
    JSValue::undefined()
}

fn create_builtin_function(ctx: &mut JSContext, name: &str) -> JSValue {
    let mut func = crate::object::function::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_typed_array(ctx: &mut JSContext) {
    let global = ctx.global();
    if !global.is_object() {
        return;
    }
    let global_obj = global.as_object_mut();

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

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

    global_obj.set(
        ctx.intern("Int8Array"),
        create_builtin_function(ctx, "Int8Array"),
    );
    global_obj.set(
        ctx.intern("Uint8Array"),
        create_builtin_function(ctx, "Uint8Array"),
    );
    global_obj.set(
        ctx.intern("Uint8ClampedArray"),
        create_builtin_function(ctx, "Uint8ClampedArray"),
    );
    global_obj.set(
        ctx.intern("Int16Array"),
        create_builtin_function(ctx, "Int16Array"),
    );
    global_obj.set(
        ctx.intern("Uint16Array"),
        create_builtin_function(ctx, "Uint16Array"),
    );
    global_obj.set(
        ctx.intern("Int32Array"),
        create_builtin_function(ctx, "Int32Array"),
    );
    global_obj.set(
        ctx.intern("Uint32Array"),
        create_builtin_function(ctx, "Uint32Array"),
    );
    global_obj.set(
        ctx.intern("Float32Array"),
        create_builtin_function(ctx, "Float32Array"),
    );
    global_obj.set(
        ctx.intern("Float64Array"),
        create_builtin_function(ctx, "Float64Array"),
    );
    global_obj.set(
        ctx.intern("BigInt64Array"),
        create_builtin_function(ctx, "BigInt64Array"),
    );
    global_obj.set(
        ctx.intern("BigUint64Array"),
        create_builtin_function(ctx, "BigUint64Array"),
    );
}

pub fn register_builtins(ctx: &mut JSContext) {
    ctx.register_builtin(
        "ArrayBuffer",
        HostFunction::new("ArrayBuffer", 1, array_buffer_constructor),
    );
    ctx.register_builtin(
        "DataView",
        HostFunction::new("DataView", 1, data_view_constructor),
    );

    ctx.register_builtin(
        "Int8Array",
        HostFunction::new("Int8Array", 1, int8_array_constructor),
    );
    ctx.register_builtin(
        "Uint8Array",
        HostFunction::new("Uint8Array", 1, uint8_array_constructor),
    );
    ctx.register_builtin(
        "Uint8ClampedArray",
        HostFunction::new("Uint8ClampedArray", 1, uint8_clamped_array_constructor),
    );
    ctx.register_builtin(
        "Int16Array",
        HostFunction::new("Int16Array", 1, int16_array_constructor),
    );
    ctx.register_builtin(
        "Uint16Array",
        HostFunction::new("Uint16Array", 1, uint16_array_constructor),
    );
    ctx.register_builtin(
        "Int32Array",
        HostFunction::new("Int32Array", 1, int32_array_constructor),
    );
    ctx.register_builtin(
        "Uint32Array",
        HostFunction::new("Uint32Array", 1, uint32_array_constructor),
    );
    ctx.register_builtin(
        "Float32Array",
        HostFunction::new("Float32Array", 1, float32_array_constructor),
    );
    ctx.register_builtin(
        "Float64Array",
        HostFunction::new("Float64Array", 1, float64_array_constructor),
    );
    ctx.register_builtin(
        "BigInt64Array",
        HostFunction::new("BigInt64Array", 1, bigint64_array_constructor),
    );
    ctx.register_builtin(
        "BigUint64Array",
        HostFunction::new("BigUint64Array", 1, biguint64_array_constructor),
    );

    ctx.register_builtin(
        "typedarray_buffer",
        HostFunction::new("buffer", 0, typed_array_get_buffer),
    );
    ctx.register_builtin(
        "typedarray_byteLength",
        HostFunction::new("byteLength", 0, typed_array_get_byte_length),
    );
    ctx.register_builtin(
        "typedarray_byteOffset",
        HostFunction::new("byteOffset", 0, typed_array_get_byte_offset),
    );
    ctx.register_builtin(
        "typedarray_length",
        HostFunction::new("length", 0, typed_array_get_length),
    );

    ctx.register_builtin(
        "arraybuffer_byteLength",
        HostFunction::new("byteLength", 0, array_buffer_get_byte_length),
    );

    ctx.register_builtin(
        "dataview_buffer",
        HostFunction::new("buffer", 0, data_view_get_buffer),
    );
    ctx.register_builtin(
        "dataview_byteLength",
        HostFunction::new("byteLength", 0, data_view_get_byte_length),
    );
    ctx.register_builtin(
        "dataview_byteOffset",
        HostFunction::new("byteOffset", 0, data_view_get_byte_offset),
    );
    ctx.register_builtin(
        "dataview_getInt8",
        HostFunction::new("getInt8", 1, data_view_get_int8),
    );
    ctx.register_builtin(
        "dataview_getUint8",
        HostFunction::new("getUint8", 1, data_view_get_uint8),
    );
    ctx.register_builtin(
        "dataview_setInt8",
        HostFunction::new("setInt8", 2, data_view_set_int8),
    );
    ctx.register_builtin(
        "dataview_setUint8",
        HostFunction::new("setUint8", 2, data_view_set_uint8),
    );
}
