﻿namespace MarionetteExt
{
    export interface IApplication
    {
        Name: string;
    }

    export interface ISubApplication extends IApplication
    {
        ShowView<TModel extends Backbone.Model>(view: Backbone.View<TModel>, destroyView?: boolean): void;
    }


    interface IManagedSubApplication extends ISubApplication
    {
        Start(options?: any): void;
        Stop(): void;
        Destroy(): void;
    }

    export interface IManagedMainApplication extends IApplication
    {
        Start(options?: any): void;
        Stop(): void;
        ShowLayoutView<TModel extends Backbone.Model>(layout?: Backbone.Marionette.LayoutView<TModel>, options?: { preventDestroy?: boolean, preserveViewEl?: boolean }): void;
        GetRegion(regName: string, subApp?: SubApplicationExt): AppRegion;
    }

    export class AppRegion
    {
        private _subApp: SubApplicationExt;
        private _region: Backbone.Marionette.Region;

        constructor(region: Backbone.Marionette.Region, subApp?: SubApplicationExt)
        {
            if (subApp !== undefined) this._subApp = subApp;
            this._region = region;
        }

        SwitchApp(subApp: SubApplicationExt): SubApplicationExt
        {
            if (this._subApp !== undefined) this._subApp.Stop();
            this._subApp = subApp;
            return subApp;
        }

        RemoveApp(subApp: SubApplicationExt|string): void
        {
            if (this._subApp !== undefined)
            {
                if (this._subApp.Name == (typeof subApp === "string" ? subApp : subApp.Name))
                {
                    this._region.empty();
                    this._subApp = undefined;
                }
            }
        }

        get el(): any { return this._region.el; }
        set el(value: any) { this._region.el = value; }

        show<TModel extends Backbone.Model>(view: Backbone.View<TModel>, options?: Backbone.Marionette.RegionShowOptions): void
        {
            this._region.show(view, options);
        }

        attachView<TModel extends Backbone.Model>(view: Backbone.View<TModel>, options?: Backbone.Marionette.RegionShowOptions): any
        {
            return this._region.attachView(view, options);
        }


        attachHtml<TModel extends Backbone.Model>(view: Backbone.View<TModel>): void
        {
            this._region.attachHtml(view);
        }

        reset(): any
        {
            return this._region.reset();
        }

        hasView(): boolean
        {
            return this._region.hasView();
        }

        empty(): any
        {
            return this._region.empty();
        }

        get currentView(): Backbone.View<Backbone.Model>
        {
            return this._region.currentView;
        }
    }

    class AppRepository<TType>
    {
        // Attributes
        private _apps: { [id: string]: TType; } = {};   // Dictionary

        // Constructor

        // Properties

        // Methods
        SetApp(appName: string, app: TType): TType
        {
            this._apps[appName] = app;
            return app;
        }

        GetApp(appName: string): TType
        {
            return this._apps[appName];
        }

        RemoveApp(appName: string): TType
        {
            let app: TType;
            if (!this._apps.hasOwnProperty(appName)) return;
            app = this._apps[appName];
            delete this._apps[appName];
            //let index = this._apps.indexOf(app);
            //if (index > -1) this._apps.splice(index, 1);
            return app;
        }

        Keys(): string[]
        {
            let keys: string[] = [];
            for (let key in this._apps)
            {
                keys.push(key);
            }
            return keys;
        }

        //Clone(): { [id: string]: TType; }
        //{
        //    let clone: { [id: string]: TType; } = {};
        //    for (let key in this._apps)
        //    {
        //        if (this._apps.hasOwnProperty(key))
        //        {
        //            clone[key] = this._apps[key];
        //        }
        //    }
        //    return clone;
        //}
    }

    export class AppManager
    {
        // Attributes
        private static _instance: AppManager = new AppManager();
        private _appRepo: AppRepository<IManagedSubApplication>;
        private _mainApp: ApplicationExt;

        // Constructor
        constructor()
        {
            if (AppManager._instance)
            {
                throw new Error("Error: Instantiation failed: Use AppRepository.getInstance() instead of new.");
            }
            AppManager._instance = this;
            this._appRepo = new AppRepository<IManagedSubApplication>();
        }

        // Properties
        static get Instance(): AppManager
        {
            return AppManager._instance;
        }

        get Repository(): AppRepository<IManagedSubApplication> { return this._appRepo; }

        // Methods

        CreateApp(appName: string, AppClass: new (name: string, options?: any) => ApplicationExt, options?: any): IManagedMainApplication
        {
            if (this._mainApp === undefined)    // Only one Backbone.Application allowed!!
            {
                this._mainApp = new AppClass(appName, options);                  
            }         
            return this._mainApp;
        }

        CreateSubApp(appName: string, SubAppClass: new (name: string, region: AppRegion, options?: SubAppExtOpts) => SubApplicationExt, region: AppRegion, options?: SubAppExtOpts): ISubApplication
        {
            return this._appRepo.SetApp(appName, new SubAppClass(appName, region, options));
        }

        // Start Sub-Application.
        // ============================================
        //Start(appN: SubApplicationExt | string, options?: any): IApplication
        //{
        //    let app: IManagedApplication = typeof appN === "string" ? this._appRepo.GetApp(appN) : appN;
        //    let currentApp: IManagedApplication;

        //    if (app === undefined) return;

        //    // App is already running
        //    if (this._currentAppName == app.Name) { return; }

        //    // A new module wants to start, so stop the existing one
        //    // first.

        //    currentApp = this._appRepo.GetApp(this._currentAppName);
        //    if (currentApp !== undefined)
        //    {
        //        currentApp.Stop();
        //    }

        //    // Start the new module.
        //    this._currentAppName = app.Name;
        //    app.Start(options);
        //    return app;
        //}

        Start(appN: IManagedSubApplication | string, options?: any): ISubApplication
        {
            let app: IManagedSubApplication = typeof appN === "string" ? this._appRepo.GetApp(appN) : appN;
            if (app === undefined) return;
            app.Start();
            return app;
        }

        Stop(appN: IManagedSubApplication | string): void
        {
            let app: IManagedSubApplication = typeof appN === "string" ? this._appRepo.GetApp(appN) : appN;
            if (app === undefined) return;
            app.Stop();
        }

        StopAll(): void
        {
            for (let appName of this._appRepo.Keys())
            {
                this._appRepo.RemoveApp(appName).Stop();
            }
        }

        Destroy(appN: IManagedSubApplication | string): void
        {
            let app: IManagedSubApplication = typeof appN === "string" ? this._appRepo.GetApp(appN) : appN;
            if (app === undefined) return;
            this._appRepo.RemoveApp(app.Name).Destroy();
        }
    }

    export interface AppExtOpts
    {
        layout?: Backbone.Marionette.LayoutView<Backbone.Model>;
        // Other options can also be provided as required by Application
    }

    export class ApplicationExt extends Backbone.Marionette.Application implements IManagedMainApplication
    {
        private _name: string;
        private _started: boolean;
        private _layoutView: Backbone.Marionette.LayoutView<Backbone.Model>;

        constructor(name: string, options?: AppExtOpts)
        {
            super(options);
            this._name = name;
            this._started = false;

            if (options !== undefined && options.layout !== undefined) this._layoutView = options.layout;

            // The callback will always be called with listening object as context
            // i.e. the ApplicationExt object. Hence, no need to use Typescript
            // callback syntax for callback method.
            this.listenToOnce(this, 'start', this.OnStarting);

            // Wait until the entire application is initialized before starting Backbone.history
            this.listenToOnce(this, 'start', this.OnStartHistory);
        }

        get Name(): string { return this._name; }

        Start(options?: any): void
        {
            if (!this._started)
            {
                this.start(options)
            }
        }

        Stop(): void
        {
            if (this._started)
            {
                this.trigger('stop');
            }
        }

        OnStartHistory(): void
        {
            if (Backbone.history)
            {
                Backbone.history.start();
            }
        }

        private OnStarting(): void
        {
            // The callback will always be called with listening object as context
            // i.e. the ApplicationExt object. Hence, no need to use Typescript
            // callback syntax for callback method.
            this.listenToOnce(this, 'stop', this.OnStopping);
            this._started = true;
            this.OnStart();
        }

        private OnStopping(): void
        {
            this.stopListening();
            this.DestroyLayout();
            this._started = false;
            this.OnStop();
        }

        // Override these
        OnStart(): void { }
        OnStop(): void { }

        ShowLayoutView<TModel extends Backbone.Model>(layout?: Backbone.Marionette.LayoutView<TModel>, options?: { preventDestroy?: boolean, preserveViewEl?: boolean }): void
        {          
            if (layout !== undefined)
            {
                if (this._layoutView !== undefined && options !== undefined)
                {
                    if (options.preventDestroy === true)
                    {
                        if (options.preserveViewEl)
                        {
                            this._layoutView.$el.empty();
                            this._layoutView.stopListening();
                        }
                        else
                        {
                            this._layoutView.remove();
                        }
                        this._layoutView = undefined;       // Prevent destroy
                    }
                }

                this.DestroyLayout();
                this._layoutView = layout;
            }
            this._layoutView.render();
        }

        private DestroyLayout(): void
        {
            if (this._layoutView !== undefined)
            {
                this._layoutView.destroy();
                this._layoutView = undefined;
            }
        }

        GetRegion(regName: string, subApp?: SubApplicationExt): AppRegion
        {
            return this._layoutView !== undefined ? new AppRegion(this._layoutView.getRegion(regName), subApp) : undefined;
        }
    }

    export interface SubAppExtOpts
    {
        preventDisplay?: boolean
        appData?: any;
    }

    export class Events implements Backbone.Events
    {
        on: {
            (eventName: string, callback?: Function, context?: any): any;
            (eventMap: Backbone.EventsHash): any;
        }
        off: (eventName?: string, callback?: Function, context?: any) => any;
        trigger: (eventName: string, ...args: any[]) => any;
        bind: (eventName: string, callback: Function, context?: any) => any;
        unbind: (eventName?: string, callback?: Function, context?: any) => any;

        once: (events: string, callback: Function, context?: any) => any;
        listenTo: (object: any, events: string, callback: Function) => any;
        listenToOnce: (object: any, events: string, callback: Function) => any;
        stopListening: (object?: any, events?: string, callback?: Function) => any;
    }
    applyEventsMixin(Events, Backbone.Events);

    export class SubApplicationExt extends Events implements IManagedSubApplication
    {
        private _name: string;
        private _started: boolean;
        private _region: AppRegion;

        constructor(name: string, region: AppRegion, options?: SubAppExtOpts)
        {
            super();
            this._name = name;
            this._region = region;
            this._started = false;

            // The callback will always be called with listening object as context
            // i.e. the ApplicationExt object. Hence, no need to use Typescript
            // callback syntax for callback method.
            this.listenTo(this, 'start', this.OnStarting);
            this.listenToOnce(this, 'destroy', this.OnDestroying);
            if (options === undefined || options.preventDisplay !== true)
            {
                this._region.SwitchApp(this);
            }
        }

        get Name(): string { return this._name; }
        get Region(): AppRegion { return this._region; }

        Start(options?: any): void
        {
            if (!this._started)
            {
                this.trigger('start', options);
            }
        }

        Stop(): void
        {
            if (this._started)
            {
                this.trigger('stop');
            }
        }

        Destroy(): void
        {
            if (this._started)
            {
                this.trigger('destroy');
            }
        }

        private OnStarting(options?: any): void
        {
            this._region.SwitchApp(this);
            this.listenToOnce(this, 'stop', this.OnStopping);
            this._started = true;
            this.OnStart(options);
        }

        private OnStopping(): void
        {
            this._region.RemoveApp(this);
            this._started = false;
            this.OnStop();
        }

        private OnDestroying(): void
        {
            this.stopListening();
            if (this._started) this.OnStopping();
            this.OnDestroy();
        }

        // Override these
        OnStart(options?: any): void { }
        OnStop(): void { }
        OnDestroy(): void { }


        ShowView<TModel extends Backbone.Model>(view: Backbone.View<TModel>, destroyView?: boolean): void
        {
            if (destroyView !== undefined && destroyView == true)
                this._region.show(view);
            else
                this._region.show(view, { preventDestroy: true });  // Default
        }
    }

    export class AppRouterExt extends Backbone.Marionette.AppRouter
    {
        private _controller: any;

        constructor(options?: Backbone.Marionette.AppRouterOptions)
        {
            super(options);
            if (options !== undefined && options.controller !== undefined) this._controller = options.controller;

            if (this._controller !== undefined
            && this._controller.RenderLayout !== undefined
            && typeof this._controller.RenderLayout === "function")
            {
                // Have to catch the case where the URL refers to the subapp when the page is first loaded
                // because the main app router will not catch this route change so the main controller won't
                // render the page.
                // We only need to do this when the page first loads, hence we listen for the event 'once'.
                Backbone.Wreqr.radio.channel('routing').vent.once('route:changed', _.bind(this._controller.RenderLayout, this._controller));
            }
        }

        get Controller(): any { return this._controller; }

        // Override
        execute(callback: Function, args: any[], name: string): void
        {
            Backbone.Wreqr.radio.channel('routing').vent.trigger('route:changed', args);
            super.execute(callback, args, name);
        }

        destroy(): void
        {
            // Only keep route handlers that don't belong to the provided router
            Backbone.history.handlers = _.filter(Backbone.history.handlers, function (handler)
            {
                return (handler.router !== this);
            }, this);
        }
    }
}

function applyEventsMixin(derivedCtor: any, events: any)
{
    Object.getOwnPropertyNames(events).forEach(name =>
    {
        derivedCtor.prototype[name] = events[name];
    });
}



/*
    export class AppRouterExt extends Backbone.Marionette.AppRouter
    {
        private _app: IApplication;
        private _controller: any;

        constructor(options?: AppRouterExtOptions)
        {
            super(options);
            if (options !== undefined)
            {
                if (options.application !== undefined)
                {
                    this._app = typeof options.application !== "string" ? options.application : AppManager.Instance.Repository.GetApp(options.application);
                }
                if (options.controller !== undefined) this._controller = options.controller;
            }
        }

        get Controller(): any { return this._controller; }

        // Override
        processAppRoutes(controller: any, appRoutes: any): void
        {
            if (appRoutes === undefined) { return; }

            let routeNames: string[] = _.keys(appRoutes).reverse(); // Backbone requires reverted order of routes
            for (let name of routeNames)
            {
                let methodName: string = appRoutes[name];
                let method = controller[methodName];
                if (method === undefined)
                {
                    throw { message: 'Method "' + methodName + '" was not found on the controller' };
                }
                this.route(name, methodName, this.Bind(method, controller, this._app));
            }
        }

        private Bind(func: Function, controller: any, app: IApplication): () => any
        {
            return this._app !== undefined ? _.bind(func, controller, this._app) : _.bind(func, controller);
        }

        destroy(): void
        {
            // Only keep route handlers that don't belong to the provided router
            Backbone.history.handlers = _.filter(Backbone.history.handlers, function (handler)
            {
                return (handler.router !== this);
            }, this);
        }
    }
*/