nw-skeleton

Source: app-wrapper/js/appWrapper.js

/**
 * @fileOverview AppWrapper class file
 * @author Dino Ivankov <dinoivankov@gmail.com>
 * @version 1.3.1
 */

const _ = require('lodash');
const os = require('os');
const fs = require('fs');
const path = require('path');

const AppBaseClass = require('./lib/appBase').AppBaseClass;
const AppTranslations = require('./lib/appTranslations').AppTranslations;
const WindowManager = require('./lib/windowManager').WindowManager;
const FileManager = require('./lib/fileManager').FileManager;
const AppConfig = require('./lib/appConfig').AppConfig;

let App;

/**
 * A "wrapper" object for nw-skeleton based apps
 *
 * @class
 * @extends {appWrapper.AppBaseClass}
 * @memberOf appWrapper
 *
 * @property {App}              app                 Property that holds reference to App class instance
 * @property {Object}           helpers             Object that contains helper instances by helper names
 * @property {WindowManager}    windowManager       Instance of WindowManager class
 * @property {FileManager}      fileManager         Instance of FileManager class
 * @property {AppConfig}        appConfig           Instance of AppConfig class
 * @property {AppTranslations}  appTranslations     Instance of AppTranslations class
 * @property {window}           debugWindow         Reference to debug window 'window' object (for main app window)
 * @property {window}           mainWindow          Reference to main window 'window' object (for debug app window)
 * @property {Object}           initialAppConfig    Object that stores initial app config that wrapper was initialized with
 * @property {Function}         noop                Reference to empty function (_.noop)
 */
class AppWrapper extends AppBaseClass {

    /**
     * Creates appWrapper instance using initial config object
     *
     * @constructor
     * @param  {Object} initialAppConfig Initial config object
     * @return {AppWrapper}              Instance of AppWrapper class
     */
    constructor (initialAppConfig) {
        super();

        this.needsConfig = false;
        this.app = null;
        this.helpers = {};
        this.windowManager = null;
        this.fileManager = null;
        this.appConfig = null;

        let isDebugWindow = false;

        if (_.isUndefined(initialAppConfig) || !_.isObject(initialAppConfig)){
            initialAppConfig = {};
        } else {
            if (initialAppConfig.isDebugWindow){
                isDebugWindow = true;
            }
        }

        initialAppConfig = this.getInitialAppConfig(initialAppConfig);

        if (isDebugWindow){
            initialAppConfig.isDebugWindow = true;
        }

        if (initialAppConfig && initialAppConfig.debug && initialAppConfig.debug.forceDebug && !_.isUndefined(initialAppConfig.debug.forceDebug.AppWrapper)){
            this.forceDebug = initialAppConfig.debug.forceDebug.AppWrapper;
        }

        if (initialAppConfig && initialAppConfig.userMessages && initialAppConfig.userMessages.forceUserMessages && !_.isUndefined(initialAppConfig.userMessages.forceUserMessages.AppWrapper)){
            this.forceUserMessages = initialAppConfig.userMessages.forceUserMessages.AppWrapper;
        }

        this.boundMethods = {
            cleanup: null,
            saveUserConfig: null,
            onWindowClose: null,
            onDebugWindowClose: null,
            handleMessageResponse: null,
            handleMainMessage: null,
        };

        this.timeouts = {
            cleanupTimeout: null,
            windowCloseTimeout: null,
            modalContentVisibleTimeout: null,
        };

        this.debugWindow = null;
        this.mainWindow = null;

        this.initialAppConfig = initialAppConfig;

        window.getAppWrapper = () => {
            return this;
        };

        window.getFeApp = () => {
            return window.feApp;
        };

        this.noop = _.noop;
        appState = this.getAppState();
        this.appTemplate = '';
        return this;
    }

    /**
     * Initializes appWrapper and its dependencies, preparing the wrapper
     * to start the application itself
     *
     * @async
     * @return {AppWrapper} Instance of AppWrapper class
     */
    async initialize(){
        let isDebugWindow = false;
        if (this.initialAppConfig.isDebugWindow){
            isDebugWindow = true;
            delete this.initialAppConfig.isDebugWindow;
        }

        this.appConfig = new AppConfig(this.initialAppConfig);
        await this.appConfig.initialize({silent: true});
        // appState is available from here;

        await super.initialize();

        if (isDebugWindow){
            appState.isDebugWindow = true;
            isDebugWindow = null;
        }

        this.fileManager = new FileManager();
        await this.fileManager.initialize();

        appState.config = await this.appConfig.initializeConfig();
        await this.initializeLogging();
        await this.appConfig.initializeLogging();
        await this.fileManager.initializeLogging();

        let tmpDataDir = this.getConfig('appConfig.tmpDataDir');
        if (tmpDataDir){
            tmpDataDir = path.resolve(tmpDataDir);
            await this.fileManager.createDirRecursive(tmpDataDir);
        }

        this.log('Initializing application wrapper.', 'group', []);

        await this.initializeDebugMessageLog();
        await this.initializeUserMessageLog();

        await this.initializeSystemHelpers();

        appState.config = await this.appConfig.loadUserConfig();
        await this.initializeLogging();
        await this.appConfig.initializeLogging();
        await this.fileManager.initializeLogging();

        this.windowManager = new WindowManager();
        await this.windowManager.initialize();

        await this.setAppInstance();

        if (!appState.isDebugWindow) {
            await this.asyncMessage({instruction: 'setConfig', data: {config: appState.config}});
        }

        await this.setDynamicAppStateValues();

        if (!appState.isDebugWindow) {
            await this.asyncMessage({instruction: 'initializeAppMenu', data: {}});
        }

        await this.helpers.themeHelper.initializeThemes();
        await this.helpers.staticFilesHelper.loadJsFiles();

        await this.initializeWrapperHelpers();

        try {
            appState.initializationTime = this.getHelper('format').formatDate(new Date(), {}, true);
            appState.initializationTimeRaw = new Date();
        } catch (ex) {
            // console.log(ex);
        }

        appState.userData = await this.getHelper('userData').loadUserData();

        await this.helpers.staticFilesHelper.loadCssFiles();

        await this.setGlobalKeyHandlers();

        if (appState.isDebugWindow){
            this.mainWindow = window.opener;
        }

        await this.initializeLanguage();

        if (!appState.appError.title){
            appState.appError.title = this.translate(appState.appError.defaultTitle);
        }
        if (!appState.appError.text){
            appState.appError.text = this.translate(appState.appError.defaultText);
        }

        await this.processCommandParams();

        if (!appState.isDebugWindow){
            await this.asyncMessage({instruction: 'initializeTrayIcon', data: {}});
        }
        if (this.getConfig('debug.devTools')){
            this.windowManager.winState.devTools = true;
        }

        this.addWrapperEventListeners();
        await this.initializeApp();
        await this.initializeFeApp();

        let showInitializationStatus = this.getConfig('appConfig.showInitializationStatus');
        let showInitializationProgress = this.getConfig('appConfig.showInitializationProgress');

        if (showInitializationStatus){
            this.getHelper('appOperation').operationStart(this.appTranslations.translate('Initializing application'), false, true, showInitializationProgress);
        }

        if (!appState.isDebugWindow){
            await this.asyncMessage({instruction: 'setupAppMenu', data: {}});
        }

        if (showInitializationStatus){
            if (showInitializationProgress){
                this.getHelper('appOperation').operationUpdate(100, 100);
            }
            this.getHelper('appOperation').operationFinish(this.appTranslations.translate('Application initialized'));
        }

        return this;
    }

    /**
     * Finalizes appWrapper and its dependencies. This method is called once
     * frontend application is created, so code here has all references that
     * are available to the application
     *
     * @async
     * @return {booelan} Result of the wrapper and its dependencies finalization
     */
    async finalize () {
        this.windowManager.showWindow();
        appState.status.appLoaded = true;
        await this.wait(parseInt(parseFloat(this.getHelper('style').getCssVarValue('--long-animation-duration'), 10) * 1000, 10));
        let retValue;
        try {
            retValue = await this.app.finalize();
        } catch (ex) {
            this.log('Error finalizing application - "{1}"', 'error', [ex.stack]);
            this.setAppError('Error finalizing application', '', ex.stack);
        }
        if (retValue){
            appState.status.appInitialized = true;
            appState.status.appReady = true;
        }
        window.feApp.$watch('appState.config', this.appConfig.configChanged.bind(this.appConfig), {deep: true});
        this.log('Initializing application wrapper.', 'groupend', []);
        if (!appState.isDebugWindow){
            this.message({instruction: 'log', data: {type: 'debug', message: 'Application initialized', force: false}});
        }
        return retValue;
    }

    /**
     * Adds event listeners for the AppWrapper instance
     *
     * @return {undefined}
     */
    addWrapperEventListeners() {
        if (!appState.isDebugWindow){
            this.windowManager.win.on('close', this.boundMethods.onWindowClose);
            this.windowManager.win.globalEmitter.on('messageResponse', this.boundMethods.handleMessageResponse);
            this.windowManager.win.globalEmitter.on('asyncMessageResponse', this.boundMethods.handleMessageResponse);
            this.windowManager.win.globalEmitter.on('mainMessage', this.boundMethods.handleMainMessage);
        } else {
            this.windowManager.win.on('close', this.boundMethods.onDebugWindowClose);
        }

    }

    /**
     * Removes event listeners for the AppWrapper instance
     *
     * @return {undefined}
     */
    removeWrapperEventListeners() {
        if (!appState.isDebugWindow){
            this.windowManager.win.removeListener('close', this.boundMethods.onWindowClose);
            this.windowManager.win.globalEmitter.removeListener('messageResponse', this.boundMethods.handleMessageResponse);
            this.windowManager.win.globalEmitter.removeListener('asyncMessageResponse', this.boundMethods.handleMessageResponse);
            this.windowManager.win.globalEmitter.removeListener('mainMessage', this.boundMethods.handleMainMessage);
        } else {
            this.windowManager.win.removeListener('close', this.boundMethods.onDebugWindowClose);
        }

    }

    /**
     * Loads language and translation data and initializes language and
     * translation systems used in the app
     *
     * @async
     * @return {Object} Translation data, containing available languages and translations in those langauges
     */
    async initializeLanguage () {
        this.appTranslations = new AppTranslations();
        await this.appTranslations.initialize();
        return await this.appTranslations.initializeLanguage();
    }

    /**
     * Loads and instantiates app instance
     *
     * @async
     * @return {undefined}
     */
    async setAppInstance(){
        let appFilePath;
        try {
            appFilePath = path.join(process.cwd(), this.getConfig('appConfig.appFile', this.getConfig('wrapper.appFile')));
            App = await this.fileManager.loadFile(appFilePath, true);
            this.app = new App();
        } catch (ex) {
            this.log('Error instantiating application - "{1}"', 'error', [ex.stack]);
            this.setAppError('Error instantiating application', '', ex.stack);
        }
    }

    /**
     * Sets global key handler listeners for config global keys
     *
     * @async
     * @return {undefined}
     */
    async setGlobalKeyHandlers() {
        let globalKeyHandlers = this.getConfig('appConfig.globalKeyHandlers');
        if (globalKeyHandlers && globalKeyHandlers.length){
            let keyboardHelper = this.getHelper('keyboard');
            for(let j=0; j<globalKeyHandlers.length; j++){
                keyboardHelper.registerGlobalShortcut(globalKeyHandlers[j]);
            }
        }
    }

    /**
     * Initializes app object
     *
     * @async
     * @return {undefined}
     */
    async initializeApp(){
        try {
            await this.app.initialize();
        } catch (ex) {
            this.log('Error initializing application - "{1}"', 'error', [ex.stack]);
            this.setAppError('Error initializing application', '', ex.stack);
        }
    }

    /**
     * Initializes wrapper system helpers
     *
     * @async
     * @return {undefined}
     */
    async initializeSystemHelpers(){
        try {
            this.helpers = await this.initializeHelpers(this.getConfig('wrapper.systemHelperDirectories'));
        } catch (ex) {
            this.log('Error initializing wrapper system helpers - "{1}"', 'error', [ex.stack]);
            this.setAppError('Error initializing wrapper system helpers', '', ex.stack);
        }
    }

    /**
     * Initializes wrapper helpers
     *
     * @async
     * @return {undefined}
     */
    async initializeWrapperHelpers(){
        try {
            this.helpers = _.merge(this.helpers, await this.initializeHelpers(this.getConfig('wrapper.helperDirectories')));
        } catch (ex) {
            this.log('Error initializing wrapper helpers - "{1}"', 'error', [ex.stack]);
            this.setAppError('Error initializing wrapper helpers', '', ex.stack);
        }
    }

    /**
     * Loads and initializes helpers from directories passed in argument
     *
     * @async
     * @param  {string[]} helperDirs An array of absolute paths where helper files are located
     * @return {Object} An object with all initialized helper instances
     */
    async initializeHelpers(helperDirs){
        let helpers = {};
        let classHelpers = await this.loadHelpers(helperDirs);

        for (let helperIdentifier in classHelpers){
            helpers[helperIdentifier] = new classHelpers[helperIdentifier]();
            if (helpers[helperIdentifier] && !_.isUndefined(helpers[helperIdentifier].initialize) && _.isFunction(helpers[helperIdentifier].initialize)){
                await helpers[helperIdentifier].initialize();
            }
        }

        return helpers;
    }

    /**
     * Loads and helpers from directories passed in argument
     *
     * @async
     * @param  {string[]} helperDirs An array of absolute paths where helper files are located
     * @return {Object} An object with all helper classes
     */
    async loadHelpers (helperDirs) {
        let helpers = {};
        if (!(helperDirs && _.isArray(helperDirs) && helperDirs.length)){
            this.log('No wrapper helper dirs defined', 'warning', []);
            this.log('You should define this in ./config/config.js file under "appConfig.templateDirectories.helperDirectories" variable', 'debug', []);
            helperDirs = [];
        } else {
            this.log('Loading wrapper helpers from {1} directories.', 'group', [helperDirs.length]);
            let currentHelpers;
            for (let i=0; i<helperDirs.length; i++){
                let helperDir = path.resolve(helperDirs[i]);
                currentHelpers = await this.fileManager.loadFilesFromDir(helperDir, /\.js$/, true);
                if (currentHelpers && _.isObject(currentHelpers) && _.keys(currentHelpers).length){
                    helpers = _.merge(helpers, currentHelpers);
                }
            }
            this.log('Loading wrapper helpers from {1} directories.', 'groupend', [helperDirs.length]);
        }

        return helpers;
    }

    /**
     * Initializes frontend part of the app, creating Vue instance
     *
     * @async
     * @param {Boolean} noFinalize Flag to prevent finalization (used when reinitializing)
     * @return {Vue} An object representing Vue app instance
     */
    async initializeFeApp(noFinalize){
        this.log('Initializing Vue app...', 'debug', []);
        let utilHelper = this.getHelper('util');
        if (!this.appTemplate){
            this.appTemplate = document.querySelector('.nw-app-wrapper').innerHTML;
        } else {
            document.querySelector('.nw-app-wrapper').innerHTML = this.appTemplate;
        }

        let returnPromise;
        let resolveReference;
        returnPromise = new Promise((resolve) => {
            resolveReference = resolve;
        });

        window.feApp = new Vue({
            el: '.nw-app-wrapper',
            template: window.indexTemplate,
            data: appState,
            mixins: this.helpers.componentHelper.vueMixins,
            filters: this.helpers.componentHelper.vueFilters,
            components: this.helpers.componentHelper.vueComponents,
            translations: appState.translations,
            mounted: async () => {
                if (this.getConfig('appConfig.disableRightClick') && !this.getConfig('debug.enabled')){
                    document.body.addEventListener('contextmenu', utilHelper.boundMethods.prevent, false);
                }
                this.addUserMessage('Application initialized.', 'info', []);
                if (!noFinalize){
                    await this.finalize();
                }
                if (appState.isDebugWindow){
                    this.addUserMessage('Debug window application initialized', 'info', [], false,  false);
                } else {
                    if (appState.activeConfigFile && appState.activeConfigFile != '../../config/config.js'){
                        this.log('Active config file: "{1}"', 'info', [appState.activeConfigFile], true);
                    }
                }

                resolveReference(window.feApp);
            },
            beforeDestroy: async () => {
                if (this.getConfig('appConfig.disableRightClick') && !this.getConfig('debug.enabled')){
                    document.body.removeEventListener('contextmenu', utilHelper.boundMethods.prevent, false);
                }
            },
            destroyed: async () => {
                this.emit('feApp:destroyed');
            }
        });

        return returnPromise;
    }

    /**
     * Reinitializes frontend app by destroying it and initializing it again
     *
     * @async
     * @return {Vue} An object representing Vue app instance
     */
    async reinitializeFeApp(){
        this.once('feApp:destroyed', async () => {
            window.feApp = null;
            appState.debugMessages = [];
            appState.allDebugMessages = [];
            appState.userMessages = [];
            appState.allUserMessages = [];
            await this.wait(appState.config.shortPauseDuration);
            await this.getHelper('component').initializeComponents();
            await this.initializeFeApp();
            this.getHelper('staticFiles').reloadCss();
        });
        // appState.status.appLoaded = false;
        appState.status.appInitialized = false;
        // appState.status.appReady = false;
        window.getFeApp().$destroy();
    }

    /**
     * Generic frontend event listener for calling methods within app scope (but not within current Vue cmponent scope)
     *
     * @async
     * @param  {Event} e  Event that triggered the handler
     * @return {undefined}
     */
    async callViewHandler (e) {
        let target = e.target;
        let eventType = e.type;
        let eventHandlerName = '';
        let dataHandlerAttrName = '';
        let eventTargetAttrName = '';
        let eventTargetInstruction = '';
        if (target){
            eventTargetAttrName = 'data-event-target';
            do {
                eventTargetInstruction = target.getAttribute(eventTargetAttrName);
                if (eventTargetInstruction && eventTargetInstruction == 'parent'){
                    target = target.parentNode;
                }
            } while (eventTargetInstruction == 'parent');

            dataHandlerAttrName = ['data', eventType, 'handler'].join('-');
            eventHandlerName = target.getAttribute(dataHandlerAttrName);

            if (!eventHandlerName){
                do {
                    target = target.parentNode;
                    eventHandlerName = target.getAttribute(dataHandlerAttrName);
                } while (!eventHandlerName);
            }

            if (eventHandlerName){
                return await this.callObjMethod(eventHandlerName, [e, target]);
            } else {
                this.log('Element {1} doesn\'t have attribute "{2}"', 'warning', [target.tagName + '.' + target.className.split(' ').join(','), dataHandlerAttrName]);
            }
        } else {
            this.log('Can\'t find event target "{1}"', 'warning', [e]);
            if (e && e.preventDefault && _.isFunction(e.preventDefault)){
                e.preventDefault();
            }
        }
    }

    /**
     * Handler that performs necessary operations when application window gets closed
     *
     * @async
     * @return {undefined}
     */
    async onWindowClose () {
        let modalHelper = this.getHelper('modal');
        let appOperationHelper = this.getHelper('appOperation');
        modalHelper.closeCurrentModal(true);
        let confirmed = true;
        if (appState.appOperation.operationActive){
            appOperationHelper.showCancelModal(false);
            return;
        }
        if (confirmed){
            appState.status.appShuttingDown = true;
            await this.cleanup();
            if (!appState.isDebugWindow){
                this.resetAppError();
                await this.asyncMessage({instruction: 'removeAppMenu', data: {}});
                await this.asyncMessage({instruction: 'removeTrayIcon', data: {}});
                appState.preventClose = false;
                this.windowManager.closeWindowForce();
                await this.finalizeLogs();
            }
        } else {
            return;
        }
    }

    /**
     * Cleanup method - calls cleanup/shutdown methods for all eligible dependencies, cleaning
     * the app state so it can be safely closed
     *
     * @async
     * @return {boolean} Cleanup result
     */
    async cleanup(){
        let utilHelper = this.getHelper('util');
        let returnPromise;
        this.addUserMessage('Performing pre-close cleanup...', 'info', [], false, false);
        let resolveReference;
        returnPromise = new Promise((resolve) => {
            resolveReference = resolve;
        });
        setTimeout(async () => {
            if (this.getConfig('appConfig.disableRightClick') && !this.getConfig('debug.enabled')){
                document.body.removeEventListener('contextmenu', utilHelper.boundMethods.prevent);
            }
            await this.shutdownApp();
            // await this.finalizeLogs();
            this.windowManager.hideWindow();
            if (window && window.feApp && window.feApp.$destroy && _.isFunction(window.feApp.$destroy)){
                window.feApp.$destroy();
            }
            resolveReference(true);
        }, 200);
        return returnPromise;
    }

    /**
     * Shuts down application, removing menus, tray icons and eventual other functionalities
     * that were initializes upon application start
     *
     * @async
     * @return {boolean} Shutdown result
     */
    async shutdownApp () {
        this.log('Shutting down...', 'group', []);
        if (this.debugWindow && this.debugWindow.getAppWrapper && _.isFunction(this.debugWindow.getAppWrapper)){
            this.debugWindow.getAppWrapper().onDebugWindowClose();
        }
        appState.mainLoaderTitle = this.appTranslations.translate('Please wait while application shuts down...');
        appState.status.appShuttingDown = true;
        this.addUserMessage('Shutting down...', 'info', [], true, false, true, false);
        if (this.app && this.app.shutdown && _.isFunction(this.app.shutdown)){
            await this.app.shutdown();
        }
        this.clearTimeouts();
        this.clearIntervals();
        await this.fileManager.unwatchAllFiles();
        await this.fileManager.unwatchAll();
        this.addUserMessage('Shutdown complete.', 'info', [], true, false, true, true);
        this.log('Shutting down...', 'groupend', []);
        appState.status.appLoaded = false;
        await this.wait(appState.config.longPauseDuration);
        return true;
    }

    /**
     * Handler that performs necessary operations before application window gets closed
     *
     * @return {undefined}
     */
    beforeWindowClose () {
        this.removeWrapperEventListeners();
        this.removeBoundMethods();
    }

    /**
     * Handler for changing current application language
     *
     * @param  {string} selectedLanguageName Name of new app language
     * @param  {Object} selectedLanguage     Object representing new app language
     * @param  {string} selectedLocale       Locale of new app language
     * @param  {boolean} skipOtherWindow     Flag that triggers language change in other app windows (if any)
     * @return {boolean}                     Result of app language change
     */
    changeLanguage (selectedLanguageName, selectedLanguage, selectedLocale, skipOtherWindow) {
        return this.appTranslations.changeLanguage(selectedLanguageName, selectedLanguage, selectedLocale, skipOtherWindow);
    }

    /**
     * Handler that is triggered before application window is reloaded (available only with debug enabled)
     *
     * @async
     * @return {undefined}
     */
    async beforeUnload () {
        let modalHelper = this.getHelper('modal');
        let appOperationHelper = this.getHelper('appOperation');
        modalHelper.closeCurrentModal(true);
        let confirmed = true;
        if (appState.appOperation.operationActive){
            appOperationHelper.showCancelModal(true);
            return;
        }
        if (confirmed){
            appState.status.appShuttingDown = true;
            await this.cleanup();
            if (!appState.isDebugWindow){
                this.resetAppError();
                await this.finalizeLogs();
                this.windowManager.reloadWindow(null, true);
            }
        } else {
            return;
        }
    }

    /**
     * Handler that is triggered before application debug window is reloaded (available only with debug enabled)
     *
     * @async
     * @return {undefined}
     */
    async onDebugWindowUnload (){
        this.windowManager.win.removeListener('close', this.boundMethods.onDebugWindowClose);
    }

    /**
     * Handler that is triggered before application window is closed (available only with debug enabled)
     *
     * @async
     * @return {undefined}
     */
    async onDebugWindowClose (){
        this.log('Closing standalone debug window', 'info', []);
        if (this.mainWindow && this.mainWindow.appState && this.mainWindow.appState.debugMessages){
            this.mainWindow.appState.debugMessages = _.cloneDeep(appState.debugMessages);
            this.mainWindow.appState.allDebugMessages = _.cloneDeep(appState.allDebugMessages);
            this.mainWindow.appState.hasDebugWindow = false;
            this.mainWindow.appWrapper.debugWindow = null;
        }
        this.windowManager.closeWindowForce();
        this.addUserMessage('Debug window closed', 'info', [], false,  false);
    }

    /**
     * Helper function to set app status variables in app state
     *
     * @param {boolean} appBusy  Flag that indicates whether entire app should be considered as 'busy'
     * @param {string} appStatus String that indicates current app status (for display in app header live info component)
     * @return {undefined}
     */
    setAppStatus (appBusy, appStatus){
        if (!appStatus){
            if (appBusy){
                appStatus = 'busy';
            } else {
                appStatus = 'idle';
            }
        }
        appState.status.appBusy = appBusy;
        appState.status.appStatus = appStatus;
    }

    /**
     * Resets app status to not busy/idle state
     *
     * @return {undefined}
     */
    resetAppStatus (){
        this.setAppStatus(false);
    }

    /**
     * Placeholder method that handles modal confirm action
     *
     * @param  {Event} e Optional event passed to method
     * @return {mixed} Return value depends on particular confirm modal handler method
     */
    confirmModalAction (e) {
        this.log('Calling appWrapper confirmModalAction', 'info', []);
        return this._confirmModalAction(e);
    }

    /**
     * Placeholder method that handles modal cancel/close action
     *
     * @param  {Event} e Optional event passed to method
     * @return {mixed} Return value depends on particular cancel/close modal handler method
     */
    cancelModalAction (e) {
        this.log('Calling appWrapper cancelModalAction', 'info', []);
        return this._cancelModalAction(e);
    }

    /**
     * Internal method that is overwritten when particular modal is opened.
     * Overwritten method contains all logic for modal confirmation
     *
     * @param  {Event} e Optional event passed to method
     * @return {mixed} Return value depends on particular confirm modal handler method
     */
    _confirmModalAction (e) {
        this.log('Calling appWrapper _confirmModalAction', 'info', []);
        return this.__confirmModalAction(e);
    }

    /**
     * Internal method that is overwritten when particular modal is opened.
     * Overwritten method contains all logic for modal cancelling or closing
     *
     * @param  {Event} e Optional event passed to method
     * @return {mixed} Return value depends on particular cancel/close modal handler method
     */
    _cancelModalAction (e) {
        this.log('Calling appWrapper _cancelModalAction', 'info', []);
        return this.__cancelModalAction(e);
    }


    /**
     * Default confirm modal action - do not change
     *
     * @param  {Event} e Optional event passed to method
     * @return {undefined}
     */
    __confirmModalAction (e) {
        this.log('Calling appWrapper __confirmModalAction', 'info', []);
        if (e && e.preventDefault && _.isFunction(e.preventDefault)){
            e.preventDefault();
        }
        if (!appState.modalData.currentModal.busy){
            this.getHelper('modal').closeCurrentModal();
        }
    }

    /**
     * Default cancel/close modal action - do not change
     *
     * @param  {Event} e Optional event passed to method
     * @return {undefined}
     */
    __cancelModalAction (e) {
        this.log('Calling appWrapper __cancelModalAction', 'info', []);
        if (e && e.preventDefault && _.isFunction(e.preventDefault)){
            e.preventDefault();
        }
        if (!appState.modalData.currentModal.busy){
            this.getHelper('modal').closeCurrentModal();
        }
    }

    /**
     * Sets dynamic (calculated) appState values (mainly language related)
     *
     * @async
     * @return {undefined}
     */
    async setDynamicAppStateValues () {
        appState.languageData.currentLanguageName = this.getConfig('currentLanguageName');
        appState.languageData.currentLanguage = this.getConfig('currentLanguage');
        appState.languageData.currentLocale = this.getConfig('currentLocale');
        appState.platformData = this.getPlatformData();
        appState.appDir = this.getAppDir();
        appState.manifest = require(path.join(appState.appDir, '../package.json'));
        appState.wrapperManifest = require(path.join(appState.appDir, '../node_modules/nw-skeleton/package.json'));
        appState.appRootDir = path.join(appState.appDir, '../');
    }

    /**
     * Returns instance of helper object based on passed parameter (or false if helper can't be found)
     *
     * @param  {string} helperName Name of the helper
     * @return {Object}            Instance of the helper object (or false if helper can't be found)
     */
    getHelper(helperName){
        let helper = false;
        if (this.app && this.app.helpers){
            helper = _.get(this.app.helpers, helperName);
            if (!helper){
                helper = _.get(this.app.helpers, helperName + 'Helper');
            }
        }
        if (!helper){
            if (this.helpers){
                helper = _.get(this.helpers, helperName);
                if (!helper){
                    helper = _.get(this.helpers, helperName + 'Helper');
                }
            }
        }
        return helper;
    }

    /**
     * Finds and returns method of the object based on passed parameters
     *
     * @async
     * @param  {string} methodString String that represents method path (i.e. 'app.appObject.method')
     * @param  {array}  methodArgs   An array of arguments to be applied to the returned method reference
     * @param  {Object} context      Context that will be applied as 'this' to returned method reference
     * @param  {boolean} silent      Flag to control logging output
     * @return {Function}            Reference to required method with context and arguments applied (or false if no method is found)
     */
    async getObjMethod(methodString, methodArgs, context, silent){
        let methodChunks = methodString.split('.');
        let targetMethod;
        let methodPath = '';
        let objMethod = false;
        if (methodChunks && methodChunks.length && methodChunks.length > 1){
            targetMethod = _.takeRight(methodChunks);
            methodPath = _.slice(methodChunks, 0, methodChunks.length-1).join('.');
        } else {
            targetMethod = methodString;
        }


        let handlerObj = this.app;
        if (methodPath){
            handlerObj = _.get(handlerObj, methodPath);
        }

        if (handlerObj && handlerObj[targetMethod] && _.isFunction(handlerObj[targetMethod])){
            if (context && _.isObject(context)){
                objMethod = async function() {
                    return await handlerObj[targetMethod].apply(context, methodArgs);
                };
            } else {
                objMethod = async function() {
                    return await handlerObj[targetMethod].apply(handlerObj, methodArgs);
                };
            }
        } else {
            handlerObj = this;
            if (methodPath){
                handlerObj = _.get(handlerObj, methodPath);
            }
            if (handlerObj && handlerObj[targetMethod] && _.isFunction(handlerObj[targetMethod])){
                if (context && _.isObject(context)){
                    objMethod = async function() {
                        return await handlerObj[targetMethod].apply(context, methodArgs);
                    };
                } else {
                    objMethod = async function() {
                        return await handlerObj[targetMethod].apply(handlerObj, methodArgs);
                    };
                }
            } else {
                if (!silent){
                    this.log('Can\'t find object method "{1}"', 'warning', [methodString]);
                }
            }
        }
        return objMethod;
    }

    /**
     * Finds and calls method of the object based on passed parameters
     *
     * @async
     * @param  {string} methodString String that represents method path (i.e. 'app.appObject.method')
     * @param  {array}  methodArgs   An array of arguments to be applied to the returned method reference
     * @param  {Object} context      Context that will be applied as 'this' to returned method reference
     * @return {mixed}               Method return value or false if no method found
     */
    async callObjMethod(methodString, methodArgs, context){
        let objMethod = await this.getObjMethod(methodString, methodArgs, context);
        if (objMethod && _.isFunction(objMethod)){
            return await objMethod();
        } else {
            return false;
        }
    }

    /**
     * Exits the app, closing app window
     *
     * @param {Boolean} force Force window closing
     * @return {undefined}
     */
    exitApp(force){
        if (force === true){
            this.windowManager.closeWindowForce();
        } else {
            this.windowManager.closeWindow();
        }
    }

    /**
     * Finalizes log files if logging to file is enabled
     *
     * @async
     * @return {boolean} Result of log finalizing
     */
    async finalizeLogs(){
        await this.finalizeUserMessageLog();
        await this.finalizeDebugMessageLog();
        return true;
    }


    /**
     * Initializes user message log file if user message logging to file is enabled
     *
     * @async
     * @return {boolean} Result of log initialization
     */
    async initializeUserMessageLog(){
        if (this.getConfig('userMessages.userMessagesToFile')){
            let userMessageFilePath = this.getUserMessageFilePath();
            if (!await this.fileManager.isFile(userMessageFilePath) || !this.getConfig('userMessages.userMessagesToFileAppend')) {
                this.fileManager.createDirFileRecursive(userMessageFilePath);
            } else if (this.getConfig('userMessages.userMessagesToFileAppend')) {
                let messageLogFile = path.resolve(userMessageFilePath);
                let messageLogContents = await this.fileManager.readFileSync(messageLogFile);
                if (messageLogContents){
                    messageLogContents = messageLogContents.replace(/\n?\[\n/g, '');
                    messageLogContents = messageLogContents.replace(/\n\],?\n/g, ',');
                    messageLogContents = messageLogContents.replace(/,+/g, ',');
                    await this.fileManager.writeFileSync(messageLogFile, messageLogContents, {flag: 'w'});
                }
            }
        }
        return true;
    }

    /**
     * Initializes debug log file if debug logging to file is enabled
     *
     * @async
     * @return {boolean} Result of log finalizing
     */
    async finalizeUserMessageLog(){
        if (this.getConfig('userMessages.userMessagesToFile')){
            let userMessageFilePath = this.getUserMessageFilePath();
            let messageLogFile = path.resolve(userMessageFilePath);
            let messageLogContents = '[\n' + await this.fileManager.readFileSync(messageLogFile) + '\n]\n';
            messageLogContents = messageLogContents.replace(/\n,\n/g, '\n');
            await this.fileManager.writeFileSync(messageLogFile, messageLogContents, {flag: 'w'});
            // this.log('Finalized user message log...', 'info', []);
        }
        return true;
    }


    /**
     * Initializes debug message log file if debug message logging to file is enabled
     *
     * @async
     * @return {boolean} Result of log initialization
     */
    async initializeDebugMessageLog(){
        if (this.getConfig('debug.debugToFile')){
            let debugMessageFilePath = this.getDebugMessageFilePath();
            if (!await this.fileManager.isFile(debugMessageFilePath) || !this.getConfig('debug.debugToFileAppend')) {
                this.fileManager.createDirFileRecursive(debugMessageFilePath);
            } else if (this.getConfig('debug.debugToFileAppend')) {
                let debugLogFile = path.resolve(debugMessageFilePath);
                let debugLogContents = await this.fileManager.readFileSync(debugLogFile);
                if (debugLogContents){
                    debugLogContents = debugLogContents.replace(/\n?\[\n/g, '');
                    debugLogContents = debugLogContents.replace(/\n\],?\n/g, ',');
                    debugLogContents = debugLogContents.replace(/,+/g, ',');
                    await this.fileManager.writeFileSync(debugLogFile, debugLogContents, {flag: 'w'});
                }
            }
        }
        return true;
    }

    /**
     * Finalizes debug log file if debug logging to file is enabled
     *
     * @async
     * @return {boolean} Result of log finalizing
     */
    async finalizeDebugMessageLog(){
        if (this.getConfig('debug.debugToFile')){
            let debugMessageFilePath = this.getDebugMessageFilePath();
            let debugLogFile = path.resolve(debugMessageFilePath);
            let debugLogContents = '[\n' + await this.fileManager.readFileSync(debugLogFile) + '\n]\n';
            debugLogContents = debugLogContents.replace(/\n,\n/g, '\n');
            await this.fileManager.writeFileSync(debugLogFile, debugLogContents, {flag: 'w'});
        }
        return true;
    }

    /**
     * Returns appState object if exists, initializing it and returning it if it doesn't
     *
     * @return {Object} appState object
     */
    getAppState () {
        let win = nw.Window.get().window;
        let appStateFile;
        let appAppState;
        let initialAppState;
        if (win && win.appState){
            return win.appState;
        } else {
            initialAppState = require('./appState').appState;
            appStateFile = path.resolve('./app/js/appState');
            try {
                appAppState = require(appStateFile).appState;
                initialAppState = this.mergeDeep(initialAppState, appAppState);
            } catch (ex) {
                console.error(ex);
            }

            if (win){
                win.appState = initialAppState;
            }
            return initialAppState;
        }
    }

    /**
     * Helper method for deep object merging
     *
     * First passed parameter is destination object, all other parameters are source
     * objects that will be merged with destination clone.
     * This method does NOT mutate original object.
     *
     * @return {Object} Result destination object with all source object values merged
     */
    mergeDeep (){
        let destination = arguments[0];
        let sources = Array.prototype.slice.call(arguments, 1);
        let result = _.cloneDeep(destination);

        for (let i=0; i < sources.length; i++){
            let source = sources[i];
            let destinationKeys = _.keys(result);
            let sourceKeys = _.keys(source);
            let newKeys = _.difference(sourceKeys, destinationKeys);
            let oldKeys = _.intersection(sourceKeys, destinationKeys);

            for (let j=0; j<newKeys.length; j++){
                result[newKeys[j]] = _.cloneDeep(source[newKeys[j]]);
            }

            for (let j=0; j<oldKeys.length; j++){
                if (_.isArray(source[oldKeys[j]])){
                    result[oldKeys[j]] = _.concat(result[oldKeys[j]], source[oldKeys[j]]);
                } else if (_.isObject(source[oldKeys[j]])){
                    result[oldKeys[j]] = this.mergeDeep(result[oldKeys[j]], source[oldKeys[j]]);
                } else if (_.isFunction(source[oldKeys[j]])){
                    console.log('func');
                } else {
                    result[oldKeys[j]] = _.cloneDeep(source[oldKeys[j]]);
                }
            }

        }
        return result;
    }

    /**
     * Helper method that stops execution for time determined by passed parameter
     *
     * @async
     * @param  {Integer} duration Pause duration in milliseconds
     * @return {boolean}      Result of waiting (always true)
     */
    async wait(duration){
        if (this.getConfig('debug.enabled')){
            this.log('Waiting {1} ms', 'debug', [duration]);
            let returnPromise = new Promise((resolve) => {
                setTimeout(() => {
                    resolve(true);
                }, duration);
            });
            return returnPromise;
        } else {
            return true;
        }
    }

    /**
     * Helper methods that waits for process.nexTick to happen before allowing
     * code execution
     *
     * @async
     * @return {boolean} Result of waiting for nextTick (always true)
     */
    async nextTick (){
        let returnPromise = new Promise((resolve) => {
            setTimeout(() => {
                resolve(true);
            }, 0);
        });
        return returnPromise;
    }

    /**
     * Helper methods that calls function passed in argument upon process.nextTick
     *
     * @async
     * @param  {function} callable Function that will be called after nextTick
     * @return {mixed}             Value that called function returns
     */
    async onNextTick(callable){
        await this.nextTick();
        return callable();
    }

    /**
     * Determines and returns absolute path to app directory.
     * Takes into consideration OS platform and way that app is
     * being run (whether by calling nwjs or running finished packaged app)
     *
     * @return {string} Absolute path to app directory
     */
    getAppDir(){
        let appDir;
        let processPath = path.dirname(process.execPath);
        let workingDir = path.resolve('./');

        if (this.isWindows()){
            if (process.execPath.match(/nw\.exe/)){
                appDir = path.join(workingDir, 'app');
            } else {
                appDir = processPath;
            }
        } else if (this.isMac()){
            if (process.execPath.match(/nwjs\sHelper$/)){
                appDir = path.join(workingDir, 'app');
            } else {
                appDir = processPath;
            }
        }
        return appDir;
    }

    /**
     * Handles message responses from main script
     *
     * @param  {Object} messageData Message response data
     * @return {undefined}
     */
    handleMessageResponse (messageData) {
        if (messageData){
            this.messageResponseLog(messageData);
            let messageIdentifierChunks = [];
            if (messageData.instruction){
                messageIdentifierChunks.push(messageData.instruction);
            }
            if (messageData.uuid){
                messageIdentifierChunks.push(messageData.uuid);
            }
            let messageType = 'Message ';
            if (messageData._async_){
                messageType = 'Async message ';
            }
            if (messageData._result_){
                this.log(messageType + ' "{1}" succeeded', 'info', [messageIdentifierChunks.join(' - ')]);
            } else {
                this.log(messageType + ' "{1}" failed', 'error', [messageIdentifierChunks.join(' - ')]);
            }
            this.log(messageData, 'debug', []);
            this.log(messageData, 'debug', []);
        } else {
            this.log('Message failed - no message data returned', 'error', []);
        }
    }

    /**
     * Logs eventual user and debug messages and displays eventual notifications from message response
     *
     * @param  {Object} messageData Message response data
     * @return {undefined}
     */
    messageResponseLog (messageData) {
        if (messageData._messages_ && _.isArray(messageData._messages_)){
            messageData._messages_.forEach( (message) => {
                let msgMessage = message.message || '';
                let msgType = message.type || 'info';
                let msgData = message.data || [];
                let msgForce = message.force || false;
                if (msgMessage){
                    this.log(msgMessage, msgType, msgData, msgForce);
                }
            });
        }
        if (messageData._userMessages_ && _.isArray(messageData._userMessages_)){
            messageData._userMessages_.forEach( (message) => {
                let msgMessage = message.message || '';
                let msgType = message.type || 'info';
                let msgData = message.data || [];
                let msgForce = message.force || false;
                if (msgMessage){
                    this.addUserMessage(msgMessage, msgType, msgData, false, true, msgForce);
                }
            });
        }
        if (messageData._notifications_ && _.isArray(messageData._notifications_)){
            messageData._notifications_.forEach( (message) => {
                let msgMessage = message.message || '';
                let msgType = message.type || 'info';
                let msgData = message.data || [];
                if (msgMessage){
                    this.addNotification(msgMessage, msgType, msgData, true);
                }
            });
        }
    }


    /**
     * Handles messages from main script
     *
     * @param  {Object} data Message data
     * @return {undefined}
     */
    handleMainMessage (data){
        if (data && data.instruction){
            if (data.instruction == 'callMethod' && data.data && data.data.method){
                let args = data.data.arguments;
                if (!args){
                    args = [];
                }
                this.callObjMethod(data.data.method, args, this);
            }
        }
    }

    /**
     * Opens app info modal
     *
     * @return {undefined}
     */
    showAppInfo (){
        let modalOptions = {
            title: this.appTranslations.translate('Application info'),
            confirmButtonText: this.appTranslations.translate('Close'),
            showCancelButton: false,
        };
        this.getHelper('modal').openModal('appInfoModal', modalOptions);
    }

    /**
     * Process eventual command line params
     *
     * @return {undefined}
     */
    async processCommandParams () {
        if (nw.App.argv && nw.App.argv.length){
            if (_.includes(nw.App.argv, 'resetAll')){
                await this.appConfig.clearUserConfig(true);
                await this.getHelper('userData').clearUserData();
                appState.userData = {};
                this.log('All data reset', 'info', [], true);
                this.message({instruction:'log', data: {message: 'All data reset', force: true}});
                this.exitApp(true);
            } else if (_.includes(nw.App.argv, 'resetData')){
                await this.getHelper('userData').clearUserData();
                appState.userData = {};
                this.log('User data reset', 'info', [], true);
                this.message({instruction:'log', data: {message: 'User data reset', force: true}});
                this.exitApp(true);
            } else if (_.includes(nw.App.argv, 'resetConfig')){
                await this.appConfig.clearUserConfig(true);
                this.log('Config data reset', 'info', [], true);
                this.message({instruction:'log', data: {message: 'Config data reset', force: true}});
                this.exitApp(true);
            }
        }
    }

    /**
     * Returns platform data for current platform
     *
     * @return {Object} Platform data
     */
    getPlatformData (){
        let name = os.platform();
        let platform = {
            isLinux: false,
            isMac: false,
            isWindows: false,
            isWindows8: false,
            version: os.release(),
            userInfo: os.userInfo(),
        };

        if(name === 'darwin'){
            platform.name = 'mac';
            platform.isMac = true;
        } else if(name === 'linux'){
            platform.name = 'linux';
            platform.isLinux = true;
        } else {
            platform.name = 'windows';
            platform.isWindows = true;
        }

        platform.is64Bit = os.arch() === 'x64' || process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432');

        return {
            platform: platform,
            versions: process.versions
        };
    }

    /**
     * Checks whether current platform is mac
     *
     * @return {Boolean} True if mac, false otherwise
     */
    isMac (){
        return this.getPlatformData().platform.isMac;
    }

    /**
     * Checks whether current platform is windows
     *
     * @return {Boolean} True if windows, false otherwise
     */
    isWindows (){
        return this.getPlatformData().platform.isWindows;
    }

    /**
     * Checks whether current platform is linux
     *
     * @return {Boolean} True if linux, false otherwise
     */
    isLinux (){
        return this.getPlatformData().platform.isLinux;
    }

    /**
     * Returns base exec path (root dir of the app)
     *
     * @return {string} Root app dir
     */
    getExecPath () {
        let execPath = nw.__dirname;
        if (this.isMac()){
            if (execPath.match(/app\.nw$/)){
                execPath = path.join(execPath, '../../../..');
            }
        }
        return execPath;
    }

    /**
     * Loads config overrides if present, and returns config object for the app
     *
     * @param  {Object} defaultAppConfig Default application config
     * @return {Object}                  Application config object
     */
    getInitialAppConfig(defaultAppConfig){
        let initialAppConfig = defaultAppConfig;
        let appStateConfig = require('../../config/appWrapperConfig').config;
        let execPath = this.getExecPath();
        let appDir = this.getAppDir();

        let configFilePath = path.resolve(path.join(execPath, 'config', 'config.js'));
        let configFileExists = fs.existsSync(configFilePath);

        if (!configFileExists){
            configFilePath = path.resolve(path.join(execPath, 'config.js'));
            configFileExists = fs.existsSync(configFilePath);
        }

        if (!configFileExists){
            configFilePath = path.join(appDir, '../config/config.js');
            configFileExists = fs.existsSync(configFilePath);
        }

        if (configFileExists){
            let initialConfigData;
            try {
                initialConfigData = require(configFilePath);
                initialAppConfig = this.mergeDeep(defaultAppConfig, initialConfigData.config);
            } catch (ex) {
                console.error(ex);
            }
        }
        initialAppConfig = this.mergeDeep(appStateConfig, initialAppConfig);
        return initialAppConfig;
    }
}
exports.AppWrapper = AppWrapper;



/**
 * @namespace
 * @name mainScript
 * @description mainScript mainScript namespace
 */

/**
 * @namespace
 * @name appWrapper
 * @description appWrapper appWrapper namespace
 */

/**
 * @namespace
 * @name appWrapper.helpers
 * @description appWrapper.helpers appWrapper.helpers namespace
 */

/**
 * @namespace
 * @name appWrapper.helpers.systemHelpers
 * @description appWrapper.helpers.systemHelpers appWrapper.helpers.systemHelpers namespace
 */

/**
 * @namespace
 * @name components
 * @description components components namespace
 */

/**
 * @namespace
 * @name directives
 * @description directives directives namespace
 */

/**
 * @namespace
 * @name filters
 * @description filters filters namespace
 */

/**
 * @namespace
 * @name mixins
 * @description mixins mixins namespace
 */