"""
**********************************************************************

  A RapydScript to JavaScript compiler.
  https://github.com/atsepkov/RapydScript

  -------------------------------- (C) ---------------------------------

                       Author: Alexander Tsepkov
                         <atsepkov@pyjeon.com>
                         http://www.pyjeon.com

  Distributed under BSD license:
    Copyright 2013 (c) Alexander Tsepkov <atsepkov@pyjeon.com>

 **********************************************************************
"""


# for convenience we'll use a convention here that will work as follows:
#
#   if function is named, assume we'll be outputting the function itself
#   if the given baselib chunk is triggered
#
#   if function is unnamed, assume the function is a container for the logic
#   to be output. We're basically ignoring the wrapper and dumping what's inside

{
"abs": def abs(n):
    return Math.abs(n)
,
"all": def all(a):
    for e in a:
        if not e: return False
    return True
,
"any": def any(a):
    for e in a:
        if e: return True
    return False
,
"bin": def bin(a): return '0b' + (a >>> 0).toString(2)
,
"bind": def ՐՏ_bind(fn, thisArg):
    if fn.orig: fn = fn.orig
    if thisArg is False: return fn
    ret = def():
        return fn.apply(thisArg, arguments)
    ret.orig = fn
    return ret
,
"rebind_all": def ՐՏ_rebindAll(thisArg, rebind):
    if rebind is undefined: rebind = True
    for JS('var p in thisArg'):
        if thisArg[p] and thisArg[p].orig:
            if rebind: thisArg[p] = bind(thisArg[p], thisArg)
            else: thisArg[p] = thisArg[p].orig
,
"with__name__": def ՐՏ_with__name__(fn, name):
    fn.__name__ = name
    return fn
,
"cmp": def cmp(a, b): return a < b ? -1 : a > b ? 1 : 0
,
"chr": def(): JS('var chr = String.fromCharCode')
,
"dir": def dir(item):
    # TODO: this isn't really representative of real Python's dir(), nor is it
    # an intuitive replacement for "for ... in" loop, need to update this logic
    # and introduce a different way of achieving "for ... in"
    arr = []
    for JS('var i in item'): arr.push(i)
    return arr
,
"enumerate": def enumerate(item):
    arr = []
    iter = ՐՏ_Iterable(item)
    for i in range(iter.length):
        arr[arr.length] = [i, item[i]]
    return arr
,
"eslice": def ՐՏ_eslice(arr, step, start, end):
    arr = arr[:]
    if JS('typeof arr') is 'string' or isinstance(arr, String):
        isString = True
        arr = arr.split('')

    if step < 0:
        step = -step
        arr.reverse()
        if JS('typeof start') is not "undefined": start = arr.length - start - 1
        if JS('typeof end') is not "undefined": end = arr.length - end - 1
    if JS('typeof start') is "undefined": start = 0
    if JS('typeof end') is "undefined": end = arr.length

    arr = arr.slice(start, end).filter(def(e, i): return i % step is 0;)
    return isString ? arr.join('') : arr
,
"extends": def ՐՏ_extends(child, parent):
    child.prototype = Object.create(parent.prototype)
    child.prototype.__base__ = parent     # since we don't support multiple inheritance, __base__ seemed more appropriate than __bases__ array of 1
    child.prototype.constructor = child
,
"filter": def filter(oper, arr):
    return arr.filter(oper)
,
"hex": def hex(a): return '0x' + (a >>> 0).toString(16)
,
"in": def ՐՏ_in(val, arr):
    if JS('typeof arr.indexOf') is 'function': return arr.indexOf(val) is not -1
    elif JS('typeof arr.has') is 'function': return arr.has(val)
    return arr.hasOwnProperty(val)
,
"iterable": def ՐՏ_Iterable(iterable):
    # can't use Symbol.iterator yet since it's not supported on all platforms until ES6 (i.e. mobile browsers don't have it)
    if iterable.constructor is [].constructor
    or iterable.constructor is ''.constructor
    or (tmp = Array.prototype.slice.call(iterable)).length:
        return tmp or iterable
    if Set and iterable.constructor is Set:
        return Array.from(iterable)
    return Object.keys(iterable)    # so we can use 'for ... in' syntax with hashes
,
"len": def len(obj):
    # can't use Symbol.iterator yet since it's not supported on all platforms until ES6 (i.e. mobile browsers don't have it)
    if obj.constructor is [].constructor
    or obj.constructor is ''.constructor
    or (tmp = Array.prototype.slice.call(obj)).length:
        return (tmp or obj).length
    if Set and obj.constructor is Set:
        return obj.size
    return Object.keys(obj).length
,
"map": def map(oper, arr):
    return arr.map(oper)
,
"max": def max(a):
    return Math.max.apply(None, Array.isArray(a) ? a : arguments)
,
"min": def min(a):
    return Math.min.apply(None, Array.isArray(a) ? a : arguments)
,
"merge": def ՐՏ_merge(target, source, overwrite):
    for JS('var i in source'):
        # instance variables
        if source.hasOwnProperty(i) and (overwrite or JS('typeof target[i]') is 'undefined'): target[i] = source[i]
    for prop in Object.getOwnPropertyNames(source.prototype):
        # methods
        if overwrite or JS('typeof target.prototype[prop]') is 'undefined':
            Object.defineProperty(target.prototype, prop, Object.getOwnPropertyDescriptor(source.prototype, prop))
,
"mixin": def ՐՏ_mixin(*classes):
    return def(baseClass):
        for cls in classes:
            for key in Object.getOwnPropertyNames(cls.prototype):
                if key not in baseClass.prototype:
                    Object.defineProperty(baseClass.prototype, key, Object.getOwnPropertyDescriptor(cls.prototype, key))
        return baseClass

,
"print": def ՐՏ_print():
    if JS('typeof console') is 'object': console.log.apply(console, arguments)
,
"range": def range(start, stop, step):
    if arguments.length <= 1:
        stop = start or 0
        start = 0
    step = arguments[2] or 1

    length = Math.max(Math.ceil((stop - start) / step), 0)
    idx = 0
    range = Array(length)

    while idx < length:
        range[JS('idx++')] = start
        start += step
    return range
,
"reduce": def reduce(f, a): return Array.reduce(a, f)
,
"reversed": def reversed(arr):
    tmp = arr[:]
    return tmp.reverse()
,
"sorted": def sorted(arr):
    tmp = arr[:]
    return tmp.sort()
,
"sum": def sum(arr, start=0):
    return arr.reduce(
        def(prev, cur): return prev+cur
        ,
        start
    )
,
"type": def ՐՏ_type(obj):
    return obj and obj.constructor and obj.constructor.name ? obj.constructor.name : Object.prototype.toString.call(obj).slice(8, -1)
,
"zip": def zip(a, b):
    return [[a[i], b[i]] for i in range(Math.min(a.length, b.length))]
,
"getattr": def getattr(obj, name):
    return obj[name]
,
"setattr": def setattr(obj, name, value):
    obj[name] = value
,
"hasattr": def hasattr(obj, name):
    return JS('name in obj')
,
"eq": def ՐՏ_eq(a, b):
    """
    Equality comparison that works with all data types, returns true if structure and
    contents of first object equal to those of second object

    Arguments:
        a: first object
        b: second object
    """
    if a is b:
        # simple object
        return True

    if a is undefined or b is undefined or a is None or b is None:
        return False

    if a.constructor is not b.constructor:
        # object type mismatch
        return False

    if Array.isArray(a):
        # arrays
        if a.length is not b.length: return False
        for i in range(a.length):
            if not ՐՏ_eq(a[i], b[i]):
                return False
        return True
    elif a.constructor is Object:
        # hashes
        # compare individual properties (order doesn't matter if it's a hash)
        if Object.keys(a).length is not Object.keys(b).length: return False
        for i in a:
            # recursively test equality of object children
            if not ՐՏ_eq(a[i], b[i]):
                return False
        return True
    elif (Set and a.constructor is Set) or (Map and a.constructor is Map):
        # sets and maps
        if a.size is not b.size: return False
        for JS('i of a'):
            if not b.has(i):
                return False
        return True
    elif (a.constructor is Date):
        # dates
        return a.getTime() is b.getTime()
    elif JS('typeof a.__eq__') is 'function':
        # everything else that implements __eq__ method
        return a.__eq__(b)
    return False
,
"kwargs": def():
    # WARNING: when using this function decorator, you will not be able to use obfuscators that rename local variables
    def kwargs(f):
        argNames = f.toString().match(/\(([^\)]+)/)[1]
        if not kwargs.memo[argNames]:
            kwargs.memo[argNames] = argNames ? argNames.split(',').map(def(s): return s.trim();) : []
        argNames = kwargs.memo[argNames]
        return def():
            args = [].slice.call(arguments)
            if args.length:
                kw = args[-1]
                if JS('typeof kw') is 'object':
                    for i in range(argNames.length):
                        if argNames[i] in kw:
                            args[i] = kw[argNames[i]]
                else:
                    args.push(kw)

            # This logic is very fragile and very subtle, it needs to work both in ES6 and ES5, don't try to optimize the
            # apply away into *args because having it in this format ensures correct 'this' context, otherwise the function
            # ends up unbound. Similarly, the fallthrough to except handles class creation in ES6.
            try:
                return f.apply(this, args)
            except as e:
                if /Class constructor \w+ cannot be invoked without 'new'/.test(e):
                    return new f(*args)
                raise
    kwargs.memo = {}
,

# Errors
# temporarily implemented via a wrapper pattern since there is no mechanism for assigning
# classes to dictionary keys yet
"AssertionError": def():
    class AssertionError(Error):
        def __init__(self, message):
            self.name = "AssertionError"
            self.message = message
,
"IndexError": def():
    class IndexError(Error):
        def __init__(self, message):
            self.name = "IndexError"
            self.message = message
,
"KeyError": def():
    class KeyError(Error):
        def __init__(self, message):
            self.name = "KeyError"
            self.message = message
,
"TypeError": def():
    class TypeError(Error):
        def __init__(self, message):
            self.name = "TypeError"
            self.message = message
,
"ValueError": def():
    class ValueError(Error):
        def __init__(self, message):
            self.name = "ValueError"
            self.message = message
,
}
