'''Most important file in Js2Py implementation: PyJs class - father of all PyJs objects'''
from copy import copy
import re

from .translators.friendly_nodes import REGEXP_CONVERTER
from .utils.injector import fix_js_args
from types import FunctionType, ModuleType, GeneratorType, BuiltinFunctionType, MethodType, BuiltinMethodType
from math import floor, log10
import traceback
try:
    import numpy
    NUMPY_AVAILABLE = True
except:
    NUMPY_AVAILABLE = False

# python 3 support
import six
if six.PY3:
    basestring = str
    long = int
    xrange = range
    unicode = str


def str_repr(s):
    if six.PY2:
        return repr(s.encode('utf-8'))
    else:
        return repr(s)


def MakeError(name, message):
    """Returns PyJsException with PyJsError inside"""
    return JsToPyException(ERRORS[name](Js(message)))


def to_python(val):
    if not isinstance(val, PyJs):
        return val
    if isinstance(val, PyJsUndefined) or isinstance(val, PyJsNull):
        return None
    elif isinstance(val, PyJsNumber):
        # this can be either float or long/int better to assume its int/long when a whole number...
        v = val.value
        try:
            i = int(v) if v == v else v  # nan...
            return v if i != v else i
        except:
            return v
    elif isinstance(val, (PyJsString, PyJsBoolean)):
        return val.value
    elif isinstance(val, PyObjectWrapper):
        return val.__dict__['obj']
    elif isinstance(val, PyJsArray) and val.CONVERT_TO_PY_PRIMITIVES:
        return to_list(val)
    elif isinstance(val, PyJsObject) and val.CONVERT_TO_PY_PRIMITIVES:
        return to_dict(val)
    else:
        return JsObjectWrapper(val)


def to_dict(js_obj,
            known=None):  # fixed recursion error in self referencing objects
    res = {}
    if known is None:
        known = {}
    if js_obj in known:
        return known[js_obj]
    known[js_obj] = res
    for k in js_obj:
        name = k.value
        input = js_obj.get(name)
        output = to_python(input)
        if isinstance(output, JsObjectWrapper):
            if output._obj.Class == 'Object':
                output = to_dict(output._obj, known)
                known[input] = output
            elif output._obj.Class in [
                    'Array', 'Int8Array', 'Uint8Array', 'Uint8ClampedArray',
                    'Int16Array', 'Uint16Array', 'Int32Array', 'Uint32Array',
                    'Float32Array', 'Float64Array'
            ]:
                output = to_list(output._obj)
                known[input] = output
        res[name] = output
    return res


def to_list(js_obj, known=None):
    res = len(js_obj) * [None]
    if known is None:
        known = {}
    if js_obj in known:
        return known[js_obj]
    known[js_obj] = res
    for k in js_obj:
        try:
            name = int(k.value)
        except:
            continue
        input = js_obj.get(str(name))
        output = to_python(input)
        if isinstance(output, JsObjectWrapper):
            if output._obj.Class in [
                    'Array', 'Int8Array', 'Uint8Array', 'Uint8ClampedArray',
                    'Int16Array', 'Uint16Array', 'Int32Array', 'Uint32Array',
                    'Float32Array', 'Float64Array', 'Arguments'
            ]:
                output = to_list(output._obj, known)
                known[input] = output
            elif output._obj.Class in ['Object']:
                output = to_dict(output._obj)
                known[input] = output
        res[name] = output
    return res


def HJs(val):
    if hasattr(val, '__call__'):  #

        @Js
        def PyWrapper(this, arguments, var=None):
            args = tuple(to_python(e) for e in arguments.to_list())
            try:
                py_res = val.__call__(*args)
            except Exception as e:
                message = 'your Python function failed!  '
                try:
                    message += e.message
                except:
                    pass
                raise MakeError('Error', message)
            return py_wrap(py_res)

        try:
            PyWrapper.func_name = val.__name__
        except:
            pass
        return PyWrapper
    if isinstance(val, tuple):
        val = list(val)
    return Js(val)


def Js(val, Clamped=False):
    '''Converts Py type to PyJs type'''
    if isinstance(val, PyJs):
        return val
    elif val is None:
        return undefined
    elif isinstance(val, basestring):
        return PyJsString(val, StringPrototype)
    elif isinstance(val, bool):
        return true if val else false
    elif isinstance(val, float) or isinstance(val, int) or isinstance(
            val, long) or (NUMPY_AVAILABLE and isinstance(
                val,
                (numpy.int8, numpy.uint8, numpy.int16, numpy.uint16,
                 numpy.int32, numpy.uint32, numpy.float32, numpy.float64))):
        # This is supposed to speed things up. may not be the case
        if val in NUM_BANK:
            return NUM_BANK[val]
        return PyJsNumber(float(val), NumberPrototype)
    elif isinstance(val, FunctionType):
        return PyJsFunction(val, FunctionPrototype)
    #elif isinstance(val, ModuleType):
    #    mod = {}
    #    for name in dir(val):
    #        value = getattr(val, name)
    #        if isinstance(value, ModuleType):
    #            continue  # prevent recursive module conversion
    #        try:
    #            jsval = HJs(value)
    #        except RuntimeError:
    #            print 'Could not convert %s to PyJs object!' % name
    #            continue
    #        mod[name] = jsval
    #    return Js(mod)
    #elif isintance(val, ClassType):

    elif isinstance(val, dict):  # convert to object
        temp = PyJsObject({}, ObjectPrototype)
        for k, v in six.iteritems(val):
            temp.put(Js(k), Js(v))
        return temp
    elif isinstance(val, (list, tuple)):  #Convert to array
        return PyJsArray(val, ArrayPrototype)
    # convert to typedarray
    elif isinstance(val, JsObjectWrapper):
        return val.__dict__['_obj']
    elif NUMPY_AVAILABLE and isinstance(val, numpy.ndarray):
        if val.dtype == numpy.int8:
            return PyJsInt8Array(val, Int8ArrayPrototype)
        elif val.dtype == numpy.uint8 and not Clamped:
            return PyJsUint8Array(val, Uint8ArrayPrototype)
        elif val.dtype == numpy.uint8 and Clamped:
            return PyJsUint8ClampedArray(val, Uint8ClampedArrayPrototype)
        elif val.dtype == numpy.int16:
            return PyJsInt16Array(val, Int16ArrayPrototype)
        elif val.dtype == numpy.uint16:
            return PyJsUint16Array(val, Uint16ArrayPrototype)

        elif val.dtype == numpy.int32:
            return PyJsInt32Array(val, Int32ArrayPrototype)
        elif val.dtype == numpy.uint32:
            return PyJsUint16Array(val, Uint32ArrayPrototype)

        elif val.dtype == numpy.float32:
            return PyJsFloat32Array(val, Float32ArrayPrototype)
        elif val.dtype == numpy.float64:
            return PyJsFloat64Array(val, Float64ArrayPrototype)
    else:  # try to convert to js object
        return py_wrap(val)
        #raise RuntimeError('Cant convert python type to js (%s)' % repr(val))
        #try:
        #    obj = {}
        #    for name in dir(val):
        #        if name.startswith('_'):  #dont wrap attrs that start with _
        #            continue
        #        value = getattr(val, name)
        #        import types
        #        if not isinstance(value, (FunctionType, BuiltinFunctionType, MethodType, BuiltinMethodType,
        #                                  dict, int, basestring, bool, float, long, list, tuple)):
        #            continue
        #        obj[name] = HJs(value)
        #    return Js(obj)
        #except:
        #    raise RuntimeError('Cant convert python type to js (%s)' % repr(val))


def Type(val):
    try:
        return val.TYPE
    except:
        raise RuntimeError('Invalid type: ' + str(val))


def is_data_descriptor(desc):
    return desc and ('value' in desc or 'writable' in desc)


def is_accessor_descriptor(desc):
    return desc and ('get' in desc or 'set' in desc)


def is_generic_descriptor(desc):
    return desc and not (is_data_descriptor(desc)
                         or is_accessor_descriptor(desc))


##############################################################################


class PyJs(object):
    PRIMITIVES = frozenset(
        ['String', 'Number', 'Boolean', 'Undefined', 'Null'])
    TYPE = 'Object'
    Class = None
    extensible = True
    prototype = None
    own = {}
    GlobalObject = None
    IS_CHILD_SCOPE = False
    CONVERT_TO_PY_PRIMITIVES = False
    value = None
    buff = None

    def __init__(self, value=None, prototype=None, extensible=False):
        '''Constructor for Number String and Boolean'''
        # I dont think this is needed anymore
        # if self.Class=='String' and not isinstance(value, basestring):
        #     raise TypeError
        # if self.Class=='Number':
        #     if not isinstance(value, float):
        #         if not (isinstance(value, int) or isinstance(value, long)):
        #             raise TypeError
        #         value = float(value)
        # if self.Class=='Boolean' and not isinstance(value, bool):
        #     raise TypeError
        self.value = value
        self.extensible = extensible
        self.prototype = prototype
        self.own = {}
        self.buff = None

    def is_undefined(self):
        return self.Class == 'Undefined'

    def is_null(self):
        return self.Class == 'Null'

    def is_primitive(self):
        return self.TYPE in self.PRIMITIVES

    def is_object(self):
        return not self.is_primitive()

    def _type(self):
        return Type(self)

    def is_callable(self):
        return hasattr(self, 'call')

    def get_own_property(self, prop):
        return self.own.get(prop)

    def get_property(self, prop):
        cand = self.get_own_property(prop)
        if cand:
            return cand
        if self.prototype is not None:
            return self.prototype.get_property(prop)

    def update_array(self):
        for i in range(self.get('length').to_uint32()):
            self.put(str(i), Js(self.buff[i]))

    def get(self, prop):  #external use!
        #prop = prop.value
        if self.Class == 'Undefined' or self.Class == 'Null':
            raise MakeError('TypeError',
                            'Undefined and null dont have properties!')
        if not isinstance(prop, basestring):
            prop = prop.to_string().value
        if not isinstance(prop, basestring): raise RuntimeError('Bug')
        if NUMPY_AVAILABLE and prop.isdigit():
            if isinstance(self.buff, numpy.ndarray):
                self.update_array()
        cand = self.get_property(prop)
        if cand is None:
            return Js(None)
        if is_data_descriptor(cand):
            return cand['value']
        if cand['get'].is_undefined():
            return cand['get']
        return cand['get'].call(self)

    def can_put(self, prop):  #to check
        desc = self.get_own_property(prop)
        if desc:  #if we have this property
            if is_accessor_descriptor(desc):
                return desc['set'].is_callable(
                )  # Check if setter method is defined
            else:  #data desc
                return desc['writable']
        if self.prototype is not None:
            return self.extensible
        inherited = self.get_property(prop)
        if inherited is None:
            return self.extensible
        if is_accessor_descriptor(inherited):
            return not inherited['set'].is_undefined()
        elif self.extensible:
            return inherited['writable']
        return False

    def put(self, prop, val, op=None):  #external use!
        '''Just like in js: self.prop op= val
           for example when op is '+' it will be self.prop+=val
           op can be either None for simple assignment or one of:
           * / % + - << >> & ^ |'''
        if self.Class == 'Undefined' or self.Class == 'Null':
            raise MakeError('TypeError',
                            'Undefined and null dont have properties!')
        if not isinstance(prop, basestring):
            prop = prop.to_string().value
        if NUMPY_AVAILABLE and prop.isdigit():
            if self.Class == 'Int8Array':
                val = Js(numpy.int8(val.to_number().value))
            elif self.Class == 'Uint8Array':
                val = Js(numpy.uint8(val.to_number().value))
            elif self.Class == 'Uint8ClampedArray':
                if val < Js(numpy.uint8(0)):
                    val = Js(numpy.uint8(0))
                elif val > Js(numpy.uint8(255)):
                    val = Js(numpy.uint8(255))
                else:
                    val = Js(numpy.uint8(val.to_number().value))
            elif self.Class == 'Int16Array':
                val = Js(numpy.int16(val.to_number().value))
            elif self.Class == 'Uint16Array':
                val = Js(numpy.uint16(val.to_number().value))
            elif self.Class == 'Int32Array':
                val = Js(numpy.int32(val.to_number().value))
            elif self.Class == 'Uint32Array':
                val = Js(numpy.uint32(val.to_number().value))
            elif self.Class == 'Float32Array':
                val = Js(numpy.float32(val.to_number().value))
            elif self.Class == 'Float64Array':
                val = Js(numpy.float64(val.to_number().value))
            if isinstance(self.buff, numpy.ndarray):
                self.buff[int(prop)] = int(val.to_number().value)
        #we need to set the value to the incremented one
        if op is not None:
            val = getattr(self.get(prop), OP_METHODS[op])(val)
        if not self.can_put(prop):
            return val
        own_desc = self.get_own_property(prop)
        if is_data_descriptor(own_desc):
            if self.Class in [
                    'Array', 'Int8Array', 'Uint8Array', 'Uint8ClampedArray',
                    'Int16Array', 'Uint16Array', 'Int32Array', 'Uint32Array',
                    'Float32Array', 'Float64Array'
            ]:
                self.define_own_property(prop, {'value': val})
            else:
                self.own[prop]['value'] = val
            return val
        desc = self.get_property(prop)
        if is_accessor_descriptor(desc):
            desc['set'].call(self, (val, ))
        else:
            new = {
                'value': val,
                'writable': True,
                'configurable': True,
                'enumerable': True
            }
            if self.Class in [
                    'Array', 'Int8Array', 'Uint8Array', 'Uint8ClampedArray',
                    'Int16Array', 'Uint16Array', 'Int32Array', 'Uint32Array',
                    'Float32Array', 'Float64Array'
            ]:
                self.define_own_property(prop, new)
            else:
                self.own[prop] = new
        return val

    def has_property(self, prop):
        return self.get_property(prop) is not None

    def delete(self, prop):
        if not isinstance(prop, basestring):
            prop = prop.to_string().value
        desc = self.get_own_property(prop)
        if desc is None:
            return Js(True)
        if desc['configurable']:
            del self.own[prop]
            return Js(True)
        return Js(False)

    def default_value(
            self, hint=None
    ):  # made a mistake at the very early stage and made it to prefer string... caused lots! of problems
        order = ('valueOf', 'toString')
        if hint == 'String' or (hint is None and self.Class == 'Date'):
            order = ('toString', 'valueOf')
        for meth_name in order:
            method = self.get(meth_name)
            if method is not None and method.is_callable():
                cand = method.call(self)
                if cand.is_primitive():
                    return cand
        raise MakeError('TypeError',
                        'Cannot convert object to primitive value')

    def define_own_property(self, prop,
                            desc):  #Internal use only. External through Object
        # prop must be a Py string. Desc is either a descriptor or accessor.
        #Messy method -  raw translation from Ecma spec to prevent any bugs. # todo check this
        current = self.get_own_property(prop)

        extensible = self.extensible
        if not current:  #We are creating a new property
            if not extensible:
                return False
            if is_data_descriptor(desc) or is_generic_descriptor(desc):
                DEFAULT_DATA_DESC = {
                    'value': undefined,  #undefined
                    'writable': False,
                    'enumerable': False,
                    'configurable': False
                }
                DEFAULT_DATA_DESC.update(desc)
                self.own[prop] = DEFAULT_DATA_DESC
            else:
                DEFAULT_ACCESSOR_DESC = {
                    'get': undefined,  #undefined
                    'set': undefined,  #undefined
                    'enumerable': False,
                    'configurable': False
                }
                DEFAULT_ACCESSOR_DESC.update(desc)
                self.own[prop] = DEFAULT_ACCESSOR_DESC
            return True
        if not desc or desc == current:  #We dont need to change anything.
            return True
        configurable = current['configurable']
        if not configurable:  #Prevent changing configurable or enumerable
            i