# cython: language_level = 3, freethreading_compatible = True
import contextlib
import os
import traceback
from cpython.object cimport PyObject_Str
from cpython.unicode cimport PyUnicode_Check, PyUnicode_AsUTF8String, PyUnicode_FromString, PyUnicode_FromFormat
from cpython.exc cimport PyErr_SetNone
from libc.stdint cimport uint8_t
from libc.stdio cimport FILE, fflush, fprintf, fwrite, stdin, stderr, stdout

cdef extern from "Python.h":
    unsigned long PyThread_get_thread_ident()


DEF DUK_TYPE_NONE = 0
DEF DUK_TYPE_UNDEFINED = 1
DEF DUK_TYPE_NULL = 2
DEF DUK_TYPE_BOOLEAN = 3
DEF DUK_TYPE_NUMBER = 4
DEF DUK_TYPE_STRING = 5
DEF DUK_TYPE_OBJECT = 6
DEF DUK_TYPE_BUFFER = 7
DEF DUK_TYPE_POINTER = 8
DEF DUK_TYPE_LIGHTFUNC = 9

DEF DUK_ENUM_OWN_PROPERTIES_ONLY = (1 << 2)

DEF DUK_ERR_ERROR = 100

# Simillar techniques to what aiohttp _http_writer & _http_parser do...
cdef object os_path_isabs = os.path.isabs

# We should really be taking Python's memory and not outside memory
# so that were able to get Better performance and recycled resources 
# as a result.
cdef extern from *:
    """
#define DUK_ANSI_MALLOC PyMem_RawMalloc
#define DUK_ANSI_REALLOC PyMem_RawRealloc
#define DUK_ANSI_FREE PyMem_RawFree 
    """

# TODO: Reseperate into a seperate file to enhance readability.

# NOTE: duktape.h is located in pyduktape3 module since 
# pyduktape3 can interlope & support cython compilations 
cdef extern from 'duktape.h':
    ctypedef struct duk_context:
        pass

    ctypedef int duk_errcode_t
    ctypedef int duk_int_t
    ctypedef size_t duk_size_t
    ctypedef duk_int_t duk_idx_t
    ctypedef int duk_bool_t
    ctypedef unsigned int duk_uint_t
    ctypedef unsigned int duk_uarridx_t
    ctypedef double duk_double_t
    ctypedef int duk_ret_t

    ctypedef void* (*duk_alloc_function) (void *udata, duk_size_t size)
    ctypedef void* (*duk_realloc_function) (void *udata, void *ptr, duk_size_t size)
    ctypedef void (*duk_free_function) (void *udata, void *ptr)
    ctypedef void (*duk_fatal_function) (duk_context *ctx, duk_errcode_t code, const char *msg)
    ctypedef duk_ret_t (*duk_c_function)(duk_context *ctx)
    ctypedef duk_ret_t (*duk_safe_call_function) (duk_context *ctx, void *udata)

    cdef duk_context* duk_create_heap(duk_alloc_function alloc_func, duk_realloc_function realloc_func, duk_free_function free_func, void *heap_udata, duk_fatal_function fatal_handler)
    cdef duk_context* duk_create_heap_default()
    cdef void duk_destroy_heap(duk_context *context)
    cdef duk_int_t duk_peval_string(duk_context *context, const char *source)
    cdef const char* duk_safe_to_string(duk_context *ctx, duk_idx_t index)
    cdef void duk_pop(duk_context *ctx)

    cdef duk_bool_t duk_get_boolean(duk_context *ctx, duk_idx_t index)
    cdef const char* duk_get_string(duk_context *ctx, duk_idx_t index)
    cdef double duk_get_number(duk_context *ctx, duk_idx_t index)
    cdef int duk_get_type(duk_context *ctx, duk_idx_t index)
    cdef void duk_enum(duk_context *ctx, duk_idx_t obj_index, duk_uint_t enum_flags)
    cdef duk_bool_t duk_next(duk_context *ctx, duk_idx_t enum_index, duk_bool_t get_value)
    cdef duk_bool_t duk_get_prop_string(duk_context *ctx, duk_idx_t obj_index, const char *key)
    cdef duk_bool_t duk_get_prop_index(duk_context *ctx, duk_idx_t obj_index, duk_uarridx_t arr_index)
    cdef duk_bool_t duk_is_array(duk_context *ctx, duk_idx_t index)
    cdef duk_int_t duk_get_int(duk_context *ctx, duk_idx_t index)
    cdef void duk_push_undefined(duk_context *ctx)
    cdef void duk_push_null(duk_context *ctx)
    cdef void duk_push_boolean(duk_context *ctx, duk_bool_t value)
    cdef duk_bool_t duk_put_prop(duk_context *ctx, duk_idx_t obj_index)
    cdef duk_idx_t duk_push_object(duk_context *ctx)
    cdef duk_bool_t duk_put_prop_index(duk_context *ctx, duk_idx_t obj_index, duk_uarridx_t arr_index)
    cdef duk_idx_t duk_push_array(duk_context *ctx)
    cdef const char *duk_push_string(duk_context *ctx, const char *str)
    cdef void duk_push_number(duk_context *ctx, duk_double_t val)
    cdef void duk_push_int(duk_context *ctx, duk_int_t val)
    cdef duk_bool_t duk_put_global_string(duk_context *ctx, const char *key)
    cdef duk_bool_t duk_get_global_string(duk_context *ctx, const char *key)
    cdef void duk_push_current_function(duk_context *ctx)
    cdef duk_idx_t duk_get_top(duk_context *ctx)
    cdef duk_bool_t duk_put_prop_string(duk_context *ctx, duk_idx_t obj_index, const char *key)
    cdef duk_idx_t duk_push_c_function(duk_context *ctx, duk_c_function func, duk_idx_t nargs)
    cdef duk_bool_t duk_is_constructor_call(duk_context *ctx)
    cdef void duk_pop_2(duk_context *ctx)
    cdef void duk_error(duk_context *ctx, duk_errcode_t err_code, const char *fmt, ...)
    cdef const char *duk_require_string(duk_context *ctx, duk_idx_t index)
    cdef duk_ret_t duk_pcall(duk_context *ctx, duk_idx_t nargs)
    cdef duk_int_t duk_pcall_method(duk_context *ctx, duk_idx_t nargs)
    cdef duk_bool_t duk_is_object(duk_context *ctx, duk_idx_t index)
    cdef void duk_push_global_stash(duk_context *ctx)
    cdef void duk_dup(duk_context *ctx, duk_idx_t from_index)
    cdef duk_bool_t duk_has_prop_index(duk_context *ctx, duk_idx_t obj_index, duk_uarridx_t arr_index)
    cdef duk_bool_t duk_del_prop_index(duk_context *ctx, duk_idx_t obj_index, duk_uarridx_t arr_index)
    cdef duk_bool_t duk_is_callable(duk_context *ctx, duk_idx_t index)
    cdef void duk_push_pointer(duk_context *ctx, void *p)
    cdef void *duk_get_pointer(duk_context *ctx, duk_idx_t index)
    cdef duk_bool_t duk_is_pointer(duk_context *ctx, duk_idx_t index)
    cdef duk_int_t duk_safe_call(duk_context *ctx, duk_safe_call_function func, void *udata, duk_idx_t nargs, duk_idx_t nrets)
    cdef void duk_new(duk_context *ctx, duk_idx_t nargs)
    cdef duk_int_t duk_require_int(duk_context *ctx, duk_idx_t index)
    cdef void duk_swap(duk_context *ctx, duk_idx_t index1, duk_idx_t index2)
    cdef void duk_dump_context_stdout(duk_context *ctx)
    cdef void duk_set_finalizer(duk_context *ctx, duk_idx_t index)
    cdef void *duk_get_heapptr(duk_context *ctx, duk_idx_t index)
    cdef void duk_push_this(duk_context *ctx)

    # New
    duk_bool_t duk_is_buffer(duk_context *thr, duk_idx_t idx)
    void *duk_get_buffer(duk_context *ctx, duk_idx_t idx, duk_size_t *out_size)
    void duk_join(duk_context *ctx, duk_idx_t count)
    void duk_insert(duk_context *ctx, duk_idx_t to_idx)
    const char *duk_to_string(duk_context *ctx, duk_idx_t idx)

    void duk_push_global_object(duk_context *ctx)
    # Flags for duk_def_prop() and its variants; base flags + a lot of convenience shorthands */
    int DUK_DEFPROP_WRITABLE              # (1U << 0)    /* set writable (effective if DUK_DEFPROP_HAVE_WRITABLE set) */
    int DUK_DEFPROP_ENUMERABLE            # (1U << 1)    /* set enumerable (effective if DUK_DEFPROP_HAVE_ENUMERABLE set) */
    int DUK_DEFPROP_CONFIGURABLE          # (1U << 2)    /* set configurable (effective if DUK_DEFPROP_HAVE_CONFIGURABLE set) */
    int DUK_DEFPROP_RESERVED3             # (1U << 3)    /* INTERNAL: reserved, internally accessor flag */
    int DUK_DEFPROP_RESERVED4             # (1U << 4)    /* INTERNAL: reserved */
    int DUK_DEFPROP_RESERVED5             # (1U << 5)    /* INTERNAL: reserved */
    int DUK_DEFPROP_RESERVED6             # (1U << 6)    /* INTERNAL: reserved */
    int DUK_DEFPROP_RESERVED7             # (1U << 7)    /* INTERNAL: reserved */
    int DUK_DEFPROP_HAVE_SHIFT_COUNT      # 8            /* INTERNAL */
    int DUK_DEFPROP_HAVE_WRITABLE         # (1U << 8)    /* set/clear writable */
    int DUK_DEFPROP_HAVE_ENUMERABLE       # (1U << 9)    /* set/clear enumerable */
    int DUK_DEFPROP_HAVE_CONFIGURABLE     # (1U << 10)   /* set/clear configurable */
    int DUK_DEFPROP_HAVE_VALUE            # (1U << 11)   /* set value (given on value stack) */
    int DUK_DEFPROP_HAVE_GETTER           # (1U << 12)   /* set getter (given on value stack) */
    int DUK_DEFPROP_HAVE_SETTER           # (1U << 13)   /* set setter (given on value stack) */
    int DUK_DEFPROP_FORCE                 # (1U << 14)   /* force change if possible, may still fail for e.g. virtual properties */
    int DUK_DEFPROP_THROW                 # (1U << 15)   /* INTERNAL: throw on errors */
    int DUK_DEFPROP_SET_WRITABLE
    int DUK_DEFPROP_SET_CONFIGURABLE
    int DUK_VARARGS
    void duk_def_prop(duk_context *ctx, duk_idx_t obj_idx, duk_uint_t flags)

# Kept around for backwards compatability
cdef extern from 'duk_module_duktape.h':
    ctypedef struct duk_context:
        pass

    cdef void duk_module_duktape_init(duk_context *ctx)


# cdef extern from 'vendor/duk_print_alert.c':
#     ctypedef struct duk_context:
#         pass

#     cdef void duk_print_alert_init(duk_context *ctx, duk_uint_t flags)


# Based off duktape example api_testcase_main.c that introduces a new method for printing out alerts
# We could always go more pythonic or allow it to have callbacks or be optionally ignored future update...
cdef duk_ret_t runtests_print_alert_helper(duk_context *ctx, FILE *fh):
    cdef duk_idx_t nargs
    cdef uint8_t *buf
    cdef duk_size_t sz_buf
    
    nargs = duk_get_top(ctx)
    if (nargs == 1 and duk_is_buffer(ctx, 0)):
        buf = <uint8_t*>duk_get_buffer(ctx, 0, &sz_buf)
        fwrite(buf, 1, <size_t> sz_buf, fh)
    else:
        duk_push_string(ctx, " ")
        duk_insert(ctx, 0)
        duk_join(ctx, nargs)
        fprintf(fh, "%s\n", duk_to_string(ctx, -1))
    fflush(fh)
    return 0

cdef duk_ret_t runtests_print(duk_context *ctx) noexcept:
    return runtests_print_alert_helper(ctx, stdout)

cdef duk_ret_t runtests_alert(duk_context *ctx) noexcept:
    return runtests_print_alert_helper(ctx, stderr)






# NOTE: It's better that these be C-Extensions for increased performance reasons...
cdef class DuktapeError(Exception):
    pass


cdef class DuktapeThreadError(DuktapeError):
    pass


cdef class JSError(Exception):
    pass


cdef class DuktapeContext(object):
    

    def __cinit__(
        self, 
        bint disable_alterts=False,
        bint disable_modules=False
    ):
        """
        Initalizes a DuktapeContext

        :param disable_alterts: disables alterts such as alert() and print() respectively...
        :param disable_modules: disables the require function 
            Good for simulating html5 javascript where this should be disabled.
            This is bad if you need modules. It's best to leave this false 
            if this is the case.
        
        :raises DuktapeError: if the `duk_create_heap_default()` failes to create a C context
        """

        self.thread_id = PyThread_get_thread_ident()
        self.js_base_path = ''
        self.next_ref_index = -1

        self.registered_objects = {}
        self.registered_proxies = {}
        self.registered_proxies_reverse = {}

        # We should really be using python's memory so that way
        # the garbage collector can recyle it if needed.
        self.ctx = duk_create_heap_default()
        if self.ctx == NULL:
            raise DuktapeError('Can\'t allocate context')

        set_python_context(self.ctx, self)
        
        if not disable_modules:
            duk_module_duktape_init(self.ctx)

        if not disable_alterts:
            self._init_alerts_backwards_compat()

        self._setup_module_search_function()

    cdef void _init_alerts_backwards_compat(self):
        # alerts were Removed in 3.0
        # This at least Creates backwards compatability for print and alert statements
        duk_push_global_object(self.ctx)
        duk_push_string(self.ctx, "print")
        duk_push_c_function(self.ctx, runtests_print, DUK_VARARGS)
        duk_def_prop(self.ctx, -3, DUK_DEFPROP_HAVE_VALUE | DUK_DEFPROP_SET_WRITABLE | DUK_DEFPROP_SET_CONFIGURABLE)
        duk_push_string(self.ctx, "alert")
        duk_push_c_function(self.ctx, runtests_alert, DUK_VARARGS)
        duk_def_prop(self.ctx, -3, DUK_DEFPROP_HAVE_VALUE | DUK_DEFPROP_SET_WRITABLE | DUK_DEFPROP_SET_CONFIGURABLE)
        duk_pop(self.ctx)

    cdef void _setup_module_search_function(self):
        
        duk_get_global_string(self.ctx, b"Duktape")
        duk_push_c_function(self.ctx, module_search, 1)
        duk_put_prop_string(self.ctx, -2, b"modSearch")
        duk_pop(self.ctx)

    
    def set_globals(self, **kwargs):
        if self._check_thread() < 0:
            raise

        for name, value in kwargs.items():
            self._set_global(PyUnicode_AsUTF8String(name), value)

    cdef void _set_global(self, const char *name, object value) except *:
        to_js(self.ctx, value)
        duk_put_global_string(self.ctx, name)

    def get_global(self, name):
        # TODO: (Vizonex) allow bytes and custom buffers to be utlized
        if not PyUnicode_Check(name):
            raise TypeError('Global variable name must be a string, {} found'.format(type(name)))

        duk_get_global_string(self.ctx, PyUnicode_AsUTF8String(name))
        try:
            value = to_python(self, -1)
        finally:
            duk_pop(self.ctx)

        return value

    def set_base_path(self, path):
        # TODO: Allow Pathlib objects?
        if not PyUnicode_Check(path):
            raise TypeError('Path must be a string, {} found'.format(type(path)))

        self.js_base_path = path

    def eval_js(self, object src):
        if isinstance(src, str):
            src = src.encode()

        # TODO: Enable buffer protocol instead so that memoryviews and custom buffers 
        # can be used
        if not isinstance(src, bytes):
            raise TypeError('Javascript source must be a string or bytes object')

        return self._eval_js(src)

    def eval_js_file(self, src_path):
        cdef object _src_path = PyObject_Str(src_path)
        with open(self.get_file_path(_src_path), 'rb') as f:
            code = f.read()

        return self._eval_js(code)

    def get_file_path(self, str src_path):
        if not src_path.endswith('.js'):
            src_path = '%s.js' % src_path

        if not os_path_isabs(src_path):
            src_path = os.path.join(self.js_base_path, src_path)

        return src_path

    cdef object _eval_js(self, bytes src):
        cdef:
            object error
            object result 
        if self._check_thread() < 0:
            raise

        if duk_peval_string(self.ctx, src) != 0:
            error = self.get_error()
            duk_pop(self.ctx)
            result = None
        else:
            error = None
            result = to_python(self, -1)
        duk_pop(self.ctx)

        if error:
            raise JSError(error)

        return result

    cdef object get_error(self):
        cdef object error
        if duk_get_prop_string(self.ctx, -1, b'stack') == 0:
            error = PyUnicode_FromString(duk_safe_to_string(self.ctx, -2))
        else:
            error = to_python(self, -1)

        return error

    cpdef JSRef make_jsref(self, duk_idx_t index):
        if self._check_thread() < 0:
            raise

        assert duk_is_object(self.ctx, index)

        self.next_ref_index += 1

        duk_push_global_stash(self.ctx)
        duk_dup(self.ctx, index - 1)
        duk_put_prop_index(self.ctx, -2, self.next_ref_index)
        duk_pop(self.ctx)

        return JSRef(self, self.next_ref_index)

    cdef void register_object(self, void *proxy_ptr, object py_obj):
        self.registered_objects[<unsigned long>proxy_ptr] = py_obj

    cdef object get_registered_object(self, void *proxy_ptr):
        return self.registered_objects[<unsigned long>proxy_ptr]

    cdef int is_registered_object(self, void *proxy_ptr):
        return <unsigned long>proxy_ptr in self.registered_objects

    cdef void unregister_object(self, void *proxy_ptr):
        del self.registered_objects[<unsigned long>proxy_ptr]

    cdef void register_proxy(self, void *proxy_ptr, void *target_ptr, object py_obj):
        self.registered_proxies[<unsigned long>proxy_ptr] = <unsigned long>target_ptr
        self.registered_proxies_reverse[<unsigned long>target_ptr] = <unsigned long>proxy_ptr
        self.register_object(target_ptr, py_obj)

    cdef object get_registered_object_from_proxy(self, void *proxy_ptr):
        return self.registered_objects[self.registered_proxies[<unsigned long>proxy_ptr]]

    cdef int is_registered_proxy(self, void *proxy_ptr):
        if <unsigned long>proxy_ptr not in self.registered_proxies:
            return 0

        return self.registered_proxies[<unsigned long>proxy_ptr] in self.registered_objects

    cdef void unregister_proxy_from_target(self, void *target_ptr):
        proxy_ptr = self.registered_proxies_reverse.pop(<unsigned long>target_ptr)
        del self.registered_objects[<unsigned long>target_ptr]
        del self.registered_proxies[proxy_ptr]

    def __dealloc__(self):
        duk_destroy_heap(self.ctx)


cdef void set_python_context(duk_context *ctx, DuktapeContext py_ctx):
    duk_push_global_stash(ctx)
    duk_push_pointer(ctx, <void*>py_ctx)
    duk_put_prop_string(ctx, -2, b"__py_ctx")
    duk_pop(ctx)


cdef DuktapeContext get_python_context(duk_context *ctx):
    duk_push_global_stash(ctx)
    duk_get_prop_string(ctx, -1, b"__py_ctx")
    py_ctx = <DuktapeContext>duk_get_pointer(ctx, -1)
    duk_pop_2(ctx)

    assert py_ctx.ctx is ctx

    return py_ctx


cdef class JSRef(object):

    def __init__(self, DuktapeContext py_ctx, int ref_index):
        py_ctx._check_thread()

        self.py_ctx = py_ctx
        self.ref_index = ref_index

    cpdef to_js(self):
        if self.py_ctx._check_thread() < 0:
            raise

        duk_push_global_stash(self.py_ctx.ctx)
        if duk_get_prop_index(self.py_ctx.ctx, -1, self.ref_index) == 0:
            duk_pop_2(self.py_ctx.ctx)
            raise DuktapeError('Invalid reference')
        duk_swap(self.py_ctx.ctx, -1, -2)
        duk_pop(self.py_ctx.ctx)

    def __del__(self):
        duk_push_global_stash(self.py_ctx.ctx)
        if not duk_has_prop_index(self.py_ctx.ctx, -1, self.ref_index):
            duk_pop(self.py_ctx.ctx)
            raise DuktapeError('Trying to delete non-existent reference')

        duk_del_prop_index(self.py_ctx.ctx, -1, self.ref_index)
        duk_pop(self.py_ctx.ctx)


cdef class JSProxy(object):

    def __init__(self, JSRef ref, object bind_proxy):
        ref.py_ctx._check_thread()

        self.__ref = ref
        self.__bind_proxy = bind_proxy

    def __setattr__(self, name, value):
        cdef duk_context* ctx
        self.__ref.py_ctx._check_thread()

        ctx = self.__ref.py_ctx.ctx

        self.__ref.to_js()
        to_js(ctx, value)
        duk_put_prop_string(ctx, -2, name)
        duk_pop(ctx)

    def __getattr__(self, name):
        cdef duk_context* ctx
        self.__ref.py_ctx._check_thread()

        ctx = self.__ref.py_ctx.ctx

        self.__ref.to_js()
        if not duk_get_prop_string(ctx, -1, name.encode()):
            duk_pop_2(ctx)
            raise AttributeError('Attribute {} missing'.format(name))

        try:
            res = to_python(self.__ref.py_ctx, -1, self)
        finally:
            duk_pop_2(ctx)

        return res

    def __getitem__(self, object name):
        self.__ref.py_ctx._check_thread()

        if not isinstance(name, (int, str)):
            raise TypeError('{} is not a valid index'.format(name))

        return getattr(self, str(name))

    def __repr__(self):
        cdef duk_context* ctx
        if self.__ref.py_ctx._check_thread() < 0:
            raise

        ctx = self.__ref.py_ctx.ctx

        self.__ref.to_js()
        res = <bytes>duk_safe_to_string(ctx, -1)
        duk_pop(ctx)

        return '<JSProxy: {}, bind_proxy={}>'.format(res.decode(), self.__bind_proxy.__repr__())

    def __call__(self, *args):
        if self.__ref.py_ctx._check_thread() < 0:
            raise

        if self.__bind_proxy is None:
            return self.__call(duk_pcall, args, None)
        else:
            return self.__call(duk_pcall_method, args, self.__bind_proxy)

    def new(self, *args):
        if self.__ref.py_ctx._check_thread() < 0:
            raise 

        return self.__call(safe_new, args, None)

    cdef object __call(self, callfunc call_type, tuple args, this):
        cdef object arg

        if self.__ref.py_ctx._check_thread() < 0:
            raise
        
        ctx = self.__ref.py_ctx.ctx

        self.__ref.to_js()

        if not duk_is_callable(ctx, -1):
            duk_pop(ctx)
            raise TypeError('Can\'t call')

        if this is not None:
            to_js(ctx, this)

        for arg in args:
            to_js(ctx, arg)

        if call_type(ctx, len(args)) == 0:
            res, error = to_python(self.__ref.py_ctx, -1), None
        else:
            res, error = None, self.__ref.py_ctx.get_error()

        duk_pop(ctx)

        if error is not None:
            raise JSError(error)

        return res

    # XXX: __nonzero__ removed in Python 3...
    def __bool__(self):
        if self.__ref.py_ctx._check_thread() < 0:
            raise

        return getattr(self, 'length', 1) > 0

    def __len__(self):
        if self.__ref.py_ctx._check_thread() < 0:
            raise
        return self.length

    def __iter__(self):
        cdef duk_context* ctx
        # unsafe to assume it's bint so lets try int instead...
        cdef int is_array, is_object

        if self.__ref.py_ctx._check_thread() < 0:
            raise
        ctx = self.__ref.py_ctx.ctx

        self.__ref.to_js()
        is_array = duk_is_array(ctx, -1)
        is_object = duk_is_object(ctx, -1)

        if is_array:
            duk_pop(ctx)
            for i in range(0, self.length):
                yield self[i]
        elif is_object:
            duk_enum(ctx, -1, DUK_ENUM_OWN_PROPERTIES_ONLY)

            keys = []
            while duk_next(ctx, -1, 0) != 0:
                keys.append(get_python_string(ctx, -1))
                duk_pop(ctx)
            duk_pop_2(ctx) # pop enumerator and self.__ref
            
            yield from keys
            # for key in keys:
            #     yield key

    cpdef to_js(self):
        if self.__ref.py_ctx._check_thread() < 0:
            raise
        self.__ref.to_js()


cdef duk_ret_t call_new(duk_context *ctx, void *udata) noexcept:
    # [ constructor arg1 arg2 ... argn nargs ]
    cdef int nargs = duk_require_int(ctx, -1)
    duk_pop(ctx)
    duk_new(ctx, nargs)
    duk_push_undefined(ctx) # replace the popped argument
    duk_swap(ctx, -1 , -2)

    return 1


cdef duk_ret_t safe_new(duk_context *ctx, int nargs):
    # [ constructor arg1 arg2 ... argn nargs ]
    duk_push_int(ctx, nargs)
    return duk_safe_call(ctx, call_new, NULL, nargs + 2, 1)


cdef duk_ret_t module_search(duk_context *ctx) noexcept:
    cdef DuktapeContext py_ctx = get_python_context(ctx)
    module_id = duk_require_string(ctx, -1)

    try:
        with open(py_ctx.get_file_path(module_id.decode()), 'rb') as module:
            source = module.read()
    except:
        duk_error(ctx, DUK_ERR_ERROR, b'Could not load module: %s', module_id)

    duk_push_string(ctx, source)

    return 1


cdef object to_python(DuktapeContext py_ctx, duk_idx_t index, JSProxy bind_proxy=None):
    cdef duk_context *ctx = py_ctx.ctx

    type_ = duk_get_type(ctx, index)

    if type_ == DUK_TYPE_NONE:
        raise DuktapeError('Nothing to convert')

    if type_ == DUK_TYPE_BUFFER or type_ == DUK_TYPE_LIGHTFUNC or type_ == DUK_TYPE_POINTER:
        raise DuktapeError('Type cannot be converted')

    if type_ == DUK_TYPE_NULL or type_ == DUK_TYPE_UNDEFINED:
        return None

    if type_ == DUK_TYPE_BOOLEAN:
        return bool(duk_get_boolean(ctx, index))

    if type_ == DUK_TYPE_NUMBER:
        value = float(duk_get_number(ctx, index))
        if value.is_integer():
            return int(value)
        else:
            return value

    if type_ == DUK_TYPE_STRING:
        return get_python_string(ctx, index)

    if type_ == DUK_TYPE_OBJECT:
        value_ptr = duk_get_heapptr(ctx, index)
        if py_ctx.is_registered_proxy(value_ptr):
            return py_ctx.get_registered_object_from_proxy(value_ptr)
        else:
            return JSProxy(py_ctx.make_jsref(index), bind_proxy)

    assert False


cdef object get_python_string(duk_context *ctx, duk_idx_t index):
    return duk_get_string(ctx, index).decode(errors='replace')



cdef void to_js(duk_context *ctx, object value) except *:
    if value is None:
        duk_push_null(ctx)
        return

    if value is False or value is True:
        duk_push_boolean(ctx, int(value))
        return

    if isinstance(value, int):
        max_positive_js_int = 1 << 53
        min_negative_js_int = -(1 << 53) - 1

        if value >= min_negative_js_int and value <= max_positive_js_int:
            duk_push_number(ctx, float(value))
        else:
            raise OverflowError('Cannot convert {}, number out of range'.format(value))
        return

    if isinstance(value, float):
        duk_push_number(ctx, value)
        return

    if isinstance(value, str):
        duk_push_string(ctx, value.encode())
        return

    if isinstance(value, JSProxy):
        value.to_js()
        return

    if callable(value):
        push_callback(ctx, value)
        return

    push_py_proxy(ctx, value)


cdef void push_py_proxy(duk_context *ctx, object obj) except *:
    cdef void* proxy_ptr
    cdef void* target_ptr
    cdef DuktapeContext py_ctx = get_python_context(ctx)

    duk_get_global_string(ctx, b'Proxy')

    duk_push_object(ctx) # proxy target
    duk_push_c_function(ctx, py_proxy_finalizer, 1)
    duk_set_finalizer(ctx, -2)
    target_ptr = duk_get_heapptr(ctx, -1)

    duk_push_object(ctx) # proxy options

    duk_push_c_function(ctx, py_proxy_get, 3)
    duk_put_prop_string(ctx, -2, b'get')

    duk_push_c_function(ctx, py_proxy_set, 4)
    duk_put_prop_string(ctx, -2, b'set')

    duk_push_c_function(ctx, py_proxy_has, 2)
    duk_put_prop_string(ctx, -2, b'has')

    if safe_new(ctx, 2) != 0:
        error = py_ctx.get_error()
        duk_pop(ctx)
        raise DuktapeError(error)

    proxy_ptr = duk_get_heapptr(ctx, -1)
    py_ctx.register_proxy(proxy_ptr, target_ptr, obj)


cdef duk_ret_t py_proxy_finalizer(duk_context *ctx) noexcept:
    py_ctx = get_python_context(ctx)

    target_ptr = duk_get_heapptr(ctx, -1)
    py_ctx.unregister_proxy_from_target(target_ptr)

    return 0


cdef duk_ret_t py_proxy_get(duk_context *ctx) noexcept:
    cdef DuktapeContext py_ctx = get_python_context(ctx)
    n_args = duk_get_top(ctx)

    with wrap_python_exception(py_ctx):
        target = py_ctx.get_registered_object(duk_get_heapptr(ctx, 0 - n_args))
        key = to_python(py_ctx, 1 - n_args)
        value = None

        if isinstance(target, (list, tuple)):
            if key == 'length':
                # special attribute
                value = len(target)
            else:
                # key is always a string,
                # but we need ints to index list and tuples
                try:
                    key = int(key)
                except (TypeError, ValueError):
                    pass

        if value is None:
            try:
                value = target[key]
            except (TypeError, IndexError, KeyError):
                if isinstance(key, str):
                    value = getattr(target, key, None)

        to_js(ctx, value)

    return 1


cdef duk_ret_t py_proxy_has(duk_context *ctx) noexcept:
    py_ctx = get_python_context(ctx)
    n_args = duk_get_top(ctx)

    with wrap_python_exception(py_ctx):
        target = py_ctx.get_registered_object(duk_get_heapptr(ctx, 0 - n_args))
        key = to_python(py_ctx, 1 - n_args)

        if isinstance(target, (list, tuple)):
            try:
                key = int(key)
            except (TypeError, ValueError):
                pass

        try:
            target[key]
            res = True
        except (KeyError, IndexError):
            res = False
        except TypeError:
            res = hasattr(target, key)

        to_js(ctx, res)

    return 1


cdef duk_ret_t py_proxy_set(duk_context *ctx) noexcept:
    cdef DuktapeContext py_ctx = get_python_context(ctx)
    cdef int n_args = <int>duk_get_top(ctx)

    with wrap_python_exception(py_ctx):
        target = py_ctx.get_registered_object(duk_get_heapptr(ctx, 0 - n_args))
        key = to_python(py_ctx, 1 - n_args)
        value = to_python(py_ctx, 2 - n_args)

        if isinstance(target, (list, tuple)):
            try:
                key = int(key)
            except (TypeError, ValueError):
                pass

        try:
            target[key] = value
        except TypeError:
            setattr(target, key, value)

    duk_push_boolean(ctx, 1)

    return 1


cdef duk_ret_t callback_finalizer(duk_context *ctx) noexcept:
    cdef DuktapeContext py_ctx = get_python_context(ctx)
    cdef void* target_ptr = duk_get_heapptr(ctx, -1)
    py_ctx.unregister_object(target_ptr)
    return 0

# TODO: -1 instead of assert for C Error handling?
cdef void push_callback(duk_context *ctx, object fn) except *:
    cdef DuktapeContext py_ctx
    assert callable(fn)

    py_ctx = get_python_context(ctx)

    duk_push_c_function(ctx, callback, DUK_VARARGS)

    duk_push_c_function(ctx, callback_finalizer, 1)
    duk_set_finalizer(ctx, -2)

    py_ctx.register_object(duk_get_heapptr(ctx, -1), fn)


cdef duk_ret_t callback(duk_context *ctx) noexcept:
    cdef DuktapeContext py_ctx
    cdef Py_ssize_t n_args, i
    if duk_is_constructor_call(ctx):
        duk_error(ctx, DUK_ERR_ERROR, b'can\'t use new on python objects')

    py_ctx = get_python_context(ctx)

    n_args = <Py_ssize_t>duk_get_top(ctx)

    with wrap_python_exception(py_ctx):
        args = []
        for i in range(0, n_args):
            args.append(to_python(py_ctx, i - n_args))

        duk_push_current_function(ctx)
        python_callback = py_ctx.get_registered_object(duk_get_heapptr(ctx, -1))
        duk_pop(ctx)
        # TODO: CPython equivent for this Call...
        res = python_callback(*args)

        to_js(ctx, res)

    return 1


# TODO: Lets try using a class instead of a contextmanager soon so we
# can cut some pure python stuff out that may trigger regessions...
@contextlib.contextmanager
def wrap_python_exception(DuktapeContext py_ctx):
    try:
        yield
    except:
        error = traceback.format_exc()
        error = error.replace('%', '%%')
        duk_error(py_ctx.ctx, DUK_ERR_ERROR, error.encode())