jQuery( document ).ready( function( $ ) {
    var map, geocoder, startLatLng, markersArray = [],
        wpslAdmin = wpslAdmin || {};

    /**
     * Verify the provided API keys
     *
     * @since 2.2.22
     */
    wpslAdmin.verifyKeys = {
        init: function() {
            var self       = this,
                $btn       = $( "#wpsl-verify-keys" ),
                preloader  = wpslSettings.url + "img/ajax-loader.gif",
                mapService = ( typeof wpslSettings.mapService !== "undefined" ) ? wpslSettings.mapService : "gmaps";

            $btn.on( "click", function() {
                $( "#wpsl-wrap .notice" ).remove();

                self[mapService].check();

                $btn.after( '<img src="' + preloader + '" class="wpsl-api-key-preloader" />' );

                return false;
            });
        },
        /**
         * Show the status of the API keys.
         *
         * @since 2.2.22
         * @param {string} response   The API response
         * @param {string} keyType 	   The type of API key we need to show the notice for
         * @param {string} noticeType Show either an error or success notice.
         * @returns {void}
         */
        showStatus: function( response, keyType, noticeType = "error" ) {

            this.createNotice( response, keyType, noticeType );

            // After the browser check has finished we remove the preloader.
            if ( keyType == "browser" ) {
                $( ".wpsl-api-key-preloader" ).remove();
            }
        },
        /**
         * Create the error notice.
         *
         * @since 2.2.10
         * @param {string} response   The API response to show
         * @param {string} keyType 	   The type of API key we need to show the notice for
         * @param {string} noticeType Show either an error or success notice.
         * @returns void
         */
        createNotice: function( response, keyType, noticeType ) {
            var notice, noticeLocation, cssClass;

            cssClass = ( noticeType == "error" ) ? "error" : "updated";

            notice = '<div class="' + cssClass + ' notice is-dismissible">';
            notice += '<p><strong>' + response + '</strong></p>';
            notice += '<button type="button" class="notice-dismiss"><span class="screen-reader-text">' + wpslL10n.dismissNotice + '</span></button>';
            notice += '</div>';

            noticeLocation = ( $( "#wpsl-tabs" ).length ) ? "wpsl-tabs" : "wpsl-settings-form";

            $( "#" + noticeLocation + "" ).before( notice );

            if ( noticeType == "error" ) {
                $( "#wpsl-api-" + keyType + "-key" ).addClass( "wpsl-error" );
            } else {
                $( "#wpsl-api-" + keyType + "-key" ).removeClass( "wpsl-error" );
            }
        },
        gmaps: {
            /**
             * Check for any issues with the used API keys.
             *
             * @since   2.2.22
             * @returns {void}
             */
            check: function() {
                this.server(function() {
                    wpslAdmin.verifyKeys.gmaps.browser();
                });
            },
            /**
             * Make a request to the Google Geocode API to
             * check if the server key is valid or not.
             *
             * @since   2.2.22
             * @returns {void}
             */
            server: function( callback ) {
                var status,
                    ajaxData = {
                        action: "validate_server_key",
                        server_key: $( "#wpsl-api-server-key" ).val()
                    };

                if ( ajaxData.server_key ) {
                    $.get( wpslSettings.ajaxurl, ajaxData, function( response ) {
                        status = ( response.valid ) ? "updated" : "error";

                        wpslAdmin.verifyKeys.showStatus( response.msg, "server", status );

                        callback();
                    });
                } else {
                    wpslAdmin.verifyKeys.showStatus( wpslL10n.serverKeyMissing, "server" );

                    callback();
                }
            },
            /**
             * Make a request to the Google JavaScript API to
             * check if the browser key is valid or not.
             *
             * @since   2.2.22
             * @returns {void}
             */
            browser: function() {
                var browserAPICheck,
                    browserKey = $( "#wpsl-api-browser-key" ).val();

                if ( browserKey ) {

                    /**
                     * Wait 3 seconds before checking if the
                     * Geocode API returned data.
                     *
                     * If this hasn't happened, then there has to
                     * be a problem with the API keys.
                     */
                    browserAPICheck = setInterval(function() {
                        wpslAdmin.verifyKeys.showStatus( wpslL10n.browserKeyError, "browser" );

                        clearInterval( browserAPICheck );
                    }, 3000 );

                    /**
                     * This will only complete if there are no issues
                     * with the API key, otherwise it won't even make a request.
                     *
                     * To check this we use the setInterval in the above section.
                     */
                    geocoder.geocode( { 'address': 'Manhattan, NY 10036, USA' }, function( response, status ) {
                        if ( status == google.maps.GeocoderStatus.OK ) {
                            wpslAdmin.verifyKeys.showStatus( wpslL10n.browserKeySuccess, "browser", "success" );
                        } else {
                            wpslAdmin.verifyKeys.showStatus( wpslL10n.browserKeyError, "browser" );
                        }

                        clearInterval( browserAPICheck );
                    });
                } else {
                    wpslAdmin.verifyKeys.showStatus( wpslL10n.browserKeyMissing, "browser" );
                }
            }
        }
    };

    /**
     * Handle the Geocode requests made from
     * the Tools section on the settings page.
     *
     * This can be used to check the API response
     * for any input the user provides, and see if
     * it's in the expected location.
     *
     * Will show error messages if there are any
     * issues with the used browser API keys.
     *
     * @since 2.2.22
     */
    wpslAdmin.showApiResponse = {
        init: function() {
            var $geocodeInput = $( "#wpsl-geocode-input" ),
                self          = this,
                mapLoaded     = false;

            $( "#wpsl-show-geocode-response" ).on( "click", function( e ) {
                self.createDialog();

                initializeGmap("wpsl-geocode-preview" );

                // Make sure we don't add the same message twice.
                if ( !$( ".wpsl-geocode-warning span" ).length ) {
                    self.createRestrictionsMsg();
                }

                // Check for map errors after it finished loading.
                google.maps.event.addListenerOnce( map, "tilesloaded", function() {
                    mapLoaded = true;
                    self.checkQuotaError();
                });

                // Check if the map was load succesfully, if not show an error message explaining it.
                setTimeout(function() {
                    if ( !mapLoaded ) {
                        $(".wpsl-geocode-warning, #wpsl-geocode-test input, #wpsl-geocode-tabs").remove();
                        $(".wpsl-geocode-api-notice").show().html( wpslL10n.loadingFailed );
                    }
                }, 1000 );

                return false;
            });

            // Submit the geocode request.
            $( "#wpsl-geocode-submit" ).on( "click", function( e ) {
                $geocodeInput.removeClass( "wpsl-error" );

                if ( !$geocodeInput.val() ) {
                    $geocodeInput.addClass( "wpsl-error" );
                    $( ".wpsl-geocode-api-notice" ).hide();
                } else {
                    self.geocoding.makeRequest();
                }
            });

            // Handle users using the enter key in the dialog box.
            $( "#wpsl-geocode-test" ).keydown( function( event ) {
                var keyCode = ( event.keyCode ? event.keyCode : event.which );

                if ( keyCode == 13 ) {
                    $( "#wpsl-geocode-submit" ).trigger( "click" );
                }
            });
        },
        /**
         * Create the dialog box
         *
         * @since   2.2.22
         * @returns {void}
         */
        createDialog: function() {
            $( "#wpsl-geocode-test" ).dialog({
                resizable: false,
                height: "auto",
                width: 550,
                modal: true,
                open: function() {

                    // Move it closer to the top then it normally would
                    $( this ).parent().css({ "top": window.pageYOffset + 50 });

                    $( "#wpsl-geocode-tabs" ).tabs();
                    $( "#wpsl-geocode-input" ).focus();
                    $( ".wpsl-geocode-api-notice" ).hide();

                    // Make sure the first tab is always selected after the dialog is opened a second time
                    $( "#wpsl-geocode-tabs" ).tabs( "option", "active", $( "li" ).index( $( "li:visible:eq(0)" ) ) );

                    // Make sure to remove any previous input
                    $( "#wpsl-geocode-input, #wpsl-geocode-response textarea" ).val( "" );

                    $( ".ui-widget-overlay" ).bind( "click", function() {
                        $( "#wpsl-geocode-test" ).dialog( "close" );
                    });
                },
                buttons: {
                    Cancel: function() {
                        $( this ).dialog( "close" );
                    }
                }
            });
        },
        /**
         * If there's a problem with the billing account,
         * then a 'dismissButton' class will exist in the map itself.
         *
         * If this is the case, then we remove everything and
         * show an error explaining the problem.
         *
         * @since   2.2.22
         * @returns {void}
         */
        checkQuotaError: function() {

            setTimeout(function() {
                if ( $( "#wpsl-geocode-preview .dismissButton" ).length > 0 ) {
                    $( ".wpsl-geocode-warning, #wpsl-geocode-test input" ).remove();

                    $( ".wpsl-geocode-api-notice" ).show();
                    $( ".wpsl-geocode-api-notice span" ).html( wpslL10n.loadingError );
                }

            }, 1000 );
        },
        /**
         * Create a message explaning the user that the
         * results are restricted to the selected map region,
         * and possibly only work for zip codes.
         *
         * @since   2.2.22
         * @returns {void}
         */
        createRestrictionsMsg: function() {
            var countryName, zipcodeOnly,
                $warningElem = $( ".wpsl-geocode-warning" ).show().find( "strong" );

            if ( $( "#wpsl-api-region" ).val() ) {
                countryName = $( "#wpsl-api-region option:selected" ).text();
                zipcodeOnly = ( $( "#wpsl-force-postalcode" ).is( ":checked" ) ) ? wpslL10n.restrictedZipCode : '';

                $warningElem.after( "<span>" + wpslL10n.resultsWarning + ' ' + countryName + ' ' + zipcodeOnly + "</span>" );
            } else {
                $warningElem.after( "<span>" + wpslL10n.noRestriction + "</span>" );

                $( ".wpsl-region-href" ).on( "click", function() {
                    $( ".ui-widget-overlay" ).trigger( "click" );
                });
            }
        },
        geocoding: {
            /**
             * Geocode the provided user input
             *
             * @since   2.2.22
             * @returns {void}
             */
            makeRequest: function() {
                var request = this.createParams();

                geocoder.geocode( request, function( response, status ) {

                    // Show an error message if there is a problem with the browser API key.
                    if ( status == "OK" || status == "ZERO_RESULTS" ) {

                        // Make sure to remove the marker from the map if one exists.
                        if ( typeof markersArray[0] !== "undefined" ) {
                            markersArray[0].setMap( null );
                            markersArray.length = 0;
                        }

                        if ( status == "OK" ) {
                            addMarker( response[0].geometry.location, false );

                            map.setZoom( 12 );
                            map.setCenter( response[0].geometry.location );
                        } else {
                            map.setZoom( parseInt( wpslSettings.defaultZoom ) );
                            map.setCenter( startLatLng );
                        }
                    } else {
                        status = wpslL10n.browserKeyError;

                        $( "#wpsl-geocode-preview, #wpsl-geocode-response textarea" ).remove();
                    }

                    $( ".wpsl-geocode-api-notice" ).show();
                    $( ".wpsl-geocode-api-notice span" ).html( status );
                    $( "#wpsl-geocode-response textarea" ).val( JSON.stringify( response, null, 4 ) );
                });
            },
            /**
             * Create the params used in the geocode request
             * made through the test tool in the tools section.
             *
             * @since  2.2.22
             * @return {object} request The parameters included in the geocode API request
             */
            createParams: function() {
                var request = {};

                // Check if we need to set the geocode component restrictions.
                if ( typeof wpslSettings.geocodeComponents !== "undefined" && !$.isEmptyObject( wpslSettings.geocodeComponents ) ) {
                    request.componentRestrictions = wpslSettings.geocodeComponents;

                    if ( typeof request.componentRestrictions.postalCode !== "undefined" ) {
                        request.componentRestrictions.postalCode = $( "#wpsl-geocode-input" ).val();
                    } else {
                        request.address = $( "#wpsl-geocode-input" ).val();
                    }
                } else {
                    request.address = $( "#wpsl-geocode-input" ).val();
                }

                return request;
            },
        },
    };

    if ( $( "#wpsl-gmap-wrap" ).length ) {
        initializeGmap();
    }

    /**
     * If we are on the settings page, then init the API tools.
     */
    if ( $( "#wpsl-map-settings").length ) {
        wpslAdmin.verifyKeys.init();
        wpslAdmin.showApiResponse.init();
    }

    /**
     * Initialize the map with the correct settings.
     *
     * @since	1.0.0
     * @param   string mapId The ID of the element to render the map in
     * @returns {void}
     */
    function initializeGmap( mapId = "wpsl-gmap-wrap" ) {
        var defaultLatLng = wpslSettings.defaultLatLng.split( "," ),
            mapOptions;

        startLatLng = new google.maps.LatLng( defaultLatLng[0], defaultLatLng[1] );
        mapOptions  = {
            zoom: parseInt( wpslSettings.defaultZoom ),
            center: startLatLng,
            mapTypeId: google.maps.MapTypeId[ wpslSettings.mapType.toUpperCase() ],
            mapTypeControl: false,
            streetViewControl: false,
            zoomControlOptions: {
                position: google.maps.ControlPosition.RIGHT_TOP
            }
        };

        geocoder = new google.maps.Geocoder();
        map		  = new google.maps.Map( document.getElementById( mapId ), mapOptions );

        checkEditStoreMarker();
    }

    /**
     * Check if we have an existing latlng value.
     *
     * If there is an latlng value, then we add a marker to the map.
     * This can only happen on the edit store page.
     *
     * @since	1.0.0
     * @returns {void}
     */
    function checkEditStoreMarker() {
        var location,
            lat = $( "#wpsl-lat" ).val(),
            lng = $( "#wpsl-lng" ).val();

        if ( ( lat ) && ( lng ) ) {
            location = new google.maps.LatLng( lat, lng );

            map.setCenter( location );
            map.setZoom( 16 );
            addMarker( location );
        }
    }

    // If we have a city/country input field enable the autocomplete.
    if ( $( "#wpsl-start-name" ).length ) {
        activateAutoComplete();
    }

    /**
     * Activate the autocomplete function for the city/country field.
     *
     * @since	1.0.0
     * @returns {void}
     */
    function activateAutoComplete() {
        var latlng,
            input = document.getElementById( "wpsl-start-name" ),
            options = {
                types: ['geocode']
            },
            autocomplete = new google.maps.places.Autocomplete( input, options );

        google.maps.event.addListener( autocomplete, "place_changed", function() {
            latlng = autocomplete.getPlace().geometry.location;
            setLatlng( latlng, "zoom" );
        });
    }

    /**
     * Add a new marker to the map based on the provided location (latlng).
     *
     * @since	1.0.0
     * @param   {object}  location   The latlng value
     * @param   {boolean} draggable Whether the marker should be draggable or not
     * @returns {void}
     */
    function addMarker( location, draggable = true ) {
        var marker = new google.maps.Marker({
            position: location,
            map: map,
            draggable: draggable
        });

        markersArray.push( marker );

        // If the marker is dragged on the map, make sure the latlng values are updated.
        google.maps.event.addListener( marker, "dragend", function() {
            setLatlng( marker.getPosition(), "store" );
        });
    }

    // Lookup the provided location with the Google Maps API.
    $( "#wpsl-lookup-location" ).on( "click", function( e ) {
        e.preventDefault();
        codeAddress();
    });

    /**
     * Update the hidden input field with the current latlng values.
     *
     * @since	1.0.0
     * @param   {object} latLng The latLng values
     * @param   {string} target The location where we need to set the latLng
     * @returns {void}
     */
    function setLatlng( latLng, target ) {
        var coordinates = stripCoordinates( latLng ),
            lat			= roundCoordinate( coordinates[0] ),
            lng			= roundCoordinate( coordinates[1] );

        if ( target == "store" ) {
            $( "#wpsl-lat" ).val( lat );
            $( "#wpsl-lng" ).val( lng );
        } else if ( target == "zoom" ) {
            $( "#wpsl-latlng" ).val( lat + ',' + lng );
        }
    }

    /**
     * Geocode the user input.
     *
     * @since	1.0.0
     * @returns {void}
     */
    function codeAddress() {
        var filteredResponse, geocodeAddress;

        // Check if we have all the required data before attempting to geocode the address.
        if ( !validatePreviewFields() ) {
            geocodeAddress = createGeocodeAddress();

            geocoder.geocode( { 'address': geocodeAddress }, function( response, status ) {
                if ( status === google.maps.GeocoderStatus.OK ) {

                    // If we have a previous marker on the map we remove it.
                    if ( typeof( markersArray[0] ) !== "undefined" ) {
                        if ( markersArray[0].draggable ) {
                            markersArray[0].setMap( null );
                            markersArray.splice(0, 1);
                        }
                    }

                    // Center and zoom to the searched location.
                    map.setCenter( response[0].geometry.location );
                    map.setZoom( 16 );

                    addMarker( response[0].geometry.location );
                    setLatlng( response[0].geometry.location, "store" );

                    filteredResponse = filterApiResponse( response );

                    $( "#wpsl-country" ).val( filteredResponse.country.long_name );
                    $( "#wpsl-country_iso" ).val( filteredResponse.country.short_name );
                } else {
                    alert( wpslL10n.geocodeFail + ": " + status );
                }
            });

            return false;
        } else {
            activateStoreTab( "first" );

            alert( wpslL10n.missingGeoData );

            return true;
        }
    }

    /**
     * Check that all required fields for the map preview are there.
     *
     * @since	1.0.0
     * @returns {boolean} error  Whether all the required fields contained data.
     */
    function validatePreviewFields() {
        var i, fieldData, requiredFields,
            error = false;

        $( ".wpsl-store-meta input" ).removeClass( "wpsl-error" );

        // Check which fields are required.
        if ( typeof wpslSettings.requiredFields !== "undefined" && _.isArray( wpslSettings.requiredFields ) ) {
            requiredFields = wpslSettings.requiredFields;

            // Check if all the required fields contain data.
            for ( i = 0; i < requiredFields.length; i++ ) {
                fieldData = $.trim( $( "#wpsl-" + requiredFields[i] ).val() );

                if ( !fieldData ) {
                    $( "#wpsl-" + requiredFields[i] ).addClass( "wpsl-error" );
                    error = true;
                }

                fieldData = '';
            }
        }

        return error;
    }

    /**
     * Build the address that's send to the Geocode API.
     *
     * @since	2.1.0
     * @returns {string} geocodeAddress The address separated by , that's send to the Geocoder.
     */
    function createGeocodeAddress() {
        var i, part,
            address      = [],
            addressParts = [ "address", "city", "state", "zip", "country" ];

        for ( i = 0; i < addressParts.length; i++ ) {
            part = $.trim( $( "#wpsl-" + addressParts[i] ).val() );

            /*
             * At this point we already know the address, city and country fields contain data.
             * But no need to include the zip and state if they are empty.
             */
            if ( part ) {
                address.push( part );
            }

            part = "";
        }

        return address.join();
    }

    /**
     * Filter out the country name from the API response.
     *
     * @since	1.0.0
     * @param   {object} response	   The response of the geocode API
     * @returns {object} collectedData The short and long country name
     */
    function filterApiResponse( response ) {
        var i, responseType, collectedData,
            country		  = {},
            addressLength = response[0].address_components.length;

        // Loop over the API response.
        for ( i = 0; i < addressLength; i++ ) {
            responseType = response[0].address_components[i].types;

            // Filter out the country name.
            if ( /^country,political$/.test( responseType ) ) {
                country = {
                    long_name: response[0].address_components[i].long_name,
                    short_name: response[0].address_components[i].short_name
                };
            }
        }

        collectedData = {
            country: country
        };

        return collectedData;
    }

    /**
     * Round the coordinate to 6 digits after the comma.
     *
     * @since	1.0.0
     * @param   {string} coordinate   The coordinate
     * @returns {number} roundedCoord The rounded coordinate
     */
    function roundCoordinate( coordinate ) {
        var roundedCoord, decimals = 6;

        roundedCoord = Math.round( coordinate * Math.pow( 10, decimals ) ) / Math.pow( 10, decimals );

        return roundedCoord;
    }

    /**
     * Strip the '(' and ')' from the captured coordinates and split them.
     *
     * @since	1.0.0
     * @param   {string} coordinates The coordinates
     * @returns {object} latLng      The latlng coordinates
     */
    function stripCoordinates( coordinates ) {
        var latLng    = [],
            selected  = coordinates.toString(),
            latLngStr = selected.split( ",", 2 );

        latLng[0] = latLngStr[0].replace( "(", "" );
        latLng[1] = latLngStr[1].replace( ")", "" );

        return latLng;
    }

    $( ".wpsl-marker-list input[type=radio]" ).click( function() {
        $( this ).parents( ".wpsl-marker-list" ).find( "li" ).removeClass();
        $( this ).parent( "li" ).addClass( "wpsl-active-marker" );
    });

    $( ".wpsl-marker-list li" ).click( function() {
        $( this ).parents( ".wpsl-marker-list" ).find( "input" ).prop( "checked", false );
        $( this ).find( "input" ).prop( "checked", true );
        $( this ).siblings().removeClass();
        $( this ).addClass( "wpsl-active-marker" );
    });

    // Detect changes in checkboxes that have a conditional option.
    $( ".wpsl-has-conditional-option" ).on( "change", function() {
        $( this ).parent().next( ".wpsl-conditional-option" ).toggle();
    });

    /*
     * Detect changes to the store template dropdown. If the template is selected to
     * show the store list under the map then we show the option to hide the scrollbar.
     */
    $( "#wpsl-store-template" ).on( "change", function() {
        var $scrollOption = $( "#wpsl-listing-below-no-scroll" );

        if ( $( this ).val() == "below_map" ) {
            $scrollOption.show();
        } else {
            $scrollOption.hide();
        }
    });

    $( "#wpsl-api-region" ).on( "change", function() {
        var $geocodeComponent = $( "#wpsl-geocode-component" );

        if ( $( this ).val() ) {
            $geocodeComponent.show();
        } else {
            $geocodeComponent.hide();
        }
    });

    // Make sure the correct hour input format is visible.
    $( "#wpsl-editor-hour-input" ).on( "change", function() {
        $( ".wpsl-" + $( this ).val() + "-hours" ).show().siblings( "div" ).hide();
        $( ".wpsl-hour-notice" ).toggle();
    });

    // Set the correct tab to active and show the correct content block.
    $( "#wpsl-meta-nav li" ).on( "click", function( e ) {
        var activeClass = $( this ).attr( "class" );
        activeClass = activeClass.split( "-tab" );

        e.stopPropagation();

        // Set the correct tab and metabox to active.
        $( this ).addClass( "wpsl-active" ).siblings().removeClass( "wpsl-active" );
        $( ".wpsl-store-meta ." + activeClass[0] + "" ).show().addClass( "wpsl-active" ).siblings( "div" ).hide().removeClass( "wpsl-active" );
    });

    // Make sure the required store fields contain data.
    if ( $( "#wpsl-store-details" ).length ) {
        $( "#publish" ).click( function() {
            var firstErrorElem, currentTabClass, elemClass,
                errorMsg	= '<div id="message" class="error"><p>' + wpslL10n.requiredFields + '</p></div>',
                missingData = false;

            // Remove error messages and css classes from previous submissions.
            $( "#wpbody-content .wrap #message" ).remove();
            $( ".wpsl-required" ).removeClass( "wpsl-error" );

            // Loop over the required fields and check for a value.
            $( ".wpsl-required" ).each( function() {
                if ( $( this ).val() == "" ) {
                    $( this ).addClass( "wpsl-error" );

                    if ( typeof firstErrorElem === "undefined" ) {
                        firstErrorElem = getFirstErrorElemAttr( $( this ) );
                    }

                    missingData = true;
                }
            });

            // If one of the required fields are empty, then show the error msg and make sure the correct tab is visible.
            if ( missingData ) {
                $( "#wpbody-content .wrap > h2" ).after( errorMsg );

                if ( typeof firstErrorElem.val !== "undefined" ) {
                    if ( firstErrorElem.type == "id" ) {
                        currentTabClass = $( "#" + firstErrorElem.val + "" ).parents( ".wpsl-tab" ).attr( "class" );
                        $( "html, body" ).scrollTop( Math.round( $( "#" + firstErrorElem.val + "" ).offset().top - 100 ) );
                    } else if ( firstErrorElem.type == "class" ) {
                        elemClass		= firstErrorElem.val.replace( /wpsl-required|wpsl-error/g, "" );
                        currentTabClass = $( "." + elemClass + "" ).parents( ".wpsl-tab" ).attr( "class" );
                        $( "html, body" ).scrollTop( Math.round( $( "." + elemClass + "" ).offset().top - 100 ) );
                    }

                    currentTabClass = $.trim( currentTabClass.replace( /wpsl-tab|wpsl-active/g, "" ) );
                }

                // If we don't have a class of the tab that should be set to visible, we just show the first one.
                if ( !currentTabClass ) {
                    activateStoreTab( 'first' );
                } else {
                    activateStoreTab( currentTabClass );
                }

                /*
                 * If not all required fields contains data, and the user has
                 * clicked the submit button. Then an extra css class is added to the
                 * button that will disabled it. This only happens in WP 3.8 or earlier.
                 *
                 * We need to make sure this css class doesn't exist otherwise
                 * the user can never resubmit the page.
                 */
                $( "#publish" ).removeClass( "button-primary-disabled" );
                $( ".spinner" ).hide();

                return false;
            } else {
                return true;
            }
        });
    }

    /**
     * Set the correct tab to visible, and hide all other metaboxes
     *
     * @since	2.0.0
     * @param   {string} $target The name of the tab to show
     * @returns {void}
     */
    function activateStoreTab( $target ) {
        if ( $target == 'first' ) {
            $target = ':first-child';
        } else {
            $target = '.' + $target;
        }

        if ( !$( "#wpsl-meta-nav li" + $target + "-tab" ).hasClass( "wpsl-active" ) ) {
            $( "#wpsl-meta-nav li" + $target + "-tab" ).addClass( "wpsl-active" ).siblings().removeClass( "wpsl-active" );
            $( ".wpsl-store-meta > div" + $target + "" ).show().addClass( "wpsl-active" ).siblings( "div" ).hide().removeClass( "wpsl-active" );
        }
    }

    /**
     * Get the id or class of the first element that's an required field, but is empty.
     *
     * We need this to determine which tab we need to set active,
     * which will be the tab were the first error occured.
     *
     * @since	2.0.0
     * @param   {object} elem			The element the error occured on
     * @returns {object} firstErrorElem The id/class set on the first elem that an error occured on and the attr value
     */
    function getFirstErrorElemAttr( elem ) {
        var firstErrorElem = { "type": "id", "val" : elem.attr( "id" ) };

        // If no ID value exists, then check if we can get the class name.
        if ( typeof firstErrorElem.val === "undefined" ) {
            firstErrorElem = { "type": "class", "val" : elem.attr( "class" ) };
        }

        return firstErrorElem;
    }

    // If we have a store hours dropdown, init the event handler.
    if ( $( "#wpsl-store-hours" ).length ) {
        initHourEvents();
    }

    /**
     * Assign an event handler to the button that enables
     * users to remove an opening hour period.
     *
     * @since	2.0.0
     * @returns {void}
     */
    function initHourEvents() {
        $( "#wpsl-store-hours .wpsl-icon-cancel-circled" ).off();
        $( "#wpsl-store-hours .wpsl-icon-cancel-circled" ).on( "click", function() {
            removePeriod( $( this ) );
        });
    }

    // Add new openings period to the openings hours table.
    $( ".wpsl-add-period" ).on( "click", function( e ) {
        var newPeriod,
            hours		= {},
            returnList  = true,
            $tr			= $( this ).parents( "tr" ),
            periodCount = currentPeriodCount( $( this ) ),
            periodCss   = ( periodCount >= 1 ) ? "wpsl-current-period wpsl-multiple-periods" : "wpsl-current-period",
            day 	    = $tr.find( ".wpsl-opening-hours" ).attr( "data-day" ),
            selectName  = ( $( "#wpsl-settings-form" ).length ) ? "wpsl_editor[dropdown]" : "wpsl[hours]";

        newPeriod = '<div class="' + periodCss +'">';
        newPeriod += '<select autocomplete="off" name="' + selectName + '[' + day + '_open][]" class="wpsl-open-hour">' + createHourOptionList( returnList ) + '</select>';
        newPeriod += '<span> - </span>';
        newPeriod += '<select autocomplete="off" name="' + selectName + '[' + day + '_close][]" class="wpsl-close-hour">' + createHourOptionList( returnList ) + '</select>';
        newPeriod += '<div class="wpsl-icon-cancel-circled"></div>';
        newPeriod += '</div>';

        $tr.find( ".wpsl-store-closed" ).remove();
        $( "#wpsl-hours-" + day + "" ).append( newPeriod ).end();

        initHourEvents();

        if ( $( "#wpsl-editor-hour-format" ).val() == 24 ) {
            hours = {
                "open": "09:00",
                "close": "17:00"
            };
        } else {
            hours = {
                "open": "9:00 AM",
                "close": "5:00 PM"
            };
        }

        $tr.find( ".wpsl-open-hour:last option[value='" + hours.open + "']" ).attr( "selected", "selected" );
        $tr.find( ".wpsl-close-hour:last option[value='" + hours.close + "']" ).attr( "selected", "selected" );

        e.preventDefault();
    });

    /**
     * Remove an openings period
     *
     * @since  2.0.0
     * @param  {object} elem The clicked element
     * @return {void}
     */
    function removePeriod( elem ) {
        var periodsLeft	= currentPeriodCount( elem ),
            $tr			= elem.parents( "tr" ),
            day 	    = $tr.find( ".wpsl-opening-hours" ).attr( "data-day" );

        // If there was 1 opening hour left then we add the 'Closed' text.
        if ( periodsLeft == 1 ) {
            $tr.find( ".wpsl-opening-hours" ).html( "<p class='wpsl-store-closed'>" + wpslL10n.closedDate + "<input type='hidden' name='wpsl[hours][" + day + "_open]' value='' /></p>" );
        }

        // Remove the selected openings period.
        elem.parent().closest( ".wpsl-current-period" ).remove();

        // If the first element has the multiple class, then we need to remove it.
        if ( $tr.find( ".wpsl-opening-hours div:first-child" ).hasClass( "wpsl-multiple-periods" ) ) {
            $tr.find( ".wpsl-opening-hours div:first-child" ).removeClass( "wpsl-multiple-periods" );
        }
    }

    /**
     * Count the current opening periods in a day block
     *
     * @since  2.0.0
     * @param  {object} elem		   The clicked element
     * @return {string} currentPeriods The ammount of period divs found
     */
    function currentPeriodCount( elem ) {
        var currentPeriods = elem.parents( "tr" ).find( ".wpsl-current-period" ).length;

        return currentPeriods;
    }

    /**
     * Create an option list with the correct opening hour format and interval
     *
     * @since  2.0.0
     * @param  {string} returnList Whether to return the option list or call the setSelectedOpeningHours function
     * @return {mixed}  optionList The html for the option list of or void
     */
    function createHourOptionList( returnList ) {
        var openingHours, openingHourInterval, hour, hrFormat,
            pm   			   = false,
            twelveHrsAfternoon = false,
            pmOrAm			   = "",
            optionList		   = "",
            openingTimes 	   = [],
            openingHourOptions = {
                "hours": {
                    "hr12": [ 12, 1, 2, 3 ,4 ,5 ,6, 7, 8, 9, 10, 11, 12, 1, 2, 3 , 4, 5, 6, 7, 8, 9, 10, 11 ],
                    "hr24": [ 0, 1, 2, 3 ,4 ,5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 ,16, 17, 18, 19, 20, 21, 22, 23 ]
                },
                "interval": [ '00', '15', '30', '45' ]
            };

        if ( $( "#wpsl-editor-hour-format" ).length ) {
            hrFormat = $( "#wpsl-editor-hour-format" ).val();
        } else {
            hrFormat = wpslSettings.hourFormat;
        }

        $( "#wpsl-store-hours td" ).removeAttr( "style" );

        if ( hrFormat == 12 ) {
            $( "#wpsl-store-hours" ).removeClass().addClass( "wpsl-twelve-format" );
            openingHours = openingHourOptions.hours.hr12;
        } else {
            $( "#wpsl-store-hours" ).removeClass().addClass( "wpsl-twentyfour-format" );
            openingHours = openingHourOptions.hours.hr24;
        }

        openingHourInterval = openingHourOptions.interval;

        for ( var i = 0; i < openingHours.length; i++ ) {
            hour = openingHours[i];

            /*
             * If the 12hr format is selected, then check if we need to show AM or PM.
             *
             * If the 24hr format is selected and the hour is a single digit
             * then we add a 0 to the start so 5:00 becomes 05:00.
             */
            if ( hrFormat == 12 ) {
                if ( hour >= 12 ) {
                    pm = ( twelveHrsAfternoon ) ? true : false;

                    twelveHrsAfternoon = true;
                }

                pmOrAm = ( pm ) ? "PM" : "AM";
            } else if ( ( hrFormat == 24 ) && ( hour.toString().length == 1 ) ) {
                hour = "0" + hour;
            }

            // Collect the new opening hour format and interval.
            for ( var j = 0; j < openingHourInterval.length; j++ ) {
                openingTimes.push( hour + ":" + openingHourInterval[j] + " " + pmOrAm );
            }
        }

        // Create the <option> list.
        for ( var i = 0; i < openingTimes.length; i++ ) {
            optionList = optionList + '<option value="' + $.trim( openingTimes[i] ) + '">' + $.trim( openingTimes[i] ) + '</option>';
        }

        if ( returnList ) {
            return optionList;
        } else {
            setSelectedOpeningHours( optionList, hrFormat );
        }
    }

    /**
     * Set the correct selected opening hour in the dropdown
     *
     * @since  2.0.0
     * @param  {string} optionList The html for the option list
     * @param  {string} hrFormat   The html for the option list
     * @return {void}
     */
    function setSelectedOpeningHours( optionList, hrFormat ) {
        var splitHour, hourType, periodBlock,
            hours = {};

        /*
         * Loop over each open/close block and make sure the selected
         * value is still set as selected after changing the hr format.
         */
        $( ".wpsl-current-period" ).each( function() {
            periodBlock = $( this ),
                hours 		= {
                    "open": $( this ).find( ".wpsl-open-hour" ).val(),
                    "close": $( this ).find( ".wpsl-close-hour" ).val()
                };

            // Set the new hour format for both dropdowns.
            $( this ).find( "select" ).html( optionList ).promise().done( function() {

                // Select the correct start/end hours as selected.
                for ( var key in hours ) {
                    if ( hours.hasOwnProperty( key ) ) {

                        // Breakup the hour, so we can check the part before and after the : separately.
                        splitHour = hours[key].split( ":" );

                        if ( hrFormat == 12 ) {
                            hourType = "";

                            // Change the hours to a 12hr format and add the correct AM or PM.
                            if ( hours[key].charAt( 0 ) == 0 ) {
                                hours[key] = hours[key].substr( 1 );
                                hourType   = " AM";
                            } else if ( ( splitHour[0].length == 2 ) && ( splitHour[0] > 12 ) ) {
                                hours[key] = ( splitHour[0] - 12 ) + ":" + splitHour[1];
                                hourType   = " PM";
                            } else if ( splitHour[0] < 12 ) {
                                hours[key] = splitHour[0] + ":" + splitHour[1];
                                hourType   = " AM";
                            } else if ( splitHour[0] == 12 ) {
                                hours[key] = splitHour[0] + ":" + splitHour[1];
                                hourType   = " PM";
                            }

                            // Add either AM or PM behind the time.
                            if ( ( splitHour[1].indexOf( "PM" ) == -1 ) && ( splitHour[1].indexOf( "AM" ) == -1 ) ) {
                                hours[key] = hours[key] + hourType;
                            }

                        } else if ( hrFormat == 24 ) {

                            // Change the hours to a 24hr format and remove the AM or PM.
                            if ( splitHour[1].indexOf( "PM" ) != -1 ) {
                                if ( splitHour[0] == 12 ) {
                                    hours[key] = "12:" + splitHour[1].replace( " PM", "" );
                                } else {
                                    hours[key] = ( + splitHour[0] + 12 ) + ":" + splitHour[1].replace( " PM", "" );
                                }
                            } else if ( splitHour[1].indexOf( "AM" ) != -1 ) {
                                if ( splitHour[0].toString().length == 1 ) {
                                    hours[key] = "0" + splitHour[0] + ":" + splitHour[1].replace( " AM", "" );
                                } else {
                                    hours[key] = splitHour[0] + ":" + splitHour[1].replace( " AM", "" );
                                }
                            } else {
                                hours[key] = splitHour[0] + ":" + splitHour[1]; // When the interval is changed
                            }
                        }

                        // Set the correct value as the selected one.
                        periodBlock.find( ".wpsl-" + key + "-hour option[value='" + $.trim( hours[key] ) + "']" ).attr( "selected", "selected" );
                    }
                }

            });
        });
    }

    // Update the opening hours format if one of the dropdown values change.
    $( "#wpsl-editor-hour-format, #wpsl-editor-hour-interval" ).on( "change", function() {
        createHourOptionList();
    });

    // Show the tooltips.
    $( ".wpsl-info" ).on( "mouseover", function() {
        $( this ).find( ".wpsl-info-text" ).show();
    });

    $( ".wpsl-info" ).on( "mouseout", function() {
        $( this ).find( ".wpsl-info-text" ).hide();
    });

    // If the start location is empty, then we color the info icon red instead of black.
    if ( $( "#wpsl-latlng" ).length && !$( "#wpsl-latlng" ).val() ) {
        $( "#wpsl-latlng" ).siblings( "label" ).find( ".wpsl-info" ).addClass( "wpsl-required-setting" );
    }

    /**
     * Try to apply the custom style data to the map.
     *
     * If the style data is invalid json we show an error.
     *
     * @since  2.0.0
     * @return {void}
     */
    function tryCustomMapStyle() {
        var validStyle = "",
            mapStyle   = $.trim( $( "#wpsl-map-style" ).val() );

        $( ".wpsl-style-preview-error" ).remove();

        if ( mapStyle ) {

            // Make sure the data is valid json.
            validStyle = tryParseJSON( mapStyle );

            if ( !validStyle ) {
                $( "#wpsl-style-preview" ).after( "<div class='wpsl-style-preview-error'>" + wpslL10n.styleError + "</div>" );
            }
        }

        map.setOptions({ styles: validStyle });
    }

    // Handle the map style changes on the settings page.
    if ( $( "#wpsl-map-style" ).val() ) {
        tryCustomMapStyle();
    }

    // Handle clicks on the map style preview button.
    $( "#wpsl-style-preview" ).on( "click", function() {
        tryCustomMapStyle();

        return false;
    });

    /**
     * Make sure the JSON is valid.
     *
     * @link   http://stackoverflow.com/a/20392392/1065294
     * @since  2.0.0
     * @param  {string} jsonString The JSON data
     * @return {object|boolean}	   The JSON string or false if it's invalid json.
     */
    function tryParseJSON( jsonString ) {

        try {
            var o = JSON.parse( jsonString );

            /*
             * Handle non-exception-throwing cases:
             * Neither JSON.parse(false) or JSON.parse(1234) throw errors, hence the type-checking,
             * but... JSON.parse(null) returns 'null', and typeof null === "object",
             * so we must check for that, too.
             */
            if ( o && typeof o === "object" && o !== null ) {
                return o;
            }
        }
        catch ( e ) { }

        return false;
    }

    // Make sure the custom error notices can be removed
    $( "#wpsl-wrap" ).on( "click", "button.notice-dismiss", function() {
        $( this ).closest( 'div.notice' ).remove();
    });

    /**
     * Handle the red warning that's shown next to the
     * force zipcode search option if the autocomplete
     * value is changed.
     *
     * The autocomplete option itself doesn't support
     * zip only searches, so having both of them enabled
     * gives the user the wrong expectation.
     */
    $( "#wpsl-search-autocomplete, #wpsl-force-postalcode" ).change( function() {
        var $info = $( "#wpsl-force-postalcode" ).parent( "p" ).find( ".wpsl-info-zip-only" );

        if ( $( "#wpsl-search-autocomplete" ).is( ":checked" ) && $( "#wpsl-force-postalcode" ).is( ":checked" ) ) {
            $info.show();
        } else {
            $info.hide();
        }
    });

    $( "#wpsl-delay-loading" ).change( function() {
        if ( $( this ).is( ":checked" ) ) {
            $( this ).parent( "p" ).find( ".wpsl-info" ).trigger( "mouseover" );
        } else {
            $( this ).parent( "p" ).find( ".wpsl-info" ).trigger( "mouseout" );
        }
    });

    $( "#wpsl-wrap" ).on( "click", function( e ) {
        $( ".wpsl-info-text" ).hide();
    });
});