# empty classes are allowed
class Blank:
    pass
blank = Blank()
assert.ok(isinstance(blank, Blank))

# basic class, also with unneeded parentheses
class Human():
    def __init__(self, name):
        self.name = name

    def greet(self):
        return "Hello, I'm " + self.name

    @staticmethod
    def getTypicalWeight():
        return "150"

    @staticmethod # with args
    def getLastName(FirstName):
        LastNames = {   'Bart' : 'Simpson',
                        'Leela' : 'Turanga',
                        'James' : 'Bond'}
        return LastNames[FirstName]


# don't have to declare __init__, it either gets inherited or stays empty
# isinstance correctly sees inheritance
class Friend(Human):
    def greet(self):
        return "Yo, it's me, " + self.name
    def nickname(self, name):
        self.name = name

# __init__ doesn't have to come first
# can call methods of other classes
class OldFriend(Friend):
    def how_long(self):
        return "I've known you for " + self.duration + " years"
    def get_bound_method(self):
        return bind(self.how_long, self)
    def __init__(self, name, duration):
        Friend.__init__(self, name)
        self.duration = duration

bob = Human("Bob")
assert.equal(bob.greet(), "Hello, I'm Bob")
assert.equal(Human.greet(bob), "Hello, I'm Bob")
assert.equal(Human.getTypicalWeight(), "150") # static method recognition
assert.equal(Human.getLastName("James"), "Bond") # static method with args - check args processing  

joe = Friend("Joe")
assert.equal(joe.greet(), "Yo, it's me, Joe")
assert.ok(isinstance(joe, Friend))
assert.ok(isinstance(joe, Human))

angela = OldFriend("Angela", 8)
assert.equal(angela.greet(), "Yo, it's me, Angela")
assert.equal(angela.how_long(), "I've known you for 8 years")

# test that function stays bound
bound = angela.get_bound_method()
assert.equal(bound(), angela.how_long())

# function methods
# this will fail in ES6 and also fail when props aren't enumerable, which may become the default later
#assert.deepEqual(dir(angela).sort(), [
#    "__base__",
#    "__init__",
#    "constructor",
#    "duration",
#    "get_bound_method",
#    "greet",
#    "how_long",
#    "name",
#    "nickname"
#])

# test that binding works in relation to the actual class, not the parent
angela.nickname("Angie")
assert.equal(angela.greet(), "Yo, it's me, Angie")

dude = None
(def fake_module():
    # test that we can declare classes inside other blocks
    # test that we can call methods of classes we didn't inherit from
    nonlocal dude
    class Stranger(Human):
        def greet(self):
            return Friend.greet(self)
    dude = Stranger("some guy")
)()
assert.equal(dude.greet(), "Yo, it's me, some guy")
# also test that classes declared this way are not globally scoped (while normal ones are)
assert.throws(
    def():
        Friend("another friend")
        Stranger("another guy")
    ,
    /Stranger is not defined/
)

# attributes
assert.ok(hasattr(dude, "greet"))
assert.equal(bind(getattr(dude, "greet"), dude)(), "Yo, it's me, some guy") # function stays bound after binding
assert.equal(hasattr(dude, "stuff"), False)
setattr(dude, "stuff", True)
assert.ok(hasattr(dude, "stuff"))

# native classes and methods
st = String("test")
assert.equal(st, "test")
assert.equal(st.toUpperCase(), "TEST")
assert.equal(String.toUpperCase(st), "TEST")
assert.equal(String.fromCharCode(65), "A") # static method recognition

# now we test RapydScript's ability to insert 'new' operator correctly
assert.ok(String('a') != 'a')   # string literal vs string object
assert.ok((String)('a') == 'a') # string literal vs string literal
assert.ok(String.call(this, 'a') == 'a') # string literal via static method on string

# self consistency
class Counter:
    def __init__(s, n=0):
        s.count = n # first arg becomes 'self'
    def getIncrementer(self):
        return def():
            self.count += 1
c = Counter(5)
inc = c.getIncrementer()
inc()
assert.equal(c.count, 6)

# nested classes
# not yet fully implemented
#class Molecule:
#   class Atom:
#       def __init__(self, element):
#           self.element = element
#
#   def __init__(self, elements):
#       self.structure = []
#       for e in elements:
#           self.structure.push(Molecule.Atom(e))
#
#water = Molecule(['H', "H", 'O'])
#assert.equal(len(water.structure), 3)
#assert.equal(water.structure[0].element, 'H')
#for atom in water.structure:
#   assert.ok(isinstance(atom, Molecule.Atom))

# starargs and method decorators
def negate(fn):
    def wrapped(*args):
        return -fn(*args)
    return wrapped

def add_pi(cls):
    cls.prototype.pi = 3.14
    return cls

@add_pi
class Math:
    def sum(s, *args):
        # fakearg simply tests that offsets work correctly
        ttl = 0
        for i in args:
            ttl += i
        return ttl
    def concatSum(s, string, *nums):
        return string + s.sum(*nums)
    @negate
    def plus(s, a, b):
        return a+b

m = Math()
assert.equal(m.sum(1,2,3), 6)
assert.equal(m.sum(1,*[2,3]), 6)
assert.equal(m.concatSum("foo", 1, 2, 5), "foo8")
assert.equal(m.plus(2, 5), -7)
assert.equal(m.pi, 3.14)

#decorate only method/staticmethod
class Plus:
    def minus(s, a, b):
        return a-b
    @negate
    def plus(s, a, b):
        return a+b

    @negate
    @staticmethod
    def mul(a, b):
        return a*b

assert.equal(Plus.mul(2, 5), -10)
p = Plus()
assert.equal(p.plus(2, 5), -7)
assert.equal(p.minus(2, 5), -3)

# class variables
class C:
    a = 1
    b = 1
    #Class variables can reference each other during initialization
    c = a + b 
    #but complex logic is not allowed, all statements must be assignments
    d = (def(): if c > 0:  return 1;)() or 0
c = C()
assert.deepEqual([c.a, c.b, c.c, c.d], [1, 1, 2, 1])


# mixins
class Flyable:
    def fly(self):
        return "flying..."

class Quackable:
    def fly(self):
        return "N/A"
    def quack(self):
        return "quack!"

class Mute:
    def fly(self):
        return "..."

class FlyingDuck(Flyable):
    def __init__(self):
        Flyable.__init__(self)
merge(FlyingDuck, Quackable)

flyingduck = FlyingDuck()

assert.equal(flyingduck.fly(), "flying...")
assert.equal(flyingduck.quack(), "quack!")

merge(FlyingDuck, Mute)
assert.equal(flyingduck.fly(), "flying...")

merge(FlyingDuck, Mute, True)
assert.equal(flyingduck.fly(), "...")

class Skateable:
    def skate(self):
        return "...zooom"

@mixin(Quackable, Skateable)
class MightyDuck(Human):
    hitPoints = 200

player = MightyDuck('Charlie Conway')
assert.equal(player.name, 'Charlie Conway')
assert.equal(player.quack(), 'quack!')
assert.equal(player.fly(), 'N/A')
assert.equal(player.skate(), '...zooom')
assert.equal(player.hitPoints, 200)

# static method test inside class definition
#  issue #128 (fixed!)
class Static_meth:
    def __init__(self):
        self.a = 'a'

    def test(self):
        assert.equal(Static_meth.me(), 'me')
        assert.equal(Static_meth.delete(), 'delete')
        class Nested:
            @staticmethod
            def nested_test():
                assert.equal(Nested.typeof(), 'nested_typeof')
                assert.equal(Static_meth.delete(), 'delete')
                return True
            @staticmethod
            def typeof():
                return 'nested_typeof'
        assert.ok(Nested.nested_test())
        assert.equal(Static_meth.typeof(self), 'a')
        assert.equal(Nested.typeof(), 'nested_typeof')

    def typeof(self):
        return self.a

    @staticmethod
    def delete():
        return 'delete'

    @staticmethod
    def me():
        return 'me'

Static_meth().test()

# JS is omnivorous regarding class method names
class WeirdNames:
    def 1(self, foo):
        #assert.equal(self['2']('foo'), 'foo')
        return  foo
    def or(self, a):
        return a
    def True(self, a):
        return a
    def this(self, a):
        return a
    def var(self, a):
        return a
    def './foo'(self, a):
        return a
    @negate
    def './decorated'(self, a):
        return a 
        
    @staticmethod
    def 2(foo):
        return  foo

weird = WeirdNames()

assert.equal(weird['1']('foo'), 'foo')
assert.equal(weird.or('foo'), 'foo')
assert.equal(weird.True('foo'), 'foo')
assert.equal(weird.this('foo'), 'foo')
assert.equal(weird.var('foo'), 'foo')
assert.equal(weird.var('foo'), 'foo')
assert.equal(WeirdNames['2']('two'), 'two')
assert.equal(weird['./foo']('str'), 'str')
assert.equal(weird['./decorated'](23), -23)
# the below tests fail in es6 mode, because it is not required to use '__name__' to store the method name
#assert.equal(WeirdNames['2'].__name__, '2')
#assert.equal(weird['./foo'].__name__, './foo')

