#![allow(clippy::empty_docs)] // TODO: remove it later. false positive by wasm-bindgen generated code

use crate::js_module;
use crate::vm_class::{WASMVirtualMachine, stored_vm_from_wasm};
use js_sys::{Array, ArrayBuffer, Object, Promise, Reflect, SyntaxError, Uint8Array};
use rustpython_vm::{
    AsObject, Py, PyObjectRef, PyPayload, PyResult, TryFromBorrowedObject, VirtualMachine,
    builtins::{PyBaseException, PyBaseExceptionRef},
    compiler::{CompileError, ParseError, parser::LexicalErrorType, parser::ParseErrorType},
    exceptions,
    function::{ArgBytesLike, FuncArgs},
    py_serde,
};
use wasm_bindgen::{JsCast, closure::Closure, prelude::*};

#[wasm_bindgen(inline_js = r"
export class PyError extends Error {
    constructor(info) {
        const msg = info.args[0];
        if (typeof msg === 'string') super(msg);
        else super();
        this.info = info;
    }
    get name() { return this.info.exc_type; }
    get traceback() { return this.info.traceback; }
    toString() { return this.info.rendered; }
}
")]
extern "C" {
    pub type PyError;
    #[wasm_bindgen(constructor)]
    fn new(info: JsValue) -> PyError;
}

pub fn py_err_to_js_err(vm: &VirtualMachine, py_err: &Py<PyBaseException>) -> JsValue {
    let js_err = vm.try_class("_js", "JSError").ok();
    let js_arg = if js_err.is_some_and(|js_err| py_err.fast_isinstance(&js_err)) {
        py_err.get_arg(0)
    } else {
        None
    };
    let js_arg = js_arg
        .as_ref()
        .and_then(|x| x.downcast_ref::<js_module::PyJsValue>());
    match js_arg {
        Some(val) => val.value.clone(),
        None => {
            let res =
                serde_wasm_bindgen::to_value(&exceptions::SerializeException::new(vm, py_err));
            match res {
                Ok(err_info) => PyError::new(err_info).into(),
                Err(_) => {
                    // Fallback: create a basic JS Error with the exception type and message
                    let exc_type = py_err.class().name().to_string();
                    let msg = match py_err.as_object().str(vm) {
                        Ok(s) => format!("{exc_type}: {s}"),
                        Err(_) => exc_type,
                    };
                    js_sys::Error::new(&msg).into()
                }
            }
        }
    }
}

pub fn js_py_typeerror(vm: &VirtualMachine, js_err: JsValue) -> PyBaseExceptionRef {
    let msg: String = js_err.unchecked_into::<js_sys::Error>().to_string().into();
    vm.new_type_error(msg)
}

pub fn js_err_to_py_err(vm: &VirtualMachine, js_err: &JsValue) -> PyBaseExceptionRef {
    match js_err.dyn_ref::<js_sys::Error>() {
        Some(err) => {
            let exc_type = match String::from(err.name()).as_str() {
                "TypeError" => vm.ctx.exceptions.type_error,
                "ReferenceError" => vm.ctx.exceptions.name_error,
                "SyntaxError" => vm.ctx.exceptions.syntax_error,
                _ => vm.ctx.exceptions.exception_type,
            }
            .to_owned();
            vm.new_exception_msg(exc_type, String::from(err.message()).into())
        }
        None => vm.new_exception_msg(
            vm.ctx.exceptions.exception_type.to_owned(),
            format!("{js_err:?}").into(),
        ),
    }
}

pub fn py_to_js(vm: &VirtualMachine, py_obj: PyObjectRef) -> JsValue {
    if let Some(ref wasm_id) = vm.wasm_id
        && py_obj.fast_isinstance(vm.ctx.types.function_type)
    {
        let wasm_vm = WASMVirtualMachine {
            id: wasm_id.clone(),
        };
        let weak_py_obj = wasm_vm.push_held_rc(py_obj).unwrap().unwrap();

        let closure = move |args: Option<Box<[JsValue]>>,
                            kwargs: Option<Object>|
              -> Result<JsValue, JsValue> {
            let py_obj = match wasm_vm.assert_valid() {
                Ok(_) => weak_py_obj
                    .upgrade()
                    .expect("weak_py_obj to be valid if VM is valid"),
                Err(err) => {
                    return Err(err);
                }
            };
            stored_vm_from_wasm(&wasm_vm).interp.enter(move |vm| {
                let args = match args {
                    Some(args) => Vec::from(args)
                        .into_iter()
                        .map(|arg| js_to_py(vm, arg))
                        .collect::<Vec<_>>(),
                    None => Vec::new(),
                };
                let mut py_func_args = FuncArgs::from(args);
                if let Some(ref kwargs) = kwargs {
                    for pair in object_entries(kwargs) {
                        let (key, val) = pair?;
                        py_func_args.kwargs.insert(
                            // JS strings coming in are UTF-16; go through Rust `String`
                            // (kwargs keys are now WTF-8, so convert String -> Wtf8Buf).
                            String::from(js_sys::JsString::from(key)).into(),
                            js_to_py(vm, val),
                        );
                    }
                }
                let result = py_obj.call(py_func_args, vm);
                pyresult_to_js_result(vm, result)
            })
        };
        let closure = Closure::wrap(Box::new(closure)
            as Box<
                dyn FnMut(Option<Box<[JsValue]>>, Option<Object>) -> Result<JsValue, JsValue>,
            >);
        let func = closure.as_ref().clone();

        // stores pretty much nothing, it's fine to leak this because if it gets dropped
        // the error message is worse
        closure.forget();

        return func;
    }
    // the browser module might not be injected
    if vm.try_class("_js", "Promise").is_ok()
        && let Some(py_prom) = py_obj.downcast_ref::<js_module::PyPromise>()
    {
        return py_prom.as_js(vm).into();
    }

    if let Ok(bytes) = ArgBytesLike::try_from_borrowed_object(vm, &py_obj) {
        bytes.with_ref(|bytes| unsafe {
            // `Uint8Array::view` is an `unsafe fn` because it provides
            // a direct view into the WASM linear memory; if you were to allocate
            // something with Rust that view would probably become invalid. It's safe
            // because we then copy the array using `Uint8Array::slice`.
            let view = Uint8Array::view(bytes);
            view.slice(0, bytes.len() as u32).into()
        })
    } else {
        py_serde::serialize(vm, &py_obj, &serde_wasm_bindgen::Serializer::new())
            .unwrap_or(JsValue::UNDEFINED)
    }
}

pub fn object_entries(obj: &Object) -> impl Iterator<Item = Result<(JsValue, JsValue), JsValue>> {
    Object::entries(obj).values().into_iter().map(|pair| {
        pair.map(|pair| {
            let key = Reflect::get(&pair, &"0".into()).unwrap();
            let val = Reflect::get(&pair, &"1".into()).unwrap();
            (key, val)
        })
    })
}

pub fn pyresult_to_js_result(vm: &VirtualMachine, result: PyResult) -> Result<JsValue, JsValue> {
    result
        .map(|value| py_to_js(vm, value))
        .map_err(|err| py_err_to_js_err(vm, &err))
}

pub fn js_to_py(vm: &VirtualMachine, js_val: JsValue) -> PyObjectRef {
    if js_val.is_object() {
        if let Some(promise) = js_val.dyn_ref::<Promise>() {
            // the browser module might not be injected
            if vm.try_class("browser", "Promise").is_ok() {
                return js_module::PyPromise::new(promise.clone())
                    .into_ref(&vm.ctx)
                    .into();
            }
        }
        if Array::is_array(&js_val) {
            let js_arr: Array = js_val.into();
            let elems = js_arr
                .values()
                .into_iter()
                .map(|val| js_to_py(vm, val.expect("Iteration over array failed")))
                .collect();
            vm.ctx.new_list(elems).into()
        } else if ArrayBuffer::is_view(&js_val) || js_val.is_instance_of::<ArrayBuffer>() {
            // unchecked_ref because if it's not an ArrayBuffer it could either be a TypedArray
            // or a DataView, but they all have a `buffer` property
            let u8_array = js_sys::Uint8Array::new(
                &js_val
                    .dyn_ref::<ArrayBuffer>()
                    .cloned()
                    .unwrap_or_else(|| js_val.unchecked_ref::<Uint8Array>().buffer()),
            );
            let mut vec = vec![0; u8_array.length() as usize];
            u8_array.copy_to(&mut vec);
            vm.ctx.new_bytes(vec).into()
        } else {
            let dict = vm.ctx.new_dict();
            for pair in object_entries(&Object::from(js_val)) {
                let (key, val) = pair.expect("iteration over object to not fail");
                let py_val = js_to_py(vm, val);
                dict.set_item(
                    String::from(js_sys::JsString::from(key)).as_str(),
                    py_val,
                    vm,
                )
                .unwrap();
            }
            dict.into()
        }
    } else if js_val.is_function() {
        let func = js_sys::Function::from(js_val);
        vm.new_function(
            vm.ctx.intern_str(String::from(func.name())).as_str(),
            move |args: FuncArgs, vm: &VirtualMachine| -> PyResult {
                let this = Object::new();
                for (k, v) in args.kwargs {
                    // WTF-8 -> JS string: lone surrogates in the key become U+FFFD
                    // (wasm-bindgen only accepts Rust `String`); acceptable at this boundary.
                    Reflect::set(&this, &k.to_string().into(), &py_to_js(vm, v))
                        .expect("property to be settable");
                }
                let js_args = args
                    .args
                    .into_iter()
                    .map(|v| py_to_js(vm, v))
                    .collect::<Array>();
                func.apply(&this, &js_args)
                    .map(|val| js_to_py(vm, val))
                    .map_err(|err| js_err_to_py_err(vm, &err))
            },
        )
        .into()
    } else if let Some(err) = js_val.dyn_ref::<js_sys::Error>() {
        js_err_to_py_err(vm, err).into()
    } else if js_val.is_undefined() {
        // Because `JSON.stringify(undefined)` returns undefined
        vm.ctx.none()
    } else {
        py_serde::deserialize(vm, serde_wasm_bindgen::Deserializer::from(js_val))
            .unwrap_or_else(|_| vm.ctx.none())
    }
}

#[must_use]
pub fn syntax_err(err: CompileError) -> SyntaxError {
    let js_err = SyntaxError::new(&format!("Error parsing Python code: {err}"));
    let _ = Reflect::set(
        &js_err,
        &"row".into(),
        &(err.location().unwrap().line.get()).into(),
    );
    let _ = Reflect::set(
        &js_err,
        &"col".into(),
        &(err.location().unwrap().character_offset.get()).into(),
    );
    // | ParseErrorType::UnrecognizedToken(Token::Dedent, _)
    let can_continue = matches!(
        &err,
        CompileError::Parse(ParseError {
            error: ParseErrorType::Lexical(
                LexicalErrorType::Eof | LexicalErrorType::IndentationError
            ),
            ..
        })
    );
    let _ = Reflect::set(&js_err, &"canContinue".into(), &can_continue.into());
    js_err
}

pub trait PyResultExt<T> {
    fn into_js(self, vm: &VirtualMachine) -> Result<T, JsValue>;
}
impl<T> PyResultExt<T> for PyResult<T> {
    fn into_js(self, vm: &VirtualMachine) -> Result<T, JsValue> {
        self.map_err(|err| py_err_to_js_err(vm, &err))
    }
}
