text: ///////////////////////////////////////////////////////////////////////////
  // 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/Deferred',
      'dojo/_base/lang',
      'dojo/_base/array',
      'dojo/topic',
      'dojo/on',
      'esri/layers/FeatureLayer',
      'esri/layers/GraphicsLayer',
      'esri/geometry/geometryEngine',
      'esri/graphic',
      'esri/Color',
      'esri/symbols/SimpleMarkerSymbol',
      'esri/symbols/SimpleLineSymbol',
      'esri/symbols/SimpleFillSymbol'
    ],
    function(declare, Deferred, lang, array, topic, on, FeatureLayer, GraphicsLayer, geometryEngine,
      Graphic, Color, SimpleMarkerSymbol, SimpleLineSymbol, SimpleFillSymbol) {
      var instance = null;
  
      var clazz = declare(null, {
  
        constructor: function(){
          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));
          }
        },
  
        /**
         * The display layers(GraphicsLayer) for layer in mapService. The display layer is used for show highlight.
         * For feature layer in map, we don't need display layer because the feature layer will show the highlight.
         * @type {Object}
         */
        _displayLayers: {},
  
        previousMapScale: null,
  
        setSelectionSymbol: function(layer){
          var type = layer.geometryType;
          var markerCircleStyle = SimpleMarkerSymbol.STYLE_CIRCLE;
          var pointSymbol = new SimpleMarkerSymbol(markerCircleStyle, 16, null, Color.fromArray([0, 255, 255]));
  
          var lineSolidStyle = SimpleLineSymbol.STYLE_SOLID;
          var lineSymbol = new SimpleLineSymbol(lineSolidStyle, Color.fromArray([0, 255, 255]), 2);
  
          var fillSolidStyle = SimpleFillSymbol.STYLE_SOLID;
          var fillSymbol = new SimpleFillSymbol(fillSolidStyle, lineSymbol, Color.fromArray([0, 255, 255, 0.3]));
  
          if (type === 'esriGeometryPoint') {
            layer.setSelectionSymbol(pointSymbol);
          } else if (type === 'esriGeometryPolyline') {
            layer.setSelectionSymbol(lineSymbol);
          } else if (type === 'esriGeometryPolygon') {
            layer.setSelectionSymbol(fillSymbol);
          }
        },
  
        //features must have objectIdField attribute, such as OBJECTID
        updateSelectionByFeatures: function(layer, features, selectionMethod){
          if(features.length > 0){
            features = array.map(features, lang.hitch(this, function(feature){
              var value = null;
              if(layer.graphics.indexOf(feature) >= 0){
                value = new Graphic(feature.toJson());
              }else{
                value = feature;
              }
              value.wabIsTemp = true;
              return value;
            }));
          }
  
          //selectionMethod default value is SELECTION_NEW
          //def must be a dojo/_base/Deferred object, because it has callback method and API will call this method
          if(!layer.getSelectionSymbol()){
            this.setSelectionSymbol(layer);
          }
          var def = new Deferred();
          //def.then(function() {}, function() {});
          var response = {
            features: features
          };
          layer._selectHandler(response, selectionMethod, null, null, def);
          if(this._isLayerNeedDisplayLayer(layer)){
            if(!this._displayLayers[layer.id]){
              this._displayLayers[layer.id] = this._createDisplayLayer(layer);
            }
            this._updateDisplayLayer(layer, features, selectionMethod);
          }
          return def;
        },
  
        //private method
        getDisplayLayer: function(featureLayerId){
          return this._displayLayers[featureLayerId];
        },
  
        setSelection: function(layer, features){
          return this.updateSelectionByFeatures(layer, features, FeatureLayer.SELECTION_NEW);
        },
  
        addFeaturesToSelection: function(layer, features){
          return this.updateSelectionByFeatures(layer, features, FeatureLayer.SELECTION_ADD);
        },
  
        removeFeaturesFromSelection: function(layer, features){
          return this.updateSelectionByFeatures(layer, features, FeatureLayer.SELECTION_SUBTRACT);
        },
  
        clearSelection: function(featureLayer){
          var def = new Deferred();
          this.clearDisplayLayer(featureLayer);
          featureLayer.clearSelection();
          def.resolve();
          return def;
        },
  
        clearDisplayLayer: function(featureLayer){
          var displayLayer = this._displayLayers[featureLayer.id];
          if(displayLayer){
            displayLayer.clear();
          }
        },
  
        getClientFeaturesByGeometry: function(layer, geometry, fullyWithinGeometry){
          var features = array.filter(layer.graphics, lang.hitch(this, function(g) {
            if(fullyWithinGeometry){
              return geometryEngine.contains(geometry, g.geometry);
            }else{
              return geometryEngine.intersects(geometry, g.geometry);
            }
          }));
          return features;
        },
  
        getUnionGeometryBySelectedFeatures: function(layer){
          var unionGeometry = null;
          var features = layer.getSelectedFeatures();
          if (features.length > 0) {
            var geometries = array.map(features, lang.hitch(this, function(feature) {
              return feature.geometry;
            }));
            unionGeometry = geometryEngine.union(geometries);
          }
          return unionGeometry;
        },
  
        _createDisplayLayer: function(featureLayer){
          var displayLayer = new GraphicsLayer();
          var idField = featureLayer.objectIdField;
          displayLayer.fields = featureLayer.fields;
          displayLayer.id = "displayLayer_of_" + featureLayer.id;
          this.map.addLayer(displayLayer);
          if (!this.map.spatialReference.equals(featureLayer.spatialReference) || featureLayer.hasWebGLSurface()) {
            var selectionSymbol = featureLayer.getSelectionSymbol();
            // feature layer need to project dynamically, update geometry of that feature
            on(featureLayer, 'update-end', lang.hitch(this, function() {
              var scale = Math.round(this.map.getScale());
              if (this.previousMapScale !== scale && displayLayer.graphics && displayLayer.graphics.length > 0) {
                this.previousMapScale = scale;
                var selectedIds = array.map(displayLayer.graphics, function(graphic) {
                  return graphic.attributes[idField];
                });
                var selectedFeatures = array.filter(featureLayer.graphics, function(graphic) {
                  return selectedIds.indexOf(graphic.attributes[idField]) >= 0;
                });
                displayLayer.clear();
                array.forEach(selectedFeatures, lang.hitch(this, function(feature){
                  var graphic = new Graphic(feature.toJson());
                  graphic.setSymbol(selectionSymbol);
                  displayLayer.add(graphic);
                }));
              }
            }));
          }
          return displayLayer;
        },
  
        _updateDisplayLayer: function(featureLayer, features, selectionMethod){
          var displayLayer = this._displayLayers[featureLayer.id];
          var selectionSymbol = featureLayer.getSelectionSymbol();
          //for featureLayer & webGL
          if(featureLayer.hasWebGLSurface()){
            this._updateDisplayLayerWebGL(featureLayer, displayLayer, selectionSymbol, features, selectionMethod);
            return;
          }
          //for map services
          var selectFeatures = featureLayer.getSelectedFeatures();
          this.clearDisplayLayer(featureLayer);
          array.forEach(selectFeatures, lang.hitch(this, function(feature){
            feature.setSymbol(null);
            var graphic = new Graphic(feature.toJson());
            graphic.setSymbol(selectionSymbol);
            displayLayer.add(graphic);
          }));
        },
  
        //for featureLayer & webGL
        _updateDisplayLayerWebGL: function(featureLayer, displayLayer, selectionSymbol, features, selectionMethod){
          if(selectionMethod === FeatureLayer.SELECTION_NEW){
            featureLayer._selectedFeaturesWebGL = {};
          }
  
          if(selectionMethod === FeatureLayer.SELECTION_NEW || selectionMethod === FeatureLayer.SELECTION_ADD){
            array.forEach(features, lang.hitch(this, function(feature){
              var idField = featureLayer.objectIdField;
              var featureIdVal = feature.attributes[idField];
              featureLayer._selectedFeaturesWebGL[featureIdVal] = feature;
            }));
          }else if(selectionMethod === FeatureLayer.SELECTION_SUBTRACT){
            array.forEach(features, lang.hitch(this, function(feature){
              var idField = featureLayer.objectIdField;
              var featureIdVal = feature.attributes[idField];
              if(featureIdVal in featureLayer._selectedFeaturesWebGL){
                delete featureLayer._selectedFeaturesWebGL[featureIdVal];
              }
            }));
          }
  
          this.clearDisplayLayer(featureLayer);
          for(var key in featureLayer._selectedFeaturesWebGL){
            var feature = featureLayer._selectedFeaturesWebGL[key];
            var graphic = new Graphic(feature.toJson());
            graphic.setSymbol(selectionSymbol);
            displayLayer.add(graphic);
          }
        },
  
        _isLayerNeedDisplayLayer: function(featureLayer){
          return featureLayer.hasWebGLSurface() || !featureLayer.getMap();
        },
  
        _onMapLoaded: function(map){
          this.map = map;
        },
  
        _onMapChanged: function(map){
          this.map = map;
        }
      });
  
      clazz.getInstance = function(){
        if(!instance){
          instance = new clazz();
          window._selectionManager = instance;
        }
        return instance;
      };
  
      return clazz;
    });