/*
 * Copyright (c) 2008-2015 The Open Source Geospatial Foundation
 *
 * Published under the BSD license.
 * See https://github.com/geoext/geoext2/blob/master/license.txt for the full
 * text of the license.
 */

/**
 * Creates a combo box that handles results from a geocoding service. By
 * default it uses OSM Nominatim, but it can be configured with a custom store
 * to use other services. If the user enters a valid address in the search
 * box, the combo's store will be populated with records that match the
 * address.  By default, records have the following fields:
 *
 * * name - `String` The formatted address.
 * * lonlat - `Array` Location matching address, for use with
 *     OpenLayers.LonLat.fromArray.
 * * bounds - `Array` Recommended viewing bounds, for use with
 *     OpenLayers.Bounds.fromArray.
 *
 * @class GeoExt.form.field.GeocoderComboBox
 */
Ext.define('GeoExt.form.field.GeocoderComboBox', {
    extend : 'Ext.form.field.ComboBox',
    requires: [
        "GeoExt.panel.Map",
        "Ext.data.JsonStore",
        "Ext.data.proxy.JsonP"
    ],
    alias : 'widget.gx_geocodercombo',
    alternateClassName : 'GeoExt.form.GeocoderComboBox',

    /**
     * Text to display for an empty field (i18n).
     *
     * @cfg {String}
     */
    emptyText: "Search",

    /**
     * The map that will be controlled by
     * this GeoCoderComboBox. Only used if this component is not added as item
     * or toolbar item to a GeoExt.panel.Map.
     *
     * @cfg {GeoExt.panel.Map/OpenLayers.Map} map
     */
    /**
     * @property {OpenLayers.Map} map
     * @private
     */

    /**
     * The srs used by the geocoder service.
     *
     * @cfg {String/OpenLayers.Projection}
     */
    srs: "EPSG:4326",

    /**
     * The minimum zoom level to use when zooming to a location.
     * Not used when zooming to a bounding box.
     *
     * @cfg {Integer}
     */
    zoom: 10,

    /**
     * Delay before the search occurs in ms.
     *
     * @cfg {Number}
     */
    queryDelay: 100,

    /**
     * Field from selected record to use when the combo's
     * getValue method is called.  Default is "bounds". This field is
     * supposed to contain an array of [left, bottom, right, top] coordinates
     * for a bounding box or [x, y] for a location.
     *
     * @cfg {String}
     */
    valueField: "bounds",

    /**
     * The field to display in the combo box. Default is
     * "name" for instant use with the default store for this component.
     *
     * @cfg {String}
     */
    displayField: "name",

    /**
     * The field to get the location from. This field is supposed
     * to contain an array of [x, y] for a location. Default is "lonlat" for
     * instant use with the default store for this component.
     *
     * @cfg {String}
     */
    locationField: "lonlat",

    /**
     * URL template for querying the geocoding service. If a store is
     * configured, this will be ignored. Note that the queryParam will be used
     * to append the user's combo box input to the url.
     *
     * Default is "http://nominatim.openstreetmap.org/search?format=json", for
     * instant use with the OSM Nominatim geolocator. However, if you intend to
     * use that, note the [Nominatim Usage
     * Policy](http://wiki.openstreetmap.org/wiki/Nominatim_usage_policy).
     *
     * @cfg {String}
     */
    url: "http://nominatim.openstreetmap.org/search?format=json",

    /**
     * The query parameter for the user entered search text.
     * Default is "q" for instant use with OSM Nominatim.
     *
     * @cfg {String}
     */
    queryParam: "q",

    /**
     * Minimum number of entered characters to trigger a search.
     *
     * @cfg {Number}
     */
    minChars: 3,

    /**
     * The store used for this combo box. Default is a
     * store with a ScriptTagProxy and the url configured as :obj:`url`
     * property.
     *
     * @cfg {Ext.data.Store}
     */
    store: null,

    /**
     * Last center that was zoomed to after selecting a location in the combo
     * box.
     *
     * @property {OpenLayers.LonLat}
     * @private
     */
    center: null,

    /**
     * Last location provided by the geolocator.
     * Only set if layer is configured.
     *
     * @property {OpenLayers.Feature.Vector}
     * @private
     */
    locationFeature: null,

    initComponent: function() {
        if (this.map) {
            this.setMap(this.map);
        }
        if (Ext.isString(this.srs)) {
            this.srs = new OpenLayers.Projection(this.srs);
        }
        if (!this.store) {
            this.store = Ext.create("Ext.data.JsonStore", {
                root: null,
                fields: [
                    {name: "name", mapping: "display_name"},
                    {name: "bounds", convert: function(v, rec) {
                        var dataKey = GeoExt.isExt4 ? 'raw' : 'data',
                            bbox = rec[dataKey].boundingbox;
                        return [bbox[2], bbox[0], bbox[3], bbox[1]];
                    }},
                    {name: "lonlat", convert: function(v, rec) {
                        var dataKey = GeoExt.isExt4 ? 'raw' : 'data',
                            data = rec[dataKey];
                        return [data.lon, data.lat];
                    }}
                ],
                proxy: Ext.create("Ext.data.proxy.JsonP", {
                    url: this.url,
                    callbackKey: "json_callback"
                })
            });
        }

        this.on({
            added: this.findMapPanel,
            select: this.handleSelect,
            focus: function() {
                this.clearValue();
                this.removeLocationFeature();
            },
            scope: this
        });
        return this.callParent(arguments);
    },

    /**
     * Find the MapPanel somewhere up in the hierarchy and set the map.
     *
     * @private
     */
    findMapPanel: function() {
        var mapPanel = this.up('gx_mappanel');
        if (mapPanel) {
            this.setMap(mapPanel);
        }
    },

    /**
     * Zoom to the selected location, and also set a location marker if this
     * component was configured with a layer.
     *
     * @private
     */
    handleSelect: function(combo, rec) {
        if (!this.map) {
            this.findMapPanel();
        }
        var value = this.getValue();
        if (Ext.isArray(value)) {
            var mapProj = this.map.getProjectionObject();
            delete this.center;
            delete this.locationFeature;
            if (value.length === 4) {
                this.map.zoomToExtent(
                    OpenLayers.Bounds.fromArray(value)
                        .transform(this.srs, mapProj)
                );
            } else {
                this.map.setCenter(
                    OpenLayers.LonLat.fromArray(value)
                        .transform(this.srs, mapProj),
                    Math.max(this.map.getZoom(), this.zoom)
                );
            }
            rec = rec[0];
            this.center = this.map.getCenter();
            var lonlat = rec.get(this.locationField);
            if (this.layer && lonlat) {
                var geom = new OpenLayers.Geometry.Point(
                    lonlat[0], lonlat[1]).transform(this.srs, mapProj);
                this.locationFeature = new OpenLayers.Feature.Vector(geom, rec.data);
                this.layer.addFeatures([this.locationFeature]);
            }
        }
    },

    /**
     * Remove the location marker from the `layer` and destroy the
     * `#locationFeature`.
     *
     * @private
     */
    removeLocationFeature: function() {
        if (this.locationFeature) {
            this.layer.destroyFeatures([this.locationFeature]);
        }
    },

    /**
     * Handler for the map's moveend event. Clears the selected location
     * when the map center has changed.
     *
     * @private
     */
    clearResult: function() {
        if (this.center && !this.map.getCenter().equals(this.center)) {
            this.clearValue();
        }
    },

    /**
     * Set the `#map` for this instance.
     *
     * @param {GeoExt.panel.Map/OpenLayers.Map} map
     * @private
     */
    setMap: function(map) {
        if (map instanceof GeoExt.panel.Map) {
            map = map.map;
        }
        this.map = map;
        map.events.on({
            "moveend": this.clearResult,
            "click": this.removeLocationFeature,
            scope: this
        });
    },

    /**
     * Called by a MapPanel if this component is one of the items in the panel.
     * @param {GeoExt.panel.Map} panel
     *
     * @private
     */
    addToMapPanel: Ext.emptyFn,

    /**
     * Unbind various event listeners and deletes #map, #layer and #center
     * properties.
     *
     * @private
     */
    beforeDestroy: function() {
        if (this.map && this.map.events) {
            this.map.events.un({
                "moveend": this.clearResult,
                "click": this.removeLocationFeature,
                scope: this
            });
        }
        this.removeLocationFeature();
        delete this.map;
        delete this.layer;
        delete this.center;
        this.callParent(arguments);
    }
});