//jshint eqnull:true

define(['./junk-drawer'], function ($goodies) {
    "use strict";

    var inherits = $goodies.inherits;
    var declareJsMethod = $goodies.declareJsMethod;
    var addElement = $goodies.addElement;
    var removeElement = $goodies.removeElement;
    var extend = $goodies.extend;
    var deleteKeysFrom = $goodies.deleteKeysFrom;

    MethodCompositionBrik.deps = ["methods"];

    function MethodCompositionBrik (brikz, st) {
        var setLocalMethods = brikz.methods.setLocalMethods;
        var updateMethod = brikz.methods.updateMethod;

        function aliased (selector, method) {
            var result = st.method(method, method.instantiateFn);
            if (method.selector !== selector) {
                result.selector = selector;
                result.source = '"Aliased as ' + selector + '"\n' + method.source;
            }
            result.owner = method.owner;
            return result;
        }

        function fillTraitTransformation (traitTransformation, obj) {
            // assert(Object.getOwnProperties(obj).length === 0)
            var traitMethods = traitTransformation.trait.methods;
            Object.keys(traitMethods).forEach(function (selector) {
                obj[selector] = aliased(selector, traitMethods[selector]);
            });
            var traitAliases = traitTransformation.aliases;
            if (traitAliases) {
                Object.keys(traitAliases).forEach(function (aliasSelector) {
                    var aliasedMethod = traitMethods[traitAliases[aliasSelector]];
                    if (aliasedMethod) obj[aliasSelector] = aliased(aliasSelector, aliasedMethod);
                    // else delete obj[aliasSelector]; // semantically correct; optimized away
                });
            }
            var traitExclusions = traitTransformation.exclusions;
            if (traitExclusions) {
                deleteKeysFrom(traitExclusions, obj);
            }
            return obj;
        }

        function buildCompositionChain (traitComposition) {
            return traitComposition.reduce(function (soFar, each) {
                return fillTraitTransformation(each, Object.create(soFar));
            }, null);
        }

        st.setTraitComposition = function (traitComposition, traitOrBehavior) {
            var oldLocalMethods = traitOrBehavior.localMethods,
                newLocalMethodsTemplate = Object.create(buildCompositionChain(traitComposition));
            setLocalMethods(traitOrBehavior, extend(newLocalMethodsTemplate, oldLocalMethods));
            (traitOrBehavior.traitComposition || []).forEach(function (each) {
                removeElement(each.trait.traitUsers, traitOrBehavior);
            });
            traitOrBehavior.traitComposition = traitComposition && traitComposition.length ? traitComposition : null;
            (traitOrBehavior.traitComposition || []).forEach(function (each) {
                addElement(each.trait.traitUsers, traitOrBehavior);
            });
        };

        function aliasesOfSelector (selector, traitAliases) {
            if (!traitAliases) return [selector];
            var result = Object.keys(traitAliases).filter(function (aliasSelector) {
                return traitAliases[aliasSelector] === selector
            });
            if (!traitAliases[selector]) result.push(selector);
            return result;
        }

        function applyTraitMethodAddition (selector, method, traitTransformation, obj) {
            var changes = aliasesOfSelector(selector, traitTransformation.aliases);
            changes.forEach(function (aliasSelector) {
                obj[aliasSelector] = aliased(aliasSelector, method);
            });
            var traitExclusions = traitTransformation.exclusions;
            if (traitExclusions) {
                deleteKeysFrom(traitExclusions, obj);
            }
            return changes;
        }

        function applyTraitMethodDeletion (selector, traitTransformation, obj) {
            var changes = aliasesOfSelector(selector, traitTransformation.aliases);
            deleteKeysFrom(changes, obj);
            return changes;
        }

        function traitMethodChanged (selector, method, trait, traitOrBehavior) {
            var traitComposition = traitOrBehavior.traitComposition,
                chain = traitOrBehavior.localMethods,
                changes = [];
            for (var i = traitComposition.length - 1; i >= 0; --i) {
                chain = Object.getPrototypeOf(chain);
                var traitTransformation = traitComposition[i];
                if (traitTransformation.trait !== trait) continue;
                changes.push.apply(changes, method ?
                    applyTraitMethodAddition(selector, method, traitTransformation, chain) :
                    applyTraitMethodDeletion(selector, traitTransformation, chain));
            }
            // assert(chain === null);
            changes.forEach(function (each) {
                updateMethod(each, traitOrBehavior);
            });
        }

        this.traitMethodChanged = traitMethodChanged;
    }

    function LanguageFactory (specialConstructors, emit) {
        function declareEvent (name) {
            declareJsMethod(emit, name);
        }

        TraitsBrik.deps = ["behaviorals", "methods", "composition", "root"];

        function TraitsBrik (brikz, st) {
            var SmalltalkObject = brikz.root.Object;
            var setupMethods = brikz.methods.setupMethods;
            var traitMethodChanged = brikz.composition.traitMethodChanged;
            var buildTraitOrClass = brikz.behaviorals.buildTraitOrClass;

            function SmalltalkTrait () {
            }

            specialConstructors.Trait = inherits(SmalltalkTrait, SmalltalkObject);

            SmalltalkTrait.prototype.trait = true;
            declareJsMethod(SmalltalkTrait.prototype, "toString");
            declareJsMethod(SmalltalkTrait.prototype, "added");
            declareJsMethod(SmalltalkTrait.prototype, "removed");
            declareJsMethod(SmalltalkTrait.prototype, "methodAdded");
            declareJsMethod(SmalltalkTrait.prototype, "methodRemoved");

            SmalltalkTrait.prototype.toString = function () {
                return 'Smalltalk Trait ' + this.name;
            };

            SmalltalkTrait.prototype.methodAdded = function (method) {
                propagateMethodChange(this, method.selector, method);
            };

            SmalltalkTrait.prototype.methodRemoved = function (method) {
                propagateMethodChange(this, method.selector, null);
            };

            function propagateMethodChange (trait, selector, method) {
                trait.traitUsers.forEach(function (each) {
                    traitMethodChanged(selector, method, trait, each);
                });
            }

            function traitBuilder (traitName, category) {
                return {
                    name: traitName,
                    make: function () {
                        var that = new SmalltalkTrait();
                        that.name = traitName;
                        that.category = category;
                        that.traitUsers = [];
                        setupMethods(that);
                        return that;
                    },
                    updateExisting: function (trait) {
                    }
                };
            }

            st.addTrait = function (className, category) {
                return buildTraitOrClass(traitBuilder(className, category));
            };
        }

        ClassModelBrik.deps = ["root", "nil"];

        function ClassModelBrik (brikz, st) {
            var SmalltalkRoot = brikz.root.Root;
            var SmalltalkObject = brikz.root.Object;
            var nilAsReceiver = brikz.nil.nilAsReceiver;

            function SmalltalkBehavior () {
            }

            function SmalltalkClass () {
            }

            function SmalltalkMetaclass () {
            }

            this.newMetaclass = function () {
                return new SmalltalkMetaclass();
            };

            specialConstructors.Behavior = inherits(SmalltalkBehavior, SmalltalkObject);
            specialConstructors.Class = inherits(SmalltalkClass, SmalltalkBehavior);
            specialConstructors.Metaclass = inherits(SmalltalkMetaclass, SmalltalkBehavior);

            SmalltalkMetaclass.prototype.meta = true;
            declareJsMethod(SmalltalkClass.prototype, "toString");
            declareJsMethod(SmalltalkMetaclass.prototype, "toString");
            declareJsMethod(SmalltalkClass.prototype, "added");
            declareJsMethod(SmalltalkClass.prototype, "removed");
            declareJsMethod(SmalltalkBehavior.prototype, "methodAdded");
            declareJsMethod(SmalltalkBehavior.prototype, "methodRemoved");

            SmalltalkClass.prototype.toString = function () {
                return 'Smalltalk ' + this.name;
            };

            SmalltalkMetaclass.prototype.toString = function () {
                return 'Smalltalk Metaclass ' + this.instanceClass.name;
            };

            declareEvent("classCreated");
            SmalltalkClass.prototype.added = function () {
                registerToSuperclass(this);
                emit.classCreated(this);
            };

            SmalltalkClass.prototype.removed = function () {
                unregisterFromSuperclass(this);
            };

            declareEvent("behaviorMethodAdded");
            SmalltalkBehavior.prototype.methodAdded = function (method) {
                emit.behaviorMethodAdded(method, this);
            };

            declareEvent("behaviorMethodRemoved");
            SmalltalkBehavior.prototype.methodRemoved = function (method) {
                emit.behaviorMethodRemoved(method, this);
            };

            // Fake root class of the system.
            // Effective superclass of all classes created with `nil subclass: ...`.
            var nilAsClass = this.nilAsClass = {
                fn: SmalltalkRoot,
                subclasses: [],
                a$cls: {fn: SmalltalkClass, methods: Object.create(null)}
            };

            this.bootstrapHierarchy = function (realClass) {
                nilAsClass.a$cls = realClass;
                nilAsClass.subclasses.forEach(function (each) {
                    each.a$cls.superclass = realClass;
                    Object.setPrototypeOf(each.a$cls.methods, realClass.methods);
                    registerToSuperclass(each.a$cls);
                });
            };

            function registerToSuperclass (klass) {
                addElement((klass.superclass || nilAsClass).subclasses, klass);
            }

            function unregisterFromSuperclass (klass) {
                removeElement((klass.superclass || nilAsClass).subclasses, klass);
            }

            function metaSubclasses (metaclass) {
                return metaclass.instanceClass.subclasses
                    .filter(function (each) {
                        return !each.meta;
                    })
                    .map(function (each) {
                        return each.a$cls;
                    });
            }

            st.metaSubclasses = metaSubclasses;

            st.traverseClassTree = function (klass, fn) {
                var queue = [klass], sentinel = {};
                for (var i = 0; i < queue.length; ++i) {
                    var item = queue[i];
                    if (fn(item, sentinel) === sentinel) continue;
                    var subclasses = item.meta ? metaSubclasses(item) : item.subclasses;
                    queue.push.apply(queue, subclasses);
                }
            };

            /**
             * This function is used all over the compiled amber code.
             * It takes any value (JavaScript or Smalltalk)
             * and returns a proper Amber Smalltalk receiver.
             *
             * null or undefined -> nilAsReceiver,
             * object having Smalltalk signature -> unchanged,
             * otherwise wrapped foreign (JS) object
             */
            this.asReceiver = function (o) {
                if (o == null) return nilAsReceiver;
                else if (o.a$cls != null) return o;
                else return st.wrapJavaScript(o);
            };

            // TODO remove, .iVarNames backward compatibility
            this.__init__ = function () {
                brikz.classConstruction.iVarNamesCompat(SmalltalkBehavior);
            };
        }

        ClassConstructionBrik.deps = ["classModel", "behaviorals", "methods"];

        function ClassConstructionBrik (brikz, st) {
            var nilAsClass = brikz.classModel.nilAsClass;
            var newMetaclass = brikz.classModel.newMetaclass;
            var buildTraitOrClass = brikz.behaviorals.buildTraitOrClass;
            var setupMethods = brikz.methods.setupMethods;
            var removeTraitOrClass = brikz.behaviorals.removeTraitOrClass;

            declareEvent("slotsChanged");

            function setSlots (klass, slots) {
                slots.forEach(function (name) {
                    if (!name.match(/^[a-zA-Z][a-zA-Z0-9]*$/))
                        throw new Error("Wrong identifier name: " + name);
                });

                klass.slots = slots;
                emit.slotsChanged(klass);
            }

            st.setSlots = setSlots;

            // TODO remove, .iVarNames backward compatibility
            this.iVarNamesCompat = function (SmalltalkBehavior) {
                Object.defineProperty(SmalltalkBehavior.prototype, "iVarNames", {
                    enumerable: true,
                    configurable: true,
                    get: function () {
                        return this.slots;
                    },
                    set: function (instanceVariableNames) {
                        setSlots(this, instanceVariableNames);
                    }
                });
            };

            /* Smalltalk class creation. A class is an instance of an automatically
             created metaclass object. Newly created classes (not their metaclass)
             should be added to the system, see smalltalk.addClass().
             Superclass linking is *not* handled here, see api.initialize()  */

            function classBuilder (className, superclass, category, fn) {
                var logicalSuperclass = superclass;
                if (superclass == null || superclass.a$nil) {
                    superclass = nilAsClass;
                    logicalSuperclass = null;
                }

                function klass () {
                    var that = metaclass().instanceClass;

                    that.superclass = logicalSuperclass;
                    that.fn = fn || inherits(function () {
                    }, superclass.fn);
                    that.slots = [];

                    that.name = className;
                    that.category = category;
                    that.subclasses = [];

                    setupMethods(that);
                    return that;
                }

                function metaclass () {
                    var that = newMetaclass();

                    that.superclass = superclass.a$cls;
                    that.fn = inherits(function () {
                    }, that.superclass.fn);
                    that.slots = [];

                    that.instanceClass = new that.fn();

                    wireKlass(that);
                    setupMethods(that);
                    return that;
                }

                return {
                    name: className,
                    make: klass,
                    updateExisting: function (klass) {
                        if (logicalSuperclass == null && klass.superclass != null || logicalSuperclass != null && klass.superclass !== logicalSuperclass || fn != null && fn !== klass.fn)
                            throw new Error("Incompatible change of class: " + klass.name);
                    }
                };
            }

            function wireKlass (klass) {
                Object.defineProperty(klass.fn.prototype, "a$cls", {
                    value: klass,
                    enumerable: false, configurable: true, writable: true
                });
            }

            this.wireKlass = wireKlass;

            /* Add a class to the system, creating a new one if needed.
             A Package is lazily created if one with given name does not exist. */

            st.addClass = function (className, superclass, category) {
                // TODO remove, backward compatibility (note: only deprecated as of this note)
                if (arguments[3]) {
                    var added = st.addClass(className, superclass, arguments[3]);
                    setSlots(added, category);
                    return added;
                }

                // While subclassing nil is allowed, it might be an error, so
                // warn about it.
                if (typeof superclass === 'undefined' || superclass && superclass.a$nil) {
                    console.warn('Compiling ' + className + ' as a subclass of `nil`. A dependency might be missing.');
                }
                return buildTraitOrClass(classBuilder(className, superclass, category, specialConstructors[className]));
            };

            st.removeClass = removeTraitOrClass;
        }

        /* Making smalltalk that can load */

        function configure (brikz) {
            brikz.traits = TraitsBrik;
            brikz.composition = MethodCompositionBrik;
            brikz.classModel = ClassModelBrik;
            brikz.classConstruction = ClassConstructionBrik;

            brikz();
        }

        return {configure: configure};
    }

    return LanguageFactory;
});