(function ($) {
    "use strict";

    // The basic svg string required to generate stars
    var BASICSTAR = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
        "<svg version=\"1.1\"" +
        "xmlns=\"http://www.w3.org/2000/svg\"" +
        "viewBox=\"0 0 576 512\"" +
        "x=\"0px\" y=\"0px\"" +
        "xml:space=\"preserve\">" +
        "<path " +
        "d=\"M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z\">" +
        "</path>" +
        "</svg>";

    // The Default values of different options available in the Plugin
    var DEFAULTS = {

        starWidth: "32px",
        normalFill: "#cccccc",
        ratedFill: "#FFD700",
        numStars: 5,
        maxValue: 5,
        precision: 1,
        rating: 0,
        fullStar: false,
        halfStar: false,
        readOnly: false,
        spacing: "0px",
        rtl: false,
        multiColor: null,
        onInit: null,
        onChange: null,
        onSet: null,
        starSvg: null
    };

    //Default colors for multi-color rating
    var MULTICOLOR_OPTIONS = {

        startColor: "#c0392b", //red
        endColor: "#f1c40f"  //yellow
    };

    // http://stackoverflow.com/questions/11381673/detecting-a-mobile-browser
    function isMobileBrowser() {
        var check = false;
        /* jshint ignore:start */
        (function (a) {
            if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) check = true
        })(navigator.userAgent || navigator.vendor || window.opera);
        /* jshint ignore:end */
        return check;
    }

    function checkPrecision(value, minValue, maxValue) {

        /*
         * This function removes the unnecessary precision, at Min and Max Values
         */

        // Its like comparing 0.0 with 0, which is true
        if (value === minValue) {

            value = minValue;
        } else if (value === maxValue) {

            value = maxValue;
        }

        return value;
    }

    function checkBounds(value, minValue, maxValue) {

        /*
         * Check if the value is between min and max values, if not, throw an error
         */

        var isValid = value >= minValue && value <= maxValue;

        if (!isValid) {

            throw Error("Invalid Rating, expected value between " + minValue +
                " and " + maxValue);
        }

        return value;
    }

    function isDefined(value) {

        // Better way to check if a variable is defined or not
        return typeof value !== "undefined";
    }

    // Regex to match Colors in Hex Format like #FF00FF
    var hexRegex = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i;

    var hexToRGB = function (hex) {

        /*
         * Extracts and returns the Red, Blue, Green Channel values,
         * in the form of decimals
         */

        if (!hexRegex.test(hex)) {

            return null;
        }

        var hexValues = hexRegex.exec(hex),
            r = parseInt(hexValues[1], 16),
            g = parseInt(hexValues[2], 16),
            b = parseInt(hexValues[3], 16);

        return {r: r, g: g, b: b};
    };

    function getChannelValue(startVal, endVal, percent) {

        /*
         * Returns a value between `startVal` and `endVal` based on the percent
         */

        var newVal = (endVal - startVal) * (percent / 100);

        newVal = Math.round(startVal + newVal).toString(16);

        if (newVal.length === 1) {

            newVal = "0" + newVal;
        }

        return newVal;
    }

    function getColor(startColor, endColor, percent) {

        /*
         * Given the percentage( `percent` ) of `endColor` to be mixed
         * with the `startColor`, returns the mixed color.
         * Colors should be only in Hex Format
         */

        if (!startColor || !endColor) {

            return null;
        }

        percent = isDefined(percent) ? percent : 0;

        startColor = hexToRGB(startColor);
        endColor = hexToRGB(endColor);

        var r = getChannelValue(startColor.r, endColor.r, percent),
            b = getChannelValue(startColor.b, endColor.b, percent),
            g = getChannelValue(startColor.g, endColor.g, percent);

        return "#" + r + g + b;
    }

    function RateYo($node, options) {

        /*
         * The Contructor, whose instances are used by plugin itself
         */

        // Storing the HTML element as a property, for future access
        this.node = $node.get(0);

        var that = this;

        // Remove any stuff that is present inside the container, and add the plugin class
        $node.empty().addClass("jq-ry-container");

        /*
         * Basically the plugin displays the rating using two rows of stars lying one above
         * the other, the row that is on the top represents the actual rating, and the one
         * behind acts just like a background.
         *
         * `$groupWrapper`: is an element that wraps both the rows
         * `$normalGroup`: is the container for row of stars thats behind and
         *                 acts as background
         * `$ratedGroup`: is the container for row of stars that display the actual rating.
         *
         * The rating is displayed by adjusting the width of `$ratedGroup`
         */
        var $groupWrapper = $("<div/>").addClass("jq-ry-group-wrapper")
            .appendTo($node);

        var $normalGroup = $("<div/>").addClass("jq-ry-normal-group")
            .addClass("jq-ry-group")
            .appendTo($groupWrapper);

        var $ratedGroup = $("<div/>").addClass("jq-ry-rated-group")
            .addClass("jq-ry-group")
            .appendTo($groupWrapper);

        /*
         * Variable `step`: store the value of the rating for each star
         *                  eg: if `maxValue` is 5 and `numStars` is 5, value of each star
         *                      is 1.
         * Variable `starWidth`: stores the decimal value of width of star in units of px
         * Variable `percentOfStar`: stores the percentage of width each star takes w.r.t
         *                           the container
         * Variable `spacing`: stores the decimal value of the spacing between stars
         *                     in the units of px
         * Variable `percentOfSpacing`: stores the percentage of width of the spacing
         *                              between stars w.r.t the container
         */
        var step, starWidth, percentOfStar, spacing,
            percentOfSpacing, containerWidth, minValue = 0;

        /*
         * `currentRating` contains rating that is being displayed at the latest point of
         * time.
         *
         * When ever you hover over the plugin UI, the rating value changes
         * according to the place where you point the cursor, currentRating contains
         * the current value of rating that is being shown in the UI
         */
        var currentRating = options.rating;

        // A flag to store if the plugin is already being displayed in the UI
        var isInitialized = false;

        function showRating(ratingVal) {

            /*
             * The function is responsible for displaying the rating by changing
             * the width of `$ratedGroup`
             */

            if (!isDefined(ratingVal)) {

                ratingVal = options.rating;
            }

            // Storing the value that is being shown in `currentRating`.
            currentRating = ratingVal;

            var numStarsToShow = ratingVal / step;

            // calculating the percentage of width of $ratedGroup with respect to its parent
            var percent = numStarsToShow * percentOfStar;

            if (numStarsToShow > 1) {

                // adding the percentage of space that is taken by the gap the stars
                percent += (Math.ceil(numStarsToShow) - 1) * percentOfSpacing;
            }

            setRatedFill(options.ratedFill);

            percent = options.rtl ? 100 - percent : percent;

            if (percent < 0) {

                percent = 0;
            } else if (percent > 100) {

                percent = 100;
            }

            $ratedGroup.css("width", percent + "%");
        }

        function setContainerWidth() {

            /*
             * Set the width of the `this.node` based on the width of each star and
             * the space between them
             */

            containerWidth = starWidth * options.numStars + spacing * (options.numStars - 1);

            percentOfStar = (starWidth / containerWidth) * 100;

            percentOfSpacing = (spacing / containerWidth) * 100;

            $node.width(containerWidth);

            showRating();
        }

        function setStarWidth(newWidth) {

            /*
             * Set the width and height of each SVG star, called whenever one changes the
             * `starWidth` option
             */

            // The width and height of the star should be the same
            var starHeight = options.starWidth = newWidth;

            starWidth = window.parseFloat(options.starWidth.replace("px", ""));

            $normalGroup.find("svg")
                .attr({
                    width: options.starWidth,
                    height: starHeight
                });

            $ratedGroup.find("svg")
                .attr({
                    width: options.starWidth,
                    height: starHeight
                });

            setContainerWidth();

            return $node;
        }

        function setSpacing(newSpacing) {

            /*
             * Set spacing between the SVG stars, called whenever one changes
             * the `spacing` option
             */

            options.spacing = newSpacing;

            spacing = parseFloat(options.spacing.replace("px", ""));

            $normalGroup.find("svg:not(:first-child)")
                .css({"margin-left": newSpacing});

            $ratedGroup.find("svg:not(:first-child)")
                .css({"margin-left": newSpacing});

            setContainerWidth();

            return $node;
        }

        function setNormalFill(newFill) {

            /*
             * Set the background fill of the Stars, called whenever one changes the
             * `normalFill` option
             */

            options.normalFill = newFill;

            var $svgs = (options.rtl ? $ratedGroup : $normalGroup).find("svg");

            $svgs.attr({fill: options.normalFill});

            return $node;
        }

        /*
         * Store the recent `ratedFill` option in a variable
         * so that if multiColor is unset, we can use the perviously set `ratedFill`
         * from this variable
         */
        var ratedFill = options.ratedFill;

        function setRatedFill(newFill) {

            /*
             * Set ratedFill of the stars, called when one changes the `ratedFill` option
             */

            /*
             * If `multiColor` option is set, `newFill` variable is dynamically set
             * based on the rating, what ever set as parameter will be discarded
             */
            if (options.multiColor) {

                var ratingDiff = currentRating - minValue,
                    percentCovered = (ratingDiff / options.maxValue) * 100;

                var colorOpts = options.multiColor || {},
                    startColor = colorOpts.startColor || MULTICOLOR_OPTIONS.startColor,
                    endColor = colorOpts.endColor || MULTICOLOR_OPTIONS.endColor;

                newFill = getColor(startColor, endColor, percentCovered);
            } else {

                ratedFill = newFill;
            }

            options.ratedFill = newFill;

            var $svgs = (options.rtl ? $normalGroup : $ratedGroup).find("svg");

            $svgs.attr({fill: options.ratedFill});

            return $node;
        }

        function setRtl(newValue) {

            newValue = !!newValue;

            options.rtl = newValue;

            setNormalFill(options.normalFill);
            showRating();
        }

        function setMultiColor(colorOptions) {

            /*
             * called whenever one changes the `multiColor` option
             */

            options.multiColor = colorOptions;

            // set the recently set `ratedFill` option, if multiColor Options are unset
            setRatedFill(colorOptions ? colorOptions : ratedFill);
        }

        function setNumStars(newValue) {

            /*
             * Set the number of stars to use to display the rating, called whenever one
             * changes the `numStars` option
             */

            options.numStars = newValue;

            step = options.maxValue / options.numStars;

            $normalGroup.empty();
            $ratedGroup.empty();

            for (var i = 0; i < options.numStars; i++) {

                $normalGroup.append($(options.starSvg || BASICSTAR));
                $ratedGroup.append($(options.starSvg || BASICSTAR));
            }

            setStarWidth(options.starWidth);
            setNormalFill(options.normalFill);
            setSpacing(options.spacing);

            showRating();

            return $node;
        }

        function setMaxValue(newValue) {

            /*
             * set the Maximum Value of rating to be allowed, called whenever
             * one changes the `maxValue` option
             */

            options.maxValue = newValue;

            step = options.maxValue / options.numStars;

            if (options.rating > newValue) {

                setRating(newValue);
            }

            showRating();

            return $node;
        }

        function setPrecision(newValue) {

            /*
             * Set the precision of the rating value, called if one changes the
             * `precision` option
             */

            options.precision = newValue;

            setRating(options.rating);

            return $node;
        }

        function setHalfStar(newValue) {

            /*
             * This function will be called if one changes the `halfStar` option
             */

            options.halfStar = newValue;

            return $node;
        }

        function setFullStar(newValue) {

            /*
             * This function will be called if one changes the `fullStar` option
             */

            options.fullStar = newValue;

            return $node;
        }

        function round(value) {

            /*
             * Rounds the value of rating if `halfStar` or `fullStar` options are chosen
             */

            var remainder = value % step,
                halfStep = step / 2,
                isHalfStar = options.halfStar,
                isFullStar = options.fullStar;

            if (!isFullStar && !isHalfStar) {

                return value;
            }

            if (isFullStar || (isHalfStar && remainder > halfStep)) {

                value += step - remainder;
            } else {

                value = value - remainder;

                if (remainder > 0) {

                    value += halfStep;
                }
            }

            return value;
        }

        function calculateRating(e) {

            /*
             * Calculates and returns the rating based on the position of cursor w.r.t the
             * plugin container
             */

            var position = $normalGroup.offset(),
                nodeStartX = position.left,
                nodeEndX = nodeStartX + $normalGroup.width();

            var maxValue = options.maxValue;

            // The x-coordinate(position) of the mouse pointer w.r.t page
            var pageX = e.pageX;

            var calculatedRating = 0;

            // If the mouse pointer is to the left of the container
            if (pageX < nodeStartX) {

                calculatedRating = minValue;
            } else if (pageX > nodeEndX) { // If the mouse pointer is right of the container

                calculatedRating = maxValue;
            } else { // If the mouse pointer is inside the continer

                /*
                 * The fraction of width covered by the pointer w.r.t to the total width
                 * of the container.
                 */
                var calcPrcnt = ((pageX - nodeStartX) / (nodeEndX - nodeStartX));

                if (spacing > 0) {

                    /*
                     * If there is spacing between stars, take the percentage of width covered
                     * and subtract the percentage of width covered by stars and spacing, to find
                     * how many stars are covered, the number of stars covered is the rating
                     *
                     * TODO: I strongly feel that this logic can be improved!, Please help!
                     */
                    calcPrcnt *= 100;

                    var remPrcnt = calcPrcnt;

                    while (remPrcnt > 0) {

                        if (remPrcnt > percentOfStar) {

                            calculatedRating += step;
                            remPrcnt -= (percentOfStar + percentOfSpacing);
                        } else {

                            calculatedRating += remPrcnt / percentOfStar * step;
                            remPrcnt = 0;
                        }
                    }
                } else {

                    /*
                     * If there is not spacing between stars, the fraction of width covered per
                     * `maxValue` is the rating
                     */
                    calculatedRating = calcPrcnt * (options.maxValue);
                }

                // Round the rating if `halfStar` or `fullStar` options are chosen
                calculatedRating = round(calculatedRating);
            }

            if (options.rtl) {

                calculatedRating = maxValue - calculatedRating;
            }

            return parseFloat(calculatedRating);
        }

        function setReadOnly(newValue) {

            /*
             * UnBinds mouse event handlers, called when whenever one changes the
             * `readOnly` option
             */

            options.readOnly = newValue;

            $node.attr("readonly", true);

            unbindEvents();

            if (!newValue) {

                $node.removeAttr("readonly");

                bindEvents();
            }

            return $node;
        }

        function setRating(newValue) {

            /*
             * Sets the rating of the Plugin, Called when option `rating` is changed
             * or, when `rating` method is called
             */

            var rating = newValue;

            var maxValue = options.maxValue;

            if (typeof rating === "string") {

                // If rating is given in percentage, maxValue should be 100
                if (rating[rating.length - 1] === "%") {

                    rating = rating.substr(0, rating.length - 1);
                    maxValue = 100;

                    setMaxValue(maxValue);
                }

                rating = parseFloat(rating);
            }

            checkBounds(rating, minValue, maxValue);

            rating = parseFloat(rating.toFixed(options.precision));

            checkPrecision(parseFloat(rating), minValue, maxValue);

            options.rating = rating;

            showRating();

            if (isInitialized) {

                $node.trigger("rateyo.set", {rating: rating});
            }

            return $node;
        }

        function setOnInit(method) {

            /*
             * set what method to be called on Initialization
             */

            options.onInit = method;

            return $node;
        }

        function setOnSet(method) {

            /*
             * set what method to be called when rating is set
             */

            options.onSet = method;

            return $node;
        }

        function setOnChange(method) {

            /*
             * set what method to be called rating in the UI is changed
             */

            options.onChange = method;

            return $node;
        }

        this.rating = function (newValue) {

            /*
             * rating getter/setter
             */

            if (!isDefined(newValue)) {

                return options.rating;
            }

            setRating(newValue);

            return $node;
        };

        this.destroy = function () {

            /*
             * Removes the Rating UI by clearing the content, and removing the custom classes
             */

            if (!options.readOnly) {

                unbindEvents();
            }

            RateYo.prototype.collection = deleteInstance($node.get(0),
                this.collection);

            $node.removeClass("jq-ry-container").children().remove();

            return $node;
        };

        this.method = function (methodName) {

            /*
             * Method to call the methods of RateYo Instance
             */

            if (!methodName) {

                throw Error("Method name not specified!");
            }

            if (!isDefined(this[methodName])) {

                throw Error("Method " + methodName + " doesn't exist!");
            }

            var args = Array.prototype.slice.apply(arguments, []),
                params = args.slice(1),
                method = this[methodName];

            return method.apply(this, params);
        };

        this.option = function (optionName, param) {

            /*
             * Method to get/set Options
             */

            if (!isDefined(optionName)) {

                return options;
            }

            var method;

            switch (optionName) {

                case "starWidth":

                    method = setStarWidth;
                    break;
                case "numStars":

                    method = setNumStars;
                    break;
                case "normalFill":

                    method = setNormalFill;
                    break;
                case "ratedFill":

                    method = setRatedFill;
                    break;
                case "multiColor":

                    method = setMultiColor;
                    break;
                case "maxValue":

                    method = setMaxValue;
                    break;
                case "precision":

                    method = setPrecision;
                    break;
                case "rating":

                    method = setRating;
                    break;
                case "halfStar":

                    method = setHalfStar;
                    break;
                case "fullStar":

                    method = setFullStar;
                    break;
                case "readOnly":

                    method = setReadOnly;
                    break;
                case "spacing":

                    method = setSpacing;
                    break;
                case "rtl":

                    method = setRtl;
                    break;
                case "onInit":

                    method = setOnInit;
                    break;
                case "onSet":

                    method = setOnSet;
                    break;
                case "onChange":

                    method = setOnChange;
                    break;
                default:

                    throw Error("No such option as " + optionName);
            }

            return isDefined(param) ? method(param) : options[optionName];
        };

        function onMouseEnter(e) {

            /*
             * If the Mouse Pointer is inside the container, calculate and show the rating
             * in UI
             */

            var rating = calculateRating(e).toFixed(options.precision);

            var maxValue = options.maxValue;

            rating = checkPrecision(parseFloat(rating), minValue, maxValue);

            showRating(rating);

            $node.trigger("rateyo.change", {rating: rating});
        }

        function onMouseLeave() {
            if (isMobileBrowser()) {
                return;
            }

            /*
             * If mouse leaves, revert the rating in UI to previously set rating,
             * when empty value is passed to showRating, it will take the previously set
             * rating
             */

            showRating();

            $node.trigger("rateyo.change", {rating: options.rating});
        }

        function onMouseClick(e) {

            /*
             * On clicking the mouse inside the container, calculate and set the rating
             */

            var resultantRating = calculateRating(e).toFixed(options.precision);
            resultantRating = parseFloat(resultantRating);

            that.rating(resultantRating);
        }

        function onInit(e, data) {

            if (options.onInit && typeof options.onInit === "function") {

                /* jshint validthis:true */
                options.onInit.apply(this, [data.rating, that]);
            }
        }

        function onChange(e, data) {

            if (options.onChange && typeof options.onChange === "function") {

                /* jshint validthis:true */
                options.onChange.apply(this, [data.rating, that]);
            }
        }

        function onSet(e, data) {

            if (options.onSet && typeof options.onSet === "function") {

                /* jshint validthis:true */
                options.onSet.apply(this, [data.rating, that]);
            }
        }

        function bindEvents() {

            $node.on("mousemove", onMouseEnter)
                .on("mouseenter", onMouseEnter)
                .on("mouseleave", onMouseLeave)
                .on("click", onMouseClick)
                .on("rateyo.init", onInit)
                .on("rateyo.change", onChange)
                .on("rateyo.set", onSet);
        }

        function unbindEvents() {

            $node.off("mousemove", onMouseEnter)
                .off("mouseenter", onMouseEnter)
                .off("mouseleave", onMouseLeave)
                .off("click", onMouseClick)
                .off("rateyo.init", onInit)
                .off("rateyo.change", onChange)
                .off("rateyo.set", onSet);
        }

        setNumStars(options.numStars);
        setReadOnly(options.readOnly);

        if (options.rtl) {

            setRtl(options.rtl);
        }

        this.collection.push(this);
        this.rating(options.rating, true);

        isInitialized = true;
        $node.trigger("rateyo.init", {rating: options.rating});
    }

    RateYo.prototype.collection = [];

    function getInstance(node, collection) {

        /*
         * Given a HTML element (node) and a collection of RateYo instances,
         * this function will search through the collection and return the matched
         * instance having the node
         */

        var instance;

        $.each(collection, function () {

            if (node === this.node) {

                instance = this;
                return false;
            }
        });

        return instance;
    }

    function deleteInstance(node, collection) {

        /*
         * Given a HTML element (node) and a collection of RateYo instances,
         * this function will search through the collection and delete the
         * instance having the node, and return the modified collection
         */

        $.each(collection, function (index) {

            if (node === this.node) {

                var firstPart = collection.slice(0, index),
                    secondPart = collection.slice(index + 1, collection.length);

                collection = firstPart.concat(secondPart);

                return false;
            }
        });

        return collection;
    }

    function _rateYo(options) {

        var rateYoInstances = RateYo.prototype.collection;

        /* jshint validthis:true */
        var $nodes = $(this);

        if ($nodes.length === 0) {

            return $nodes;
        }

        var args = Array.prototype.slice.apply(arguments, []);

        if (args.length === 0) {

            //If args length is 0, Initialize the UI with default settings
            options = args[0] = {};
        } else if (args.length === 1 && typeof args[0] === "object") {

            //If an Object is specified as first argument, it is considered as options
            options = args[0];
        } else if (args.length >= 1 && typeof args[0] === "string") {

            /*
             * if there is only one argument, and if its a string, it is supposed to be a
             * method name, if more than one argument is specified, the remaining arguments
             * except the first argument, will be passed as a params to the specified method
             */

            var methodName = args[0],
                params = args.slice(1);

            var result = [];

            $.each($nodes, function (i, node) {

                var existingInstance = getInstance(node, rateYoInstances);

                if (!existingInstance) {

                    throw Error("Trying to set options before even initialization");
                }

                var method = existingInstance[methodName];

                if (!method) {

                    throw Error("Method " + methodName + " does not exist!");
                }

                var returnVal = method.apply(existingInstance, params);

                result.push(returnVal);
            });

            /*
             * If the plugin in being called on only one jQuery Element, return only the
             * first value, to support chaining.
             */
            result = result.length === 1 ? result[0] : result;

            return result;
        } else {

            throw Error("Invalid Arguments");
        }

        /*
         * if only options are passed, extend default options, and if the plugin is not
         * initialized on a particular jQuery element, initalize RateYo on it
         */
        options = $.extend({}, DEFAULTS, options);

        return $.each($nodes, function () {

            var existingInstance = getInstance(this, rateYoInstances);

            if (existingInstance) {

                return existingInstance;
            }

            var $node = $(this),
                dataAttrs = {},
                optionsCopy = $.extend({}, options);

            $.each($node.data(), function (key, value) {

                if (key.indexOf("rateyo") !== 0) {

                    return;
                }

                var optionName = key.replace(/^rateyo/, "");

                optionName = optionName[0].toLowerCase() + optionName.slice(1);

                dataAttrs[optionName] = value;

                delete optionsCopy[optionName];
            });

            return new RateYo($(this), $.extend({}, dataAttrs, optionsCopy));
        });
    }

    function rateYo() {

        /* jshint validthis:true */
        return _rateYo.apply(this, Array.prototype.slice.apply(arguments, []));
    }

    window.RateYo = RateYo;
    $.fn.rateYo = rateYo;

}(window.jQuery));