text: goog.provide('ga_search_type_directives');
  
  goog.require('ga_debounce_service');
  goog.require('ga_layerfilters_service');
  goog.require('ga_layermetadatapopup_service');
  goog.require('ga_layers_service');
  goog.require('ga_maputils_service');
  goog.require('ga_marker_overlay_service');
  goog.require('ga_previewfeatures_service');
  goog.require('ga_previewlayers_service');
  goog.require('ga_search_service');
  goog.require('ga_topic_service');
  goog.require('ga_urlutils_service');
  
  (function() {
  
    // We can't put strings in zoomlevel attribute of search results. That's
    // why we put huge numbers to indicate that we want to use the bbox for
    // zooming instead of the delivered zoomlevel.
    var ZOOM_LIMIT = 100;
  
    var parseExtent = function(stringBox2D) {
      var extent = stringBox2D.replace(/(BOX\(|\))/gi, '').replace(',', ' ').
          split(' ');
      return $.map(extent, parseFloat);
    };
  
    var addOverlay = function(gaMarkerOverlay, map, res) {
      var visible = /^(address|parcel|gazetteer)$/.test(res.attrs.origin);
      var center = [res.attrs.y, res.attrs.x];
      if (!res.attrs.y || !res.attrs.x) {
        center = ol.proj.transform([res.attrs.lon, res.attrs.lat],
            'EPSG:4326', map.getView().getProjection());
      }
      gaMarkerOverlay.add(map,
          center,
          visible,
          parseExtent(res.attrs.geom_st_box2d));
  
    };
  
    var removeOverlay = function(gaMarkerOverlay, map) {
      gaMarkerOverlay.remove(map);
    };
  
    var listenerMoveEnd;
    var registerMove = function(gaMarkerOverlay, gaDebounce, map) {
      listenerMoveEnd = map.on('moveend', gaDebounce.debounce(function() {
        var zoom = map.getView().getZoom();
        gaMarkerOverlay.setVisibility(zoom);
      }, 200, false, false));
    };
  
    var unregisterMove = function() {
      if (listenerMoveEnd) {
        ol.Observable.unByKey(listenerMoveEnd);
        listenerMoveEnd = undefined;
      }
    };
  
    var tabStarts = [
      100000,
      200000,
      300000
    ];
  
    var nextTabGroup = function(val) {
      for (var i = 0; i < tabStarts.length - 1; i++) {
        if (val >= tabStarts[i] &&
            val < tabStarts[i + 1]) {
          return tabStarts[i + 1];
        }
      }
      return undefined;
    };
  
    var prevTabGroup = function(val) {
      for (var i = tabStarts.length - 1; i > 0; i--) {
        if (val >= tabStarts[i]) {
          return tabStarts[i - 1];
        }
      }
      return undefined;
    };
  
    var focusElement = function(el, evt) {
      evt.preventDefault();
      el.trigger('focus');
    };
  
    var elExists = function(el) {
      if (el.length === 1 &&
          el[0].className.indexOf('ga-search-result') > -1) {
        return true;
      }
      return false;
    };
  
    var focusToElement = function(next, step, evt) {
      var newEl;
      if (next) {
        newEl = $(evt.target).nextAll('.ga-search-result').first();
      } else {
        newEl = $(evt.target).prevAll('.ga-search-result').first();
      }
      if (elExists(newEl)) {
        var existingEl = newEl;
        step -= 1;
        while (step > 0 && elExists(newEl)) {
          existingEl = newEl;
          step -= 1;
          if (next) {
            newEl = newEl.nextAll('.ga-search-result').first();
          } else {
            newEl = newEl.prevAll('.ga-search-result').first();
          }
        }
        focusElement(existingEl, evt);
      } else {
        focusToCategory(next, evt);
      }
    };
  
    var focusToCategory = function(next, evt) {
      var el = $(evt.target);
      if (el.length && el[0] && el[0].attributes && el[0].attributes.tabindex) {
        var jumpGroup, newEl;
        if (next) {
          jumpGroup = nextTabGroup(el[0].attributes.tabindex.value);
          while (jumpGroup) {
            newEl = $('[tabindex=' + jumpGroup + ']');
            if (elExists(newEl)) {
              focusElement(newEl, evt);
              break;
            }
            jumpGroup = nextTabGroup(jumpGroup);
          }
        } else {
          jumpGroup = prevTabGroup(el[0].attributes.tabindex.value);
          while (jumpGroup) {
            newEl = $('[tabindex=' + jumpGroup + ']');
            if (elExists(newEl)) {
              var existingEl = newEl;
              // Go to last element of category
              while (elExists(newEl)) {
                existingEl = newEl;
                jumpGroup += 1;
                newEl = $('[tabindex=' + jumpGroup + ']');
              }
              focusElement(existingEl, evt);
              return;
            }
            jumpGroup = prevTabGroup(jumpGroup);
          }
          // Nothing found, so jump back to input (ignore bad design...)
          newEl = $('.ga-search-input');
          if (newEl.length === 1 &&
              newEl[0].className.indexOf('ga-search-input') > -1) {
            focusElement(newEl, evt);
          }
        }
      }
    };
  
    var module = angular.module('ga_search_type_directives', [
      'ga_debounce_service',
      'ga_layerfilters_service',
      'ga_layermetadatapopup_service',
      'ga_layers_service',
      'ga_maputils_service',
      'ga_marker_overlay_service',
      'ga_previewfeatures_service',
      'ga_previewlayers_service',
      'ga_search_service',
      'ga_urlutils_service',
      'pascalprecht.translate',
      'ga_topic_service'
    ]);
  
    /*
     * We have 3 distinct directives for each type of result
     * set (locations, features and layers)
     *
     * All 3 result directives share the same template and the
     * same controller code. Put anything that is common for
     * all 3 types in the controller code.
     *
     * Put type specific behaviour in the corresponding
     * directive's code.
     */
  
    module.controller('GaSearchTypesController',
        function($scope, $http, $q, $sce, gaUrlUtils, gaSearchLabels,
            gaMarkerOverlay, gaDebounce) {
  
          // This value is used to block blur/mouseleave event, when a value
          // is selected. See #2284. It's reinitialized when a new search is
          // triggered.
          var blockEvent = false;
          var canceler;
  
          var cancel = function() {
            $scope.results = [];
            $scope.fuzzy = '';
            if (canceler !== undefined) {
              canceler.resolve();
              canceler = undefined;
            }
          };
  
          var triggerSearch = gaDebounce.debounce(function() {
            if (!$scope.doSearch()) {
              $scope.options.announceResults($scope.type, 0);
              return;
            }
  
            canceler = $q.defer();
  
            var url = gaUrlUtils.append($scope.options.searchUrl,
                'type=' + $scope.type);
            url = $scope.typeSpecificUrl(url);
            $http.get(url, {
              cache: true,
              timeout: canceler.promise
            }).then(function(response) {
              var data = response.data;
              $scope.results = data.results;
              if (data.fuzzy) {
                $scope.fuzzy = '_fuzzy';
              }
              $scope.options.announceResults($scope.type, data.results.length);
            }, function(response) {
            // If request is canceled, statuscode is 0 and we don't announce it
              if (response.status !== 0) {
                $scope.options.announceResults($scope.type, 0);
              }
            });
          }, 133, false, false);
          // 133 filters out 'stuck key' events while staying responsive
  
          $scope.doSearch = function() {
            return true;
          };
  
          $scope.typeSpecificUrl = function(url) {
            return url;
          };
  
          $scope.keydown = function(evt, res) {
            if (evt.keyCode === 13) {
            // Enter key
              $scope.removePreview();
              blockEvent = true;
              $scope.select(res);
            } else if (evt.keyCode === 9) {
            // Tab key
              focusToCategory(!evt.shiftKey, evt);
            } else if (evt.keyCode === 40 || evt.keyCode === 34) {
            // Down Arrow or PageDown key
              focusToElement(true, evt.keyCode === 40 ? 1 : 5, evt);
            } else if (evt.keyCode === 38 || evt.keyCode === 33) {
            // Up Arrow or PageUp key
              focusToElement(false, evt.keyCode === 38 ? 1 : 5, evt);
            }
          };
  
          $scope.click = function(res) {
            $scope.removePreview();
            blockEvent = true;
            $scope.select(res);
          };
  
          $scope.out = function(evt) {
            if (!blockEvent) {
              $scope.removePreview();
            }
          };
  
          $scope.preview = function(res) {
            addOverlay(gaMarkerOverlay, $scope.map, res);
          };
  
          $scope.removePreview = function() {
            removeOverlay(gaMarkerOverlay, $scope.map);
          };
  
          $scope.prepareLabel = function(attrs) {
            var h = gaSearchLabels.highlight(attrs.label, $scope.options.query);
            return $sce.trustAsHtml(h);
          };
  
          $scope.cleanLabel = function(attrs) {
            return gaSearchLabels.cleanLabel(attrs.label);
          };
  
          $scope.fuzzy = '';
  
          $scope.$watch('options.searchUrl', function() {
          // cancel old requests
            cancel();
            if ($scope.options.query !== '') {
              blockEvent = false;
              triggerSearch();
            } else {
              unregisterMove();
            }
          });
        }
    );
  
    module.directive('gaSearchLocations',
        function($sce, $translate, gaMarkerOverlay,
            gaSearchLabels, gaMapUtils, gaDebounce) {
          return {
            restrict: 'A',
            templateUrl: 'components/search/partials/searchtypes.html',
            scope: {
              options: '=gaSearchLocationsOptions',
              map: '=gaSearchLocationsMap',
              ol3d: '=gaSearchLocationsOl3d'
            },
            controller: 'GaSearchTypesController',
            link: function($scope, element, attrs) {
              $scope.type = 'locations';
              $scope.tabstart = tabStarts[0];
  
              // Can be removed onnce real type contains gazetter
              $scope.typeSpecificUrl = function(url) {
                return url;
              };
  
              $scope.select = function(res) {
                var isGazetteerPoly = false;
                var e = parseExtent(res.attrs.geom_st_box2d);
                unregisterMove();
                // Gazetteer results that are not points zoom to full bbox extent
                if (res.attrs.origin === 'gazetteer') {
                  isGazetteerPoly = (Math.abs(e[0] - e[2]) > 100 &&
                                     Math.abs(e[1] - e[3]) > 100);
  
                }
                if (res.attrs.zoomlevel < ZOOM_LIMIT &&
                    !isGazetteerPoly) {
                  gaMapUtils.moveTo($scope.map, $scope.ol3d,
                      res.attrs.zoomlevel,
                      [res.attrs.y, res.attrs.x]);
                } else {
                  gaMapUtils.zoomToExtent($scope.map, $scope.ol3d, e);
                }
                addOverlay(gaMarkerOverlay, $scope.map, res);
                $scope.options.valueSelected(
                    gaSearchLabels.cleanLabel(res.attrs.label));
  
                registerMove(gaMarkerOverlay, gaDebounce, $scope.map);
              };
  
              $scope.prepareLabel = function(attrs) {
                var l = gaSearchLabels.highlight(attrs.label,
                    $scope.options.query);
                if (attrs.origin === 'zipcode') {
                  l = '<span>' + $translate.instant('plz') + ' ' + l +
                      '</span>';
                } else if (attrs.origin === 'kantone') {
                  l = '<span>' + $translate.instant('ct') + ' ' + l +
                      '</span>';
                } else if (attrs.origin === 'district') {
                  l = '<span>' + $translate.instant('district') + ' ' + l +
                      '</span>';
                } else if (attrs.origin === 'parcel') {
                  l += ' <span>' + $translate.instant('parcel') + ' ' +
                       '</span>';
                }
                return $sce.trustAsHtml(l);
              };
  
            }
          };
        });
  
    module.directive('gaSearchFeatures',
        function($rootScope, $http, $q, $sce, $timeout, gaUrlUtils,
            gaLayerFilters, gaSearchLabels, gaLayers,
            gaMarkerOverlay, gaPreviewFeatures, gaTopic) {
  
          var selectedFeatures = {};
          var loadGeometry = function(layerId, featureId, topic, urlbase, cb) {
            var key = layerId + featureId;
            if (!selectedFeatures.hasOwnProperty(key)) {
              var featureUrl = urlbase.replace('{Topic}', topic).
                  replace('{Layer}', layerId).
                  replace('{Feature}', featureId);
              $http.get(featureUrl, {
                params: {
                  geometryFormat: 'geojson'
                }
              }).then(function(response) {
                var result = response.data;
                selectedFeatures[key] = result.feature;
                cb(result.feature);
              });
            } else {
              $timeout(function() {
                cb(selectedFeatures[key]);
              }, 0, false);
            }
          };
  
          return {
            restrict: 'A',
            templateUrl: 'components/search/partials/searchtypes.html',
            scope: {
              options: '=gaSearchFeaturesOptions',
              map: '=gaSearchFeaturesMap',
              ol3d: '=gaSearchFeaturesOl3d'
            },
            controller: 'GaSearchTypesController',
            link: function($scope, element, attrs) {
              var geojsonParser = new ol.format.GeoJSON();
              var searchableLayers = [];
              var timeEnabled = [];
              var timeStamps = [];
  
              $scope.type = 'featuresearch';
              $scope.tabstart = tabStarts[1];
  
              $scope.doSearch = function() {
                return searchableLayers.length > 0;
              };
  
              $scope.typeSpecificUrl = function(url) {
                url = gaUrlUtils.append(url,
                    'features=' + searchableLayers.join(','));
                url = gaUrlUtils.append(url,
                    'timeEnabled=' + timeEnabled.join(','));
                return gaUrlUtils.append(url,
                    'timeStamps=' + timeStamps.join(','));
              };
  
              $scope.select = function(res) {
                unregisterMove();
                loadGeometry(res.attrs.layer, res.attrs.featureId,
                    gaTopic.get().id,
                    $scope.options.featureUrl, function(f) {
                      $rootScope.$broadcast('gaTriggerTooltipRequest', {
                        features: [f],
                        onCloseCB: angular.noop
                      });
                      var feature = geojsonParser.readFeature(f);
                      gaPreviewFeatures.zoom($scope.map, $scope.ol3d, feature);
                    });
                $scope.options.valueSelected(
                    gaSearchLabels.cleanLabel(res.attrs.label));
              };
  
              $scope.prepareLabel = function(attrs) {
                var l = gaSearchLabels.highlight(attrs.label,
                    $scope.options.query);
                if (attrs.origin === 'feature') {
                  l = '<b>' +
                      gaLayers.getLayerProperty(attrs.layer, 'label') +
                      '</b><br>' + l;
                }
                return $sce.trustAsHtml(l);
              };
  
              $scope.layers = $scope.map.getLayers().getArray();
              $scope.searchableLayersFilter = gaLayerFilters.searchable;
  
              $scope.$watchCollection('layers | filter:searchableLayersFilter',
                  function(layers) {
                    // TODO: this isn't updated when layers param (like 'time')
                    // changes
                    searchableLayers = [];
                    timeEnabled = [];
                    timeStamps = [];
                    angular.forEach(layers, function(layer) {
                      var ts = '';
                      if (layer.time && layer.time.substr(0, 4) !== '9999' &&
                          layer.timeEnabled) {
                        ts = layer.time.substr(0, 4);
                      }
                      searchableLayers.push(layer.bodId);
                      timeEnabled.push(layer.timeEnabled);
                      timeStamps.push(ts);
                    });
                  });
  
            }
          };
        });
  
    module.directive('gaSearchLayers',
        function($http, $q, $sce, gaUrlUtils, gaSearchLabels, gaPreviewLayers,
            gaMapUtils, gaLayers, gaLayerMetadataPopup) {
          return {
            restrict: 'A',
            templateUrl: 'components/search/partials/searchtypes.html',
            scope: {
              options: '=gaSearchLayersOptions',
              map: '=gaSearchLayersMap'
            },
            controller: 'GaSearchTypesController',
            link: function($scope, element, attrs) {
              $scope.type = 'layers';
              $scope.tabstart = tabStarts[2];
  
              $scope.preview = function(res) {
                var layer = gaMapUtils.getMapOverlayForBodId($scope.map,
                    res.attrs.layer);
  
                // Don't add preview layer if the layer is already on the map
                if (!layer || !layer.visible) {
                  gaPreviewLayers.addBodLayer($scope.map, res.attrs.layer);
                }
              };
  
              $scope.removePreview = function() {
                gaPreviewLayers.removeAll($scope.map);
              };
  
              $scope.select = function(res) {
                unregisterMove();
                var l = gaMapUtils.getMapOverlayForBodId($scope.map,
                    res.attrs.layer);
                if (!angular.isDefined(l)) {
                  var olLayer = gaLayers.getOlLayerById(res.attrs.layer);
                  $scope.map.addLayer(olLayer);
                } else {
                  // Assure layer is visible
                  l.visible = true;
                }
                $scope.options.valueSelected(
                    gaSearchLabels.cleanLabel(res.attrs.label));
              };
  
              $scope.getLegend = function(evt, bodId) {
                gaLayerMetadataPopup.toggle(bodId);
                evt.stopPropagation();
              };
            }
          };
        });
  
  })();
  