/*global define,Promise*/

/**
 * Module defining ServiceCompositor. Created by vwoeltje on 11/5/14.
 */
define(
    [],
    function () {
        "use strict";

        /**
         * Handles service compositing; that is, building up services
         * from provider, aggregator, and decorator components.
         *
         * @constructor
         */
        function ServiceCompositor(app, $log) {
            var latest = {},
                providerLists = {}; // Track latest services registered

            // Log a warning; defaults to "no service provided by"
            function warn(extension, category, message) {
                var msg = message || "No service provided by";
                $log.warn([
                    msg,
                    " ",
                    category,
                    " ",
                    extension.key,
                    " from bundle ",
                    (extension.bundle || { path: "unknown bundle" }).path,
                    "; skipping."
                ].join(""));
            }

            // Echo arguments; used to represent groups of non-built-in
            // extensions as a single dependency.
            function echoMany() {
                return Array.prototype.slice.call(arguments);
            }

            // Echo arguments; used to represent groups of non-built-in
            // extensions as a single dependency.
            function echoSingle(value) {
                return value;
            }

            // Generates utility functions to match types (one of
            // provider, aggregator, or decorator) of component. Used
            // to filter down to specific types, which are handled
            // in order.
            function hasType(type) {
                return function (extension) {
                    return extension.type === type;
                };
            }

            // Make a unique name for a service component.
            function makeName(category, service, index) {
                return [
                    service,
                    "[",
                    category,
                    "#",
                    index,
                    "]"
                ].join("");
            }

            // Register a specific provider instance with Angular, and
            // record its name for subsequent stages.
            function registerProvider(provider, index) {
                var service = provider.provides,
                    dependencies = provider.depends || [],
                    name = makeName("provider", service, index);

                if (!service) {
                    return warn(provider, "provider");
                }

                providerLists[service] = providerLists[service] || [];
                providerLists[service].push(name);

                // This provider is the latest candidate for resolving
                // the composite service.
                latest[service] = name;

                app.service(name, dependencies.concat([provider]));
            }

            // Register an array of providers as a single dependency;
            // aggregators will then depend upon this to consume all
            // aggregated providers as a single dependency.
            function registerProviderSets() {
                Object.keys(providerLists).forEach(function (service) {
                    var name = makeName("provider", service, "*"),
                        list = providerLists[service];

                    app.service(name, list.concat([echoMany]));
                });
            }

            // Registers an aggregator via Angular, including both
            // its declared dependencies and the additional, implicit
            // dependency upon the array of all providers.
            function registerAggregator(aggregator, index) {
                var service = aggregator.provides,
                    dependencies = aggregator.depends || [],
                    providerSetName = makeName("provider", service, "*"),
                    name = makeName("aggregator", service, index);

                if (!service) {
                    return warn(aggregator, "aggregator");
                }

                // Aggregators need other services to aggregate, otherwise they
                // do nothing.
                if (!latest[service]) {
                    return warn(
                        aggregator,
                        "aggregator",
                        "No services to aggregate for"
                    );
                }

                dependencies = dependencies.concat([providerSetName]);
                latest[service] = name;

                app.service(name, dependencies.concat([aggregator]));
            }

            // Registers a decorator via Angular, including its implicit
            // dependency on the latest service component which has come
            // before it.
            function registerDecorator(decorator, index) {
                var service = decorator.provides,
                    dependencies = decorator.depends || [],
                    name = makeName("decorator", service, index);

                if (!service) {
                    return warn(decorator, "decorator");
                }

                // Decorators need other services to decorate, otherwise they
                // do nothing.
                if (!latest[service]) {
                    return warn(
                        decorator,
                        "decorator",
                        "No services to decorate for"
                    );
                }

                dependencies = dependencies.concat([latest[service]]);
                latest[service] = name;

                app.service(name, dependencies.concat([decorator]));
            }

            // Alias the latest services of various types back to the
            // more general service declaration.
            function registerLatest() {
                Object.keys(latest).forEach(function (service) {
                    app.service(service, [latest[service], echoSingle]);
                });
            }

            // Register composite services in phases:
            // * Register providers
            // * Register aggregators (which use providers)
            // * Register decorators (which use anything)
            // Then, register the latest candidate as a plain service.
            function registerComposites(providers, aggregators, decorators) {
                providers.forEach(registerProvider);
                registerProviderSets();
                aggregators.forEach(registerAggregator);
                decorators.forEach(registerDecorator);
                registerLatest();
            }

            // Initial point of entry; just separate components by type
            function registerCompositeServices(components) {
                registerComposites(
                    components.filter(hasType("provider")),
                    components.filter(hasType("aggregator")),
                    components.filter(hasType("decorator"))
                );
            }

            return {
                /**
                 * Register composite services with Angular. This will build
                 * up a dependency hierarchy between providers, aggregators,
                 * and/or decorators, such that a dependency upon the service
                 * type they expose shall be satisfied by their fully-wired
                 * whole.
                 *
                 * Note that this method assumes that a complete set of
                 * components shall be provided. Multiple calls to this
                 * method may not behave as expected.
                 *
                 * @param {Array} components extensions of category component
                 */
                registerCompositeServices: registerCompositeServices
            };
        }

        return ServiceCompositor;
    }
);