///////////////////////////////////////////////////////////////////////////
// Copyright © Esri. All Rights Reserved.
//
// Licensed under the Apache License Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///////////////////////////////////////////////////////////////////////////

define(['dojo/_base/declare',
  'dojo/_base/lang',
  'dojo/_base/array',
  'dojo/_base/html',
  'dojo/Deferred',
  'dojo/topic',
  'dojo/Evented',
  'dojo/on',
  'dijit/Tooltip',
  'dojo/aspect',
  'dojo/json',
  'dojo/query',
  'dojo/request/xhr',
  'dojo/promise/all',
  'dijit/registry',
  './utils',
  'jimu/tokenUtils',
  './dijit/Message',
  './DataSourceManager',
  './portalUrlUtils',
  './portalUtils'
],
function(declare, lang, array, html, Deferred, topic, Evented, on, Tooltip, aspect,
  json, query, xhr, all, registry, utils, tokenUtils, Message, DataSourceManager, portalUrlUtils, portalUtils) {
  var instance = null,
  clazz = declare(Evented, {

    constructor: function() {
      //the loaded widget list
      this.loaded = [];

      //action is triggered, but the widget has not been loaded
      //{id: widgetId, action: {}}
      this.missedActions = [];

      this.activeWidget = null;

      this.widgetsUri = {}; // {widgetId: Promise<widgetUri>}

      if(window.isBuilder){
        topic.subscribe("app/mapLoaded", lang.hitch(this, this._onMapLoaded));
        topic.subscribe("app/mapChanged", lang.hitch(this, this._onMapChanged));
      }else{
        topic.subscribe("mapLoaded", lang.hitch(this, this._onMapLoaded));
        topic.subscribe("mapChanged", lang.hitch(this, this._onMapChanged));
      }

      if(window.isBuilder){
        topic.subscribe("app/sceneViewLoaded", lang.hitch(this, this._onSceneViewLoaded));
        topic.subscribe("app/sceneViewChanged", lang.hitch(this, this._onSceneViewChanged));
      }else{
        topic.subscribe("sceneViewLoaded", lang.hitch(this, this._onSceneViewLoaded));
        topic.subscribe("sceneViewChanged", lang.hitch(this, this._onSceneViewChanged));
      }

      if(window.isBuilder){
        topic.subscribe("app/appConfigLoaded", lang.hitch(this, this._onAppConfigLoaded));
        topic.subscribe("app/appConfigChanged", lang.hitch(this, this._onAppConfigChanged));
      }else{
        topic.subscribe("appConfigLoaded", lang.hitch(this, this._onAppConfigLoaded));
        topic.subscribe("appConfigChanged", lang.hitch(this, this._onAppConfigChanged));
      }


      topic.subscribe('userSignIn', lang.hitch(this, this._onUserSignIn));
      topic.subscribe('userSignOut', lang.hitch(this, this._onUserSignOut));

      //events from builder
      topic.subscribe('builder/actionTriggered', lang.hitch(this, this._onActionTriggered));

      //see panel manager
      topic.subscribe('/dnd/move/start', lang.hitch(this, this._onMoveStart));
    },

    loadWidget: function(setting) {
      // summary:
      //    load and create widget, return deferred. when defer is resolved,
      //    widget is returned.
      // description:
      //    setting should contain 2 properties:
      //    id: id should be unique, same id will return same widget object.
      //    uri: the widget's main class

      var def = new Deferred(),
        findWidget;

      setting = lang.clone(setting);

      findWidget = this.getWidgetById(setting.id);

      if (findWidget) {
        //widget have loaded(identified by id)
        def.resolve(findWidget);
      } else {
        this.getWidgetUri(setting).then(lang.hitch(this, function(uri){
          setting.uri = uri;
          all([this.loadWidgetClass(setting), this.loadWidgetManifest(setting)])
          .then(lang.hitch(this, function(results) {
            var clazz = results[0];
            var setting = results[1];
            this.loadWidgetResources(setting).then(lang.hitch(this, function(resouces) {
              try {
                var widget = this.createWidget(setting, clazz, resouces);
                html.setAttr(widget.domNode, 'data-widget-name', setting.name);
                console.log('widget [' + setting.uri + '] created.');
              } catch (err) {
                console.log('create [' + setting.uri + '] error:' + err.stack);
                new Message({
                  message: window.jimuNls.widgetManager.createWidgetError + ': ' + setting.uri
                });

                //when creation is failed, but the dijit is still registered
                if(registry.byId(setting.id)){
                  registry.byId(setting.id).destroy();
                }
                def.reject(err);
              }

              //use timeout to let the widget can get the correct dimension in startup function
              setTimeout(lang.hitch(this, function() {
                def.resolve(widget);
                this.emit('widget-created', widget);
                topic.publish('widgetCreated', widget);
              }), 50);

            }), function(err) {
              def.reject(err);
            });
          }), function(err) {
            def.reject(err);
          });
        }));
      }
      return def;
    },

    loadWidgetClass: function(setting) {
      // summary:
      //    load the widget's main class, and return deferred
      var def = new Deferred();

      var uri;
      if(setting.isRemote){
        uri = setting.uri + '.js';
      }else{
        uri = setting.uri;
      }
      require(utils.getRequireConfig(), [uri], lang.hitch(this, function(clazz) {
        def.resolve(clazz);
      }));

      utils.checkError(setting.uri, def);
      return def;
    },

    loadWidgetResources: function(setting) {
      // summary:
      //    load the widget's resources(local, style, etc.), and return deferred

      var def = new Deferred(),
        defConfig, defI18n, defStyle, defTemplate, defs = [];

      var setting2 = lang.clone(setting);

      defConfig = this.tryLoadWidgetConfig(setting2);
      defI18n = this._tryLoadResource(setting2, 'i18n');
      defStyle = this._tryLoadResource(setting2, 'style');
      defTemplate = this._tryLoadResource(setting2, 'template');

      defs.push(defConfig);
      defs.push(defI18n);
      defs.push(defTemplate);
      defs.push(defStyle);

      all(defs).then(lang.hitch(this, function(results) {
        var res = {};
        res.config = results[0];
        res.i18n = results[1];
        res.template = results[2];
        res.style = results[3];
        def.resolve(res);
      }), function(err) {
        def.reject(err);
      });

      return def;
    },


    getWidgetUri: function(widgetJson){
      if(this.widgetsUri[widgetJson.id]){
        return this.widgetsUri[widgetJson.id];
      }
      var def;
      if(widgetJson.itemId){
        def = portalUtils.getPortal(window.portalUrl).getItemById(widgetJson.itemId).then(function(item){
          if(portalUrlUtils.isOnline(window.portalUrl) && !portalUrlUtils.isOnline(item.url)){
            return new Deferred().reject('Not allow.', widgetJson.itemId);
          }
          return utils.widgetJson.getUriFromItem(item);
        });
      }else{
        var info = utils.getUriInfo(widgetJson.uri);
        if(info.isRemote){
          def = new Deferred().reject('Not allow.', widgetJson.uri);
        }else{
          def = new Deferred().resolve(widgetJson.uri);
        }
      }
      this.widgetsUri[widgetJson.id] = def;
      return def;
    },

    loadWidgetManifest: function(widgetJson){
      var def = new Deferred();

      return this.getWidgetUri(widgetJson).then(lang.hitch(this, function(uri){
        widgetJson.uri = uri;

        if(widgetJson.manifest){
          def.resolve(widgetJson);
          return def;
        }

        var info = utils.getUriInfo(uri);
        var manifestUrl = info.folderUrl + 'manifest.json';
        return xhr(manifestUrl, {
          handleAs:'json',
          headers: {
            "X-Requested-With": null
          }
        }).then(lang.hitch(this, function(manifest){
          if(manifest.error && manifest.error.code){
            //request manifest from AGOL item, and there is an error
            //error code may be: 400, 403
            return def.reject(manifest.error);
          }

          manifest.category = 'widget';
          lang.mixin(manifest, utils.getUriInfo(widgetJson.uri));

          return utils.manifest.addI18NLabel(manifest).then(lang.hitch(this, function(){
            this._processManifest(manifest);
            utils.widgetJson.addManifest2WidgetJson(widgetJson, manifest);
            def.resolve(widgetJson);

            return def;
          }));
        }));
      }), function(err){
        def.reject(err);
      });
    },

    getWidgetMarginBox: function(widget) {
      if (typeof widget === 'string') {
        widget = this.getWidgetById(widget);
        if (!widget) {
          return {};
        }
      }
      if(widget._marginBox){
        return widget._marginBox;
      }

      var position = {
        left: -9999,
        top: -9999,
        relativeTo: widget.position.relativeTo
      };
      widget._isTestSizeFlag = true;//set flag for getSize
      widget.setPosition(position);
      this.openWidget(widget);

      widget._marginBox = widget.getMarginBox();

      this.closeWidget(widget);

      if ("undefined" !== typeof widget._isTestSizeFlag) {
        delete widget._isTestSizeFlag;//delete flag
      }
      return widget._marginBox;
    },

    _processManifest: function(manifest){
      utils.manifest.addManifestProperies(manifest);
      utils.manifest.processManifestLabel(manifest, window.dojoConfig.locale);
    },

    createWidget: function(setting, clazz, resouces) {
      var widget;

      //here, check whether widget have loaded again because loading and create a widget
      //needs some time. If in this period time, more then one loading request will create
      //more widgets with the same id, it's a error.

      if (this.getWidgetById(setting.id)) {
        return this.getWidgetById(setting.id);
      }

      //the config can contain i18n placeholders
      if (resouces.config && resouces.i18n) {
        resouces.config = utils.replacePlaceHolder(resouces.config, resouces.i18n);
      }

      setting.rawConfig = setting.config;
      setting.config = resouces.config || {};
      if (this.appConfig._appData) {
        this._mergeAgolConfig(setting);
      }
      setting.nls = resouces.i18n || {};
      if (resouces.template) {
        setting.templateString = resouces.template;
      }

      setting['class'] = 'jimu-widget';
      if (!setting.label) {
        setting.label = setting.name;
      }
      if (this.map) {
        setting.map = this.map;
      }
      setting.appConfig = this.appConfig;

      // for IE8
      var setting2 = {};
      for (var prop in setting) {
        if (setting.hasOwnProperty(prop)) {
          setting2[prop] = setting[prop];
        }
      }

      setting2.widgetManager = this;

      widget = new clazz(setting2);
      widget.clazz = clazz;
      aspect.after(widget, 'startup', lang.hitch(this, this._postWidgetStartup, widget));
      aspect.before(widget, 'destroy', lang.hitch(this, this._onDestroyWidget, widget));


      // on(widget.domNode, 'click', lang.hitch(this, this._onClickWidget, widget));
      widget.domNode.addEventListener(this._getActiveEventName(),
                                      lang.hitch(this, this._onClickWidget, widget), {capture: true});

      this.loaded.push(widget);
      return widget;
    },

    getAllWidgets: function() {
      return this.loaded;
    },

    destroyAllWidgets: function() {
      var allWidgetIds = array.map(this.loaded, function(widget) {
        return widget.id;
      });
      array.forEach(allWidgetIds, function(widgetId) {
        this.destroyWidget(widgetId);
      }, this);
      this.loaded = [];
    },

    //load the widget setting page class and create setting page object
    //do not cache for now.
    loadWidgetSettingPage: function(setting) {
      var def = new Deferred();
      setting = lang.clone(setting);

      setting.id = setting.id + '_setting';
      all([this.loadWidgetSettingClass(setting)]).
      then(lang.hitch(this, function(results) {
        var clazz = results[0];

        this.loadWidgetSettingPageResources(setting).then(lang.hitch(this, function(resources) {
          var settingObject; // style = results[2]

          // for IE8
          var setting2 = {
            nls: resources.i18n,
            appConfig: this.appConfig,
            // map: this.map,
            'class': 'jimu-widget-setting'
          };
          if (null !== resources.template) {
            setting2.templateString = resources.template;//for false=hasSettingUIFile
          }

          if(window.appInfo.appType === "HTML3D"){
            //for 3D Map, we pass sceneView
            setting2.sceneView = this.sceneView;
          }else{
            //for 2D Map, we pass map
            setting2.map = this.map;
          }

          for (var prop in setting) {
            if (setting.hasOwnProperty(prop)) {
              setting2[prop] = setting[prop];
            }
          }
          try {
            settingObject = new clazz(setting2);
            html.setAttr(settingObject.domNode, 'data-widget-name-setting', setting2.name);
            aspect.before(settingObject, 'destroy',
              lang.hitch(this, this._onDestroyWidgetSetting, settingObject));
            def.resolve(settingObject);
          } catch (err) {
            new Message({
              message: window.jimuNls.widgetManager.createWidgetSettingPageError + ':' + setting.uri
            });
            //when creation is failed, but the dijit is still registered
            if(registry.byId(setting2.id)){
              registry.byId(setting2.id).destroy();
            }
            def.reject(err);
          }
        }), function(err) {
          console.log(err);
        });

      }), function(err) {
        def.reject(err);
      });
      return def;
    },

    loadWidgetSettingClass: function(setting) {
      // summary:
      //    load the widget's main class, and return deferred
      var def = new Deferred();

      var uri;
      if(setting.isRemote){
        uri = setting.folderUrl + 'setting/Setting.js';
      }else{
        uri = setting.amdFolder + 'setting/Setting';
      }
      require(utils.getRequireConfig(), [uri],
      lang.hitch(this, function(clazz) {
        def.resolve(clazz);
      }));

      utils.checkError(setting.folderUrl + 'setting/Setting.js', def);
      return def;
    },

    loadWidgetSettingPageResources: function(setting) {
      var def = new Deferred();
      var defI18n, defStyle, defTemplate, defs = [];

      setting = lang.clone(setting);
      defI18n = this._tryLoadResource(setting, 'settingI18n');
      defTemplate = this._tryLoadResource(setting, 'settingTemplate');
      defStyle = this._tryLoadResource(setting, 'settingStyle');

      defs.push(defI18n);
      defs.push(defTemplate);
      defs.push(defStyle);

      all(defs).then(lang.hitch(this, function(results) {
        var res = {};
        res.i18n = results[0] || {};
        res.template = results[1];
        res.style = results[2];
        def.resolve(res);
      }), function(err) {
        console.log(err);
      });

      return def;
    },

    getWidgetById: function(id) {
      var ret;
      array.some(this.loaded, function(w) {
        if (w.id === id) {
          ret = w;
          return true;
        }
      }, this);
      return ret;
    },

    getWidgetByLabel: function(label) {
      var ret;
      array.some(this.loaded, function(w) {
        if (w.label === label) {
          ret = w;
          return true;
        }
      }, this);
      return ret;
    },

    getWidgetsByName: function(name) {
      var ret = [];
      array.some(this.loaded, function(w) {
        if (w.name === name) {
          ret.push(w);
        }
      }, this);
      return ret;
    },

    //normal, minimized, maximized
    changeWindowStateTo: function(widget, state) {
      if (state === 'normal') {
        this.normalizeWidget(widget);
      } else if (state === 'minimized') {
        this.minimizeWidget(widget);
      } else if (state === 'maximized') {
        this.maximizeWidget(widget);
      } else {
        console.log('error state: ' + state);
      }
    },

    closeWidget: function(widget) {
      if (typeof widget === 'string') {
        widget = this.getWidgetById(widget);
        if (!widget) {
          return;
        }
      }
      if (widget.state !== 'closed') {
        if(this.activeWidget && this.activeWidget.id === widget.id){
          this.activeWidget.onDeActive();
          this.activeWidget = null;
        }
        html.setStyle(widget.domNode, 'display', 'none');
        widget.setState('closed');
        try {
          widget.onClose();
        } catch (err) {
          console.log(console.error('fail to close widget ' + widget.name + '. ' + err.stack));
        }
      }

      if(!this.appConfig.mode){
        this._removeDataSourceUsage(widget);
      }
    },

    openWidget: function(widget) {
      if (typeof widget === 'string') {
        widget = this.getWidgetById(widget);
        if (!widget) {
          return;
        }
      }
      if(!widget.started){
        try {
          widget.started = true;
          widget.startup();
        } catch (err) {
          console.error('fail to startup widget ' + widget.name + '. ' + err.stack);
        }
      }
      if (widget.state === 'closed') {
        html.setStyle(widget.domNode, 'display', '');
        widget.setState('opened');
        try {
          widget.onOpen();
        } catch (err) {
          console.error('fail to open widget ' + widget.name + '. ' + err.stack);
        }
      }else{
        return;//Prevent repeated opening of the same widget.#14994
      }

      if (!widget._isTestSizeFlag && utils.isAutoFocusFirstNodeWidget(widget)) {//Special case for _isTestSizeFlag: timeSlider, swipe
        utils.focusFirstFocusNode(widget.domNode);
      }

      if(!this.appConfig.mode){
        this._addDataSourceUsage(widget);
      }
    },

    activateWidget: function(widget){
      //activate a widget, the widget must be opened first
      if (typeof widget === 'string') {
        widget = this.getWidgetById(widget);
        if (!widget) {
          return;
        }
      }
      if(widget.state !== 'opened'){
        return;
      }

      this._activeWidget(widget);
    },

    maximizeWidget: function(widget) {
      if (typeof widget === 'string') {
        widget = this.getWidgetById(widget);
        if (!widget) {
          return;
        }
      }
      if (widget.state === 'closed') {
        this.openWidget(widget);
      }

      widget.setWindowState('maximized');
      try {
        widget.onMaximize();
      } catch (err) {
        console.log(console.error('fail to maximize widget ' + widget.name + '. ' + err.stack));
      }
    },

    minimizeWidget: function(widget) {
      if (typeof widget === 'string') {
        widget = this.getWidgetById(widget);
        if (!widget) {
          return;
        }
      }

      if (widget.state === 'closed') {
        this.openWidget(widget);
      }
      widget.setWindowState('minimized');
      try {
        widget.onMinimize();
      } catch (err) {
        console.log(console.error('fail to minimize widget ' + widget.name + '. ' + err.stack));
      }
    },

    normalizeWidget: function(widget) {
      if (typeof widget === 'string') {
        widget = this.getWidgetById(widget);
        if (!widget) {
          return;
        }
      }

      if (widget.state === 'closed') {
        this.openWidget(widget);
      }
      widget.setWindowState('normal');
      try {
        widget.onNormalize();
      } catch (err) {
        console.log(console.error('fail to normalize widget ' + widget.name + '. ' +
        err.stack));
      }
    },

    destroyWidget: function(widget) {
      var m;
      if (typeof widget === 'string') {
        m = this.getWidgetById(widget);
        if (!m) {
          //maybe, the widget is loading
          return;
        } else {
          widget = m;
        }
      }
      this._removeWidget(widget);
      try {
        widget.destroy();
      } catch (err) {
        console.log(console.error('fail to destroy widget ' + widget.name + '. ' + err.stack));
      }
    },

    tryLoadWidgetConfig: function(setting) {
      return this._tryLoadWidgetConfig(setting).then(lang.hitch(this, function(config) {
        return this._upgradeWidgetConfig(setting, config).then(function(widgetConfig){
          setting.config = widgetConfig;
          return widgetConfig;
        });
      }));
    },

    triggerWidgetOpen: function(widgetId){
      var def = new Deferred();
      var widget = this.getWidgetById(widgetId);
      if(widget){
        if(widget.state !== 'closed'){
          def.resolve(widget);
          return def;
        }else{
          resolveAfterOpen(widget);
        }
      }else{
        var handle = topic.subscribe('widgetCreated', lang.hitch(this, function(_widget){
          if(_widget.id === widgetId){
            handle.remove();
            //we resolve here instead of waiting for onOpen, because onOpen may have been
            //triggered before this call back.
            def.resolve(_widget);
          }
        }));
      }

      topic.publish('openWidget', widgetId);

      function resolveAfterOpen(widget){
        var handle = aspect.after(widget, 'onOpen', function(){
          handle.remove();
          def.resolve(widget);
        });
      }

      return def;
    },

    _tryLoadWidgetConfig: function(setting) {
      var def = new Deferred();
      //need load config first, because the template may be use the config data
      if (setting.config && lang.isObject(setting.config)) {
        //if widget is configurated in the app config.json, the i18n has beed processed
        def.resolve(setting.config);
        return def;
      } else if (setting.config) {
        if(require.cache['url:' + setting.config]){
          def.resolve(json.parse(require.cache['url:' + setting.config]));
          return def;
        }
        var configFile = utils.processUrlInAppConfig(setting.config);
        // The widgetConfig filename is dependent on widget label,
        // IE8 & IE9 do not encode automatically while attempt to request file.
        var configFileArray = configFile.split('/');
        configFileArray[configFileArray.length - 1] =
          encodeURIComponent(configFileArray[configFileArray.length - 1]);
        configFile = configFileArray.join('/');
        return xhr(configFile, {
          handleAs: "json",
          headers: {
            "X-Requested-With": null
          }
        });
      } else {
        return this._tryLoadResource(setting, 'config').then(function(config){
          //this property is used in map config plugin
          setting.isDefaultConfig = true;
          return config;
        });
      }
    },

    _upgradeWidgetConfig: function(json, oldConfig){
      var def = new Deferred();
      var widgetVersion = json.manifest.version;
      var configVersion = json.version;
      var newConfig;

      if(widgetVersion === configVersion){
        def.resolve(oldConfig);
        return def;
      }

      if(json.hasVersionManager === false){
        //widget config loaded from builder needs update version
        json.version = widgetVersion;

        //if widget doesn't have version manager, we assume the widget
        //is old version compatible
        def.resolve(oldConfig);
        return def;
      }

      require(utils.getRequireConfig(), [json.amdFolder + (json.isRemote? 'VersionManager.js': 'VersionManager')],
      lang.hitch(this, function(VersionManager) {
        var versionManager = new VersionManager();

        var configVersionIndex = versionManager.getVersionIndex(configVersion);
        var widgetVersionIndex = versionManager.getVersionIndex(widgetVersion);
        if(configVersionIndex > widgetVersionIndex){
          def.reject('Bad widget version number, ' + json.name + ',' + configVersion);
        }else{
          try{
            newConfig = versionManager.upgrade(oldConfig, configVersion, widgetVersion);
            json.version = widgetVersion;
            def.resolve(newConfig);
          }catch(err){
            console.log('Read widget [' + json.name + '] old config error,' + err.stack);
            def.resolve(oldConfig);
          }
        }
      }));

      return def;
    },

    /*
     * Load the css file in a widget.
     * This function load the widget's css file and insert it into the HTML page through <link>.
     * It also ensure that the css file is inserted only one time.
     */
    loadWidgetStyle: function(widgetSetting) {
      var id = this._getStyleIdFromWidgetJson(widgetSetting, false),
        def = new Deferred();
      if (html.byId(id)) {
        def.resolve('load');
        return def;
      }
      var themeCommonStyleId = 'theme_' + this.appConfig.theme.name + '_style_common';
      //insert widget style before theme style, to let theme style over widget style
      return utils.loadStyleLink(id, widgetSetting.styleFile, themeCommonStyleId);
    },

    loadWidgetSettingStyle: function(widgetSetting) {
      var id = this._getStyleIdFromWidgetJson(widgetSetting, true),
        def = new Deferred();
      if (html.byId(id)) {
        def.resolve('load');
        return def;
      }
      return utils.loadStyleLink(id, widgetSetting.settingStyleFile);
    },

    loadWidgetConfig: function(widgetSetting) {
      var configFilePath = require(utils.getRequireConfig()).toUrl(widgetSetting.configFile);
      if(require.cache['url:' + configFilePath]){
        var def = new Deferred();
        def.resolve(json.parse(require.cache['url:' + configFilePath]));
        return def;
      }
      return xhr(configFilePath, {
        handleAs: "json",
        headers: {
          "X-Requested-With": null
        }
      });
    },

    loadWidgetI18n: function(widgetSetting) {
      var def = new Deferred();
      require(utils.getRequireConfig(), ['dojo/i18n!' + widgetSetting.i18nFile],
      function(bundle) {
        def.resolve(bundle);
      });
      return def;
    },

    loadWidgetSettingI18n: function(widgetSetting) {
      var def = new Deferred();
      require(utils.getRequireConfig(), ['dojo/i18n!' + widgetSetting.settingI18nFile],
      function(bundle) {
        def.resolve(bundle);
      });
      return def;
    },

    loadWidgetTemplate: function(widgetSetting) {
      var def = new Deferred();
      require(utils.getRequireConfig(), ['dojo/text!' + widgetSetting.templateFile],
      function(template) {
        def.resolve(template);
      });

      utils.checkError(widgetSetting.templateFile, def);
      return def;
    },

    loadWidgetSettingTemplate: function(widgetSetting) {
      var def = new Deferred();
      require(utils.getRequireConfig(), ['dojo/text!' + widgetSetting.settingTemplateFile],
      function(template) {
        def.resolve(template);
      });

      utils.checkError(widgetSetting.settingTemplateFile, def);
      return def;
    },

    removeWidgetStyle: function(widget) {
      this._removeStyle(this._getStyleIdFromWidgetJson(widget, false));
    },

    removeWidgetSettingStyle: function(widget) {
      this._removeStyle(this._getStyleIdFromWidgetJson(widget, true));
    },

    _removeStyle: function(styleId){
      if(!html.byId(styleId)){
        return;
      }
      var importStyles = html.attr(styleId, 'data-import-styles');
      if(importStyles){
        for(var i = 0; i < parseInt(importStyles, 10); i ++){
          html.destroy(styleId + '_import_' + i);
        }
      }
      html.destroy(styleId);
    },

    _getStyleIdFromWidgetJson: function(widgetJson, isSetting){
      var p = widgetJson.itemId? widgetJson.itemId: widgetJson.uri;
      var id = 'widget/style/' + p + (isSetting? '/setting': '');
      return this._replaceId(id);
    },

    getControllerWidgets: function() {
      return array.filter(this.loaded, function(widget) {
        return widget.isController;
      });
    },

    getOffPanelWidgets: function() {
      return array.filter(this.loaded, function(widget) {
        return !widget.inPanel;
      });
    },

    getOnScreenOffPanelWidgets: function() {
      return array.filter(this.loaded, function(widget) {
        return widget.isOnScreen && !widget.inPanel;
      });
    },

    closeOtherWidgetsInTheSameGroup: function(widget){
      if (typeof widget === 'string') {
        widget = this.getWidgetById(widget);
        if (!widget) {
          return;
        }
      }
      for(var i = 0; i < this.loaded.length; i++){
        if(this.loaded[i].gid === widget.gid && this.loaded[i].id !== widget.id){
          this.closeWidget(this.loaded[i]);
        }
      }
    },

    closeAllWidgetsInGroup: function (groupId){
      for(var i = 0; i < this.loaded.length; i++){
        if(this.loaded[i].gid === groupId){
          this.closeWidget(this.loaded[i]);
        }
      }
    },

    _addDataSourceUsage: function(widget){
      array.forEach(this._getUsedDataSourceIdFromWidget(widget), function(dsId){
        DataSourceManager.getInstance().addDataSourceUsage(dsId, widget.id);
      });
    },

    _removeDataSourceUsage: function(widget){
      array.forEach(this._getUsedDataSourceIdFromWidget(widget), function(dsId){
        DataSourceManager.getInstance().removeDataSourceUsage(dsId, widget.id);
      });
    },

    _getUsedDataSourceIdFromWidget: function(widget){
      var widgetJson = this.appConfig.getConfigElementById(widget.id);
      if(!widgetJson || !widgetJson.config){
        return;
      }
      var configStr = json.stringify(widgetJson.config);
      return array.filter(Object.keys(this.appConfig.dataSource.dataSources), function(dsId){
        return configStr.indexOf(dsId) > -1;
      }, this);
    },

    // Merge AGOL configs when first open widget (because the
    // widgetConfig just loaded if the widget has not been edited yet).
    // This method only merge fields in widget config,
    // the other fields were merged in ConfigManager.js
    _mergeAgolConfig: function(setting) {
      var values = this.appConfig._appData.values;
      function doMerge(sectionKey) {
        for (var key in values) {
          var sectionKeyIndex = key.replace(/\//g, '_').indexOf(sectionKey + '_config');
          if (sectionKeyIndex >= 0){
            utils.template.setConfigValue(setting,
                        key.replace(/\//g, '_').substr(sectionKeyIndex, key.length).
                        replace(sectionKey, 'widget'),
                        values[key]);
          }
        }
      }
      var sectionKey;
      sectionKey = 'widgets[' + setting.id + ']';
      doMerge(sectionKey);
    },

    _onUserSignIn: function(credential) {
      array.forEach(this.loaded, function(m) {
        m.onSignIn(credential);
      }, this);
    },

    _onUserSignOut: function() {
      array.forEach(this.loaded, function(m) {
        m.onSignOut();
      }, this);
    },

    _activeWidget: function(widget){
      if(this.activeWidget){
        if(this.activeWidget.id === widget.id){
          //zIndex may be reset by widget self, we do not set in-panel widget zindex
          if(this.activeWidget.inPanel === false && this.activeWidget.moveTopOnActive){
            html.setStyle(this.activeWidget.domNode, 'zIndex', 101);
          }
          return;
        }
        if(this.activeWidget.state === 'active'){
          this.activeWidget.setState('opened');
          if(this.activeWidget.inPanel === false){
            html.setStyle(widget.domNode, 'zIndex',
            'zIndex' in widget.position? widget.position.zIndex: 'auto');
          }
          this.activeWidget.onDeActive();
        }
      }
      this.activeWidget = widget;
      if(this.activeWidget.state !== 'opened'){
        return;
      }
      this.activeWidget.setState('active');
      if(this.activeWidget.inPanel === false && this.activeWidget.moveTopOnActive){
        html.setStyle(this.activeWidget.domNode, 'zIndex', 101);
      }
      this.activeWidget.onActive();
      // utils.focusFirstFocusNode(widget.domNode);
      topic.publish('widgetActived', widget);
    },

    _onClickWidget: function(widget, evt){
      var childWidgets = query('.jimu-widget', widget.domNode);
      if(childWidgets.length > 0){
        for(var i = 0; i < childWidgets.length; i++){
          if(evt.target === childWidgets[i] || html.isDescendant(evt.target, childWidgets[i])){
            //click on the child widget or child widget's children dom
            return;
          }
        }
      }
      this._activeWidget(widget);
    },

    _onMoveStart: function(mover){
      array.forEach(this.loaded, function(widget){
        if(widget.domNode === mover.node){
          this._activeWidget(widget);
        }
      }, this);
    },

    _onAppConfigLoaded: function(_appConfig) {
      var appConfig = lang.clone(_appConfig);
      this.appConfig = appConfig;
      tokenUtils.setPortalUrl(appConfig.portalUrl);
    },

    _onMapLoaded: function(map) {
      this.map = map;
    },

    _onMapChanged: function(map) {
      this.map = map;
    },

    _onSceneViewLoaded: function(sceneView){
      this.sceneView = sceneView;
    },

    _onSceneViewChanged: function(sceneView){
      this.sceneView = sceneView;
    },

    _onAppConfigChanged: function(_appConfig, reason, changedData, otherOptions) {
      var appConfig = lang.clone(_appConfig);
      this.appConfig = appConfig;
      array.forEach(this.loaded, function(w) {
        if (!w) {
          //widget maybe deleted in the handler of appConfigChange event
          return;
        }
        w.onAppConfigChanged(appConfig, reason, changedData, otherOptions);
        if (reason === 'widgetChange') {
          this._onConfigChanged(changedData.id, changedData.config, otherOptions);
        }
      }, this);
    },

    _onConfigChanged: function(id, config) {
      //summary:
      //  widget which care it's own config change should override onConfigChanged function
      var w = this.getWidgetById(id);
      if (!w) {
        return;
      }

      w.onConfigChanged(config);
      lang.mixin(w.config, config);
    },

    _onActionTriggered: function(info) {
      if (info.elementId === 'map' || info.elementId === 'app') {
        return;
      }
      var m = this.getWidgetById(info.elementId);
      if (!m) {
        this.missedActions.push({
          id: info.elementId,
          action: {
            name: info.action,
            data: info.data
          }
        });
      } else {
        m.onAction(info.action, info.data);
      }
      //may be the controller widget also need process the action
      array.forEach(this.getControllerWidgets(), function(ctrlWidget) {
        if (ctrlWidget.widgetIsControlled(info.elementId)) {
          ctrlWidget.onAction(info.action, {
            widgetId: info.elementId,
            data: info.data
          });
        }
      }, this);
    },


    //Go to the expected Dom that's should be focusing on when using screenReader-chrome&chromeVox and FF/IE&nvda
    //Chrome&chromeVox will focus first focusable node that tabindex > 0, not min-tabindex > 0
    //FF&nvda will focus second focusable node that tabindex > 0, not min-tabindex > 0 when all of nodes're using role="application"
    _resetFirstFocusNode: function(widgetObject){
      if(window.isMoveFocusWhenInit){
        var splashWidget = this.getWidgetsByName('Splash')[0];
        if(splashWidget && html.getStyle(splashWidget.domNode, 'display') !== 'none'){
          window.isMoveFocusWhenInit = false;
          if(widgetObject && widgetObject.name === 'Splash'){
          }else{
            splashWidget.domNode.focus();
          }
        }else if(window.currentMsgPopup && window.currentMsgPopup.firstFocusNode){
          window.isMoveFocusWhenInit = false;
          window.currentMsgPopup.focusedNodeBeforeOpen = utils.getFirstSkipLink();
          window.currentMsgPopup.firstFocusNode.focus();
        }else if(window.isMoveFocusWhenInit){
          window.isMoveFocusWhenInit = false;
          if(utils.isInNavMode()){
            utils.focusOnFirstSkipLink();
          }
        }else{
          return false;
        }
        return true;
      }
    },
    _postWidgetStartup: function(widgetObject) {
      widgetObject.started = true;//for backward compatibility
      utils.setVerticalCenter(widgetObject.domNode);
      aspect.after(widgetObject, 'resize', lang.hitch(this,
        utils.setVerticalCenter, widgetObject.domNode));

      //init styles and event for first/last nodes
      utils.initTabIndexAndOrder(widgetObject);
      //init cancel event for widgetDom
      utils.initWidgetCancelEvent(widgetObject);

      var pos = widgetObject.position || widgetObject.panel && widgetObject.panel.position;
      if(pos && pos.relativeTo === 'browser'){
        on(widgetObject.domNode, 'focus', lang.hitch(this, function() {
          this._resetFirstFocusNode();
        }));
      }

      this.openWidget(widgetObject);
      // if(widgetObject.defaultState){
      //   this.changeWindowStateTo(widgetObject, widgetObject.defaultState);
      // }

      var portalUrl = this.appConfig.portalUrl;
      var credential = tokenUtils.getPortalCredential(portalUrl);
      if (credential) {
        widgetObject.onSignIn(credential);
      } else {
        widgetObject.onSignOut();
      }
      this._triggerMissedAction(widgetObject);

      //add tooltips
      if(!widgetObject.inPanel){
        utils.addTooltipByDomNode(Tooltip, widgetObject.domNode, widgetObject.label);
      }
    },

    _triggerMissedAction: function(widget) {
      this.missedActions.forEach(function(info) {
        if (info.id === widget.id) {
          widget.onAction(info.action.name, info.action.data);
        }
      });
    },

    _remove: function(id) {
      delete this.widgetsUri[id]
      return array.some(this.loaded, function(w, i) {
        if (w.id === id) {
          this.loaded.splice(i, 1);
          return true;
        }
      }, this);
    },

    _tryLoadResource: function(setting, flag) {
      var file, hasp,
        def = new Deferred(),
        doLoad = function() {
          var loadDef;
          if (flag === 'config') {
            loadDef = this.loadWidgetConfig(setting);
          } else if (flag === 'style') {
            loadDef = this.loadWidgetStyle(setting);
          } else if (flag === 'i18n') {
            loadDef = this.loadWidgetI18n(setting);
          } else if (flag === 'template') {
            loadDef = this.loadWidgetTemplate(setting);
          } else if (flag === 'settingTemplate') {
            loadDef = this.loadWidgetSettingTemplate(setting);
          } else if (flag === 'settingStyle') {
            loadDef = this.loadWidgetSettingStyle(setting);
          } else if (flag === 'settingI18n') {
            loadDef = this.loadWidgetSettingI18n(setting);
          } else {
            return def;
          }
          loadDef.then(function(data) {
            def.resolve(data);
          }, function(err) {
            console.error('Load widget resource error. resource:', flag);
            console.error(err);
            new Message({
              message: window.jimuNls.widgetManager.loadWidgetResourceError + ': ' + setting.uri
            });
            def.reject(err);
          });
        };

      if (flag === 'config') {
        file = setting.amdFolder + 'config.json';
        setting.configFile = file;
        hasp = 'hasConfig';
      } else if (flag === 'style') {
        file = setting.amdFolder + 'css/style.css';
        setting.styleFile = file;
        hasp = 'hasStyle';
      } else if (flag === 'i18n') {
        file = setting.amdFolder + 'nls/strings.js';
        if(setting.isRemote){
          setting.i18nFile = file;
        }else{
          setting.i18nFile = setting.amdFolder + 'nls/strings';
        }
        hasp = 'hasLocale';
      } else if (flag === 'template') {
        file = setting.amdFolder + 'Widget.html';
        setting.templateFile = file;
        hasp = 'hasUIFile';
      } else if (flag === 'settingTemplate') {
        file = setting.amdFolder + 'setting/Setting.html';
        setting.settingTemplateFile = file;
        hasp = 'hasSettingUIFile';
      } else if (flag === 'settingI18n') {
        file = setting.amdFolder + 'setting/nls/strings.js';
        if(setting.isRemote){
          setting.settingI18nFile = file;
        }else{
          setting.settingI18nFile = setting.amdFolder + 'setting/nls/strings';
        }
        hasp = 'hasSettingLocale';
      } else if (flag === 'settingStyle') {
        file = setting.amdFolder + 'setting/css/style.css';
        setting.settingStyleFile = file;
        hasp = 'hasSettingStyle';
      } else {
        return def;
      }

      if (setting[hasp]){
        doLoad.apply(this);
      }else {
        def.resolve(null);
      }
      return def;
    },

    _replaceId: function(id) {
      return id.replace(/\//g, '_').replace(/\./g, '_');
    },

    _onDestroyWidget: function(widget) {
      if (widget.state !== 'closed') {
        this.closeWidget(widget);
      }
      this._removeWidget(widget);
      this.emit('widget-destroyed', widget.id);
      topic.publish('widgetDestroyed', widget.id);
      console.log('destroy widget [' + widget.uri + '].');
    },

    _onDestroyWidgetSetting: function(settingWidget) {
      this.removeWidgetSettingStyle(settingWidget);
    },

    _removeWidget: function(widget) {
      var m;
      if (typeof widget === 'string') {
        m = this.getWidgetById(widget);
        if (!m) {
          //maybe, the widget is loading
          return;
        } else {
          widget = m;
        }
      }

      if(this.activeWidget && this.activeWidget.id === widget.id){
        this.activeWidget = null;
      }
      this._remove(widget.id);
      if(this.getWidgetsByName(widget.name).length === 0){
        this.removeWidgetStyle(widget);
      }
    },

    _getActiveEventName: function() {
      var hasPointer = "PointerEvent" in window ? "pointer" :
                          "MSPointerEvent" in window? "MSPointer" : false;
      var hasMSPointer = hasPointer && hasPointer.slice(0, 2) === "MS";
      var activeEventName = hasPointer ? hasPointer + (hasMSPointer ? "Down" : "down") : "mousedown";
      return activeEventName;
    }
  });

  clazz.getInstance = function(urlParams) {
    if (instance === null) {
      instance = new clazz(urlParams);
      window._widgetManager = instance;
    }
    return instance;
  };
  return clazz;
});