define([
                                'dojo/Deferred',
                                'dojo/dom-style',
                                'esri/tasks/query',
                                'esri/tasks/QueryTask',
                                'esri/graphic',
                                'esri/layers/GraphicsLayer',
                                'esri/symbols/SimpleFillSymbol',
                                'esri/symbols/SimpleLineSymbol',
                                'esri/Color',
                                'esri/renderers/DotDensityRenderer',
                                'esri/layers/FeatureLayer',
                                'Hud.Common/Viewport/EsriMapController',
                                'Hud.Common/MapLegend/MapLegendController'
                            ],
                            function (
                                Deferred,
                                domStyle,
                                Query,
                                QueryTask,
                                Graphic,
                                GraphicsLayer,
                                SimpleFillSymbol,
                                SimpleLineSymbol,
                                Color,
                                DotDensityRenderer,
                                FeatureLayer,
                                EsriMap,
                                MapLegend) {
                            
                                var thisMap = null, //live map
                                    legend = null,
                                    config = null,
                                    graphicsLayer = new GraphicsLayer({ id: 'affht-graphics-layer' }),
                                    selectedAffhtMap = null,
                                    selectedJurisdiction = null,
                                    selectedRegion = null;
                            
                                return {
                                    /**
                                    * Sets the module config to the input.
                                    * @method setConfig
                                    */
                                    setConfig: function (inputConfig) {
                                        config = inputConfig;
                                    },
                            
                                    /**
                                    * Loads an Esri JS API map from a JSON definiton object. Returns a deferred that resolves when the map is loaded.
                                    * @method loadMap
                                    * 
                                    * @param {String} dotValue
                                    * @param {Array} fields
                                    *
                                    * @return {Deferred} def
                                    */
                                    loadMap: function (mapDefinition, mapElementId) {
                                        var def = new Deferred();
                                        //build options for custom map class
                                        var options = {
                                            config: mapDefinition,
                                            mapElementId: mapElementId
                                        };
                                        //instantiate custom map class and create a map
                                        var eMap = new EsriMap(options);
                                        var d = eMap.initializeMap();
                                        d.then(function (map) {
                                            //set local reference
                                            thisMap = map;
                                            //add the graphics layer
                                            map.addLayer(graphicsLayer);
                                            //resolve the deferred returned by loadMap()
                                            def.resolve(map);
                                        });
                                        //TODO: Handle deferred error (failed map load)
                            
                                        //return deferred - resolved on map load
                                        return def;
                                    },
                            
                                    /**
                                    * Returns the object that represents the currently selected AFFHT map.
                                    * @method getSelectedAffhtMap
                                    * @return {Object}
                                    */
                                    getSelectedAffhtMap: function () {
                                        return selectedAffhtMap;
                                    },
                            
                                    /**
                                    * Sets the local property to an object that represents the currently selected AFFHT map.
                                    * @method getSelectedAffhtMap
                                    * @return {Object}
                                    */
                                    setSelectedAffhtMap: function (affthMap) {
                                        selectedAffhtMap = affthMap;
                                    },
                            
                                    /**
                                    * Returns the graphics layer used by the custom affht components.
                                    * @method getGraphicsLayer
                                    * @return {GraphicsLayer}
                                    */
                                    getGraphicsLayer: function () {
                                        return graphicsLayer;
                                    },
                            
                                    /**
                                    * Returns an Esri JS API feature object representing the selected AFFHT grantee.
                                    * @method getSelectedJurisdiction
                                    * @return {Object}
                                    */
                                    getSelectedJurisdiction: function () {
                                        return selectedJurisdiction;
                                    },
                            
                                    /**
                                    * Sets the local property to an Esri JS API feature object representing the selected AFFHT grantee.
                                    * @method setSelectedJurisdiction
                                    */
                                    setSelectedJurisdiction: function (feature) {
                                        selectedJurisdiction = feature;
                                    },
                            
                                    /**
                                    * Returns an Esri JS API feature object representing the selected AFFHT CBSA.
                                    * @method getSelectedRegion
                                    * @return {Object}
                                    */
                                    getSelectedRegion: function () {
                                        return selectedRegion;
                                    },
                            
                                    /**
                                    * Sets the local property to an Esri JS API feature object representing the selected AFFHT CBSA.
                                    * @method setSelectedRegion
                                    */
                                    setSelectedRegion: function (feature) {
                                        selectedRegion = feature;
                                    },
                            
                                    /**
                                    * Queries the ArcGIS Server rest endpoint for a list of AFFHT grantees within the specified state.
                                    * @method queryJurisdictionsByState
                                    * 
                                    * @param {String} stateAbbr
                                    *
                                    * @return {Deferred} d
                                    */
                                    queryJurisdictionsByState: function (stateAbbr) {
                                        var d = new Deferred();
                                        //query task
                                        var qt = new QueryTask(config.url + "/" + config.jurisdictionLayerId);
                                        //query parameters
                                        var qp = new Query();
                                        qp.returnGeometry = false;
                                        qp.where = "STUSAB = '" + stateAbbr + "'";
                                        qp.outFields = config.outFields;
                                        qp.orderByFields = ["NAME"];
                                        //async query
                                        var q = qt.execute(qp);
                                        q.then(function (results) {
                                            d.resolve(results);
                                        });
                            
                                        return d;
                                    },
                            
                                    /**
                                    * Queries the ArcGIS Server rest endpoint for a grantee feature with the matching ID.
                                    * @method queryJurisdictionById
                                    *
                                    * @param {String} jurisdictionId
                                    *
                                    * @return {Deferred} d
                                    */
                                    queryJurisdictionById: function (jurisdictionId, uogid) {
                                        var d = new Deferred();
                                        var queryField = (uogid ? "UOGID" : "OBJECTID");
                            
                                        //query object
                                        var qt = new QueryTask(config.url + "/" + config.jurisdictionLayerId);
                                        //query parameters
                                        var qp = new Query();
                                        qp.returnGeometry = true;
                                        qp.where = queryField + " = '" + jurisdictionId + "'";
                                        qp.outFields = ["*"];
                                        //async query
                                        var q = qt.execute(qp);
                                        q.then(function (results) {
                                            d.resolve(results);
                                        });
                                        //TODO: handler error result
                            
                                        return d;
                                    },
                            
                                    /**
                                    * Queries the ArcGIS Server rest endpoint for a CBSA feature with the matching ID.
                                    * @method queryRegionById
                                    *
                                    * @param {String} regionId
                                    *
                                    * @return {Deferred} d
                                    */
                                    queryRegionById: function (regionID) {
                                        var d = new Deferred();
                            
                                        //query object
                                        var qt = new QueryTask(config.url + "/" + config.regionLayerId);
                                        //query parameters
                                        var qp = new Query();
                                        qp.returnGeometry = true;
                                        qp.where = "CBSAID = '" + regionID + "'";
                                        qp.outFields = ["*"];
                                        //async query
                                        var q = qt.execute(qp);
                                        q.then(function (results) {
                                            d.resolve(results);
                                        });
                                        //TODO: handle error result
                            
                                        return d;
                                    },
                            
                                    /**
                                    * Zooms the live map to the extent of the currently selected AFFHT grantee.
                                    * @method zoomToJurisdiction
                                    *
                                    * @return {Deferred}
                                    */
                                    zoomToJurisdiction: function () {
                                        var extent = selectedJurisdiction.geometry.getExtent().expand(1.5);
                                        return thisMap.setExtent(extent);
                                    },
                            
                                    /**
                                    * Zooms the live map to the extent of the currently selected AFFHT CBSA.
                                    * @method zoomToRegion
                                    *
                                    * @return {Deferred}
                                    */
                                    zoomToRegion: function () {
                                        var extent = null;
                                        for (var i = 0, length = selectedRegion.length; i < length; i++) {
                                            var feature = selectedRegion[i];
                                            if (extent == null) { extent = feature.geometry.getExtent(); }
                                            else { extent = extent.union(feature.geometry.getExtent()); }
                                        }
                            
                                        return thisMap.setExtent(extent);
                                    },
                            
                                    /**
                                    * Draws the currently selected AFFHT grantee boundary on the live map.
                                    * @method drawJurisdiction
                                    */
                                    drawJurisdiction: function () {
                                        //remove existing jurisdiction graphics if they exist
                                        for (var i = 0, length = graphicsLayer.graphics.length; i < length; i++) {
                                            var graphic = graphicsLayer.graphics[i];
                                            try {
                                                if (graphic.attributes.groupId && graphic.attributes.groupId == "cdbg") { graphicsLayer.remove(graphic); }
                                            }
                                            catch (e) { }
                                        }
                            
                                        var sfs = new SimpleFillSymbol(SimpleFillSymbol.STYLE_NULL,
                                            new SimpleLineSymbol(SimpleLineSymbol.STYLE_SOLID,
                                            new Color("#660000"), 1.5), new Color([120, 120, 120, 0.9])
                                          );
                            
                                        graphicsLayer.add(new Graphic(selectedJurisdiction.geometry, sfs, { groupId: "cdbg" }));
                                    },
                            
                                    /**
                                    * Draws the currently selected AFFHT CBSA boundary on the live map.
                                    * @method drawRegion
                                    */
                                    drawRegion: function () {
                                        //remove existing region/cbsa graphics if they exist
                                        for (var i = 0, length = graphicsLayer.graphics.length; i < length; i++) {
                                            try {
                                                var graphic = graphicsLayer.graphics[i];
                                                if (graphic.attributes.groupId && graphic.attributes.groupId == "cbsa") { graphicsLayer.remove(graphic); }
                                            }
                                            catch (e) { }
                                        }
                            
                                        //add new region/cbsa features 
                                        var extent = null;
                                        for (var i = 0, length = selectedRegion.length; i < length; i++) {
                                            var feature = selectedRegion[i];
                                            var sfs = new SimpleFillSymbol(SimpleFillSymbol.STYLE_NULL,
                                                new SimpleLineSymbol(SimpleLineSymbol.STYLE_SOLID,
                                                new Color("#000000"), 1.5), new Color([120, 120, 120, 0.9])
                                              );
                                            graphicsLayer.add(new Graphic(feature.geometry, sfs, { groupId: "cbsa" }));
                                        }
                                    },
                            
                                    /**
                                    * A utitility method to update all dot density layers in the map with the input dot density ratio value.
                                    * @method updateDotDensity
                                    * @param {String} dotDensity
                                    */
                                    updateDotDensity: function (dotDensity) {
                                        var layerIds = thisMap.layerIds;
                                        for (var i = 0, length = layerIds.length; i < length; i++) {
                                            var layer = thisMap.getLayer(layerIds[i]);
                            
                                            if (layer.id.indexOf("dot-density") > -1) {
                                                var fields = layer.renderer.fields;
                                                var dotSize = layer.renderer.dotSize;
                                                var dotShape = layer.renderer.dotShape;
                                                layer.setRenderer(this.createDotDensityRenderer(dotDensity, fields, dotSize, dotShape));
                                                layer.redraw();
                            
                                                this.renderLegend();
                                            }
                                        }
                                    },
                            
                                    /**
                                    * A utility method that builds and returns an Esri JS API DotDensityRenderer object.
                                    * @method createDotDensityRenderer
                                    *
                                    * @param {Number} dotValue
                                    * @param {Array} fields
                                    * @param {Number} dotSize
                                    * @param {String} dotShape
                                    * @param {string} valueUnit (optional)
                                    *
                                    * @return {DotDensityRenderer}
                                    */
                                    createDotDensityRenderer: function (dotValue, fields, dotSize, dotShape, valueUnit) {
                                        //validate the valueUnit param
                                        if (!valueUnit || valueUnit === '') { valueUnit = 'People'; }
                            
                                        return new DotDensityRenderer({
                                            fields: fields,
                                            dotValue: dotValue,
                                            dotSize: dotSize,
                                            dotShape: dotShape,
                                            legendOptions: {
                                                valueUnit: valueUnit
                                            }
                                        });
                                    },
                            
                                    /**
                                    * Creates an AFFHT map legend and injects the HTML into the DOM. An AFFHT map legend has enhanced features to show custom dot density renderer fields based on configuration.
                                    * @method renderLegend
                                    * @return {Deferred} d
                                    */
                                    renderLegend: function (aoi) {
                                        var self = this;
                                        var d = new Deferred();
                            
                                        var mapDefinition = JSON.parse(selectedAffhtMap.attributes.MAP_DEF);
                            
                                        //clear the legend
                                        if (legend) {
                                            try {
                                                legend.destroy();
                                            }
                                            catch (e) { }
                                        }
                            
                                        //hide legend while it is updated
                                        domStyle.set("affht-sidebar-legend", "display", "none");
                            
                                        if (mapDefinition.options && mapDefinition.options.legendLabels && mapDefinition.options.legendLabels.length > 0) {
                                            //query ArcGIS Server for the custom dot density legend labels
                                            var len = mapDefinition.options.legendLabels.length;
                                            for (var x = 0; x < len; x++) {
                                                var layer = thisMap.getLayer(mapDefinition.options.legendLabels[x].id),
                                                    legendInfo = mapDefinition.options.legendLabels[x];
                            
                                                if (layer) {
                                                    //build query parameters
                                                    var labelQuery = new Query();
                                                    if (aoi == "jurisdiction") {
                                                        labelQuery.where = "CDBGUOGID = '" + selectedJurisdiction.attributes.UOGID + "'";
                                                    }
                                                    else {
                                                        labelQuery.where = "CBSAID = '" + selectedRegion[0].attributes.CBSAID + "'";
                                                    }
                                                    labelQuery.outFields = ["*"];
                            
                                                    //execute query
                                                    var labelQT = new QueryTask(layer.url);
                                                    var ld = labelQT.execute(labelQuery);
                                                    ld.then(function (results) {
                                                        //create dummy layer for the legend layerinfos using query results
                                                        var feature = results.features[0];
                                                        //get the real dot density layer                            
                                                        var dotLayer;
                                                        for (var i = 0, length = thisMap.layerIds.length; i < length; i++) {
                                                            if (thisMap.layerIds[i].indexOf("dot-density") > -1) {
                                                                dotLayer = thisMap.getLayer(thisMap.layerIds[i]);
                                                                break;
                                                            }
                                                        }
                                                        //create dummy layer (never added to the map)
                                                        var fl = new FeatureLayer(dotLayer.url, { id: "legend-dummy" });
                                                        //build new fields object and set renderer
                                                        var fields = [];
                                                        for (var m = 0, _fl = dotLayer.renderer.fields.length; m < _fl; m++) {
                                                            for (var j = 0, l = legendInfo.labels.length; j < l; j++) {
                                                                if (dotLayer.renderer.fields[m].name == legendInfo.labels[j][0]) {
                                                                    fields.push({ name: feature.attributes[legendInfo.labels[j][1]], color: dotLayer.renderer.fields[m].color });
                                                                    break;
                                                                }
                                                            }
                            
                                                        }
                                                        //dummy layer needs real dot density renderer
                                                        fl.setRenderer(self.createDotDensityRenderer(dotLayer.renderer.dotValue, fields, dotLayer.renderer.dotSize, dotLayer.renderer.dotShape));
                                                        fl.redraw();
                                                        //set the legend fields to a layer property to be used by the legend class
                                                        dotLayer._dotDensityTocFields = fields;
                                                        //render legend (legend will look for special property on the layer)
                                                        legend = new MapLegend({
                                                            layerOverride: { layer: fl, id: dotLayer.id },
                                                            config: {
                                                                containerId: "affht-sidebar-legend",
                                                                enableGui: false
                                                            },
                                                            map: thisMap
                                                        });
                            
                                                        domStyle.set("affht-sidebar-legend", "display", "block");
                                                        //resolve deferred
                                                        d.resolve("Legend loaded");
                                                    });
                                                }
                                            }
                                        }
                                        else {
                                            legend = new MapLegend({
                                                config: {
                                                    containerId: "affht-sidebar-legend",
                                                    enableGui: false
                                                },
                                                map: thisMap
                                            });
                                            domStyle.set("affht-sidebar-legend", "display", "block");
                                            //resolve deferred
                                            d.resolve("Legend loaded");
                                        }
                            
                                        return d;
                                    },
                                };
                            });