# Source: ui/loader.reel/loader.js

    /**
     * @module "montage/ui/loader.reel"
     */
    var ComponentModule = require("../component"),
        Component = ComponentModule.Component,
        RootComponent = ComponentModule.__root__,
        logger = require("../../core/logger").logger("loader"),
        defaultEventManager = require("../../core/event/event-manager").defaultEventManager,
        MONTAGE_LOADER_ELEMENT_ID = "montage-app-loader",
        BOOTSTRAPPING_CLASS_NAME = "montage-app-bootstrapping",
        LOADING_CLASS_NAME = "montage-app-loading",
        FIRST_LOADING_CLASS_NAME = "montage-app-first-load",
        LOADED_CLASS_NAME = "montage-app-loaded";

    /**
     * @const
     * @type {number}
     * @default
     */
    var BOOTSTRAPPING = 0,
        LOADING = 1,
        LOADED = 2;

    /**
     @class Loader
     @extends Component
     */
    exports.Loader = Component.specialize( /** @lends Loader.prototype # */ {

        // Configuration Properties

        /**
         * The main module to require
         */
        mainModule: {
            value: "ui/main.reel"
        },

        /**
         * The name of the object to read from the mainModule exports
         */
        mainName: {
            value: "Main"
        },

        /**
         * Whether or not to include framework modules in the collection of required and initialized modules
         */
        includeFrameworkModules: {
            value: false
        },

        /**
         * The minimum amount of time the bootstrapping indicator must be shown for
         */
        minimumBootstrappingDuration: {
            value: 0
        },

        /**
         * The minimum amount of time the loading indicator must be shown for
         */
        minimumLoadingDuration: {
            value: 0
        },

        minimumFirstLoadingDuration: {
            value: null
        },

        minimumFirstBootstrappingDuration: {
            value: null
        },

        _initializedModules: {
            value: null
        },

        element: {
            get: function () {
                if (!this._element) {
                    var loaderElement = document.getElementsByClassName("loading")[0];
                    if (!loaderElement) {
                        loaderElement = document.createElement("div");
                        document.body.appendChild(loaderElement);
                    }
                    this.element = loaderElement;
                }

                return this._element;
            },
            set: function (element) {
                Object.getOwnPropertyDescriptor(Component.prototype, "element").set.call(this, element);
            }
        },

        /**
         */
        initializedModules: {
            dependencies: ["includeFrameworkModules"],
            enumerable: false,
            get: function () {
                if (!this._initializedModules || this.includeFrameworkModules) {
                    return this._initializedModules;
                } else {
                    return this._initializedModules.slice(this._frameworkModuleCount - 1);
                }
            },
            set: function (value) {
                this._initializedModules = value;
            }
        },

        _requiredModules: {
            value: null
        },

        /**
         */
        requiredModules: {
            dependencies: ["includeFrameworkModules"],
            enumerable: false,
            get: function () {
                if (!this._requiredModules || this.includeFrameworkModules) {
                    return this._requiredModules;
                } else {
                    return this._requiredModules.slice(this._frameworkModuleCount - 1);
                }
            },
            set: function (value) {
                this._requiredModules = value;
            }
        },

        // States

        _currentStage: {
            value: BOOTSTRAPPING
        },

        /**
         */
        currentStage: {
            get: function () {
                return this._currentStage;
            },
            set: function (currentStage) {
                if (currentStage === this._currentStage) {
                    return;
                }

                if (logger.isDebug) {
                    logger.debug(this, "CURRENT STAGE: " + currentStage);
                }

                this._currentStage = currentStage;

                // Reflect the current loading stage
                if (LOADING === currentStage) {
                    RootComponent.classList.remove(BOOTSTRAPPING_CLASS_NAME);
                    RootComponent.classList.add(LOADING_CLASS_NAME);

                } else if (LOADED === currentStage) {
                    RootComponent.classList.remove(BOOTSTRAPPING_CLASS_NAME);
                    RootComponent.classList.remove(LOADING_CLASS_NAME);
                    RootComponent.classList.add(LOADED_CLASS_NAME);

                    this.needsDraw = true;
                }
            }
        },

        _readyToShowLoader: {
            value: false
        },

        /**
         * Whether the loader is loading the application's main component at this
         * time.
         * @type {boolean}
         */
        isLoadingMainComponent: {
            value: null
        },

        /**
         */
        readyToShowLoader: {
            get: function () {
                return this._readyToShowLoader;
            },
            set: function (value) {
                if (value !== this._readyToShowLoader) {
                    return;
                }

                this._readyToShowLoader = value;
                this.needsDraw = true;
            }
        },

        /**
         * Specifies whether the main component is ready to be displayed.
         */
        readyToShowMainComponent: {
            get: function () {
                return !!this._mainComponent;
            }
        },

        // Internal Properties

        _frameworkModuleCount: {
            enumerable: false,
            value: null
        },

        hasTemplate: {
            enumerable: false,
            value: false
        },

        _mainComponent: {
            value: null
        },

        _mainComponentEnterDocument: {
            value: null
        },

        _showLoadingTimeout: {
            enumerable: false,
            value: null
        },

        _showMainComponentTimeout: {
            enumerable: false,
            value: null
        },

        // Implementation

        enterDocument: {
            value: function () {
                if (logger.isDebug) {
                    logger.debug(this, "enterDocument");
                }

                this._loadLoaderContext();
                this._loadMainComponent();

                this.readyToShowLoader = true;

                var timing = document._montageTiming,
                    bootstrappingEndTime = Date.now(),
                    remainingBootstrappingDelay = this.minimumBootstrappingDuration - (bootstrappingEndTime - timing.bootstrappingStartTime);

                if (remainingBootstrappingDelay > 0) {
                    if (logger.isDebug) {
                        logger.debug(this, "still need to show bootstrapper for another " + remainingBootstrappingDelay + "ms");
                    }

                    var self = this;

                    this._showLoadingTimeout = setTimeout(function () {
                        timing.bootstrappingEndTime = Date.now();
                        self._showLoadingTimeout = null;
                        self._revealLoader();
                    }, remainingBootstrappingDelay);

                } else {
                    timing.bootstrappingEndTime = bootstrappingEndTime;

                    this._revealLoader();
                }
            }
        },

        _loadLoaderContext: {
            value: function () {
                if (this.application.isFirstLoad) {
                    // RootComponent
                    RootComponent.classList.add(FIRST_LOADING_CLASS_NAME);

                    if (this.minimumFirstLoadingDuration !== null) {
                        this.minimumLoadingDuration = this.minimumFirstLoadingDuration;
                    }

                    if (this.minimumFirstBootstrappingDuration !== null) {
                        this.minimumBootstrappingDuration = this.minimumFirstBootstrappingDuration;
                    }
                }
            }
        },

        _revealLoader: {
            value: function () {
                if (logger.isDebug) {
                    logger.debug(this, "_revealLoader");
                }

                document._montageTiming.loadingStartTime = Date.now();
                this.currentStage = LOADING;
                this._waitForLoadingIndicatorIfNeeded();

                var i,
                    loaderElement = document.getElementById(MONTAGE_LOADER_ELEMENT_ID), // ???
                    children,
                    iChild,
                    iComponent;

                if (loaderElement) {
                    children = loaderElement.children;

                    for (i = 0; (iChild = children[i]); i++) {
                        if ((iComponent = iChild.component)) {
                            iComponent.attachToParentComponent();
                            iComponent.needsDraw = true;
                        }
                    }
                }

            }
        },

        _revealMainComponent: {
            value: function () {
                if (logger.isDebug) {
                    logger.debug(this, "_revealMainComponent");
                }
                this.currentStage = LOADED;
            }
        },

        _loadMainComponent: {
            value: function () {
                if (logger.isDebug) {
                    logger.debug(this, "_loadMainComponent");
                }

                this.isLoadingMainComponent = true;
                var self = this;

                return global.require.async(this.mainModule).then(function (exports) {
                    if (!(self.mainName in exports)) {
                        throw new Error(self.mainName + " was not found in " + self.mainModule);
                    }
                    return self._mainLoadedCallback(exports);
                });
            }
        },

        _mainLoadedCallback: {
            value: function (exports) {
                if (logger.isDebug) {
                    logger.debug(this, "_mainLoadedCallback");
                }
                // We've loaded the class for the mainComponent
                // instantiate it and lets find out what else we need to load
                // based on its template
                this._mainComponent = new exports[this.mainName]();
                this._mainComponentEnterDocument = this._mainComponent.enterDocument;
                this._mainComponent.enterDocument = this.mainComponentEnterDocument.bind(this);
                this._mainComponent.setElementWithParentComponent(document.createElement("div"), this);
                this._mainComponent.attachToParentComponent();
                this._mainComponent._canDrawOutsideDocument = true;
                this._mainComponent.needsDraw = true;
                return this;
            }
        },

        mainComponentEnterDocument: {
            value: function () {
                var mainComponent = this._mainComponent;

                if (logger.isDebug) {
                    logger.debug(this, "main preparing to draw");
                }

                this.isLoadingMainComponent = false;

                // Add new content so mainComponent can actually draw
                this.childComponents = [this._mainComponent];
                this.element.parentElement.appendChild(this._mainComponent.element);

                this._waitForLoadingIndicatorIfNeeded();

                // Remove the connection from the Loader to the DOM tree and add
                // the main component to the component tree.
                defaultEventManager.unregisterEventHandlerForElement(this.element);
                mainComponent.attachToParentComponent();

                mainComponent.enterDocument = this._mainComponentEnterDocument;

                if (mainComponent.enterDocument) {
                    return mainComponent.enterDocument.apply(mainComponent, arguments);
                }
            }
        },

        _waitForLoadingIndicatorIfNeeded: {
            value: function () {
                if (!this._showMainComponentTimeout && !this.isLoadingMainComponent && !this._showLoadingTimeout) {
                    var timing = document._montageTiming,
                        now = Date.now(),
                        self = this,
                        remainingLoadingDelay = this.minimumLoadingDuration - (now - timing.loadingStartTime);

                    if (remainingLoadingDelay > 0) {
                        if (logger.isDebug) {
                            logger.debug(this, "show loader for another " + remainingLoadingDelay + "ms");
                        }

                        this._showMainComponentTimeout = setTimeout(function () {
                            if (logger.isDebug) {
                                logger.debug(this, "ok, shown loader long enough");
                            }

                            timing.loadingEndTime = Date.now();
                            self._revealMainComponent();

                        }, remainingLoadingDelay);

                    } else { // we showed loading indicator long enough, go ahead and show mainComponent
                        timing.loadingEndTime = now;
                        this._revealMainComponent();
                    }
                }
            }
        },

        draw: {
            value: function () {
                if (LOADED === this._currentStage) {
                    this._dispatchLoadEvent();
                    // Remove the Loader from the component tree, we can only do
                    // this after the last draw the Loader needs to make.
                    this.detachFromParentComponent();
                    this.element.parentElement.removeChild(this.element);
                }
            }
        },

        _dispatchLoadEvent: {
            value: function() {
                var loadEvent = document.createEvent("CustomEvent");
                    loadEvent.initCustomEvent("componentLoaded", true, true, this._mainComponent);
                this.dispatchEvent(loadEvent, true, true);
            }
        }

    });