var Namespace = Namespaces.constructor;

var Thinglish = Namespace.declare("tla.EAM.layer1",
    class Thinglish {
        static start() {
                var Loader = Object.getPrototypeOf(Namespaces.loader.constructor).name;
                if (Loader && Loader !== "")
                    Loader = Object.getPrototypeOf(Namespaces.loader.constructor);
                else
                    return false;
                
            Namespace.bootstrapLoaderInterface("tla.EAM.layer3.OnceServices", Loader);

            Thinglish.implement(Namespaces.loader, Loader);
            Thinglish.implement(this.IOR.loader, Loader);

            /* 
            var test = Namespaces.test.Thinglish.Test.getInstance();
            test.untypedMethod(
                "Hallo Welt",
                3,
                test.untypedMethod,
                Namespaces.loader,
                test
            );
           */

        }

        static get SILENT_ON_TYPE_ERROR() { return "0"; }
        static get WARN_ON_TYPE_ERROR() { return "1"; }
        static get STOP_ON_TYPE_ERROR() { return "2"; }
        static get onTypeError() {
            if (!Thinglish._onTypeError)
                Thinglish._onTypeError = Thinglish.WARN_ON_TYPE_ERROR;
            return Thinglish._onTypeError;
        }
        static set onTypeError(value) { Thinglish._onTypeError = value }


        constructor() {
            console.log("Thinglish is unloadable")
        }

        init() {
            super.init();
            return Object.freeze(object);
        }

            static lookupInObject(object, thingName) {
                return Namespaces.tmp.light.Thinglish.lookupInObject(object, thingName)
            }
        static initType(object, packageName) {
            return Namespaces.tmp.light.Thinglish.initType(object, packageName);
        };

        static defineAlias(object, name, alias) {
            return Namespaces.tmp.light.Thinglish.defineAlias(object, name, alias);
        };

        static defineAccessors(object, name, getter, setter) {
            return Namespaces.tmp.light.Thinglish.defineAccessors(object, name, getter, setter);
        };
        static startThing(ior) {
            return Namespaces.tmp.light.Thinglish.startThing(ior);
        }
        static loadAndStartAll(aClass, IORs) {
            return Namespaces.tmp.light.Thinglish.loadAndStartAll(aClass, IORs);
        };
        static startAll(classes) {
            return Namespaces.tmp.light.Thinglish.startAll(classes);
        };

        static startDependencies(aClass) {
            return Namespaces.tmp.light.Thinglish.startDependencies(aClass);
        };
        static startClass(aClass) {
            return Namespaces.tmp.light.Thinglish.startClass(aClass);
        };

        static definePrivateAttribute(object, name, defaultValue) {
            Object.defineProperty(object, name, {
                enumerable: false,
                configurable: false,
                writable: true,
                value: defaultValue
            });
        }

        static defineProperty(object, name, defaultValue, onWillChange) {
            var _propertyValue = defaultValue;
            if (!onWillChange)
                onWillChange = object.defaultOnChange;
            var _onWillChangeFunction = onWillChange;

            Object.defineProperty(object, name, {
                enumerable: false,
                configurable: true,
                get: () => { return _propertyValue; },
                set: (newValue) => {
                    console.log("set '" + _name + "' to '" + newValue + "'");
                    if (_onWillChangeFunction)
                        newValue = _onWillChangeFunction();
                    _propertyValue = newValue;
                }
            });
        }


        static addLazyGetter(object, name) {
            var _propertyValue = null;

            Object.defineProperty(object, name, {
                enumerable: true,
                configurable: false,
                get: () => {
                    if (!_propertyValue)
                        _propertyValue = eval("new " + name + "();");
                    return _propertyValue;
                },
                set: (newValue) => {
                    console.log("set '" + _name + "' to '" + newValue + "'");
                    if (_onWillChangeFunction)
                        newValue = _onWillChangeFunction();
                    _propertyValue = newValue;
                }
            });
        }

        static addConstant(object, name, constantValue) {
            var _propertyValue = null;

            Object.defineProperty(object, name, {
                enumerable: true,
                configurable: false,
                value: constantValue
            });
        }



        defaultOnChange(name, oldValue, newValue) {
            console.log("property '" + name + "' changes from '" + oldValue + "' to '" + newValue + "'");
            return newValue;
        }


        static addTypeing(theFunctionOwner, theFunction, theArguments) {
            if (theFunction.name === "bound defaultTypedFunction") return;

            var newTypedFunction = new TypedFunction().init(theFunctionOwner, theArguments, theFunction);
            theFunctionOwner[theFunction.name] = newTypedFunction.defaultTypedFunction.bind(newTypedFunction);
        }

        static isInstanceOf(argValue, expectedArgType, warnOrError) {
            //console.log("instanceof ",argValue.name,expectedArgType.name);
            if (expectedArgType == Interface) return false;
            var argType = typeof argValue;
            if (argType === "object") {

                if (!argValue.type)
                    Thinglish.initType(argValue);


                argType = argValue.type.name;

            }


            if (argType === expectedArgType) {
                //                    console.log(argName + ":" + argType + "  = " + argValue);
                return true;
            } else {
                if (argType === "function") {
                    if (argValue.name === expectedArgType) {
                        //                    console.log(argName + ":" + argType + "  = " + argValue);
                        return true;
                    }
                    if (argValue.name === expectedArgType.name) {
                        //                    console.log(argName + ":" + argType + "  = " + argValue);
                        return true;
                    }
                }
                if (argType === expectedArgType.name) {
                    return true;
                }
                var result = false;
                if (argValue.type && argValue.type.extends)
                    result = Thinglish.isInstanceOf(argValue.type.extends, expectedArgType, warnOrError);
                if (result == true)
                    return true;
                if (expectedArgType.type && expectedArgType.type.extends)
                    result = Thinglish.isInstanceOf(argValue, expectedArgType.type.extends, warnOrError);

                if (argValue.type && argValue.type.implements) {
                    result = argValue.type.implements.some(
                        type => {
                            if (expectedArgType == Interface) return false;
                            return Thinglish.isInstanceOf(type, expectedArgType, warnOrError);
                        }
                    );
                }
                if (expectedArgType.type && expectedArgType.type.implements) {
                    result = expectedArgType.type.implements.some(
                        type => {
                            if (type == Interface) return false;
                            return Thinglish.isInstanceOf(argValue, type, warnOrError);
                        }
                    );
                }

                return result;
            }
        }

        static throwTypeError(argName, argValue, expectedArgType, contextMessage) {
            var argType = typeof argValue;
            if (!contextMessage)
                contextMessage = "TypeError: "
            var message = contextMessage +
                "expected type [" + expectedArgType + "] does not match " +
                "values type [" + argType + "] in value " + argValue

            var behaviour = Thinglish.onTypeError;
            switch (behaviour) {
                case Thinglish.SILENT_ON_TYPE_ERROR:
                    return false;
                case Thinglish.WARN_ON_TYPE_ERROR:
                    console.warn(message);
                    return true;
                case Thinglish.SILENT_ON_TYPE_ERROR:
                    throw new TypeError(message);
            }
        }

        static implement(object, anInterface, silent) {
            var typeMaches = Thinglish.isInstanceOf(anInterface, "Interface");
            if (!typeMaches) {
                Thinglish.throwTypeError("anInterface", anInterface, "Interface",
                    "Impplement " + anInterface.name + "  "
                )
            }
            // @todo for classes: on Object creation initialize all implemented interfaces
            /*
            if (Namespace.isClass(object))
                console.error("implement is for objects not for classes: ",object);
            */

            //var testInstance = anInterface.getInstance();
            var prototype = Object.getOwnPropertyDescriptors(anInterface.prototype);
            var name = null;
            var descriptor = null;
            var objectPrototype = Object.getPrototypeOf(object);
            var objectProperty = null;
            if (!object.type)
                Thinglish.initType(object);

            for (var name in prototype) {
                descriptor = prototype[name];
                objectProperty = Object.getOwnPropertyDescriptor(objectPrototype, name);
                if (name == "init") {
                    continue;
                }
                if (name == "constructor") {
                    /*
                    var interfaceInitialization = new anInterface();
                    if (interfaceInitialization.init)
                        interfaceInitialization.init(object);
                    object.type.intefaceDelegate[anInterface.name] = interfaceInitialization;
                    */
                    continue;
                }

            if (!object._private.interfaces) object._private.interfaces = {};
            if (!object._private.interfaces[anInterface.name]) object._private.interfaces[anInterface.name] = {eamLayer:3};
            if (descriptor.value)// @todo assign the original function of the interface
                object._private.interfaces[anInterface.name][name] = descriptor.value.bind(object);
            else 
              if (!object._private.interfaces[anInterface.name][name]) 
                object._private.interfaces[anInterface.name][name] = anInterface.name+"."+name+"() not found. Its a property.";

                if (!objectProperty && !object[name]) {


                    Object.defineProperty(objectPrototype, name, descriptor);
                    if (!(silent == true)) {
                        Thinglish.throwTypeError(object.name, objectProperty, name, "Class " + object.type.name + " did not implement method: ")
                        console.log("implement " + name);
                    }
                }
            }
            if (object.constructor.type) {
                if (!Namespace.isClass(object))
                    Thinglish._addInterface(object.constructor.type, anInterface);
                Thinglish._addInterface(object.type, anInterface);
                Thinglish._addImplementation(object.type.class, anInterface);
            }
        }

        static _addInterface(type, anInterface) {
            var interfaces = type.implements
            if (!interfaces) {
                interfaces = type.implements = [];
            }
            if (interfaces.indexOf(anInterface) == -1)
                interfaces.push(anInterface);
        }

        static _addImplementation(type, anInterface) {
            if (!anInterface.type)
                Namespace.declare("tla.EAM.layer3", anInterface);
            //                Thinglish.initType(anInterface);

            var implementations = anInterface.type.implementations
            if (!implementations) {
                implementations = anInterface.type.implementations = [];
            }
            if (implementations.indexOf(type) == -1)
                implementations.push(type);
        }

        static isInterface(anInterface, createInterface) {
            if (!anInterface.type)
                Thinglish.initType(anInterface);
            if (createInterface) {
                anInterface.type.extends = Interface;
            }
            return Thinglish.isInstanceOf(anInterface, "Interface");
        }
        static isClass(aClass) {
            return aClass.toString().substring(0, 6) === 'class ';
        }
        isClass(aClass) {
            if (!aClass)
                aClass = this;
            return Namespace.isClass(aClass);
        }
        
        static isFunction(aFunction){
        	return typeof(aFunction) === "function";        	
        }

        static isPrimitive(aPrimitive){
        	return (aPrimitive !== Object(aPrimitive)) || 
        			(typeof(aPrimitive) == "string" || aPrimitive instanceof String) ||
        			(typeof(aPrimitive) == "number" || aPrimitive instanceof Number) ||
        			(typeof(aPrimitive) == "boolean" || aPrimitive instanceof Boolean);
        }
        
        static getOwnPropertyDescriptorArray(object) {
            var ownPropertyDescriptors = Object.getOwnPropertyDescriptors(object);
            var array = [];
            for (var prop in ownPropertyDescriptors) { 
                ownPropertyDescriptors[prop].propertyName = prop;
                array.push(ownPropertyDescriptors[prop]);
            }
            return array;
        }

        static cloneValue(source,target) {
            this.getOwnPropertyDescriptorArray(source).forEach(
            descriptor => {
                if (!descriptor.propertyName.startsWith("_"))
                        target[descriptor.propertyName] = source[descriptor.propertyName];
            });
            return target;
        }


    }
);

var TypedFunction = Namespace.declare("tla.EAM.layer1.ThinglishHelper",
    class TypedFunction {
        constructor() {
            this.name = "uninitalizedTypedFunction";
            //                Object.defineProperty(this,"name",{value:untypedFunction.name, writable:false})                
            this.type = {
                name: this.constructor.name,
                "extends": ["function"]
            }
            this.owner = null;
            this.untypedFunction = null;
            this.argumentTypes = null;
        }
        init(owner, argumentDescriptor, untypedFunction) {
            this.name = this.defaultTypedFunction;
            this.owner = owner;

            this.untypedFunction = untypedFunction;
            this.argumentDescriptor = argumentDescriptor;
            this.argumentTypes = [];
            this.argumentNames = [];
            this.returnDescriptor = null;
            for (var name in argumentDescriptor) {
                //console.log(name + ": " + argumentDescriptor[name]);
                if (name !== "return") {
                    this.argumentNames.push(name);
                    this.argumentTypes.push(argumentDescriptor[name]);
                } else
                    this.returnDescriptor = { "return": argumentDescriptor[name] };
            }

            return this;
        }

        defaultTypedFunction() {

            //console.log(this.type.name + " defaultTypedFunction arguments:"
            var expectedArgType = this.argumentTypes;

            if (arguments.length != expectedArgType.length)
                Thinglish.throwTypeError(expectedArgType, arguments.length, expectedArgType.length,
                    "call " + this.owner.type.name + "." + this.untypedFunction.name + "( WRONG NUMBER OF ARGUMENTS )   "
                )

            var argValue = null;
            var argName = null;
            var argType = null;
            for (var argIndex in arguments) {
                argValue = arguments[argIndex];
                argName = this.argumentNames[argIndex];
                argType = typeof argValue;
                expectedArgType = this.argumentTypes[argIndex];

                var typeMaches = Thinglish.isInstanceOf(argValue, expectedArgType, false);
                if (!typeMaches) {
                    Thinglish.throwTypeError(argName, argValue, expectedArgType,
                        "call " + this.owner.type.name + "." + this.untypedFunction.name + "(...Parameter#" + argIndex + " " + argName + ", ...)   "
                    )
                }

            }
            var result = this.untypedFunction.apply(this.owner, arguments);
            if (this.returnDescriptor) {
                var typeMaches = Thinglish.isInstanceOf(result, this.returnDescriptor.return, false);
                if (!typeMaches) {
                    Thinglish.throwTypeError(argName, argValue, expectedArgType,
                        "Invalid ReturnType " + this.owner.type.name + "." + this.untypedFunction.name + "():" + this.returnDescriptor.return+"   "
                    )
                }
            }
        }
    }
);


var Interface = Namespace.declare("tla.EAM.layer1.ThinglishHelper",
    class Interface {
        static discover() {
            return this.type.implementations;
        }
        static isInterface(object) {
            if (!object)
                object = this;
            return Thinglish.isInstanceOf(object, "Interface");
        }
        constructor() {
            //console.error("Interface cannot be instanciated");
        }
        init(object) {
            Thinglish.implement(object, this);
            this.implementation = object;
            return this;
        }
        get eamLayer() { return 3 };

        interfaceMethod() {
            return "Inferface Hello World"
        }


    }
);

var TestInterface = Namespace.declare("test.Thinglish",
    class TestInterface extends Interface {
        constructor() {
            //            throw Error("cannot instanciate an interface: " + this.constructor.name);
            super();
        }
        init() {
            throw Error("cannot instanciate an interface: " + this.constructor.name);
            return this;
        }
        testInterfaceMethod() {
            return "TestInferface Hello World"
        }
    }
);


Namespace.declare("test.Thinglish",
    class Test extends Thing {
        constructor() {
            super();
            Thinglish.implement(this, TestInterface);

            Thinglish.addTypeing(this, this.untypedMethod, {
                aString: "string",
                aNumber: "number",
                aFunction: "function",
                anLoader: "Loader",
                aThing: "Thing",
                "return": "Promise"
            });

        }
        init() {
            return this;
        }

        untypedMethod(aString, aNuber, aFunction, anInterface, aThing) {
            console.log(this.type.name + " untypedMethod arguments:")
            for (var a in arguments) {
                console.log(a + ": " + arguments[a]);
            }
        }
    }
);