var SequentialLoader = function() {
    var SL = {
        loadJS: function(src, onload) {
            //console.log(src);
            // add to pending list
            this._load_pending.push({'src': src, 'onload': onload});
            // check if not already loading
            if ( ! this._loading) {
                this._loading = true;
                // load first
                this.loadNextJS();
            }
        },

        loadNextJS: function() {
            // get next
            var next = this._load_pending.shift();
            if (next == undefined) {
                // nothing to load
                this._loading = false;
                return;
            }
            // check not loaded
            if (this._load_cache[next.src] != undefined) {
                next.onload();
                this.loadNextJS();
                return; // already loaded
            }
            else {
                this._load_cache[next.src] = 1;
            }
            // load
            var el = document.createElement('script');
            el.type = 'application/javascript';
            el.src = next.src;
            // onload callback
            var self = this;
            el.onload = function(){
                //console.log('Loaded: ' + next.src);
                // trigger onload
                next.onload();
                // try to load next
                self.loadNextJS();
            };
            document.body.appendChild(el);
        },

        _loading: false,
        _load_pending: [],
        _load_cache: {}
    };

    return {
        loadJS: SL.loadJS.bind(SL)
    }
};


!function($){
    var LocationFieldCache = {
        load: [],
        onload: {},

        isLoading: false
    };

    var LocationFieldResourceLoader;

    $.locationField = function(options) {
        var LocationField = {
            options: $.extend({
                provider: 'google',
                providerOptions: {
                    google: {
                        api: '//maps.google.com/maps/api/js',
                        mapType: 'ROADMAP'
                    }
                },
                searchProvider: 'google',
                id: 'map',
                latLng: '0,0',
                mapOptions: {
                    zoom: 9
                },
                basedFields: $(),
                inputField: $(),
                suffix: '',
                path: '',
                fixMarker: true
            }, options),

            providers: /google|openstreetmap|mapbox/,
            searchProviders: /google|yandex|nominatim|addok/,

            render: function() {
                this.$id = $('#' + this.options.id);

                if ( ! this.providers.test(this.options.provider)) {
                    this.error('render failed, invalid map provider: ' + this.options.provider);
                    return;
                }

                if ( ! this.searchProviders.test(this.options.searchProvider)) {
                    this.error('render failed, invalid search provider: ' + this.options.searchProvider);
                    return;
                }

                var self = this;

                this.loadAll(function(){
                    var mapOptions = self._getMapOptions(),
                        map = self._getMap(mapOptions);

                    var marker = self._getMarker(map, mapOptions.center);                      

                    // fix issue w/ marker not appearing
                    if (self.options.provider == 'google' && self.options.fixMarker)
                        self.__fixMarker();

                    // watch based fields
                    self._watchBasedFields(map, marker);
                });
            },

            fill: function(latLng) {
                this.options.inputField.val(latLng.lat + ',' + latLng.lng);
            },

            search: function(map, marker, address) {
                if (this.options.searchProvider === 'google') {
                    var provider = new GeoSearch.GoogleProvider({ apiKey: this.options.providerOptions.google.apiKey });
                    provider.search({query: address}).then(data => {
                        if (data.length > 0) {
                            var result = data[0],
                                latLng = new L.LatLng(result.y, result.x);

                            marker.setLatLng(latLng);
                            map.panTo(latLng);
                        }
                    });
                }

                else if (this.options.searchProvider === 'yandex') {
                    // https://yandex.com/dev/maps/geocoder/doc/desc/concepts/input_params.html
                    var url = 'https://geocode-maps.yandex.ru/1.x/?format=json&geocode=' + address;

                    if (typeof this.options.providerOptions.yandex.apiKey !== 'undefined') {
                        url += '&apikey=' + this.options.providerOptions.yandex.apiKey;
                    }

                    var request = new XMLHttpRequest();
                    request.open('GET', url, true);

                    request.onload = function () {
                        if (request.status >= 200 && request.status < 400) {
                            var data = JSON.parse(request.responseText);
                            var pos = data.response.GeoObjectCollection.featureMember[0].GeoObject.Point.pos.split(' ');
                            var latLng = new L.LatLng(pos[1], pos[0]);
                            marker.setLatLng(latLng);
                            map.panTo(latLng);
                        } else {
                            console.error('Yandex geocoder error response');
                        }
                    };

                    request.onerror = function () {
                        console.error('Check connection to Yandex geocoder');
                    };

                    request.send();
                }

                else if (this.options.searchProvider === 'addok') {
                    var url = 'https://api-adresse.data.gouv.fr/search/?limit=1&q=' + address;

                    var request = new XMLHttpRequest();
                    request.open('GET', url, true);

                    request.onload = function () {
                        if (request.status >= 200 && request.status < 400) {
                            var data = JSON.parse(request.responseText);
                            var pos = data.features[0].geometry.coordinates;
                            var latLng = new L.LatLng(pos[1], pos[0]);
                            marker.setLatLng(latLng);
                            map.panTo(latLng);
                        } else {
                            console.error('Addok geocoder error response');
                        }
                    };

                    request.onerror = function () {
                        console.error('Check connection to Addok geocoder');
                    };

                    request.send();
                }

                else if (this.options.searchProvider === 'nominatim') {
                    var url = '//nominatim.openstreetmap.org/search?format=json&q=' + address;

                    var request = new XMLHttpRequest();
                    request.open('GET', url, true);

                    request.onload = function () {
                        if (request.status >= 200 && request.status < 400) {
                            var data = JSON.parse(request.responseText);
                            if (data.length > 0) {
                                var pos = data[0];
                                var latLng = new L.LatLng(pos.lat, pos.lon);
                                marker.setLatLng(latLng);
                                map.panTo(latLng);
                            } else {
                                console.error(address + ': not found via Nominatim');
                            }
                        } else {
                            console.error('Nominatim geocoder error response');
                        }
                    };

                    request.onerror = function () {
                        console.error('Check connection to Nominatim geocoder');
                    };

                    request.send();
                }
            },

            loadAll: function(onload) {
                this.$id.html('Loading...');

                // resource loader
                if (LocationFieldResourceLoader == undefined)
                    LocationFieldResourceLoader = SequentialLoader();

                this.load.loader = LocationFieldResourceLoader;
                this.load.path = this.options.path;

                var self = this;

                this.load.common(function(){
                    var mapProvider = self.options.provider,
                        onLoadMapProvider = function() {
                            var searchProvider = self.options.searchProvider + 'SearchProvider',
                                onLoadSearchProvider = function() {
                                    self.$id.html('');
                                    onload();
                                };

                            if (self.load[searchProvider] != undefined) {
                                self.load[searchProvider](self.options.providerOptions[self.options.searchProvider] || {}, onLoadSearchProvider);
                            }
                            else {
                                onLoadSearchProvider();
                            }
                        };

                    if (self.load[mapProvider] != undefined) {
                        self.load[mapProvider](self.options.providerOptions[mapProvider] || {}, onLoadMapProvider);
                    }
                    else {
                        onLoadMapProvider();
                    }
                });
            },

            load: {
                google: function(options, onload) {
                    var js = [
                        this.path + '/@googlemaps/js-api-loader/index.min.js',
                        this.path + '/Leaflet.GoogleMutant.js',
                    ];

                    this._loadJSList(js, function(){
                        const loader = new google.maps.plugins.loader.Loader({
                          apiKey: options.apiKey,
                          version: "weekly",
                        });
                        loader.load().then(() => onload());
                    });
                },

                googleSearchProvider: function(options, onload) {
                    onload();
                    //var url = options.api;

                    //if (typeof options.apiKey !== 'undefined') {
                    //    url += url.indexOf('?') === -1 ? '?' : '&';
                    //    url += 'key=' + options.apiKey;
                    //}

                    //var js = [
                    //        url,
                    //        this.path + '/l.geosearch.provider.google.js'
                    //    ];

                    //this._loadJSList(js, function(){
                    //    // https://github.com/smeijer/L.GeoSearch/issues/57#issuecomment-148393974
                    //    L.GeoSearch.Provider.Google.Geocoder = new google.maps.Geocoder();

                    //    onload();
                    //});
                },

                yandexSearchProvider: function (options, onload) {
                    onload();
                },

                mapbox: function(options, onload) {
                    onload();
                },

                openstreetmap: function(options, onload) {
                    onload();
                },

                common: function(onload) {
                    var self = this,
                        js = [
                            // map providers
                            this.path + '/leaflet/leaflet.js',
                            // search providers
                            this.path + '/leaflet-geosearch/geosearch.umd.js',
                        ],
                        css = [
                            // map providers
                            this.path + '/leaflet/leaflet.css'
                        ];

                    // Leaflet docs note:
                    // Include Leaflet JavaScript file *after* Leaflet’s CSS
                    // https://leafletjs.com/examples/quick-start/
                    this._loadCSSList(css, function(){
                        self._loadJSList(js, onload);
                    });
                },

                _loadJS: function(src, onload) {
                    this.loader.loadJS(src, onload);
                },

                _loadJSList: function(srclist, onload) {
                    this.__loadList(this._loadJS, srclist, onload);
                },

                _loadCSS: function(src, onload) {
                    if (LocationFieldCache.onload[src] != undefined) {
                        onload();
                    }
                    else {
                        LocationFieldCache.onload[src] = 1;
                        onloadCSS(loadCSS(src), onload);
                    }
                },

                _loadCSSList: function(srclist, onload) {
                    this.__loadList(this._loadCSS, srclist, onload);
                },

                __loadList: function(fn, srclist, onload) {
                    if (srclist.length > 1) {
                        for (var i = 0; i < srclist.length-1; ++i) {
                            fn.call(this, srclist[i], function(){});
                        }
                    }

                    fn.call(this, srclist[srclist.length-1], onload);
                }
            },

            error: function(message) {
                console.log(message);
                this.$id.html(message);
            },

            _getMap: function(mapOptions) {
                var map = new L.Map(this.options.id, mapOptions), layer;

                if (this.options.provider == 'google') {
                    layer = new L.gridLayer.googleMutant({
                        type: this.options.providerOptions.google.mapType.toLowerCase(),
                    });
                }
                else if (this.options.provider == 'openstreetmap') {
                    layer = new L.tileLayer(
                        '//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
                            maxZoom: 18
                        });
                }
                else if (this.options.provider == 'mapbox') {
                    layer = new L.tileLayer(
                        'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
                            maxZoom: 18,
                            accessToken: this.options.providerOptions.mapbox.access_token,
                            id: 'mapbox/streets-v11'
                        });
                }

                map.addLayer(layer);

                return map;
            },

            _getMapOptions: function() {
                return $.extend(this.options.mapOptions, {
                    center: this._getLatLng()
                });
            },

            _getLatLng: function() {
                var l = this.options.latLng.split(',').map(parseFloat);
                return new L.LatLng(l[0], l[1]);
            },

            _getMarker: function(map, center) {
                var self = this,
                    markerOptions = {
                        draggable: true
                    };

                var marker = L.marker(center, markerOptions).addTo(map);

                marker.on('dragstart', function(){
                    if (self.options.inputField.is('[readonly]'))
                        marker.dragging.disable();
                    else
                        marker.dragging.enable();
                });

                // fill input on dragend
                marker.on('dragend move', function(){
                    if (!self.options.inputField.is('[readonly]'))
                        self.fill(this.getLatLng());
                });

                // place marker on map click
                map.on('click', function(e){
                    if (!self.options.inputField.is('[readonly]')) {
                        marker.setLatLng(e.latlng);
                        marker.dragging.enable();
                    }
                });

                return marker;
            },

            _watchBasedFields: function(map, marker) {
                var self = this,
                    basedFields = this.options.basedFields,
                    onchangeTimer,
                    onchange = function() {
                        if (!self.options.inputField.is('[readonly]')) {
                            var values = basedFields.map(function() {
                                var value = $(this).val();
                                return value === '' ? null : value;
                            });
                            var address = values.toArray().join(', ');
                            clearTimeout(onchangeTimer);
                            onchangeTimer = setTimeout(function(){
                                self.search(map, marker, address);
                            }, 300);
                        }
                    };

                basedFields.each(function(){
                    var el = $(this);

                    if (el.is('select'))
                        el.change(onchange);
                    else
                        el.keyup(onchange);
                });
                
                if (this.options.inputField.val() === '') {
                    var values = basedFields.map(function() {
                        var value = $(this).val();
                        return value === '' ? null : value;
                    });
                    var address = values.toArray().join(', ');
                    if (address !== '')
                        onchange();
                }
            },

            __fixMarker: function() {
                $('.leaflet-map-pane').css('z-index', '2 !important');
                $('.leaflet-google-layer').css('z-index', '1 !important');
            }
        }

        return {
            render: LocationField.render.bind(LocationField)
        }
    }

    function dataLocationFieldObserver(callback) {
      function _findAndEnableDataLocationFields() {
        var dataLocationFields = $('input[data-location-field-options]');

        dataLocationFields
          .filter(':not([data-location-field-observed])')
          .attr('data-location-field-observed', true)
          .each(callback);
      }

      var observer = new MutationObserver(function(mutations){
        _findAndEnableDataLocationFields();
      });

      var container = document.documentElement || document.body;

      $(container).ready(function(){
        _findAndEnableDataLocationFields();
      });

      observer.observe(container, {attributes: true});
    }

    dataLocationFieldObserver(function(){
        var el = $(this);

        var name = el.attr('name'),
            options = el.data('location-field-options'),
            basedFields = options.field_options.based_fields,
            pluginOptions = {
                id: 'map_' + name,
                inputField: el,
                latLng: el.val() || '0,0',
                suffix: options['search.suffix'],
                path: options['resources.root_path'],
                provider: options['map.provider'],
                searchProvider: options['search.provider'],
                providerOptions: {
                    google: {
                        api: options['provider.google.api'],
                        apiKey: options['provider.google.api_key'],
                        mapType: options['provider.google.map_type']
                    },
                    mapbox: {
                        access_token: options['provider.mapbox.access_token']
                    },
                    yandex: {
                        apiKey: options['provider.yandex.api_key']
                    },
                },
                mapOptions: {
                    zoom: options['map.zoom']
                }
            };

        // prefix
        var prefixNumber;

        try {
            prefixNumber = name.match(/-(\d+)-/)[1];
        } catch (e) {}

        if (options.field_options.prefix) {
            var prefix = options.field_options.prefix;

            if (prefixNumber != null) {
                prefix = prefix.replace(/__prefix__/, prefixNumber);
            }

            basedFields = basedFields.map(function(n){
                return prefix + n
            });
        }

        // based fields
        pluginOptions.basedFields = $(basedFields.map(function(n){
            return '#id_' + n
        }).join(','));

        // render
        $.locationField(pluginOptions).render();
    });

}(jQuery || django.jQuery);

/*!
loadCSS: load a CSS file asynchronously.
[c]2015 @scottjehl, Filament Group, Inc.
Licensed MIT
*/
(function(w){
	"use strict";
	/* exported loadCSS */
	var loadCSS = function( href, before, media ){
		// Arguments explained:
		// `href` [REQUIRED] is the URL for your CSS file.
		// `before` [OPTIONAL] is the element the script should use as a reference for injecting our stylesheet <link> before
			// By default, loadCSS attempts to inject the link after the last stylesheet or script in the DOM. However, you might desire a more specific location in your document.
		// `media` [OPTIONAL] is the media type or query of the stylesheet. By default it will be 'all'
		var doc = w.document;
		var ss = doc.createElement( "link" );
		var ref;
		if( before ){
			ref = before;
		}
		else {
			var refs = ( doc.body || doc.getElementsByTagName( "head" )[ 0 ] ).childNodes;
			ref = refs[ refs.length - 1];
		}

		var sheets = doc.styleSheets;
		ss.rel = "stylesheet";
		ss.href = href;
		// temporarily set media to something inapplicable to ensure it'll fetch without blocking render
		ss.media = "only x";

		// Inject link
			// Note: the ternary preserves the existing behavior of "before" argument, but we could choose to change the argument to "after" in a later release and standardize on ref.nextSibling for all refs
			// Note: `insertBefore` is used instead of `appendChild`, for safety re: http://www.paulirish.com/2011/surefire-dom-element-insertion/
		ref.parentNode.insertBefore( ss, ( before ? ref : ref.nextSibling ) );
		// A method (exposed on return object for external use) that mimics onload by polling until document.styleSheets until it includes the new sheet.
		var onloadcssdefined = function( cb ){
			var resolvedHref = ss.href;
			var i = sheets.length;
			while( i-- ){
				if( sheets[ i ].href === resolvedHref ){
					return cb();
				}
			}
			setTimeout(function() {
				onloadcssdefined( cb );
			});
		};

		// once loaded, set link's media back to `all` so that the stylesheet applies once it loads
		ss.onloadcssdefined = onloadcssdefined;
		onloadcssdefined(function() {
			ss.media = media || "all";
		});
		return ss;
	};
	// commonjs
	if( typeof module !== "undefined" ){
		module.exports = loadCSS;
	}
	else {
		w.loadCSS = loadCSS;
	}
}( typeof global !== "undefined" ? global : this ));


/*!
onloadCSS: adds onload support for asynchronous stylesheets loaded with loadCSS.
[c]2014 @zachleat, Filament Group, Inc.
Licensed MIT
*/

/* global navigator */
/* exported onloadCSS */
function onloadCSS( ss, callback ) {
	ss.onload = function() {
		ss.onload = null;
		if( callback ) {
			callback.call( ss );
		}
	};

	// This code is for browsers that don’t support onload, any browser that
	// supports onload should use that instead.
	// No support for onload:
	//	* Android 4.3 (Samsung Galaxy S4, Browserstack)
	//	* Android 4.2 Browser (Samsung Galaxy SIII Mini GT-I8200L)
	//	* Android 2.3 (Pantech Burst P9070)

	// Weak inference targets Android < 4.4
	if( "isApplicationInstalled" in navigator && "onloadcssdefined" in ss ) {
		ss.onloadcssdefined( callback );
	}
}
