/*
 *  Copyright 2019 Anyware Services
 *
 *  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.
 */

/**
 * This UI helper provides a dialog to select coordinates on a Map, using Leaflet.<br>
 * This dialog box embeds an address search bar and a Map.<br>
 * This creates a marker on the map. That marker is draggable at will.<br>
 * An initial address can be provided - to initialize the search field - in the configuration parameter of the #open method.
 */
Ext.define('Ametys.helper.ChooseLocationLeaflet', {
	
	singleton: true,	
    
    /**
     * The default zoom level
     * @private
     * @readonly
     */
    __DEFAULT_ZOOM_LEVEL: 17,
    
    /**
     * The default map center
     * @private
     * @readonly
     */
    __DEFAULT_MAP_CENTER: [0.0, 0.0],
    
    /**
     * The default map tiles url
     * @private
     * @readonly
     */
    __DEFAULT_TILES_URL: "https://{s}.tile.osm.org/{z}/{x}/{y}.png",
    
    /**
     * The default map attribution
     * @private
     * @readonly
     */
    __DEFAULT_MAP_ATTRIBUTION: '&copy; <a href="https://osm.org/copyright">OpenStreetMap</a> contributors',

    /**
     * The regexp to check if a given address is coordinates
     * @private
     * @readonly
     */
    __ADDRESS_COORDINATES: /^((\\+|-)?[0-9]+(\.[0-9]+)?,( )?(\\+|-)?[0-9]+(\.[0-9]+)?)$/,

    /**
     * The marker displayed on the map
     * @type {L.marker}
     * @private
     */
    _marker: null,
    
    /**
     * The Leaflet map
     * @type {L.map}
     * @private
     */
    _map: null,
    
    /**
     * The Leaflet geocoder
     * @type {L.Control.geocoder}
     * @private
     */
    _geocoder: null,
	
	/**
	 * Allow the user to setup a GoogleMaps marker
	 * @param {Object} [config] The initial parameters of the widget.
	 * @param {Object} [config.zoomLevel] The default zoom level.
	 * @param {String} [config.mapTypeId=google.maps.MapTypeId#HYBRID] The map type. Defaults to hybrid (roadmap + satellite)
	 * @param {String} [config.zoomControlStyle=google.maps.ZoomControlStyle.LARGE] The zoom control style.
	 * @param {Object} [config.initialLatLng] The initial position of the marker :
	 * @param {Number} [config.initialLatLng.latitude] The initial latitude of the marker.
	 * @param {Number} [config.initialLatLng.longitude] The initial longitude of the marker.
	 * @param {String} [config.initialAddress] The initial value of the address textfield. If config.initialLatLng is not provided in the config object, then this string also setups the initial position of the marker.
	 * @param {String} [config.helpMessage] The help message to display at the top of the window
	 * @param {String} [config.icon] The full icon path of the dialog box
	 * @param {String} [config.title] The title of the dialog box
	 * @param {Function} [callback] The method that will be called when the dialog box is closed. The method signature is :
	 * @param {Object} [callback.latlng] The chosen latitude and longitude
	 * @param {Number} [callback.latlng.latitude] The chosen latitude
	 * @param {Number} [callback.latlng.longitude] The chosen longitude
	 */
	open: function (config, callback)
	{
		this._cbFn = callback;
		this._validated = false;
        
        this._mapPanel = Ext.create('Ext.Component', {
            name : 'mappanel',
            flex: 1
        });
        
        var me = this;
        this._searchErrorMsg = '';
        
        this._mapwindow = Ext.create("Ametys.window.DialogBox",{
            title: config.title || "{{i18n PLUGINS_CORE_UI_GEOCODE_GMAP_DIALOG_DEFAULT_TITLE}}",
            icon: config.icon || Ametys.getPluginResourcesPrefix('cms') + '/img/widgets/geolocation/geolocation_16.png', 
            
            closeAction : 'destroy',
            width : 550,
            height : 450,
            layout: 'fit',
            
            items: {
                xtype: 'panel',
                layout: {
                    type: 'vbox',
                    align : 'stretch',
                    pack  : 'start'
                },
                border: false,
                items : [
                        {
                            xtype: 'component',
                            cls: 'a-text',
                            html: config.helpMessage || "{{i18n PLUGINS_CORE_UI_GEOCODE_GMAP_DIALOG_HELP_MSG_1}}"
                        },
                        {
                            // Search address bar
                            xtype: 'panel',
                            layout: {
                                type: 'hbox',
                                align: 'stretch'
                            },
                            height: 26,
                            border: false,
                            items: [
                                    {
                                        xtype: 'textfield',
                                        id: 'geo-search-textfield',
                                        fieldLabel: "{{i18n PLUGINS_CORE_UI_GEOCODE_GMAP_DIALOG_SEARCH_LABEL}}",
                                        labelAlign: 'right',
                                        labelSeparator: '',
                                        labelWidth: 80,
                                        value: config.initialAddress.replace(/\+/g, ' '),
                                        flex: 1,
                                        listeners: {
                                            specialkey: Ext.bind(this._pinAddressOnEnterKeyPress, this)
                                        } 
                                    },
                                    {
                                        xtype: 'button',
                                        iconCls: 'ametysicon-magnifier12',
                                        tooltip : "{{i18n PLUGINS_CORE_UI_GEOCODE_GMAP_DIALOG_SEARCH}}",                
                                        handler : Ext.bind(this._pinAtTextfieldAddress, this)
                                    }
                            ]
                        },
                        {
                            xtype: 'component',
                            cls: 'a-text-error',
                            itemId: 'geo-search-textfield-error',
                            html: me._searchErrorMsg || ''
                        },
                        this._mapPanel,
                        {
                            xtype: 'component',
                            cls: 'a-text',
                            html: config.bottomText || ""
                        }
                ]
            },
            
            defaultButton: 'okButton',
            referenceHolder: true,
            
            // Buttons
            buttons : [{
                    reference: 'okButton',
                    text :"{{i18n PLUGINS_CORE_UI_GEOCODE_GMAP_DIALOG_OK}}",
                    handler: Ext.bind(this._ok, this)
                }, {
                    text :'{{i18n PLUGINS_CORE_UI_GEOCODE_GMAP_DIALOG_CANCEL}}',
                    handler: Ext.bind(this._cancel, this)
                }
            ],
            
            listeners: {
                close: Ext.bind(this._onClose, this),
                show: Ext.bind(this._loadLeafLet, this, [config])
            }
        });
        
        this._mapwindow.fireDefaultButton = function(e)
        {
            if (e.target.tagName === 'INPUT')
            {
                return true;
            }
            return Ametys.window.DialogBox.prototype.fireDefaultButton.apply(this, arguments);
        }
        Ametys.loadStyle(Ametys.getPluginDirectPrefix('leaflet') + "/resources/css/leaflet.css");
        Ametys.loadStyle(Ametys.getPluginDirectPrefix('leaflet-control-geocoder') + "/resources/css/Control.Geocoder.css");
        this._mapwindow.show();
        
	},
    
    /**
     * Load leaflet map and geocoder
     * @param {Object} [config] The initial parameters of the widget. (see "open")
     * @private
     */
    _loadLeafLet: function(config)
    {
        Ametys.loadScript(Ametys.getPluginDirectPrefix('leaflet') + "/resources/js/leaflet.js", 
                Ext.bind(
                        function(){
                            this._loadLeafLetGeoCoder(config);
                        }, 
                this)
        );
    },
    
    /**
     * Load leaflet geocoder
     * @param {Object} [config] The initial parameters of the widget. (see "open")
     * @private
     */
    _loadLeafLetGeoCoder: function(config)
    {
        Ametys.loadScript(Ametys.getPluginDirectPrefix('leaflet-control-geocoder') + "/resources/js/Control.Geocoder.js", 
                Ext.bind(
                        function(){
                            this._delayedInitialize(config);
                        }, 
                this)
        );
    },
    
	/**
	 * Creates the dialog box if it is not already created and initialize it
	 * @param {Object} [config] The dialog box configuration object
	 * @param {Object} [config.initialLatLng] The initial position of the marker.
	 * @param {Object} [config.defaultLatLng] The default position of the marker when there is no initial address.
	 * @param {Number} [config.defaultZoomLevel=6] The default zoom level when for default latitude/longitude
	 * @param {Number} config.initialLatLng.latitude The initial latitude of the marker.
	 * @param {Number} config.initialLatLng.longitude The initial longitude of the marker.
	 * @param {String} [config.initialAddress] The initial value of the address textfield. If config.initialLatLng is not provided in the config object, then this string also setups the initial position of the marker. 
	 * @param {String} [config.helpMessage] The help message to display at the top of the window
	 * @param {String} [config.icon] The relative path of the window icon
	 * @param {String} [config.title] The title of the window
	 */
	_delayedInitialize: function(config) 
	{
		// Initial position of marker
		var initialLatLng = config.initialLatLng ? L.latLng(config.initialLatLng.latitude, config.initialLatLng.longitude) : null;
		var defaultLatLng = config.defaultLatLng ? L.latLng(config.defaultLatLng.latitude, config.defaultLatLng.longitude) : L.latLng(0.0, 0.0);
		var initialAddress = config.initialAddress.replace(/\+/g, ' ');
		
		// Zoom level
		var zoomLevel = initialLatLng || initialAddress ? (config.zoomLevel || this.__DEFAULT_ZOOM_LEVEL) : (defaultLatLng ? (config.defaultZoomLevel || 6) : 1);
		
        this._map = L.map(this._mapPanel.getId()).setZoom(zoomLevel);
        L.tileLayer(this.__DEFAULT_TILES_URL, {
            attribution: this.__DEFAULT_MAP_ATTRIBUTION
        }).addTo(this._map);
        
        var geoCoderOptions = {
            collapsed: false, // Opened by default
            showResultIcons: true, // Display custom icons in the search results
            defaultMarkGeocode: false // Custom marker
        };
        var geocoder = L.Control.geocoder(geoCoderOptions);
        var me = this;
        geocoder.on('markgeocode', function(e) {
                me._pinAtLatLngAndZoom(e.geocode);

            });
            //.addTo(this._map);
        this._geocoder = geocoder.options.geocoder;
        this._pinInitialMarker(initialLatLng, initialAddress, defaultLatLng);
		
		return true;
	},
	
    
    /**
     * Set the error message under the search field
     * @param {String} [msg] The message to display, or undefined/null to remove the message.
     * @private
     */
    _setErrorSearchMessage: function(msg)
    {
        this._searchErrorMsg = msg || '';
        var searchErrorCmp = this._mapwindow.down('#geo-search-textfield-error');
        
        if (searchErrorCmp)
        {
            searchErrorCmp.update(this._searchErrorMsg);
        }
    },

    /**
     * Remove the error message under the search field
     * @private
     */
    _cleanErrorSearchMessage: function()
    {
        this._setErrorSearchMessage();
    },
    
	/**
     * Search for the address on the map whenever you type Enter in the search address textfield
     * @param {Ext.form.field.Text} input The input text
     * @param {Ext.event.Event} e The event object
     * @private
     */
    _pinAddressOnEnterKeyPress: function(input, e)
    {
        if (e.getKey() == e.ENTER) 
        {
            e.preventDefault();
            e.stopPropagation();
            this._pinAtTextfieldAddress();
        }   
    },
	/**
	 * @private 
	 * Pin the marker to the initial position.
	 * @param {L.latLng|Number[]} initialLatLng
	 * @param {String} initialAddress The (postal) address to center the map. Can be null to center the map to the default position.
	 * @param {L.latLng|Number[]} defaultLatLng The default position of the marker if initialLatLng and initialAddress are empty (default to : 0°N 0°E)
	 */
	_pinInitialMarker: function (initialLatLng, initialAddress, defaultLatLng)
	{
		if (initialLatLng)
		{
			this._pinAtLatLng(initialLatLng);
		} 
		else if (initialAddress)
		{
			this.pinAtAddress(initialAddress);
		}
		else
		{
			this._pinAtLatLng(defaultLatLng);
		}
	},
    
    /**
     * @private 
     * Moves the marker at the location described in the address search bar
     */
    _pinAtTextfieldAddress: function() 
    {
        var textfieldAddress = this._mapwindow.down('#geo-search-textfield').getValue();
        if (textfieldAddress)
        {
            if (this.__ADDRESS_COORDINATES.test(textfieldAddress))
            {
                var coordinatesArray = textfieldAddress.split(",");
                this._pinAtLatLng(coordinatesArray);
                this._map.fitBounds([
                    coordinatesArray,
                    coordinatesArray
                ]);
                this._cleanErrorSearchMessage();
            }
            else
            {
                this.pinAtAddress(textfieldAddress);
            }
            //this._gmapPanel.gmap.setZoom(this.__DEFAULT_ZOOM_LEVEL);
        }
    },

	/**
	 * Moves the marker at a given latitude/longitude
	 * @param {Number} latitude The latitude to setup the marker
	 * @param {Number} longitude The longitude to setup the marker
	 */
	pinAtLatitudeLongitude: function(latitude, longitude)
	{
		this._pinAtLatLng([latitude, longitude]);
	},
	
	
	/**
	 * Moves the marker at a given (postal) address
	 * @param {String} anAddress A string representing a (postal) address
	 */
	pinAtAddress: function(anAddress) 
	{
		var geocoderRequest = [anAddress];
		
		var me = this;
		this._geocoder.geocode(geocoderRequest, function (geocoderResults){
			if (geocoderResults && geocoderResults.length > 0)
			{
				// On a successful address->location translation, move the marker to that location
				var firstResult = geocoderResults[0];
				me._pinAtLatLngAndZoom(firstResult);
                me._cleanErrorSearchMessage();
			}
            else
            {
                me._setErrorSearchMessage("{{i18n PLUGINS_CORE_UI_GEOCODE_GMAP_DIALOG_SEARCH_NOT_FOUND}}" + anAddress);
            }
		});
	},
	
	/**
	 * Center the map at a given latLng
	 * @param {L.latLng|Number[]} latLng The new location of the center of the map
	 */	
	centerMap: function(latLng)
	{
		if (this.map)
		{
			if(latLng)
			{
				this._map.panTo(geocode.center);
			} 
			else 
			{
				var defaultLocation = [0, 0];
				this._map.panTo(defaultLocation);
			}
		}
	},
	
	/**
	 * @private 
	 * Move the marker at a given latLng and center the map
	 * @param {L.latLng|Number[]} latLng The new location of the marker
	 */	
	_pinAtLatLng: function(latLng) 
	{
        if (this._marker == null)
        {
            var markerOptions = {
                    draggable: true
                };
            this._marker = L.marker(latLng, markerOptions).addTo(this._map);
        }
        else
        {
            this._marker.setLatLng(latLng);
        }
        this._map.panTo(latLng);
	},
    
    /**
     * @private 
     * Move the marker at a given latLng and center the map
     * @param {L.Control.Geocoder.GeocodingResult} result of a search, contains the center and also the bbox to zoom properly
     */ 
    _pinAtLatLngAndZoom: function(geocode)
    {
        this._pinAtLatLng(geocode.center);
        var bbox = geocode.bbox;
        var poly = L.polygon([
            bbox.getSouthEast(),
            bbox.getNorthEast(),
            bbox.getNorthWest(),
            bbox.getSouthWest()
        ]);
        this._map.fitBounds(poly.getBounds());
    },
	
    /**
     * @private 
     * Returns the position of the current marker
     */
    _getMarkerLatLng: function()
    {
        if (this._marker)
        {
            return this._marker.getLatLng();
        }
        else
        {
            return null;
        }
    },
	
	/**
	 * @private 
	 * Function called when pressing the 'ok' button of the dialog box.<br>
	 * Calls the user-defined callback and closes the dialog box.
	 */
	_ok: function()
	{
		// Call the callback with the current latLng
        var currentLatLng = this._getMarkerLatLng();
		if (Ext.isFunction (this._cbFn) && currentLatLng != null) 
		{
			var value = {
					latitude: currentLatLng.lat, 
					longitude: currentLatLng.lng
			};
			
			this._cbFn(value);
		}
		
		if (this._marker)
		{
			// Forget about the location marker
			this._marker.remove();
			this._marker = null;
		}
		
		// Close the window
        this._validated = true;
		this._mapwindow.close();
	},
    
	/**
	 * @private 
	 * Function called when pressing the 'cancel' button of the dialog box.<br>
	 * Calls the user-defined callback with no arguements and closes the dialog box.
	 */
	_cancel: function()
	{
		if (this._marker)
		{
			// Forget about the location marker
			this._marker.remove();
			this._marker = null;
		}
		
		// Close the window
		this._mapwindow.close();
	},
    
    /**
     * @private
     * Listener when closing the dialog box
     * @param {Boolean} validate true the dialog box was closing after cliking on 'Ok' button
     */
    _onClose: function(validate)
    {
        this._map.remove();
        if (!this._validated && this._cbFn)
        {
            // Call the callback with no value
            this._cbFn(null);
        }
    }
});