var Textbox = new Class({
    input: null,
    type: null,
    //TODO consolidate
    
    keydown: function(){
        var temp = {
            'sender': this.SenderDown.bind(this),
            'word': this.WordDown.bind(this)
        }
        return temp[this.type];
    },
    keypress: function(){
        var temp = {
            'sender': this.SenderPress.bind(this),
            'word': this.WordPress.bind(this)
        }
        return temp[this.type];
    },
    keyup: function(){
        var temp = {
            'sender': $empty,
            'word': $empty
        }
        return temp[this.type];
    },
    
    attach: function(input, type){
        if (this.input == null){
            this.input = input;
        }
        this.type = type;
        this.input.addEvents({
            'keydown': this.keydown(type),
            'keypress': this.keypress(type),
            'keyup': this.keyup(type)
        });
    },
    
    //behaviours:
        //Sender - functions like a regular textbox, but executes parent/child class' send method on return
        //Word - adds unicode line breaks and tabs to a textbox
    SenderDown: function(event){
        var e = new Event(event);
        switch (e.code){
            case 13:
                if (this.send) this.send();
                return false;
            default:
                return true;
        }
    },
    
    SenderPress: function(event){
        var e = new Event(event);
        switch (e.code){
            case 13:
                return false;
            default:
                return true;
        }
    },
    
    WordDown: function(event){
        var e = new Event(event);
        switch (e.code){
            case 13:
                add_text(this.input, '\u000D');
                return false;
            case 9:
                add_text(this.input, '\u0009');
                return false;
            default:
                return true;       
        }
    },
    
    WordPress: function(event){
        var e = new Event(event);
        if (e.code == 9 || e.code == 13){
            return false;
        } else {
            return true;
        }
    }
});


var Inline = new Class({
    Extends: Textbox,
    close_wrapper: null,
    input: null,
    options: {
        'target': null,
        'fields': null,
        'id': null,
        'type': null,
        'orig': null,
        'style': 'standard'
    },
    
    open: function(target, fields, id, options){
        if ($('inline_edit')) this.destroy();;
        //save values
        this.options.target = $(target);
        this.options.fields = fields;
        this.options.id = id;
        this.options.orig = this.options.target.get('html').cleanStringJS();
            
        if (options != null){
            this.options = $merge(this.options, options);
        } else {
            this.options.type = 'input';
        }
        //set up display
        target.setStyle('visibility', 'hidden');
        var coords = this.options.target.getCoordinates();
        add_template('inline_edit.xml', document.body, this.options);
        this.input = $('inline_text');
        
        $('inline_edit').setStyles({
            'position': 'absolute',
            'border': 'solid 1px #999',
            'top': coords.top,
            'background-color': 'white',
            'left': coords.left,
            'padding-bottom': '3px'
        });
        $('inline_text').setStyles({
            'margin': '0px 0px 10px 0px',
            'text-align': target.getStyle('text-align'),
            'font-size': target.getStyle('font-size'),
            'font-family': target.getStyle('font-family'),
            'border': 'none',
            'width': coords.width,
            'height': (this.options.type == 'textarea')? '200px': coords.height
        });
        
        //set behaviors 
        var textbox_type = (this.options.type == 'textarea')? 'word': 'sender';
        this.attach(this.input, textbox_type);
        
        this.input.addEvent('keyup', this.key_up.bind(this));
        this.input.focus();
        this.input.select();
        
        this.close_wrapper = this.close.bind(this);
        document.addEvent('click', this.close_wrapper);
    },
    
    close: function(event){
        var e = new Event(event);
        if ($(e.target).getParent('#inline_edit') == null){
            if ($(e.target).id != this.options.target.id && $(e.target).id != this.input.id){
                this.destroy();        
            }
        }
    },
    
    destroy: function(saved){
        if (!saved) this.options.target.set('html', this.options.orig.cleanStringHTML());
        
        document.removeEvent('click', this.close_wrapper);
        $('inline_edit').dispose();
        this.options.target.setStyle('visibility', 'visible');
    },
    
    //rename save to work with Textbox
    send: function(){
        this.save();
    },
    
    save: function(){
        var db_info = this.options.fields.split('.');
        if (this.options.save_callback){
            this.options.save_callback.attempt(this);
        } else {
            var db_info = this.options.fields.split('.');
            var text = this.input.get('value').cleanStringHTML();
            new Model(db_info[0]).Update([db_info[1]], [text], 'id=' + this.options.id).Eval();
            //Page Specific, but I'm feeling lazy at the moment
            setup_page(Cookie.read('username').split('&')[1]);
        }
        this.destroy(true);
    },
    
    key_up: function(){
        this.options.target.set('html', this.input.get('value').cleanStringHTML());
        return false;
    }
});

var inline = new Inline();

//this will need to be customized slightly for each website
function add_to_photos(file){
    $(file.name.replace('.', '_')).fade('out');
    (function(){$(file.name.replace('.', '_')).dispose();}.delay(300));
    var new_pic = new Element('li', {
        'id': 'id_' + file.id,
        'html': file.name,
        'events': {
            'click': function(){
                show_pic(file.id);
            }
        },
        'styles': {
            'opacity': 0
        }
    });
    $('list').adopt(new_pic);
    new_pic.fade('in');
}

//TODO -includes
//     -forloop tracking
//     -error checking

var includes = {}
var forloop = {
    'last': false,
    'first': false,
    'counter': 1,
    'counter0': 0,
    'revcounter': 1,
    'revcounter0': 0
}
///////////////////////////////////////////
    //The base object which just stores the template string, and data object
function Template(template, data){
    this.template = template, this.data = data;
    this.render();
}

/////////////////////////////////////////
    //to incorperate new template methods, you have to add the search params here
    //so the blocks of HTML can be picked out
Template.prototype.search_regs = {
    'for': new RegExp('{% *(for|endfor) *%?}?', 'g'),
    'if': new RegExp('{% *(if|else|endif) *%?}?', 'g')
}

Template.prototype.match_vars = {
    'for': {'plus': 'for', 'neutral': '', 'minus': 'endfor'},
    'if': {'plus': 'if', 'neutral': 'else', 'minus': 'endif'}
}

/////////////////////////////////////////
    //The main rendering loop, searches out {{ }} and {% %} blocks
Template.prototype.render = function(){
    var reg = new RegExp(/{/g);
    var result = null;
    while (result = reg.exec(this.template)){
        if (this.template.charAt(result.index + 1) == "{"){
            this.set_value();
        } else if (this.template.charAt(result.index + 1) == "%"){
            this.run_method(result.index + 2);
        }
    }
    return this.template;
}

////////////////////////////////////////
    //The function for turning a {{ }} variable into a value
Template.prototype.set_value = function(){
    try{
        if (this.template.split(/{{/).length > 1){
            var text = this.template.split(/{{/)[1].split(/}}/)[0].split('.');
        } else {
            var text = this.template.split(/{{/)[0].split(/}}/)[0].split('.');   
        }
        var temp_data = this.data;
        for (var i=0; i<text.length; i++) temp_data = temp_data[text[i].clean()];
        var reg = new RegExp('{{ *' + text.join('.') + ' *}}');
        this.template = this.template.replace(reg, temp_data);
    } catch(e) {
        alert(' there is a problem with ' + text);
    }
}

///////////////////////////////////////
    //if a method is found, this function breaks up the args and dispatches them to the appropriate function
Template.prototype.run_method = function(start){
     var end = this.template.search(/%}/);
     var args = this.template.substring(start, end).clean().split(' ');
     switch (args[0].clean())
     {
        case 'for':
            this.run_forloop(args, start - 2, end + 2);
            break
        case 'if':
            this.if_block(args, start - 2, end + 2);
            break
        default:
            break
    }
}

//////////////////////////////////////
    //this is the function that parses the template and extracts method blocks
Template.prototype.get_sub = function(type, start){
    var match_counter = 0, match = null, results = {};
    var reg = this.search_regs[type];
    var match_vars = this.match_vars[type];
    reg.lastIndex = start;
    while (match = reg.exec(this.template)){
        if (match[1] == match_vars['plus']){
            match_counter++;
        } else if (match[1] == match_vars['neutral'] || match[1] == match_vars['minus']) {
            if (match_counter == 0){
                results[match[1]] = match.index + match[0].length;
                results[match[1] + '_sub'] = this.template.substring(start, match.index);
                if (match[1] == match_vars['neutral']) start = match.index + match[0].length;
                if (match[1] == match_vars['minus']) break;
            }
            if (match[1] == match_vars['minus']) match_counter--;
        }
    }
    return results;
}

////////////////////////////////////////////
   //Methods (where the magic happens)//
///////////////////////////////////////////

///////////////////////////////////////
    //the function for parsing for loops. Loops over arrays, or key/value objects are supported
Template.prototype.run_forloop = function(args, start, end){
    var results = this.get_sub('for', end);
    var replacement = '';
    var temp_data = this.data;
    if (args.length == 4){ //simple loop over array i.e. {% for x in y %}
        for (var i=0; i<args[3].split('.').length; i++) temp_data = temp_data[args[3].split('.')[i]];
        for (var i=0; i<temp_data.length; i++){
            this.data[args[1]] = temp_data[i];
            var piece = new Template(results['endfor_sub'], this.data);
            replacement += piece.template;
        }
    } else { //loop over key/value object i.e. {% for x, y in z %}
        for (var i=0; i<args[4].split('.').length; i++) temp_data = temp_data[args[4].split('.')[i]];
        for (var key in temp_data){
            this.data[args[1].replace(',', '')] = key;
            this.data[args[2]] = temp_data[key];
            var piece = new Template(results['endfor_sub'], this.data);
            replacement += piece.template;
        }
    }
    this.template = this.template.replace(this.template.substring(start, results['endfor']), replacement);
}

////////////////////////////////////////
    //if blocks
Template.prototype.if_block = function(args, start, end){
    var results = this.get_sub('if', end);
    var replacement = '';
    var first_value = this.data;
    for (var i=0; i<args[1].split('.').length; i++) first_value = first_value[args[1].split('.')[i]];
    //TODO refactor down until if (truth)
    if (!args[2]){
        var truth = this.eval_truth(first_value);
    } else if (args[2] == 'in'){ //check to see if value is in an array
        if (args[3] == 'var'){
            var arr = this.data;
            for (var i=0; i<args[4].split('.').length; i++) arr = arr[args[4].split('.')[i]];
        } else {
            var arr = args[3];
        }
        if ($type(first_value) == 'object'){
            var truth = this.check_object(first_value, arr);
        } else {
            var truth = arr.contains(first_value);
        }
    } else {
        if (args[3] == 'var'){
            var second_value = this.data;
            for (var i=0; i<args[4].split('.').length; i++) second_value = second_value[args[4].split('.')[i]];
        } else {
            var second_value = args[3];
        }
        var truth = eval("'" + first_value + "'" + args[2] + "'" + second_value + "'");
    }
    if (truth){
        var piece = (results['else_sub'])? new Template(results['else_sub'], this.data): new Template(results['endif_sub'], this.data);
        replacement += piece.template;
    } else {
        var piece = (results['else_sub'])? new Template(results['endif_sub'], this.data): new Template('', this.data);
        replacement += piece.template;
    }
    this.template = this.template.replace(this.template.substring(start, results['endif']), replacement);
}

////////////////////////////////////////
    //check for object in array of objects
    //should change but right now
    //it just compares the id property
Template.prototype.check_object = function(obj, arr){
    for (var i=0; i<arr.length; i++){
        if (obj.id == arr[i].id) return true;
    }
    return false;
}
////////////////////////////////////////
    //evals the truth of an object
    //currently supports the standards
    //i.e. null == false, undefined == false ect...
    //plus an empty array is false and 
    //an empty string is false
Template.prototype.eval_truth = function(value){
    var truth = null;
    switch ($type(value)){
        case 'array': 
            truth = (value.length >= 1);
            break;
        case 'string': 
            truth = (value.clean() != '');
            break;
        default:
            truth = (value)
    }
    return truth;
}

var xml_cache = {}

function string_to_xml(text){
    var xmlDoc = null;
    try {
        xmlDoc=new ActiveXObject("Microsoft.XMLDOM");
        xmlDoc.async="false";
        xmlDoc.loadXML(text);
    } catch(e) {
        try {
            parser=new DOMParser();
            xmlDoc=parser.parseFromString(text,"text/xml");
        } catch(e) {
            alert(e.message);
            return;
        }
    }
    return xmlDoc;
}

function add_template(xml_name, parent, data){
    var request = {};
    request['file_name'] = xml_name;
    var options = {
        method:    'post',
        async:     false,
        url:       'php/echo.php',
        data:      request,
        onSuccess: function(text){
            xml_cache[xml_name] = text;
            if (data != null) text = new Template(text, data);
            var xmlDoc = string_to_xml(text.template || text);
            var html = xmlDoc.getElementsByTagName('root')[0];
            add_xhtml(html, $(parent));
        }
    }
    if (xml_cache[xml_name]){
        var text = (data != null)? new Template(xml_cache[xml_name], data): xml_cache[xml_name];
        var xmlDoc = string_to_xml(text.template || text);
        var html = xmlDoc.getElementsByTagName('root')[0];
        add_xhtml(html, $(parent));
    } else {
        var submit = new Request(options).send();
    }
}

function add_xhtml(object, parent){
    for (var i=0; i<object.childNodes.length; i++){
        var node = null;
        if (object.childNodes[i].nodeName == "#text"){
            var text = (object.childNodes[i].nodeValue == null)? '': object.childNodes[i].nodeValue;
            if (text.clean() != ''){
                text = text.replace(/\t/g, '\u0009');
                text = text.replace(/\n/g, '\u000D');
            }
            node = document.createTextNode(text);
        } else {
            if (Browser.Engine.trident){
                var nodename = object.childNodes[i].nodeName;
                var obj = "<" + nodename + " ";
                for (var j=0; j<object.childNodes[i].attributes.length; j++){
                    obj += object.childNodes[i].attributes[j].nodeName + "=\"" + unescape(object.childNodes[i].attributes[j].nodeValue) + "\" ";
                }
                if (nodename == ('br' || 'input' || 'img')){
                    obj += '/>';
                } else {
                    obj += "><" + nodename + " />";
                }
                node = document.createElement(obj);
            } else {
                var properties = {}
                for (var j=0; j<object.childNodes[i].attributes.length; j++){
                    properties[object.childNodes[i].attributes[j].nodeName] = unescape(object.childNodes[i].attributes[j].nodeValue);
                }
                node = new Element(object.childNodes[i].nodeName, properties);
            }
        }
        parent.appendChild(node);
        if (object.childNodes[i].childNodes.length > 0){
            add_xhtml(object.childNodes[i], node);
        }
    }
}


var DB = new Class({
    table: null,
    onQueryBuild: $empty,
    
    Select: function(fields, where, extra){
        var query = {
            'action': 'select',
            'table': this.table,
            'fields': fields || ['*'],
            'where': where,
            'extra': extra
        }
        this.Q.push(query);
        return this;
    },
    Update: function(fields, values, where, extra){
        var query = {
            'action': 'update',
            'table': this.table,
            'fields': fields,
            'values': values,
            'where': where,
            'extra': extra            
        }
        this.Q.push(query);
        return this;
    },
    
    MultiUpdate: function(field, check_field, field_values, input_values){
        if (input_values.length != field_values.length){alert('you have a problem with your multiUpdate values'); return false;}
        var query = {
            'action': 'multiUpdate',
            'table': this.table,
            'field': field,
            'check_field': check_field,
            'field_values': field_values,
            'input_values': input_values
        }
        this.Q.push(query);
        return this;
    },
    
    Insert: function(fields, values, where){
        var action = (where)? 'insert_unique': 'insert';
        var query = {
            'action': action,
            'table': this.table,
            'fields': fields,
            'values': values,
            'where': where
        }
        this.Q.push(query);
        return this;
    },
    
    Delete: function(where){
        var query = {
            'action': 'delete',
            'table': this.table,
            'where': where
        }
        this.Q.push(query);
        return this;
    }, 
    
    SubmitQuery: function(data, notify){
        var request = {};
        request['data'] = ($type(data) != 'array')? new Array(data): data;
        var options = {
            method:    'post',
            url:       'php/DB.php',
            data:      request,
            async:     false,
            onRequest: function() {
            
            },
            onSuccess: function(resp) {
                if (notify){
                    alert(resp);
                }
            },
            onFailure: function() {
                
            }
        };
        alert('que?');
        var submit = new Request(options).send();
        alert(submit.response);
        return JSON.decode(submit.response['text']);
    }
}); 

var Query = new Class({
    Extends: DB,
    Q: [],
    initialize: function(){
        for (var i=0; i<arguments.length; i++){
            this.Q.push(arguments[i].Q);
        }
        this.Q = this.Q.flatten();
    },
    
    add: function(query){
        this.Q.push(query);
    },    
    
    flush: function(){
        this.Q = [];
    },
    
    Eval: function(notify){
        var result = this.SubmitQuery(this.Q, notify);
        this.Q = [];
        return result;
    }
});

var Model = new Class({
    Extends: Query,
    table: null,
    
    initialize: function(table, options){
        if (!table) alert('you must choose a table');
        this.options = $merge(this.options, options);
        this.table = table;
    }
});


//taken from  http://www.csie.ntu.edu.tw/~b88039/html/jslib/caret.html
function caretPos(node) {
    node.focus(); 
    /* without node.focus() IE will returns -1 when focus is not on node */
    if(node.selectionStart) return node.selectionStart;
    else if(!document.selection) return 0;
    var c = "\001";
    var sel	= document.selection.createRange();
    var dul	= sel.duplicate();
    var len	= 0;
    var OperaError = false;
    try{
        dul.moveToElementText(node);
    } catch(e){
        //Opera fucks up here if caret is at the begining of the input
        OperaError = true;
    }
    sel.text	= c;
    len		= (node.get('text').indexOf(c));
    if (OperaError) len++;
    sel.moveStart('character',-1);
    sel.text	= "";
    return len;
}

function add_text(target, str){
    var pos = caretPos(target);
    var text = target.get('value');
    
    var start = text.substring(0, pos);
    var end = text.substring(pos);
    target.set('value', start + str + end);
    target.focus();
    
    if (Browser.Engine.trident){
        var range = target.createTextRange();
        range.collapse(true);
        range.moveEnd('character', pos + str.length);
        range.moveStart('character', pos + str.length);
        range.select();
    } else {
        target.setSelectionRange(pos + str.length, pos + str.length);
    }
}

//function for creating unobtrusive pop-ups
function Notification(message, return_value, options){
    var default_options = {
        'xml': 'notification.xml',
        'class': 'standard_message',
        'delay': 3000
    }
    
    options = $merge(default_options, options);
    
    if ($('notification') != null) $('notification').dispose();
    add_template(options.xml, document.body, {'message': message});
    
    $('notification').setStyles({
        'left': (window.getSize().x / 2) - ($('notification').getSize().x / 2),
        'top': '5%'
    });
    if (options.delay != 'wait'){
        (function(){$('notification').fade('out');}).delay(options['delay'] - 250);
        (function(){$('notification').dispose();}).delay(options['delay']);
    }
    
    if ($type(return_value) == 'function') 
        return return_value.attempt();
    else 
        return return_value;
}

//function for generating a random key
function newKey(length){
     var table = {
        0: '0', 1: '1', 2: '2', 3: '3', 4: '4',
        5: '5', 6: '6', 7: '7', 8: '8', 9: '9',
        10: 'A', 11: 'B', 12: 'C', 13: 'D', 14: 'E',
        15: 'F', 16: 'G', 17: 'F', 18: 'H', 19: 'I',
        20: 'J', 21: 'K', 22: 'L', 23: 'M', 24: 'N',
        25: 'O', 26: 'P', 27: 'Q', 28: 'R', 29: 'S',
        30: 'T', 31: 'U', 32: 'V', 33: 'W', 34: 'X',
        35: 'Y', 36: 'Z'
    }
    
    var len = length || 10;
    var key = '';
    for (var i=0; i<len; i++){
        key += table[$random(0, 36)];
    }
    return key;    
}

//Prototypes
String.prototype.cleanStringJS = function(){
    var str = this;
    //Opera's first character is always a linebreak for some reason, probably because of the html
    if (Browser.Engine.presto && str.charCodeAt(0) == 13) str = str.replace('\u000d', '')
    str = (Browser.Engine.presto)? str: str.clean();
    str = str.replace(/< *br *\/?>/gi, '\u000D').replace(/((&nbsp;)+|(\u00A0)+)/g, '\u0009').replace(/^\u0020+|\u0020+$/g, '');
    return str;
}
    
String.prototype.cleanStringHTML = function(){
    var str = this;
    if (Browser.Engine.trident || Browser.Engine.presto){
        str =  str.replace(/\u000D/g, '<br />').replace(/\u0009/g, String.fromCharCode(160, 160, 160, 160));
        str = str.replace(/\n|\t/g, '');
    } else {
        str = str.replace(/\u000D|\n/g, '<br />').replace(/\u0009|\t/g, String.fromCharCode(160, 160, 160, 160));
    }
    return str;
}

Element.prototype.hide = function(){
    this.setStyle('display', 'none');
    return this;
}

Element.prototype.show = function(){
    this.setStyle('display', '');
    return this;
}

String.prototype.count = function(string){
    var reg = new RegExp(string, 'g');
    return this.match(reg).length;
}