/**
 * Contains a library of classes that are to serve as a base for more complex
 *
 * @author Xedin Unknown <xedin.unknown@gmail.com>
 * @version 0.1.0
 * @depends jQuery [http://jquery.com/], jQuery.Class [https://github.com/kilhage/class.js]
 */

// Misc functions
String.prototype.capitalize = function() {
    return this.charAt(0).toUpperCase() + this.slice(1);
};

String.prototype.trim = function() {
    return this.replace(/^\s+|\s+$/, '');
};

/**
 * Base class for all objects.
 * Encapsulates data in an internal member, which can be serialized and retreived and what not.
 * Access to data placeholders is provided also through polymorphic functions. 
 * For example, to retreive data in placeholder 'some_var', you could write
 * var a = obj.getData('some_var');
 * However, the following is identical, but is somewhat more convenient at times:
 * var a = obj.getSomeVar();
 * 
 * Special thanks to the Magento's Varien_Object.
 *
 * @author Xedin Unknown <xedin.uknown@gmail.com>
 * @version 0.1.1
 * + Added escapeSelector() used to escape CSS selectors with backslashes.
 */
var Xedin_Object = jQuery.Class({
                
    init: function(data) {
        if( data === undefined ) {
            this._data = {};
            return this;
        }

        this._data = data;
        return this;
    },

    validateKey: function(key) {
        if( key === '' ) return false;
        if( key === null ) return false;

        return true;
    },

    getData: function(key) {
        if( key === undefined ) return this._data;
        if( !this.validateKey(key) ) return null;
        if( this._data[key] === undefined ) return null;

        return this._data[key];
    },

    setData: function(key, value) {
        if( !this.validateKey(key) ) return false;
        if( value === undefined ) {
            this._data = key;
            return true;
        }

        this._data[key] = value;
        return this;
    },

    getCount: function() {
        return this.countProperties(this._data);
    },

    hasData: function(key) {
        if( key === undefined ) {
            return this.getCount();
        }

        return ( (this._data[key] !== undefined) && (this._data[key] !== null) && (this._data[key] !== false) );
    },

    unsetData: function(key) {
        if( key === undefined ) {
            this._data = {};
            return this;
        }

        if( this.validateKey(key) && this._data[key] !== undefined ) {
            delete this._data[key];
        }

        return this;
    },

    /*
     * Counts the number of properties of an object.
     * Very useful in an associative array.
     */
    countProperties: function(object) {
        if( object === undefined ) object = this;
        
        var count = 0;
        for( var k in object ) {
            if( object.hasOwnProperty(k) ) {
                count++;
            }
        }

        return count;
    },

    __noSuchMethod__: function(name, args) {

        switch( name.substring(0, 3) ) {
            case 'get':
                return this.getData(this._underscore(name.substring(3)));
                break;

            case 'set':
                this.setData(this._underscore(name.substring(3)), args[0]);
                return this;
                break;

            case 'has':
                return this.hasData(this._underscore(name.substring(3)));
                break;

            case 'uns':
                this.unsetData(this._underscore(name.substring(3)));
                return this;
                break;
        }

        console.log('Method "' + name + '" does not exist.');
    },

    camelize: function(string) {
        var result = string.replace('_', ' ');
        result = this.ucwords(result);
        result = result.replace(' ', '');

        return result;
    },

    ucwords: function(string) {
        return (string + '').replace(/^([a-z])|\s+([a-z])/g, function ($1) {
            return $1.toUpperCase();
        });
    },
    
    escapeSelector: function(selector) {
        return selector.replace(/([ #;&,.+*~\':"!^$[\]()=>|\/@])/g,'\\$1');
    },

    _underscore: function(string) {
        var result = string.replace(/(.)([A-Z])/g, '$1_$2').toLowerCase();

        return result;
    },

    setDataUsingMethod : function(key, value) {
        this.call('set' + this._camelize(key), value);
        return this;
    },

    getDataUsingMethod: function(key) {
        return this.call('get' + this._camelize(key));
    },
    
    raiseEvent: function(eventName, param) {
        var eventPrefix = 'on';
        var functionName = eventPrefix + this.camelize(eventName);
        if( !(this[functionName] && typeof this[functionName] == 'function') ) return this;
        
        this[functionName].apply(this, arguments.shift());
        return this;
    },
    
    _toArray: function(object) {
        if( object === undefined ) return [];
        if( jQuery.type(object) == 'array' ) return object;
        return [object];
    }
});

/**
 * Base class for lists
 * @author Xedin Unknown <xedin.unknown@gmail.com>
 * @version 0.1.0
 */
var Xedin_List = Xedin_Object.extend({
    
    init: function() {
        this._parent.init();
    },
    
    /**
     * Merges two arrays, two objects, or an array and an object.
     * If both parameters are arrays, and the third one is false, concatenates the two by adding the values of the second array at the end of the first one.
     * If both parameters are arrays, and the third one is true, replaces values from the first one with values of corresponding indexes from the second one, and adds the rest at the end.
     * If both parameters are objects, and the third one is false, adds values of keys in the second one that are not present in the first one, to the first one. 
     * If both parameters are objects, and the third one is true, does the same as if the third parameter is false, but replaces corresponding values in the first object with values from the second one;
     * If the first parameter is an array, the second one is an object, and the third is false, adds all values of the object to the end of the array.
     * If the first parameter is an array, the second one is an object, and the third is true, replaces values in the array with values of the corresponding _position_ of the object, and adds the rest at the end.
     * In all other cases, returns the first parameter unmodified.
     * 
     * @param array|object object1 An object or an array to merge.
     * @param array|object object2 An object or an array to merge with.
     * @
     * @return array|object If both parameters are arrays, returns array. If both are objects, returns object. If first parameter is an aray and the second one is an object, returns array.
     */
    _merge: function(object1, object2, overwrite) {
        overwrite = overwrite ? true : false;
        var attrname;
        if( object1 instanceof Array && object2 instanceof Array ) {
            if( !overwrite) return object1.concat(object2);
            for( var index in object2 ) {
                object1[index] = object2[index];
            }
            return object1;
        }
        
        if( object1 instanceof Array && typeof(object2) == 'object' ) {
            if( !overwrite ) {
                for( attrname in object2 ) {
                    object1.push(object2[attrname]);
                }
                return object1;
            }
            
            var i = 0;
            for( attrname in object2 ) {
                object1[i] = object2[attrname];
                i++;
            }
            return object1;
        }
        
        if( typeof(object1) == 'object' && typeof(object2) == 'object' ) {
            if( !overwrite ) {
                for( attrname in object2 ) {
                    if( !object1.hasOwnProperty(attrname) ) object1[attrname] = object2[attrname];
                }
                
                return object1;
            }
            
            for( attrname in object2 ) {
                object1[attrname] = object2[attrname]
            }
            return object1;
        }
        
        return object1;
    },
    
    merge: function(list) {
        
    },
    
    count: function() {
    },
    
    remove: function() {
    },
    
    hasItems: function(index) {
        return (this.getItems(index) === null);
    },
    
    getItems: function(index) {
        if( index === undefined ) return this._items;
        if( this._items[index] == undefined ) return null;
        return this._items[index];
    },
    
    validateIndex: function(index) {
        return index ? true : false;
    },
    
    getItemIndex: function(searchItem, strict) {
        var index = false;
        
        if( strict ) {
            jQuery.each(this.getItems(), function(i, item) {
                if( searchItem === item ) {
                    index = i;
                    return false;
                }
            });
            
            return index;
        }
        
        jQuery.each(this.getItems(), function(i, item) {
            if( searchItem == item ) {
                index = i;
                return false;
            }
        });
        
        return index;
    }
});

/**
 * A base for any numeric list, e.g. a list where keys are sequential numbers.
 * Can be filled at initialization time by providing an array as the only parameter to the constructor.
 * Can be merged with another array, or another Xedin_List_Numeric object.
 * 
 * @author Xedin Unknown <xedin.unknown@gmail.com>
 * @version 0.1.0
 */
var Xedin_List_Numeric = Xedin_List.extend({
    
    merge: function(list) {
        if( list instanceof Xedin_List_Numeric ) list = list.getItems();
        if( list instanceof Array ) this._items = this._items.concat(list);
        return this;
    },
    
    init: function(array) {
        this._parent.init();
        this._items = [];
        if( array !== undefined ) this.merge(array);
        return this;
    },
    
    count: function() {
        return this._items.length;
    },
    
    validateIndex: function(index) {
        return !isNaN(index) ? true : false;
    },
    
    hasItems: function(index) {
        if( index === undefined ) {
            return this.count() ? true : false;
        }
        
        return (this.validateIndex(index) && this._items[index] !== undefined);
    },
    
    insert: function(elements, position) {
        if( position === undefined ) position = this.count();
        if( jQuery.type(elements) == 'array' ) {
            this._items.splice.apply(this._items, [position, 0].concat(elements));
            return this;
        }
        this._items.splice(position, 0, elements);
        return this;
    },
    
    remove: function(position, amount) {
        if( !this.count() ) return this;
        if( position === undefined ) position = (this.count() - 1);
        if( amount === undefined ) amount = 1;
        this._items.splice(position, amount);
        return this;
    },
    
    clear: function() {
        this._items = [];
    },
    
    getLastIndex: function() {
        return this.count() - 1;
    },
    
    getFirstIndex: function() {
        return 0;
    },
    
    getLastItem: function() {
        return this.getItems( this.getLastIndex() );
    }
});

/**
 * A base class for any associative lists, e.g. a list where keys are strings.
 * Can be filled at initialization time by providing an object as the only parameter to the constructor.
 * Can be merged with another object, or another Xedin_List_Associative object;
 *
 * @author Xedin Unknow <xedin.unknown@gmail.com>
 * @version 0.1.0
 */
var Xedin_List_Associative = Xedin_List.extend({
    init: function(object) {
        this._parent.init();
        this._items = {};
        if( object !== undefined ) this.merge(object);
        return this;
    },
    
    merge: function(list) {
        if( list instanceof Xedin_List_Associative ) list = list.getItems();
        if( typeof(list) != 'object' ) return this;
        return this._items = this._merge(this.getItems(), list);
    },
    
    validateIndex: function() {
        return true;
    },
    
    hasItems: function(index) {
        return this._parent.hasItems(index) && this._items.hasOwnProperty(index);
    },
    
    count: function() {
        return this.countProperties(this);
    },
    
    insert: function(key, value) {
        if( value === undefined && (jQuery.type(key) == 'array' || jQuery.type(key) == 'object')  ) {
            this._items.concat(key);
            return this;
        }
        
        if( !key ) return this;
        this._items[key] = value;
        return this;
    },
    
    remove: function(key) {
        if( this.hasItems(key) ) delete this._items[key];
        return this;
    },
    
    clear: function() {
        this._items = {};
    }
});

/**
 * 
 * @author Xedin Unknown <xedin.unknown@gmail.com>
 * @version 0.1.0
 */
var Xedin_Event_List = Xedin_Object.extend({
    
    _eventTypes: null,
    _notificationTypes: null,
    _events: null,
    
    init: function() {
        this._parent.init();
        
        this._eventTypes = new Xedin_List_Numeric([
            'notification',
            'warning',
            'error'
            ]);
        
        this._notificationTypes = new Xedin_List_Numeric([
            'alert',
            'log',
            'ignore'
            ]);
        
        this._events = new Xedin_List_Numeric();
        
        this.setDefaultEventType('notification');
    },
    
    getEventTypes: function() {
        return this._eventTypes;
    },
    
    addEventType: function(eventType) {
        this.raizeEvent('before_add_event_type', eventType);
    }
});

/**
 * Aggregates behaviour that is similar in all queues.
 * 
 * @author Xedin Unknown <xedin.unknown@gmail.com>
 * @version 0.1.0
 */
var Xedin_Queue = Xedin_List_Numeric.extend({
    
    _currentIndex: null,
    
    init: function(list, index) {
        if( index === undefined ) index = 0;
        
        this._parent.init(list);
        if( this.hasItems(index) ) this._currentIndex = index;
        
        return this;
    },
    
    getCurrentIndex: function() {
        return this._currentIndex;
    },
    
    getValue: function(index) {
        if( index === undefined ) index = this.getCurrentIndex();
        
        if( !this.hasItems(index) ) return null;
        return this.getItems(index);
    },
    
    seek: function(index) {
        if( !this.hasItems(index) ) return null;
        this._currentIndex = index;
        return this.getValue(this._currentIndex);
    },
    
    next: function() {
        var newIndex = this._currentIndex + 1;
        return this.seek(newIndex);
    },
    
    prev: function() {
        var newIndex = this._currentIndex - 1;
        return this.seek(newIndex);
    } 
});


/**
 * A queue where the next element after the last one is the first one, and 
 * the element before the first one is the last one.
 * Any index is possible; the queue will hadle rotating of the index around it
 * and give a correct result.
 * Example:
 * var queue = new Xedin_Queue_Circular(['apple'], ['banana'], ['strawberry'], ['chocolate'], 0);
 * alert(queue.next()); // "banana"
 * alert(queue.next()); // "strawberry"
 * alert(queue.next()); // "chocolate"
 * alert(queue.next()); // "apple"
 * alert(queue.prev()); // "chocolate"
 * alert(queue.seek(5)); // "banana"
 * alert(queue.seek(6)); // "strawberry"
 * alert(queue.seek(-7)); // "apple"
 * alert(queue.seek(-5)); // "strawberry"
 * 
 * @author Xedin Unknown <xedin.unknown@gmail.com>
 * @version 0.1.0
 */
var Xedin_Queue_Circular = Xedin_Queue.extend({
    
    init: function(list, index) {
        this._parent.init(list, index);
        return this;
    },
    
    /**
     * Translates any index into an index in range of the queue.
     * This way, both negative and positive out of range values are also allowed.
     *
     * @author Xedin Unknown <xedin.unknown@gmail.com>
     * @author Lincoln Grixti <lincoln@brndwgn.com>
     */
    seek: function(index) {
        index = ( (index+1) % (this.getLastIndex()+1) ) - 1;
        if( index < 0 ) index = this.count() + index;
        return this._parent.seek(index);
    }
});

/**
 * A queue where the next element after the last one is the one before the last one,
 * and the element before the first one is after the first one.
 * 
 * @author Xedin Unknown <xedin.unknown@gmail.com>
 * @version 0.1.0
 */
var Xedin_Queue_Flexing = Xedin_Queue.extend({
    
});

/**
 * A list of unique primitive values.
 * No two equal values may exist in this list.
 * 
 * @author Xedin Unknown <xedin.unknown@gmail.com>
 * @version 0.1.0
 */
var Xedin_Queue_Unique = Xedin_Queue.extend({
    
});

/**
 * An object that provides an interface to a collection of events, by event type.
 * This object also handles automatic notifications (event raising) for the 
 * events.
 * The intended purpose of this class is to serve as a base for a class of an
 * event-driven object.
 * 
 * @author Xedin Unknown <xedin.unknown@gmail.com>
 * @version 0.1.0
 */
var Xedin_Event_Object = Xedin_Event_List.extend({
    
});

/**
 * A widget is a reusable functional element of design.
 * This class aggregates behaviour that is common to all widgets.
 * 
 * @author Xedin Unknown <xedin.unknown@gmail.com>
 * @version 0.1.0
 */
var Xedin_Widget = Xedin_Event_List.extend({
    
});

/**
 * A class which encapsulates functionality needed to build one or more "tabs"
 * that fit at screen edges and can be animated: pulled in and out.
 * Provides a high level of separation between aggregated and specialized
 * behaviour.
 * 
 * @author Xedin Unknown <xedin.unknown@gmail.com>
 * @version 0.1.0
 */
var Xedin_Widget_Pullouts = Xedin_Widget.extend({
    
    _animations: null,
    _triggers: null,
    _state: null,
    _states: null,
    
    _defaultTrigger: {
        'selector':         '.trigger',
        'in':           {
            'css':            {
                right:          -230
            },
            speed:          1000,
            delay:          0,
            onBefore:       function(){},
            onAfter:        function(){}
        },
        'out':          {
            'css':            {
                right:          0
            },
            speed:          250,
            delay:          0,
            onBefore:       function(){},
            onAfter:        function(){}
        },
        timer:              false,
        state:              'out'
    },
    
    init: function() {
        this._animations = new Xedin_List_Associative();
        this._triggers = new Xedin_List_Associative();
        this._states = new Xedin_Queue_Circular(['in', 'out']);
    },
    
    getTriggers: function(triggerCode) {
        if( triggerCode ) {
            return this._triggers.getItems(triggerCode);
        }
        return this._triggers.getItems();
    },
    
    addTrigger: function(code, params) {
        this._triggers.insert(code, jQuery.extend(true, {}, this._defaultTrigger, params));
        return this;
    },
                
    getNextTriggerState: function(triggerCode) {
        var trigger = this.getTriggers(triggerCode);
        var curStateIndex = this._states.getItemIndex(trigger.state);
        this._states.seek(curStateIndex);
        return this._states.next();
    },
                
    removeTrigger: function(code) {
        this._triggers.remove(code);
        return this;
    },
                
    _getAnimationKey: function(triggerCode, direction) {
        return 'animating-' + triggerCode + '-' + direction;
    },
                
    _canAnimate: function(triggerCode, direction) {
        return !this._animations.hasItems( this._getAnimationKey(triggerCode, direction) );
    },
                
    _addStatus: function(statusCode) {
        if( this._statuses.hasItems(statusCode) ) return this;
        this._statuses.insert(statusCode, true);
        return this;
    },
                
    _removeStatus: function(statusCode) {
        if( !this._statuses.hasItems(statusCode, true) ) return this;
        this._statuses.remove(statusCode);
        return this;
    },
                
    hasStatus: function(statusCode) {
        return this._statuses.hasItems(statusCode)
    },
                
    getStatus: function() {
                    
    },
                
    attempt: function(triggerCode, animateDirection) {
        if( !this._canAnimate(triggerCode, animateDirection) ) return false;
        var triggerSettings = this.getTriggers(triggerCode);
        clearTimeout(this._triggers.getItems(triggerCode).timer);
        var This = this;
        this._triggers.getItems(triggerCode).timer = setTimeout(function() {
            This.animate(triggerCode, animateDirection)
        }, triggerSettings[animateDirection]['delay'])
        
        return this;
    },
                
    prepare: function() {
        var This = this;
        jQuery.each(this.getTriggers(), function(triggerCode, triggerSettings) {
            var animateDirection = This.getNextTriggerState(triggerCode);
            
            This.css = triggerSettings[triggerSettings.state];
            
            This.attempt(triggerCode, animateDirection);
            jQuery(triggerSettings.selector)/*.animate(
                            triggerSettings['in']['css'],
                            triggerSettings['in']['speed']
                        )*/
            /*.bind('click', function() {
                            if(  )
                        })*/
            .bind('click', function() {
                animateDirection = This.getNextTriggerState(triggerCode);
                This.attempt(triggerCode, animateDirection);
            })
            .bind('click', function() {
                animateDirection = This.getNextTriggerState(triggerCode);
                This.attempt(triggerCode, animateDirection);
            });
        });
    },
                
    animate: function(triggerCode, direction) {
        var animationKey = this._getAnimationKey(triggerCode, direction);
        var triggerSettings = this._triggers.getItems(triggerCode);
        this._animations.insert(animationKey, true);
        var This = this;
        triggerSettings[direction].onBefore();
        jQuery(triggerSettings['selector']).animate(
            triggerSettings[direction]['css'],
            triggerSettings[direction]['speed'],
            function() {
                This._animations.remove(animationKey);
                triggerSettings.state = direction;
                triggerSettings[direction].onAfter();
            }
        );
    }
});

var Xedin_Loader = Xedin_Event_Object.extend({
    
});

/**
 * Aggregates behavior that is similar across parts that potentially make up
 * a widget. However, it is not necessarily a _visible_ element.
 * 
 * @author Xedin Unknown <xedin.unknown@gmail.com>
 * @version 0.1.0
 */
var Xedin_Element = Xedin_Event_Object.extend({
    
});

/**
 * Abstracts an image.
 * Allows on-demand image loading.
 * Provides events such as onLoad(), onError() and onAbort(), which correctly
 * work independently of cache behaviour.
 * Notice: This class will not trigger the onLoad() event more than once per object! 
 * 
 * @author Xedin Unknown <xedin.unknown@gmail.com>
 * @version 0.1.0
 */
var Xedin_Element_Image = Xedin_Element.extend({
    
    _obj: null,
    _source: null,
    _hasFiredOnLoad: null,
    
    init: function(source, width, height) {
        if( width === undefined && height === undefined ) this._obj = new Image();
        if( width !== undefined && height === undefined ) this._obj = new Image(width);
        
        // Not sure if this is the proper way, or if it's possible at all. Time will show...
        if( width === undefined && height !== undefined ) this._obj = new Image(undefined,height);
        if( width !== undefined && height !== undefined ) this._obj = new Image(width, height);
        
        var This = this;
        this._obj.onload = function() {return This._onLoad(This);};
        this._obj.onabort = function() {return This._onAbort(This._source);};
        this._obj.onerror = function() {return This._onError(This._source);};
        if( source ) this._source = source; 
        this._hasFiredOnLoad = false;
        
        return this;
    },
    
    getObject: function() {
        return this._obj;
    },
    
    setSource: function(source) {
        this.getObject().src = source;
        return this;
    },
    
    getSource: function() {
        return this._source;
    },
    
    isComplete: function() {
        return this.getObject().complete;
    },
    
    isLoaded: function() {
        return this._hasFiredOnLoad;
    },
    
    getWidth: function() {
        return this.getObject().width;
    },
    
    setWidth: function(width) {
        this.getObject().width = width;
        return this;
    },
    
    getHeight: function() {
        return this.getObject().height;
    },
    
    setHeight: function(height) {
        this.getObject().height = height;
        return this;
    },
    
    /**
     * Please remember that this method is asynchronous.
     */
    load: function() {
        this._obj.onload = this._onLoad(this);
        this._obj.src = this._source;
        if( this.isComplete() ) {this._onLoad(this);}
        
        return this;
    },
    
    _onLoad: function(source) {
        if( this._hasFiredOnLoad ) return;
        this.onLoad(this);
        this._hasFiredOnLoad = true;
    },
    
    onLoad: function(image) {
        
    },
    
    _onAbort: function(source) {
        this.onAbort(source);
    },
    
    onAbort: function() {
        
    },
    
    _onError: function(source) {
        this.onError(source);
    },
    
    onError: function(source) {
        
    }
});

/**
 * A class that is responsible for loading arrays of images.
 * Provides means of monitoring progress.
 * Correctly handles missing images.
 * A single object can be reused multiple times.
 * 
 * @author Xedin Unknown <xedin.unknown@gmail.com>
 * @version 0.1.0
 */
var Xedin_Loader_Image = Xedin_Loader.extend({
    
    _images: null,
    _loadedCount: null,
    _toLoadCount: null,
    
    getLastImage: function() {
        return this._images.getLastItem();
    },
    
    _cancelLoad: function(index) {
        this._toLoadCount--;
        return this;
    },
    
    addImages: function(images, width, height) {
        var This = this;
        if( width !== undefined || height !== undefined ) {
            var _image = new Xedin_Element_Image(images, width, height);
            this._images.insert(_image);
            This._images.getLastItem().onLoad = function(_image) { This._onImageLoad(_image)};
            This._images.getLastItem().onError = function(source) { This._cancelLoad(i)};
            This._images.getLastItem().onAbort = function(source) { This._cancelLoad(i)};
            return this;
        }
        
        images = this._toArray(images);
        for( var i in images ) {
            this.addImages(images[i][0], images[i][1], images[i][2])
        }
        return this;
    },
    
    init: function(images) {
        
        if( images !== undefined ) {
            images = this._toArray(images);
        }
        
        this._images = new Xedin_Queue_Unique();
        if( images ) this.addImages(images);
        this._loadedCount = 0;
        this._toLoadCount = this._images.count();
        return this;
    },
    
    getCount: function() {
        return this._images.count();
    },
    
    getLoadedCount: function() {
        return this._loadedCount;
    },
    
    getToLoadCount: function() {
        return this._toLoadCount;
    },
    
    /**
     * Loads all the images
     */
    load: function() {
        for( var i in this._images.getItems() ) {
            var _image = this._images.getItems(i);
            _image.load();
        }
        
        return this;
    },
    
    _onImageLoad: function(image) {
        this._loadedCount++;
        this.onImageLoad(image);
        if( this.getLoadedCount() === this.getCount() ) {
            this.onLoad();
        }
        return this;
    },
    
    onImageLoad: function(image) {
        
    },
    
    onLoad: function() {
        
    },
    
    clear: function() {
        this._images.clear();
        this._loadedCount = 0;
        this._toLoadCount = 0;
    }
});


var miscObject = new Xedin_Object();


/**
 * Represents an HTML form. Acts as a wrapper for handling various standard form
 * actions, as well as providing additional functionality and automation.
 *
 * @author Xedin Unknown <xedin.unknown@gmail.com>
 * @version 0.1.0
 */
function Xedin_Html_Form(formId, params) {
    var This = this;
                
    This._defaultParams = {
        'altAction':                '',
        'submitElement':            'submit',
        'invalidClass':             'invalid-form-field',
        'defaultEmptyText':         'Search...',
        'defaultEmptyClass':        'empty-field',
        'defaultFocusEmptyText':    '',
        'defaultDisplayEmptyText':  false,
        'fieldConfig':              {} // Per ID
    };
    This.params = jQuery.extend(true, This._defaultParams, params);
                
    This.formId = formId;
    var $this = jQuery('#' + This.formId);
    
    /**
     * Gets the jQuery object representing the HTML element in the context
     * of this form by the element's ID.
     */
    This.getElement = function(id) {
        id = '#' + miscObject.escapeSelector(id);
        return jQuery(id, $this);
    }
    
    This.getFieldOption = function(fieldId, optionName, defaultValue) {
        if( defaultValue === undefined ) defaultValue = null;
        
        var option = undefined;
        if( (option = This.params['fieldConfig'][fieldId]) !== undefined &&
            (option = This.params['fieldConfig'][fieldId][optionName]) !== undefined  )return option;
        
        optionName = 'default' + optionName.capitalize();
        option = This.params[optionName];
        
        if( option !== undefined ) return option;
        
        return defaultValue;
    }
                
    This.submit = function() {
        
        jQuery.each(This.params['fieldConfig'], function(fieldId, fieldOptions) {
            var $element = This.getElement(fieldId);
            if( fieldOptions['emptyText'] && $element.val() == fieldOptions['emptyText'] ) {
                $element.val('');
            }
        });
        
        if( !This.validate() ) {
            
            jQuery.each(This.params['fieldConfig'], function(fieldId, fieldOptions) {
                var $element = This.getElement(fieldId);
                if( fieldOptions['emptyText'] && $element.val() == '' ) {
                    $element.val(fieldOptions['emptyText']);
                }
            });
            
            return false;
        }
                    
        if( This.params['altAction'] && This.params['altAction'] !== '' ) {
            $this.attr('action', This.params['altAction']);
        }
                    
        $this.trigger('submit');
        return true;
    }
                
    This.validate = function() {
        if( !jQuery.validator ) {
            return true;
        }
        $this.validate();
        return $this.valid();
    }
    
    This.setFieldEmpty = function(id, focused) {
        var $element = This.getElement(id);
        if( !focused && $element.val().trim() == '' ) {
            $element.attr('value', This.getFieldOption(id, 'emptyText'))
                .addClass(This.getFieldOption(id, 'emptyClass'));
            return This;
        }
        
        if( !($element.hasClass(This.getFieldOption(id, 'emptyClass')) && !$element.attr('value').trim() == '') ) {
            return This;
        }
        
        $element.attr('value', This.getFieldOption(id, 'focusEmptyText'))
            .removeClass(This.getFieldOption(id, 'emptyClass'));
        return This;
    }
                
    This._init = function() {
        jQuery('input[type="text"]', $this).each(function(index, element) {
            var $element = jQuery(element);
            var elementId = $element.attr('id');
            $element.val('');
            This.setFieldEmpty(elementId, false);
            if( This.getFieldOption(elementId, 'displayEmptyText') ) {
                $element.bind('focus', function() {
                    This.setFieldEmpty(elementId, true);
                });
                
                $element.bind('blur', function() {
                    This.setFieldEmpty(elementId, false);
                });
            }
        });
        
        This.getElement(This.params['submitElement']).bind('click', function() {
            This.submit();
        });
    }
    This._init();
}

//var queue = new Xedin_Queue_Circular(['Anton', 'Mike', 'Lincoln']);
//console.log(queue);
//console.log(queue.next());
//console.log(queue.next());
//console.log(queue.next());
//console.log(queue.next());
//console.log(queue.next());
//console.log(queue.next());
//console.log(queue.prev());
//console.log(queue.prev());
//console.log(queue.prev());
//console.log(queue.prev());
//console.log(queue.prev());
//console.log(queue.prev());
//console.log(queue.seek(12));
//console.log(queue.seek(12));
//console.log(queue.seek(13));
//console.log(queue.seek(14));
//console.log(queue.seek(-1));
//console.log(queue.seek(-2));
//console.log(queue.seek(-3));
//console.log(queue.seek(-4));
//console.log(queue.seek(-5));
//console.log(queue.seek(-5));

//var queue = new Xedin_Queue(['Anton', 'Mike', 'Lincoln']);
//console.log(queue.getValue());
//console.log(queue.next());
//console.log(queue.next());
//console.log(queue.next());
//console.log(queue.prev());
//console.log(queue.prev());
//console.log(queue.prev());
//console.log(queue.prev());
//console.log(queue.next());


//var list = new Xedin_List_Numeric(['Apple', 'Banana']);
//console.log(list.getItems());
//list.merge(new Xedin_List_Numeric(['Orange', 'Pineapple']));
//console.log(list.getItems());

//var list = new Xedin_List_Associative({name: 'Anton', surname: 'Ukhanev'});

//var list = new Xedin_List_Associative();
//var obj1 = ['Anton', 'Anderson'];
//var obj2 = ['Mikhail', 'Doubrovsky', 'Foosball'];
//obj3 = list._merge(obj2, obj1, false);
//console.log(obj3);

//var objectList = new Xedin_List_Associative();
//console.log(objectList.getItems());
//objectList.insert(['Apple', 'Strawberry', 'Banana']);
//console.log(objectList.getItems());
//objectList.insert(['Orange', 'Dragon Fruit']);
//console.log(objectList.getItems());
//objectList.insert('Passion Fruit');
//console.log(objectList.getItems());


