﻿(function($){
    "use strict";
    
    var root = window;
    
    // keep index of form tracker instances
    var trackerIndex = {};


    // namespace
    var intouch = window.intouch || (window.intouch = {});

    // util
    var slice = Array.prototype.slice;

    // FormTracker
    var FormTracker = intouch.FormTracker = function(name){
        if (trackerIndex[name] && trackerIndex[name] !== this) {
            throw new Error("Cannot instantiate multiple FormTracker's with the same names.");
        }

        trackerIndex[name] = this;
        this.initialize.apply(this, arguments);
        return this;
    };

    // default options
    FormTracker.defaults = {
        cookieOptions: {
            expires: "", // session cookie
            path: "/"
        }
    };

    $.extend(FormTracker.prototype, {
        
        initialize: function(name, options) {
            this.name = name;
            this.historyName = "formtracker_" + name;
            this.options = $.extend({}, FormTracker.defaults, options || {});
            
            // load default data from cookie
            var history = $.cookie(this.historyName);
            if (history !== null) {
                try {
                    history = JSON.parse(history);
                }
                catch(e) {
                    history = {};
                }
            }
            else {
                history = {};
            }
            
            this.history = history;            
            this.onTrackedChange = $.proxy(this.onTrackedChange, this);
            return this;
        },

        trackField: function() {                        
            return this.trackItem.apply(this, [intouch.FormTrackerField].concat(slice.call(arguments)));
        },

        trackGroup: function() {
            return this.trackItem.apply(this, [intouch.FormTrackerGroup].concat(slice.call(arguments)));
        },

        trackItem: function(tracktype, selector, handler, options) {            
            var tracked = new tracktype(this, selector, handler, $.extend({
                hasTracked: this.getTrackedState(selector)                
            }, options || {}));

            $(tracked).bind("track", this.onTrackedChange);

            return tracked;        
        },

        getTrackedState: function(selector) {            
            return !!(this.history[selector]);
        },

        setTrackedState: function(selector, value) {
            this.history[selector] = 1;
            this.save();
            return this;
        },

        onTrackedChange: function(event, tracked) {            
            this.setTrackedState(tracked.selector);
        },

        save: function() {
            $.cookie(this.historyName, JSON.stringify(this.history), this.options.cookieOptions);
            return this;
        }
    
    });

    // maps the event type for each input type
    var eventTypeMap = {
        "radio": "click",
        "checkbox": "click",
        "text": "blur",
        "select": "change",
        "textarea": "blur",
        "div": "click"
    };

    // base for tracked types
    var TrackedBase = {
        defaults: {
            alwaysTrack: false,
            hasTracked: false
        },
        
        tagType: null,
        eventType: null,

        initialize: function(manager, selector, handler, options) {            
            this.manager = manager;
            this.selector = selector;
            this.handler = handler;
            this.el = $(selector).first();            
            
            var opts = this.options = $.extend({}, this.defaults, options || {});    
            
            this.onTrack = $.proxy(this.onTrack, this);
            this.tagType = this.getTagType();
            this.eventType = this.getEventType();            
            this.validator = new intouch.FormTrackerValidator(this.options.validatorName || this.getValidatorName(), this.el);

            // only attach if you're supposed to always track, or if you haven't tracked before
            if (this.shouldTrack()) this.bind();

            return this;
        },

        shouldTrack: function() {
            var opts = this.options;            
            return opts.alwaysTrack || !opts.hasTracked;
        },

        getValidatorName: function() {
            return this.tagType;
        },

        getTagType: function() {
            return this.tagType || (this.tagType = function(self){
                var el = self.el;
                return (el.attr("type") || el[0].tagName).toLowerCase();
            }(this));
        },

        getEventType: function() {
            return this.eventType || (this.eventType = eventTypeMap[this.tagType]);
        },

        onTrack: function(event) {            
            if (this.validator.validates(this)) {                
                if (this.shouldTrack()) {
                    $(this).trigger("track", [this]);
                    this.handler();
                }                                
                this.options.hasTracked = true;               
            }
        },

        bind: function() {            
            this.el.bind(this.eventType, this.onTrack);
            return this;
        },

        unbind: function() {
            this.el.unbind(this.eventType, this.onTrack);
            return this;        
        }
    };


    // Field tracker
    intouch.FormTrackerField = function() {
        this.initialize.apply(this, arguments);
        return this;
    };

    $.extend(intouch.FormTrackerField.prototype, TrackedBase);


    // Group tracker
    intouch.FormTrackerGroup = function() {
        this.initialize.apply(this, arguments);
        return this;
    };

    $.extend(intouch.FormTrackerGroup.prototype, TrackedBase, {
        
        getValidatorName: function() {
            return this.tagType.charAt(0).toUpperCase() + this.tagType.slice(1) + "Group";
        },
        
        getTagType: function() {
            return this.tagType || (this.tagType = function(self){
                var el = self.el, tagType, tagSelector;
                
                var tagPairs = [
                    ["input[type=radio]",       "radio"],
                    ["input[type=checkbox]",    "checkbox"],
                    ["input[type=text]",    "text"]
                ];

                for (var i = 0, l = tagPairs.length, pair, found; i < l; i++) {
                    pair = tagPairs[i];
                    found = $(pair[0], el);
                    if (found.length > 0) {
                        tagSelector = pair[0];
                        tagType = pair[1];
                        break;
                    }
                }
                
                if (tagType === undefined) {
                    throw new Error("FormTrackerGroup with selector " + self.selector + " could not find a tagType match.");
                }

                self.tagSelector = tagSelector;

                return tagType;
            }(this));
        },
                
        bind: function() {
            this.el.delegate(this.tagSelector, this.eventType, this.onTrack);
            return this;
        },

        unbind: function() {
            this.el.undelegate(this.tagSelector, this.eventType, this.onTrack);
            return this;        
        }

    });

    // validator
    var validator = intouch.FormTrackerValidator = function(){
        // cache of validators
        var validators = {};
        
        // validator base
        var validatorBase = {            
            getDefaults: function() {
                throw new Error("getDefaults method not implemented for validator " + this.name);
            },

            validate: function() {
                throw new Error("Validate method not implemented for validator " + this.name);
            }
        };

        // constructor for all validators
        var test = function(name, elements, props) {
            this.name = name;
            this.el = $(elements);  
            $.extend(this, props);
            this.defaults = this.getDefaults();
            return this;
        };
        $.extend(test.prototype, validatorBase);

        // the returned constructor for FormTrackerValidator
        var returned = function(name, elements) {
            var props = validators[name];
            if (props) {
                return new test(name, elements, props);
            }
            else {
                throw new Error("Validator " + name + " does not exist.");
            }
        };

        // hook for registering validators
        returned.register = function(name, props) {
            validators[name] = props;
            return this;
        };

        return returned;
    }();

    // register default validators
    validator.register("text", {
        validates: function(tracked) {
            var val = this.el.val();
            return !!(val && val !== this.defaults);
        },
        getDefaults: function() {
            return this.el.val();
        }
    });

    validator.register("radio", {
        validates: function(tracked) {
            var val = this.el.attr("checked");
            return !!(val && val !== this.defaults);                        
        },
        getDefaults: function() {
            return this.el.attr("checked"); 
        }
    });

    validator.register("checkbox", {
        validates: function(tracked) {
            var val = this.el.attr("checked");
            return !!(val && val !== this.defaults);
        },
        getDefaults: function() {
            return this.el.attr("checked");
        }
    });

    validator.register("select", {
        validates: function(tracked) {        
            var val = this.el.val();
            return !!(val && val !== this.defaults);
        },
        getDefaults: function() {
            return this.el.val();          
        }
    });

    validator.register("textarea", {
        validates: function(tracked) {
            var val = this.el.val();
            return !!(val && val !== this.defaults);
        },
        getDefaults: function() {            
            return this.el.val();        
        }
    });

    validator.register("RadioGroup", {
        
        getCached: function() {
            return this.cache || (this.cache = $("input[type=radio]", this.el));
        },

        validates: function(tracked) {
            var els = this.getCached(),
                defaults = this.defaults,
                hasValue = false;

            for (var i = 0, l = els.length, el; i < l; i++) {
                el = $(els[i]);
                if (!!el.attr("checked") && el.attr("checked") !== defaults[i]) {
                    hasValue = true;
                    break;
                }
            }

            return hasValue;  
        },
        getDefaults: function() {
            var els = this.getCached(),
                defaults = [];

            $.each(els, function(index, el) {
                defaults.push($(el).attr("checked"));
            });

            return defaults;
        }
    });

    validator.register("CheckboxGroup", {
        
        getCached: function() {
            return this.cache || (this.cache = $("input[type=checkbox]", this.el));
        },
                
        validates: function(tracked) {
            var els = this.getCached(),
                defaults = this.defaults,
                hasValue = false;

            for (var i = 0, l = els.length, el; i < l; i++) {
                el = $(els[i]);
                if (!!el.attr("checked") && el.attr("checked") !== defaults[i]) {
                    hasValue = true;
                    break;
                }
            }

            return hasValue;            
        },
        getDefaults: function() {
            var els = this.getCached(),
                defaults = [];

            $.each(els, function(index, el) {
                defaults.push($(el).attr("checked"));
            });

            return defaults;        
        }
    });

    validator.register("TextGroup", {
        
        getCached: function() {
            return this.cache || (this.cache = $("input[type=text]", this.el));
        },

        validates: function(tracked) {
            var els = this.getCached(),
                defaults = this.defaults,
                hasValue = false;

            for (var i = 0, l = els.length, el; i < l; i++) {
                el = $(els[i]);
                if (!!el.val() && el.val() !== defaults[i]) {
                    hasValue = true;
                    break;
                }
            }

            return hasValue;             
        },
        getDefaults: function() {
            var els = this.getCached(),
                defaults = [];

            $.each(els, function(index, el) {
                defaults.push($(el).val());
            });

            return defaults;        
        }
    });



}(jQuery));
