///////////////////////////////////////////////////////////////////////////
// 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/on',
    'dojo/Evented',
    'dojo/_base/declare',
    'dijit/_WidgetBase',
    'dijit/_TemplatedMixin',
    'dijit/_WidgetsInTemplateMixin',
    'dojo/store/Memory',
    'dojo/Deferred',
    'dojo/store/Observable',
    'dijit/tree/ObjectStoreModel',
    'dojo/promise/all',
    'dojo/_base/lang',
    'dojo/_base/html',
    'dojo/_base/array',
    'jimu/utils',
    'jimu/dijit/_Tree',
    'jimu/LayerInfos/LayerInfos',
    'jimu/dijit/LoadingIndicator'
  ],
  function(on, Evented, declare, _WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin, Memory, Deferred, Observable,
    ObjectStoreModel, all, lang, html, array, jimuUtils, JimuTree, LayerInfos, LoadingIndicator) {

    var LayerChooser = declare([_WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin, Evented], {
      templateString:'<div style="width:100%;">' +
        '<div data-dojo-attach-point="errorTipSection" class="error-tip-section">' +
          '<span class="jimu-icon jimu-icon-error"></span>' +
          '<span class="jimu-state-error-text" data-dojo-attach-point="errTip">' +
          '${nls.noLayersTip}</span>' +
        '</div>' +
      '</div>',
      _store: null,
      _id: 0,
      _treeClass: 'layer-chooser-tree',

      //constructor options:
      createMapResponse: null, //The response of method createMap.
      multiple: false, //Can select multiple layers or a single layer.
      onlyShowVisible: false,
      updateWhenLayerInfosIsShowInMapChanged: false,
      onlyShowWebMapLayers: false,
      displayTooltipForTreeNode: false,
      // when click the item, the filter only take effect on current layer, does not take effect on sublayers.
      onlyFilterCurrentLayerWhenAddItem: false,

      //public methods:
      //getSelectedItems

      //methods need to override:
      //getSelectedItems
      //filter

      //attributes:
      //tree

      //events:
      //tree-click
      //update

      postMixInProperties:function(){
        this.nls = window.jimuNls.basicLayerChooserFromMap;
      },

      postCreate: function() {
        this.inherited(arguments);
        html.addClass(this.domNode, 'jimu-basic-layer-chooser-from-map');
        this.multiple = !!this.multiple;

        this.shelter = new LoadingIndicator({hidden:true});
        this.shelter.placeAt(this.domNode);
        this.shelter.startup();

        this._createTree();
        this.basicFilter = lang.hitch(this, this.basicFilter);
        this.filter = LayerChooser.andCombineFilters([this.basicFilter, this.filter]);

        if(this.createMapResponse){
          this.setCreateMapResponse(this.createMapResponse);
        }
      },

      basicFilter: function(layerInfo){
        var def = new Deferred();
        if(this.onlyShowVisible){
          def.resolve(layerInfo.isShowInMap());
        }else{
          def.resolve(true);
        }
        return def;
      },

      //to be override, return Deferred object
      //if resolve true, means layerInfo can be displayed in tree
      filter: function(layerInfo){
        /*jshint unused: false*/
        var def = new Deferred();
        def.resolve(true);
        return def;
      },

      //return an array, each element has 'name' and 'layerInfo' attribute
      getSelectedItems: function(){
        var items = this.tree.getSelectedItems();
        var handledItems = array.map(items, lang.hitch(this, function(item){
          return this.getHandledItem(item);
        }));
        return handledItems;
      },

      //return an array, each element has 'name' and 'layerInfo' attribute
      getAllItems: function(){
        var items = this.tree.getAllItems();
        var handledItems = [];
        array.forEach(items, lang.hitch(this, function(item){
          if(item.id !== 'root'){
            var handledItem = this.getHandledItem(item);
            handledItems.push(handledItem);
          }
        }));
        return handledItems;
      },

      //to be override
      getHandledItem: function(item){
        return {
          name: item.name,
          layerInfo: item.layerInfo
        };
      },

      _isLeafItem: function(item) {
        return item.isLeaf;
      },

      setCreateMapResponse: function(createMapResponse){
        this.createMapResponse = createMapResponse;
        var map = this.createMapResponse.map;
        var mapItemInfo = this.createMapResponse.itemInfo;
        LayerInfos.getInstance(map, mapItemInfo).then(lang.hitch(this, function(layerInfosObj) {
          this.layerInfosObj = layerInfosObj;
          this.own(
            on(this.layerInfosObj, 'layerInfosChanged', lang.hitch(this, this._onLayerInfosChanged))
          );
          if(this.updateWhenLayerInfosIsShowInMapChanged){
            this.own(
              on(this.layerInfosObj, 'layerInfosIsShowInMapChanged',
                lang.hitch(this, this._onLayerInfosIsShowInMapChanged))
            );
          }
          this._buildTree(this.layerInfosObj);
        }));
      },

      _onLayerInfosChanged: function(layerInfo, changedType) {
        /*jshint unused: false*/
        this._buildTree(this.layerInfosObj);
        this.emit('update');
      },

      _onLayerInfosIsShowInMapChanged: function(changedLayerInfos){
        /*jshint unused: false*/
        this._buildTree(this.layerInfosObj);
        this.emit('update');
      },

      _buildTree: function(layerInfosObj){
        this._clear();
        html.setStyle(this.errorTipSection, 'display', 'block');
        var layerInfos = [];

        if(this.onlyShowWebMapLayers){
          layerInfos = layerInfosObj.getLayerInfoArrayOfWebmap();
          layerInfos = layerInfos.concat(layerInfosObj.getTableInfoArrayOfWebmap());
        }else{
          layerInfos = layerInfosObj.getLayerInfoArray();
          layerInfos = layerInfos.concat(layerInfosObj.getTableInfoArray());
        }

        if(layerInfos.length === 0){
          return;
        }

        html.setStyle(this.errorTipSection, 'display', 'none');
        array.forEach(layerInfos, lang.hitch(this, function(layerInfo){
          this._addDirectLayerInfo(layerInfo);
        }));
      },

      _addDirectLayerInfo: function(layerInfo){
        if(!layerInfo){
          return;
        }
        layerInfo.getLayerObject().then(lang.hitch(this, function(){
          this._addItem('root', layerInfo);
        }), lang.hitch(this, function(err){
          console.error(err);
        }));
      },

      _clear:function(){
        var items = this._store.query({parent:'root'});
        array.forEach(items, lang.hitch(this, function(item){
          if(item && item.id !== 'root'){
            this._store.remove(item.id);
          }
        }));
      },

      _addItem: function(parentId, layerInfo) {
        var item = null;
        var layerTypeDef = layerInfo.getLayerType();
        var validDef = this.filter(layerInfo);
        all({
          layerType: layerTypeDef,
          valid: validDef
        }).then(lang.hitch(this, function(result) {
          if(result.valid) {
            var callback = lang.hitch(this, function(isLeaf, hasChildren){
              this._id++;
              item = {
                name: layerInfo.title || "",
                parent: parentId,
                layerInfo: layerInfo,
                type: result.layerType,
                layerClass: layerInfo.layerObject.declaredClass,
                id: this._id.toString(),
                isLeaf: isLeaf,
                hasChildren: hasChildren
              };
              this._store.add(item);
            });

            var subLayerInfos = layerInfo.getSubLayers();

            var isLeaf = subLayerInfos.length === 0;

            var hasChildren = true;

            if(this.onlyFilterCurrentLayerWhenAddItem) {
              if(isLeaf) {
                hasChildren = false;
              }
              callback(isLeaf, hasChildren);
            } else if(isLeaf){
              hasChildren = false;
              callback(isLeaf, hasChildren);
            }else{
              var defs = array.map(subLayerInfos, lang.hitch(this, function(subLayerInfo){
                return this.filter(subLayerInfo);
              }));
              all(defs).then(lang.hitch(this, function(filterResults){
                var hasChildren = array.some(filterResults, function(filterResult){
                  return filterResult;
                });
                if(hasChildren){
                  callback(isLeaf, hasChildren);
                }
              }));
            }
          }
        }));
      },

      _getRootItem:function(){
        return { id: 'root', name:'Map Root', type:'root', isLeaf: false, hasChildren: true};
      },

      _createTree: function() {
        var rootItem = this._getRootItem();
        var myMemory = new Memory({
          data: [rootItem],
          getChildren: function(object) {
            return this.query({
              parent: object.id
            });
          }
        });

        // Wrap the store in Observable so that updates to the store are reflected to the Tree
        this._store = new Observable(myMemory);

        var myModel = new ObjectStoreModel({
          store: this._store,
          query: {
            id: "root"
          },
          mayHaveChildren: lang.hitch(this, this._mayHaveChildren)
        });

        this.tree = new JimuTree({
          multiple: this.multiple,
          model: myModel,
          showRoot: false,
          isLeafItem: lang.hitch(this, this._isLeafItem),

          style: {
            width: "100%"
          },

          onOpen: lang.hitch(this, function(item, node) {
            if (item.id === 'root') {
              return;
            }
            this._onTreeOpen(item, node);
          }),

          onClick: lang.hitch(this, function(item, node, evt) {
            this._onTreeClick(item, node, evt);
            this.emit('tree-click', item, node, evt);
          }),

          getIconStyle: lang.hitch(this, function(item, opened) {
            var icon = null;
            if (!item || item.id === 'root') {
              return null;
            }

            var a = {
              width: "20px",
              height: "20px",
              backgroundRepeat: 'no-repeat',
              backgroundPosition: 'center center',
              backgroundImage: ''
            };

            var baseUrl = window.location.protocol + "//" + window.location.host + require.toUrl("jimu");

            var imageName = this._getIconInfo(item, opened).imageName;

            if (imageName) {
              a.backgroundImage = "url(" + baseUrl + "/css/images/" + imageName + ")";
              icon = a;
            }

            return icon;
          }),

          getIconClass: lang.hitch(this, function(item, opend) {
            return this._getIconInfo(item, opend).className;
          }),

          getTooltip: lang.hitch(this, function(item){
            return this.displayTooltipForTreeNode ? item.layerInfo.title : "";
          })
        });
        html.addClass(this.tree.domNode, this._treeClass);
        this.tree.placeAt(this.shelter.domNode, 'before');
      },

      _mayHaveChildren: function(item) {
        return item.hasChildren;
      },

      _getIconInfo: function(item, opened) {
        var imageName = '';
        var className = '';

        if (item.type === 'ArcGISDynamicMapServiceLayer' ||
          item.type === 'ArcGISTiledMapServiceLayer') {
          if (opened) {
            imageName = 'mapserver_open.png';
            className = 'mapservice-layer-icon open';
          } else {
            imageName = 'mapserver_close.png';
            className = 'mapservice-layer-icon close';
          }
        } else if (item.type === 'GroupLayer') {
          if (opened) {
            imageName = 'group_layer2.png';
            className = 'group-layer-icon open';
          } else {
            imageName = 'group_layer1.png';
            className = 'group-layer-icon close';
          }
        } else if (item.type === 'FeatureLayer') {
          var geoType = jimuUtils.getTypeByGeometryType(item.layerInfo.layerObject.geometryType);
          if (geoType === 'point') {
            imageName = 'point_layer1.png';
            className = 'point-layer-icon';
          } else if (geoType === 'polyline') {
            imageName = 'line_layer1.png';
            className = 'line-layer-icon';
          } else if (geoType === 'polygon') {
            imageName = 'polygon_layer1.png';
            className = 'polygon-layer-icon';
          }
        } else if(item.type === 'Table'){
          imageName = "table.png";
          className = 'table-icon';
        } else if(item.type === 'ArcGISImageServiceLayer' ||
         item.type === 'ArcGISImageServiceVectorLayer'){
          imageName = 'image_layer.png';
          className = 'iamge-layer-icon';
        } else {
          if (opened) {
            imageName = 'mapserver_open.png';
            className = 'mapservice-layer-icon open';
          } else {
            imageName = 'mapserver_close.png';
            className = 'mapservice-layer-icon close';
          }
        }
        return {
          imageName: imageName,
          className: className
        };
      },

      _onTreeOpen: function(item, node) { /*jshint unused: false*/
        if(item.id === 'root'){
          return;
        }
        var layerInfo = item.layerInfo;
        var subLayerInfos = [];
        var defs = [];
        subLayerInfos = layerInfo.getSubLayers();
        if (item.checked) {
          return;
        }
        this.shelter.show();
        defs = array.map(subLayerInfos, lang.hitch(this, function(subLayerInfo) {
          return subLayerInfo.getLayerObject();
        }));

        all(defs).then(lang.hitch(this, function() {
          if (!this.domNode) {
            return;
          }
          array.forEach(subLayerInfos, lang.hitch(this, function(subLayerInfo) {
            this._addItem(item.id, subLayerInfo);
          }));
          item.checked = true;
          this.shelter.hide();
        }), lang.hitch(this, function(err) {
          console.error(err);
          this.shelter.hide();
          if (!this.domNode) {
            return;
          }
        }));
      },

      //to be override
      _onTreeClick: function(item, node, evt){/*jshint unused: false*/},

      destroy: function(){
        if(this.shelter){
          this.shelter.destroy();
          this.shelter = null;
        }
        if(this.tree){
          this.tree.destroy();
        }
        this.inherited(arguments);
      }
    });

    //layerTypes: array, such as ['FeatureLayer']
    //supports layers:
    //    "FeatureLayer"
    //    "ArcGISDynamicMapServiceLayer"
    //    "ArcGISTiledMapServiceLayer"
    //    "GeoRSSLayer"
    //    "KMLLayer"
    //    "WMSLayer"
    //    "WTMSLayer"
    //    "FeatureCollection"
    //the returned filter will filter layers by layerType
    LayerChooser.createFilterByLayerType = function(layerTypes) {
      if (!lang.isArrayLike(layerTypes)) {
        layerTypes = [];
      }
      return function(layerInfo) {
        var defResult = new Deferred();
        if (layerTypes.length === 0) {
          defResult.resolve(true);
        } else {
          var layerTypeDefs = [];

          layerInfo.traversal(function(layerInfo) {
            layerTypeDefs.push(layerInfo.getLayerType());
          });

          all(layerTypeDefs).then(function(layerTypeDefResults) {
            for (var i = 0; i < layerTypeDefResults.length; i++) {
              for (var j = 0; j < layerTypes.length; j++) {
                if (layerTypeDefResults[i] === layerTypes[j]) {
                  defResult.resolve(true);
                  return;
                }
              }
            }
            defResult.resolve(false);
          }, function(err){
            console.error(err);
            defResult.reject(err);
          });
        }

        return defResult;
      };
    };

    //the returned filter only filters FeatureLayer
    LayerChooser.createFeaturelayerFilter = function(types, showLayerFromFeatureSet, showTable, mustSupportStatistics){
      var allTypes = ['point', 'polyline', 'polygon'];
      if(types && types.length > 0){
        types = array.filter(types, function(type){
          return allTypes.indexOf(type) >= 0;
        });
        if(types.length === 0){
          types = allTypes;
        }
      }
      else{
        types = allTypes;
      }

      return function(layerInfo){
        var defLayerType = layerInfo.getLayerType();
        var defLayerObject = layerInfo.getLayerObject();
        return all({
          layerType: defLayerType,
          layerObject: defLayerObject
        }).then(function(result){
          var layerType = result.layerType;
          var layerObject = result.layerObject;
          if (layerType === 'ArcGISDynamicMapServiceLayer') {
            return true;
          } else if (layerType === 'ArcGISTiledMapServiceLayer') {
            return true;
          } else if (layerType === 'GroupLayer'){
            return true;
          } else if (layerType === 'FeatureCollection'){
            return true;
          }else if (layerType === 'FeatureLayer') {
            var geoType = jimuUtils.getTypeByGeometryType(layerObject.geometryType);
            var isValidGeoType = array.indexOf(types, geoType) >= 0;
            var isLayerValidStatistics = LayerChooser._shouldPassStatisticsCheck(mustSupportStatistics, layerObject);

            if (layerObject.url) {
              //featurelayer by url
              var isLayerSupportQuery = jimuUtils.isFeaturelayerUrlSupportQuery(layerObject.url,
                  layerObject.capabilities);
              return (isValidGeoType && isLayerSupportQuery && isLayerValidStatistics);
            } else {
              //featurelayer by featureset
              return (showLayerFromFeatureSet && isValidGeoType);
            }
          } else if(layerType === 'Table'){
            //if showTable is true, we will ignore types
            var isTableSupportQuery = jimuUtils.isFeaturelayerUrlSupportQuery(layerObject.url,
                  layerObject.capabilities);
            var isTableValidStatistics = LayerChooser._shouldPassStatisticsCheck(mustSupportStatistics, layerObject);
            return (showTable && isTableSupportQuery && isTableValidStatistics);
          }else{
            return false;
          }
        });
      };
    };

    //the returned filter only filters ArcGISImageServiceLayer and ArcGISImageServiceVectorLayer
    LayerChooser.createImageServiceLayerFilter = function(isSupportQuery, mustSupportStatistics){
      return function(layerInfo){
        var defLayerType = layerInfo.getLayerType();
        var defLayerObject = layerInfo.getLayerObject();
        return all({
          layerType: defLayerType,
          layerObject: defLayerObject
        }).then(function(result){
          var layerType = result.layerType;
          var layerObject = result.layerObject;
          if(layerType === 'ArcGISImageServiceLayer' ||
           layerType === 'ArcGISImageServiceVectorLayer'){
            if(isSupportQuery){
              if(jimuUtils.isImageServiceSupportQuery(result.layerObject.capabilities)){
                if(mustSupportStatistics){
                  return LayerChooser._shouldPassStatisticsCheck(mustSupportStatistics, layerObject);
                }else{
                  return true;
                }
              }else{
                return false;
              }
            }else{
              return true;
            }
          }else{
            return false;
          }
        });
      };
    };

    LayerChooser._shouldPassStatisticsCheck = function(mustSupportStatistics, layerObject){
      if(mustSupportStatistics){
        var isSupport = false;
        if (layerObject.advancedQueryCapabilities) {
          isSupport = !!layerObject.advancedQueryCapabilities.supportsStatistics;
        } else {
          isSupport = !!layerObject.supportsStatistics;
        }
        return isSupport;
      }else{
        return true;
      }
    };

    LayerChooser.createQueryableLayerFilter = function(mustSupportStatistics){
      var types = ['point', 'polyline', 'polygon'];
      var featureLayerFilter = LayerChooser.createFeaturelayerFilter(types, false, true, mustSupportStatistics);
      var imageServiceLayerFilter = LayerChooser.createImageServiceLayerFilter(true, mustSupportStatistics);
      var filters = [featureLayerFilter, imageServiceLayerFilter];
      var combinedFilter = LayerChooser.orCombineFilters(filters);
      return combinedFilter;
    };

    //combine multiple filters into one filter
    //if all filters pass, the combined filter will pass
    LayerChooser.andCombineFilters = function(filters){
      return LayerChooser._combineFilters(filters, true);
    };

    //combine multiple filters into one filter
    //if one of the filters passes, the combined filter will pass
    LayerChooser.orCombineFilters = function(filters){
      return LayerChooser._combineFilters(filters, false);
    };

    LayerChooser._combineFilters = function(filters, isAnd){
      return function(layerInfo){
        var defResult = new Deferred();
        var defs = array.map(filters, function(filter){
          return filter(layerInfo);
        });
        all(defs).then(function(filterResults){
          var isPass = false;
          if(isAnd){
            isPass = array.every(filterResults, function(filterResult){
              return filterResult;
            });
          }else{
            isPass = array.some(filterResults, function(filterResult){
              return filterResult;
            });
          }

          defResult.resolve(isPass);
        }, function(err){
          console.error(err);
          defResult.reject(err);
        });
        return defResult;
      };
    };

    return LayerChooser;
  });
