;(function ($, window, document, undefined) {

    "use strict";

    // Create the defaults once
    var pluginName = "dateDropdowns",
        pluginDefaults = {
            defaultDate: null,
            defaultDateFormat: "yyyy-mm-dd",
            displayFormat: "dmy",
            submitFormat: "yyyy-mm-dd",
            minAge: 0,
            maxAge: 120,
            minYear: null,
            maxYear: null,
            submitFieldName: "date",
            wrapperClass: "date-dropdowns",
            dropdownClass: null,
            daySuffixes: true,
            monthSuffixes: true,
            monthFormat: "long",
            required: false
        };

    // The actual plugin constructor
    function Plugin (element, options) {
        this.element = element;                                 // Element handle
        this.$element = $(element);                             // jQuery element handle
        this.config = $.extend({}, pluginDefaults, options);    // Plugin options
        this.internals = {                                      // Internal variables
            objectRefs: {}
        };
        this._defaults = pluginDefaults;                        // Reference to the plugin defaults
        this._name = pluginName;                                // Reference to the plugin name
        this.init();

        return this;
    }

	Plugin.message = {
		day: "Day",
		month: "Month",
		year: "Year"
	};

    // Avoid Plugin.prototype conflicts
    $.extend(Plugin.prototype, {

        /**
         * Initialise the plugin
         */
        init: function () {
            this.checkForDuplicateElement();
            this.setInternalVariables();
            this.setupMarkup();
            this.buildDropdowns();
            this.attachDropdowns();
            this.bindChangeEvent();

            if (this.config.defaultDate) {
                this.populateDefaultDate();
            }
        },

        /**
         * Check whether an element exists with the same name attribute. If so, throw an error
         */
        checkForDuplicateElement: function() {
            if ($("input[name=\"" + this.config.submitFieldName + "\"]").length) {
                $.error("Duplicate element found");
                return false;
            }

            return true;
        },

        /**
         * Set the plugin"s internal variables
         */
        setInternalVariables: function() {
            var date = new Date();
            this.internals.currentDay = date.getDate();
            this.internals.currentMonth = date.getMonth() + 1;
            this.internals.currentYear = date.getFullYear();
            this.internals.monthShort = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
            this.internals.monthLong = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
        },

        /**
         * Set the container which will hold the date dropdowns.
         *
         * - If the element on which the plugin was called is an input of type text or hidden, set it to
         *   be hidden and wrap it in a div. This outer div will become the container.
         * - If the element was an HTML element (e.g. <div/>), create a hidden text field within it, and use
         *   the div as the container.
         */
        setupMarkup: function() {
            var wrapper, hiddenField;

            if (this.element.tagName.toLowerCase() === "input") {
	            if (!this.config.defaultDate) {
		            this.config.defaultDate = this.element.value;
	            }
                
                // Configure the input element and wrap
                hiddenField = this.$element.attr("type", "hidden")
                    .attr("name", this.config.submitFieldName)
                    .wrap("<div class=\"" + this.config.wrapperClass + "\"></div>");

                wrapper = this.$element.parent();

            } else {

                // Build a hidden input and set this.$element as the wrapper
                hiddenField = $("<input/>", {
                    type: "hidden",
                    name: this.config.submitFieldName
                });

                this.$element.append(hiddenField).addClass(this.config.wrapperClass);

                wrapper = this.$element;
            }

            // Store a reference to the wrapper and hidden field elements for later use
            this.internals.objectRefs.pluginWrapper = wrapper;
            this.internals.objectRefs.hiddenField = hiddenField;

            return true;
        },

        /**
         * Generate the Day, Month and Year dropdowns
         */
        buildDropdowns: function() {
            var $dayDropdown, $monthDropdown, $yearDropdown;

            // Build the day dropdown element
            $dayDropdown = this.buildDayDropdown();
            this.internals.objectRefs.dayDropdown = $dayDropdown;

            $monthDropdown = this.buildMonthDropdown();
            this.internals.objectRefs.monthDropdown = $monthDropdown;

            $yearDropdown = this.buildYearDropdown();
            this.internals.objectRefs.yearDropdown = $yearDropdown;

            return true;

            //this.internals.objectRefs.pluginWrapper.append($dayDropdown)
            //		 .append($monthDropdown)
            //		 .append($yearDropdown);
        },

        /**
         * Attach the generated dropdowns to the container
         */
        attachDropdowns: function() {
            var $element = this.internals.objectRefs.pluginWrapper,
                $daySelect = this.internals.objectRefs.dayDropdown,
                $monthSelect = this.internals.objectRefs.monthDropdown,
                $yearSelect = this.internals.objectRefs.yearDropdown;

            switch (this.config.displayFormat) {
                case "mdy":
                    $element.append($monthSelect, $daySelect, $yearSelect);
                    break;
                case "ymd":
                    $element.append($yearSelect, $monthSelect, $daySelect);
                    break;
                case "dmy":
                default:
                    $element.append($daySelect, $monthSelect, $yearSelect);
                    break;
            }

            return true;
        },

        /**
         * Bind the change event to the generated dropdowns
         */
        bindChangeEvent: function() {
            var $daySelect = this.internals.objectRefs.dayDropdown,
                $monthSelect = this.internals.objectRefs.monthDropdown,
                $yearSelect = this.internals.objectRefs.yearDropdown,
                pluginHandle = this,
                objectRefs = this.internals.objectRefs;

            objectRefs.pluginWrapper.on("change", "select", function() {
                var day = $daySelect.val(),
                    month = $monthSelect.val(),
                    year = $yearSelect.val(),
                    invalidDate = true,
                    newDate;

                // Find out whether the change has made the date invalid (e.g. 31st Feb)
                invalidDate = pluginHandle.checkDate(day, month, year);

                // If invalid - add an error class to the day dropdown and return
                if (invalidDate) {
                    objectRefs.dayDropdown.addClass("invalid");
                    return false;
                }

                if ("00" !== objectRefs.dayDropdown.val()) {
                    objectRefs.dayDropdown.removeClass("invalid");
                }

                // Empty the hidden field after each change
                objectRefs.hiddenField.val("");

                // Only format the submit date if a full date has been selected
                if (!invalidDate && (day * month * year !== 0)) {
                    newDate = pluginHandle.formatSubmitDate(day, month, year);

                    objectRefs.hiddenField.val(newDate);
                }

	            objectRefs.hiddenField.change();
            });
        },

        /**
         * Take a provided default date and populate both the hidden field and the
         * dropdown elements with the relevant formatted values
         *
         * @returns {boolean}
         */
        populateDefaultDate: function() {
            var date    = this.config.defaultDate,
                parts   = [],
                day     = "",
                month   = "",
                year    = "";

            switch (this.config.defaultDateFormat) {
                case "yyyy-mm-dd":
                default:
                    parts = date.split("-");
                    day = parts[2];
                    month = parts[1];
                    year = parts[0];
                    break;

                case "dd/mm/yyyy":
                    parts = date.split("/");
                    day = parts[0];
                    month = parts[1];
                    year = parts[2];
                    break;

                case "mm/dd/yyyy":
                    parts = date.split("/");
                    day = parts[1];
                    month = parts[0];
                    year = parts[2];
                    break;

	            case "unix":
		            parts = new Date();
		            parts.setTime(date * 1000);
		            day = parts.getDate() + "";
		            month = (parts.getMonth() + 1) + "";
		            year = parts.getFullYear();

		            if (day.length < 2) {
			            day = "0" + day;
		            }
		            if (month.length < 2) {
			            month = "0" + month;
		            }
		            break;
            }

            // Set the values on the dropdowns
            this.internals.objectRefs.dayDropdown.val(day);
            this.internals.objectRefs.monthDropdown.val(month);
            this.internals.objectRefs.yearDropdown.val(year);
            this.internals.objectRefs.hiddenField.val(date);

            if (true === this.checkDate(day, month, year)) {
                this.internals.objectRefs.dayDropdown.addClass("invalid");
            }

            return true;
        },

        /**
         * Build a generic dropdown element
         *
         * @param type
         * @returns {*|HTMLElement}
         */
        buildBaseDropdown: function(type) {
            var classString = type;

            if (this.config.dropdownClass) {
                classString += " " + this.config.dropdownClass;
            }

            return $("<select></select>", {
                class: classString,
                name: this.config.submitFieldName + "_[" + type + "]",
                required: this.config.required
            });
        },

        /**
         * Build the day dropdown element
         *
         * @returns {*|HTMLElement}
         */
        buildDayDropdown: function() {
            var day,
                dropdown = this.buildBaseDropdown("day"),
	            option = document.createElement("option");

	        option.setAttribute("value", "");
	        option.appendChild(document.createTextNode(Plugin.message.day));
	        dropdown.append(option);

            // Days 1-9
            for (var i = 1;  i < 10; i++) {
                if (this.config.daySuffixes) {
                    day = i + this.getSuffix(i);
                } else {
                    day = "0" + i;
                }
	            option = document.createElement("option");
	            option.setAttribute("value", "0" + i);
	            option.appendChild(document.createTextNode(day));
	            dropdown.append(option);
            }

            // Days 10-31
            for (var j = 10;  j <= 31; j++) {
                day = j;

                if (this.config.daySuffixes) {
                    day = j + this.getSuffix(j);
                }
	            option = document.createElement("option");
	            option.setAttribute("value", j);
	            option.appendChild(document.createTextNode(day));
	            dropdown.append(option);
            }

            return dropdown;
        },

        /**
         * Build the month dropdown element
         *
         * @returns {*|HTMLElement}
         */
        buildMonthDropdown: function() {
            var dropdown = this.buildBaseDropdown("month"),
	            option = document.createElement("option");

	        option.setAttribute("value", "");
	        option.appendChild(document.createTextNode(Plugin.message.month));
            dropdown.append(option);

            // Populate the month values
            for (var monthNo = 1; monthNo <= 12; monthNo++) {

                var month;

                switch (this.config.monthFormat) {
                    case "short":
                        month = this.internals.monthShort[monthNo - 1];
                        break;
                    case "long":
                        month = this.internals.monthLong[monthNo - 1];
                        break;
                    case "numeric":
                        month = monthNo;

                        if (this.config.monthSuffixes) {
                            month += this.getSuffix(monthNo);
                        }
                        break;
                }

                if (monthNo < 10) {
                    monthNo = "0" + monthNo;
                }

	            option = document.createElement("option");
	            option.setAttribute("value", monthNo);
	            option.appendChild(document.createTextNode(month));
	            dropdown.append(option);
            }

            return dropdown;
        },

        /**
         * Build the year dropdown element.
         *
         * By default minYear and maxYear are null, however if provided they take precedence over
         * the minAge and maxAge values.
         *
         * @returns {*|HTMLElement}
         */
        buildYearDropdown: function() {
            var minYear = this.config.minYear,
                maxYear = this.config.maxYear,
                dropdown = this.buildBaseDropdown("year"),
	            option = document.createElement("option");

	        option.setAttribute("value", "");
	        option.appendChild(document.createTextNode(Plugin.message.year));
	        dropdown.append(option);

            if (!minYear) {
                minYear = this.internals.currentYear - (this.config.maxAge + 1);
            }

            if (!maxYear) {
                maxYear = this.internals.currentYear - this.config.minAge;
            }

            for (var i = maxYear; i >= minYear; i--) {
	            option = document.createElement("option");
	            option.setAttribute("value", i);
	            option.appendChild(document.createTextNode(i));
	            dropdown.append(option);
            }

            return dropdown;
        },

        /**
         * Get the relevant suffix for a day/month number
         *
         * @param number
         * @returns {string}
         */
        getSuffix: function(number) {
            var suffix = "";

            switch (number % 10){
                case 1:
                    suffix = (number % 100 === 11) ? "th" : "st";
                    break;
                case 2:
                    suffix = (number % 100 === 12) ? "th" : "nd";
                    break;
                case 3:
                    suffix = (number % 100 === 13) ? "th" : "rd";
                    break;
                default:
                    suffix = "th";
                    break;
            }

            return suffix;
        },

        /**
         * Check whether the date entered is invalid, e.g. 31st Feb
         *
         * @param day
         * @param month
         * @param year
         * @returns {boolean}
         */
        checkDate: function(day, month, year) {
            var invalidDate;

            if (month !== "00") {
                var numDaysInMonth = (new Date(year, month, 0).getDate()),
                    selectedDayInt = parseInt(day, 10);

                invalidDate = this.updateDayOptions(numDaysInMonth, selectedDayInt);

                // If the date is invalid, empty the hidden field to prevent invalid submissions
                if (invalidDate) {
                    this.internals.objectRefs.hiddenField.val("");
                }
            }

            return invalidDate;
        },

        /**
         * Update the day options available based on the month now selected
         *
         * @param numDaysInMonth
         * @param selectedDayInt
         * @returns {boolean}
         */
        updateDayOptions: function(numDaysInMonth, selectedDayInt) {
            var lastDayOption = parseInt(this.internals.objectRefs.dayDropdown.children(":last").val(), 10),
                newDayOption = "",
                newDayText = "",
                invalidDay = false;

            // If the selected month has less days than the Day dropdown currently contains - remove the extra days
            if (lastDayOption > numDaysInMonth) {

                while (lastDayOption > numDaysInMonth) {
                    this.internals.objectRefs.dayDropdown.children(":last").remove();
                    lastDayOption--;
                }

                // If the user-selected day is removed, indicate this so a message can be shown to the user
                if (selectedDayInt > numDaysInMonth) {
                    invalidDay = true;
                }

                // If the month contains more days than the Day dropdown contains - add the missing options
            } else if (lastDayOption < numDaysInMonth) {

                while (lastDayOption < numDaysInMonth) {

                    newDayOption = ++lastDayOption;
                    newDayText = newDayOption;

                    // Add the suffix if required
                    if (this.config.daySuffixes) {
                        newDayText += this.getSuffix(lastDayOption);
                    }

                    // Build the option and append to the dropdown
	                var option = document.createElement("option");
	                option.setAttribute("value", newDayOption);
	                option.appendChild(document.createTextNode(newDayText));
	                this.internals.objectRefs.dayDropdown.append(option);
                }
            }

            return invalidDay;
        },

        /**
         * Format the selected date based on the submitFormat config value provided
         *
         * @param day
         * @param month
         * @param year
         * @returns {*}
         */
        formatSubmitDate: function(day, month, year) {
            var formattedDate,
	            _date;

            switch (this.config.submitFormat) {
                case "yyyy-mm-dd":
                default:
                    formattedDate = year + "-" + month + "-" + day;
                    break;

                case "mm/dd/yyyy":
                    formattedDate = month + "/" + day + "/" + year;
                    break;

                case "dd/mm/yyyy":
                    formattedDate = day + "/" + month + "/" + year;
                    break;

	            case "unix":
		            _date = new Date();
		            _date.setDate(day);
		            _date.setMonth(month - 1);
		            _date.setYear(year);
		            formattedDate = Math.round(_date.getTime() / 1000);
            }

            return formattedDate;
        },

        /**
         * Revert the changes applied by the plugin on the specified element
         */
        destroy: function() {
            var wrapperClass = this.config.wrapperClass;

            if (this.$element.hasClass(wrapperClass)) {
                this.$element.empty();
            } else {
                var $parent = this.$element.parent(),
                    $select = $parent.find("select");

                this.$element.unwrap();
                $select.remove();
            }
        }
    });

    // A really lightweight plugin wrapper around the constructor,
    // preventing against multiple instantiations
    $.fn[ pluginName ] = function ( options ) {
        this.each(function() {
            if(typeof options === "string") {
                var args = Array.prototype.slice.call(arguments, 1);
                var plugin = $.data(this, "plugin_" + pluginName);

                if (typeof plugin === "undefined") {
                    $.error("Please initialize the plugin before calling this method.");
                    return false;
                }
                plugin[options].apply(plugin, args);
            } else {
                if ( !$.data( this, "plugin_" + pluginName ) ) {
                    $.data( this, "plugin_" + pluginName, new Plugin( this, options ) );
                }
            }
        });
        // chain jQuery functions
        return this;
    };

})( jQuery, window, document );
