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

    "use strict";

    // Create the defaults once
    var pluginName = "conditionals",
        defaults = {
            /**
             * This callback type is called `onInit` and offers the plugin caller a chance to run custom code on plugin initialization.
             *
             * @type Function
             * @callback onInit
             */
            onInit: function () {},

            /**
             * An array of Rule objects.
             *
             * @type Object[] e.g.
             * {
             *     name: 'field_name',
             *     triggers: ['field_name'],
             *     fields: [
             *         'field_name',
             *     ],
             *     conditions: [
             *         {
             *             checks: [
             *                 {
             *                     field: 'field_name',
             *                     op: '=',
             *                     value: 'Field Value'
             *                 }
             *             ],
             *             show: [
             *                 'field_name',
             *             ],
             *             required: [
             *                 'field_name',
             *             ],
             *             onHandle: function (condition) {}
             *         }
             *     ]
             * }
             */
            rules: []
        };

    /**
     * The actual plugin constructor.
     *
     * @param {Node} element
     * @param {Object} options
     */
    function Plugin(element, options) {
        this.element = element;
        this.settings = $.extend({}, defaults, options);
        this._defaults = defaults;
        this._name = pluginName;
        this.init();
    }

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

        /**
         * Initialize the rules and trigger necessary changes on load.
         */
        init: function () {
            for (var i = 0; i < this.settings.rules.length; i++) {
                this.initRule(this.settings.rules[i]);
            }

            for (var j = 0; j < this.settings.rules.length; j++) {
                this.triggerRule(null, this.settings.rules[j]);
            }

            this.settings.onInit.apply(this);
        },

        /**
         * Initialize a individual rule and register necessary onChange functionality.
         *
         * @param {Object} rule
         */
        initRule: function (rule) {
            var conditionals = this;
            for (var i = 0; i < rule.triggers.length; i++) {
                var node = $('#' + rule.triggers[i]);
                if (node) {
                    node.on('change', function (e) {
                        conditionals.triggerRule(e, rule);
                    });
                }
            }
        },

        /**
         * Trigger an individual rule.
         *
         * @param {Object} event
         * @param {Object} rule
         */
        triggerRule: function (event, rule) {
            var shownFields = [];

            for (var i = 0; i < rule.conditions.length; i++) {
                var fields = this.handleCondition(rule.conditions[i]);
                shownFields = shownFields.concat(fields);
            }

            if (rule.fields.length > 0) {
                // hide/empty all fields not shown
                for (var j = 0; j < rule.fields.length; j++) {
                    if (shownFields.indexOf(rule.fields[j]) === -1) {
                        this.hideField(rule.fields[j], true);
                    }
                }
            }
        },

        /**
         * Handle an individual condition.
         * 
         * @param {Object} condition
         * @return Array
         */
        handleCondition: function (condition) {
            var isMet = true;

            for (var i = 0; i < condition.checks.length; i++) {
                isMet = this.handleCheck(condition.checks[i]) && isMet;
            }

            if (! isMet) {
                return [];
            }

            // show fields
            if (condition.show && condition.show.length > 0) {
                for (var j = 0; j < condition.show.length; j++) {
                    this.showField(condition.show[j]);
                }
            }

            // required fields
            if (condition.required && condition.required.length > 0) {
                for (var k = 0; k < condition.required.length; k++) {
                    this.requireField(condition.required[k]);
                }
            }

            // custom handle
            if (condition.onHandle) {
                condition.onHandle.apply(this, [condition]);
            }

            return condition.show;
        },

        /**
         * Handle an individual check.
         *
         * @param {Object} check
         * @return Boolean
         */
        handleCheck: function (check) {
            var node = $('#' + check.field);
            var value = null;

            if (! node) {
                return false;
            }

            // In display only mode, you need to find the input inside the given div node
            if (node.hasClass('displayOnly')) {
                value = $('input', node).val();
            } else {
                value = node.val();
            }

            // TODO: Support more operation types
            if ('=' !== check.op) {
                throw "Unrecognized op: " + check.op;
            }

            if (typeof check.value === "undefined") {
                throw "Condition based on value is the only option currently implemented.";
            }

            return (check.value == value);
        },

        /**
         * Show an individual field.
         *
         * @param {String} name
         */
        showField: function (name) {
            var n = $('#' + name);
            if (n) {
                n.show();
                // get 2 divs up and show row
                n.closest('.form-group').show();
            }
        },

        /**
         * Set an individual field as required.
         *
         * @param {String} name
         */
        requireField: function (name) {
            var n = $('#' + name);
            if (n) {
                n.prop('required', true);
            }
        },

        /**Set an individual field as required.
         *
         * @param {String} name
         * @param {Boolean=} isEmpty
         */
        hideField: function (name, isEmpty) {
            var n = $('#' + name);
            if (n) {
                n.hide();

                // in display only mode, you need to find the input inside the given div node
                if (n.hasClass('displayOnly')) {
                    // get 1 divs up and hide row
                    n.closest('.form-group').hide();
                } else {
                    n.prop('required', false);
                    // get 2 divs up and hide row
                    n.closest('.form-group').hide();
                    if (isEmpty) {
                        n.val('');
                    }
                }
            }
        },

        /**
         * Replace the options within a select2 component.
         *
         * @param {Node} node
         * @param {Array} options
         */
        replaceSelect2Options: function (node, options) {
            // if is displayOnly with input instead of select2
            // then no need to re-do the options,
            // (which would convert it into select2 from displayOnly)
            if (node.hasClass('displayOnly')) {
                return;
            }

            var oldValue = node.val();
            var data     = [];

            node.select2().empty();
            node.val(null).trigger('change');

            // data.push({id: '', text: '--- select ---' });
            for (var i in options) {
                if (options.hasOwnProperty(i)) {
                    data.push({id: i, text: options[i] });
                }
            }

            // Re-create the select2 component
            node.select2({
                data: data,         // Add new options
                theme: 'bootstrap'  // Add bootstrap class back
            });

            // set previous value again if available
            var found = false;
            $.each(options, function (key, label) {
                if (key === oldValue) {
                    found = true;
                }
            });

            if (found) {
                node.val(oldValue);
            }
        }
    });

    /**
     * A really lightweight plugin wrapper around the constructor,
     * preventing against multiple instantiations.
     *
     * @param {Object} options
     */
    $.fn[pluginName] = function (options) {
        return this.each(function () {
            if (!$.data(this, "plugin_" + pluginName)) {
                $.data(this, "plugin_" + pluginName, new Plugin(this, options));
            }
        });
    };

})(jQuery, window, document);
