(function () {


    var rsplit = function (string, regex) {
        var result = regex.exec(string), retArr = new Array(), first_idx, last_idx, first_bit;
        while (result != null) {
            first_idx = result.index; last_idx = regex.lastIndex;
            if ((first_idx) != 0) {
                first_bit = string.substring(0, first_idx);
                retArr.push(string.substring(0, first_idx));
                string = string.slice(first_idx);
            }
            retArr.push(result[0]);
            string = string.slice(result[0].length);
            result = regex.exec(string);
        }
        if (!string == '') {
            retArr.push(string);
        }
        return retArr;
    },
chop = function (string) {
    return string.substr(0, string.length - 1);
},
extend = function (d, s) {
    for (var n in s) {
        if (s.hasOwnProperty(n)) d[n] = s[n]
    }
}


    EJS = function (options) {
        options = typeof options == "string" ? { view: options} : options
        this.set_options(options);
        if (options.precompiled) {
            this.template = {};
            this.template.process = options.precompiled;
            EJS.update(this.name, this);
            return;
        }
        if (options.element) {
            if (typeof options.element == 'string') {
                var name = options.element
                options.element = document.getElementById(options.element)
                if (options.element == null) throw name + 'does not exist!'
            }
            if (options.element.value) {
                this.text = options.element.value
            } else {
                this.text = options.element.innerHTML
            }
            this.name = options.element.id
            this.type = '['
        } else if (options.url) {
            options.url = EJS.endExt(options.url, this.extMatch);
            this.name = this.name ? this.name : options.url;
            var url = options.url
            //options.view = options.absolute_url || options.view || options.;
            var template = EJS.get(this.name /*url*/, this.cache);
            if (template) return template;
            if (template == EJS.INVALID_PATH) return null;
            try {
                this.text = EJS.request(url + (this.cache ? '' : '?' + Math.random()));
            } catch (e) { }

            if (this.text == null) {
                throw ({ type: 'EJS', message: 'There is no template at ' + url });
            }
            //this.name = url;
        }
        var template = new EJS.Compiler(this.text, this.type);

        template.compile(options, this.name);


        EJS.update(this.name, this);
        this.template = template;
    };
    /* @Prototype*/
    EJS.prototype = {
        /**
        * Renders an object with extra view helpers attached to the view.
        * @param {Object} object data to be rendered
        * @param {Object} extra_helpers an object with additonal view helpers
        * @return {String} returns the result of the string
        */
        render: function (object, extra_helpers) {
            object = object || {};
            this._extra_helpers = extra_helpers;
            var v = new EJS.Helpers(object, extra_helpers || {});
            return this.template.process.call(object, object, v);
        },
        update: function (element, options) {
            if (typeof element == 'string') {
                element = document.getElementById(element)
            }
            if (options == null) {
                _template = this;
                return function (object) {
                    EJS.prototype.update.call(_template, element, object)
                }
            }
            if (typeof options == 'string') {
                params = {}
                params.url = options
                _template = this;
                params.onComplete = function (request) {
                    var object = eval(request.responseText)
                    EJS.prototype.update.call(_template, element, object)
                }
                EJS.ajax_request(params)
            } else {
                element.innerHTML = this.render(options)
            }
        },
        out: function () {
            return this.template.out;
        },
        /**
        * Sets options on this view to be rendered with.
        * @param {Object} options
        */
        set_options: function (options) {
            this.type = options.type || EJS.type;
            this.cache = options.cache != null ? options.cache : EJS.cache;
            this.text = options.text || null;
            this.name = options.name || null;
            this.ext = options.ext || EJS.ext;
            this.extMatch = new RegExp(this.ext.replace(/\./, '\.'));
        }
    };
    EJS.endExt = function (path, match) {
        if (!path) return null;
        match.lastIndex = 0
        return path + (match.test(path) ? '' : this.ext)
    }




    /* @Static*/
    EJS.Scanner = function (source, left, right) {

        extend(this,
        { left_delimiter: left + '%',
            right_delimiter: '%' + right,
            double_left: left + '%%',
            double_right: '%%' + right,
            left_equal: left + '%=',
            left_comment: left + '%#'
        })

        this.SplitRegexp = left == '[' ? /(\[%%)|(%%\])|(\[%=)|(\[%#)|(\[%)|(%\]\n)|(%\])|(\n)/ : new RegExp('(' + this.double_left + ')|(%%' + this.double_right + ')|(' + this.left_equal + ')|(' + this.left_comment + ')|(' + this.left_delimiter + ')|(' + this.right_delimiter + '\n)|(' + this.right_delimiter + ')|(\n)');

        this.source = source;
        this.stag = null;
        this.lines = 0;
    };

    EJS.Scanner.to_text = function (input) {
        if (input == null || input === undefined)
            return '';
        if (input instanceof Date)
            return input.toDateString();
        if (input.toString)
            return input.toString();
        return '';
    };

    EJS.Scanner.prototype = {
        scan: function (block) {
            scanline = this.scanline;
            regex = this.SplitRegexp;
            if (!this.source == '') {
                var source_split = rsplit(this.source, /\n/);
                for (var i = 0; i < source_split.length; i++) {
                    var item = source_split[i];
                    this.scanline(item, regex, block);
                }
            }
        },
        scanline: function (line, regex, block) {
            this.lines++;
            var line_split = rsplit(line, regex);
            for (var i = 0; i < line_split.length; i++) {
                var token = line_split[i];
                if (token != null) {
                    try {
                        block(token, this);
                    } catch (e) {
                        throw { type: 'EJS.Scanner', line: this.lines };
                    }
                }
            }
        }
    };


    EJS.Buffer = function (pre_cmd, post_cmd) {
        this.line = new Array();
        this.script = "";
        this.pre_cmd = pre_cmd;
        this.post_cmd = post_cmd;
        for (var i = 0; i < this.pre_cmd.length; i++) {
            this.push(pre_cmd[i]);
        }
    };
    EJS.Buffer.prototype = {

        push: function (cmd) {
            this.line.push(cmd);
        },

        cr: function () {
            this.script = this.script + this.line.join('; ');
            this.line = new Array();
            this.script = this.script + "\n";
        },

        close: function () {
            if (this.line.length > 0) {
                for (var i = 0; i < this.post_cmd.length; i++) {
                    this.push(pre_cmd[i]);
                }
                this.script = this.script + this.line.join('; ');
                line = null;
            }
        }

    };


    EJS.Compiler = function (source, left) {
        this.pre_cmd = ['var ___ViewO = [];'];
        this.post_cmd = new Array();
        this.source = ' ';
        if (source != null) {
            if (typeof source == 'string') {
                source = source.replace(/\r\n/g, "\n");
                source = source.replace(/\r/g, "\n");
                this.source = source;
            } else if (source.innerHTML) {
                this.source = source.innerHTML;
            }
            if (typeof this.source != 'string') {
                this.source = "";
            }
        }

        //shurik1251
        this.source = this.source.replace(/&lt;/g, '<').replace(/&gt;/g, '>');

        left = left || '<';
        var right = '>';
        switch (left) {
            case '[':
                right = ']';
                break;
            case '<':
                break;
            default:
                throw left + ' is not a supported deliminator';
                break;
        }
        this.scanner = new EJS.Scanner(this.source, left, right);
        this.out = '';
    };
    EJS.Compiler.prototype = {
        compile: function (options, name) {
            options = options || {};
            this.out = '';
            var put_cmd = "___ViewO.push(";
            var insert_cmd = put_cmd;
            var buff = new EJS.Buffer(this.pre_cmd, this.post_cmd);
            var content = '';
            var clean = function (content) {
                content = content.replace(/\\/g, '\\\\');
                content = content.replace(/\n/g, '\\n');
                content = content.replace(/"/g, '\\"');
                return content;
            };
            this.scanner.scan(function (token, scanner) {
                if (scanner.stag == null) {
                    switch (token) {
                        case '\n':
                            content = content + "\n";
                            buff.push(put_cmd + '"' + clean(content) + '");');
                            buff.cr();
                            content = '';
                            break;
                        case scanner.left_delimiter:
                        case scanner.left_equal:
                        case scanner.left_comment:
                            scanner.stag = token;
                            if (content.length > 0) {
                                buff.push(put_cmd + '"' + clean(content) + '")');
                            }
                            content = '';
                            break;
                        case scanner.double_left:
                            content = content + scanner.left_delimiter;
                            break;
                        default:
                            content = content + token;
                            break;
                    }
                }
                else {
                    switch (token) {
                        case scanner.right_delimiter:
                            switch (scanner.stag) {
                                case scanner.left_delimiter:
                                    if (content[content.length - 1] == '\n') {
                                        content = chop(content);
                                        buff.push(content);
                                        buff.cr();
                                    }
                                    else {
                                        buff.push(content);
                                    }
                                    break;
                                case scanner.left_equal:
                                    buff.push(insert_cmd + "(EJS.Scanner.to_text(" + content + ")))");
                                    break;
                            }
                            scanner.stag = null;
                            content = '';
                            break;
                        case scanner.double_right:
                            content = content + scanner.right_delimiter;
                            break;
                        default:
                            content = content + token;
                            break;
                    }
                }
            });
            if (content.length > 0) {
                // Chould be content.dump in Ruby
                buff.push(put_cmd + '"' + clean(content) + '")');
            }
            buff.close();
            this.out = buff.script + ";";
            var to_be_evaled = '/*' + name + '*/this.process = function(_CONTEXT,_VIEW) { try { with(_VIEW) { with (_CONTEXT) {' + this.out + " return ___ViewO.join('');}}}catch(e){e.lineNumber=null;throw e;}};";

            try {
                eval(to_be_evaled);
            } catch (e) {
                if (typeof JSLINT != 'undefined') {
                    JSLINT(this.out);
                    for (var i = 0; i < JSLINT.errors.length; i++) {
                        var error = JSLINT.errors[i];
                        if (error.reason != "Unnecessary semicolon.") {
                            error.line++;
                            var e = new Error();
                            e.lineNumber = error.line;
                            e.message = error.reason;
                            if (options.view)
                                e.fileName = options.view;
                            throw e;
                        }
                    }
                } else {
                    throw e;
                }
            }
        }
    };


    //type, cache, folder
    /**
    * Sets default options for all views
    * @param {Object} options Set view with the following options
    * <table class="options">
    <tbody><tr><th>Option</th><th>Default</th><th>Description</th></tr>
    <tr>
    <td>type</td>
    <td>'<'</td>
    <td>type of magic tags.  Options are '&lt;' or '['
    </td>
    </tr>
    <tr>
    <td>cache</td>
    <td>true in production mode, false in other modes</td>
    <td>true to cache template.
    </td>
    </tr>
    </tbody></table>
    * 
    */
    EJS.config = function (options) {
        EJS.cache = options.cache != null ? options.cache : EJS.cache;
        EJS.type = options.type != null ? options.type : EJS.type;
        EJS.ext = options.ext != null ? options.ext : EJS.ext;

        var templates_directory = EJS.templates_directory || {}; //nice and private container
        EJS.templates_directory = templates_directory;
        EJS.get = function (path, cache) {
            if (cache == false) return null;
            if (templates_directory[path]) return templates_directory[path];
            return null;
        };

        EJS.update = function (path, template) {
            if (path == null) return;
            templates_directory[path] = template;
        };

        EJS.INVALID_PATH = -1;
    };
    EJS.config({ cache: true, type: '<', ext: '.ejs' });



    /**
    * @constructor
    * By adding functions to EJS.Helpers.prototype, those functions will be available in the 
    * views.
    * @init Creates a view helper.  This function is called internally.  You should never call it.
    * @param {Object} data The data passed to the view.  Helpers have access to it through this._data
    */
    EJS.Helpers = function (data, extras) {
        this._data = data;
        this._extras = extras;
        extend(this, extras);
    };
    /* @prototype*/
    EJS.Helpers.prototype = {
        /**
        * Renders a new view.  If data is passed in, uses that to render the view.
        * @param {Object} options standard options passed to a new view.
        * @param {optional:Object} data
        * @return {String}
        */
        view: function (options, data, helpers) {
            if (!helpers) helpers = this._extras
            if (!data) data = this._data;
            return new EJS(options).render(data, helpers);
        },
        /**
        * For a given value, tries to create a human representation.
        * @param {Object} input the value being converted.
        * @param {Object} null_text what text should be present if input == null or undefined, defaults to ''
        * @return {String} 
        */
        to_text: function (input, null_text) {
            if (input == null || input === undefined) return null_text || '';
            if (input instanceof Date) return input.toDateString();
            if (input.toString) return input.toString().replace(/\n/g, '<br />').replace(/''/g, "'");
            return '';
        }
    };
    EJS.newRequest = function () {
        var factories = [function () { return new ActiveXObject("Msxml2.XMLHTTP"); }, function () { return new XMLHttpRequest(); }, function () { return new ActiveXObject("Microsoft.XMLHTTP"); } ];
        for (var i = 0; i < factories.length; i++) {
            try {
                var request = factories[i]();
                if (request != null) return request;
            }
            catch (e) { continue; }
        }
    }

    EJS.request = function (path) {
        var request = new EJS.newRequest()
        request.open("GET", path, false);

        try { request.send(null); }
        catch (e) { return null; }

        if (request.status == 404 || request.status == 2 || (request.status == 0 && request.responseText == '')) return null;

        return request.responseText
    }
    EJS.ajax_request = function (params) {
        params.method = (params.method ? params.method : 'GET')

        var request = new EJS.newRequest();
        request.onreadystatechange = function () {
            if (request.readyState == 4) {
                if (request.status == 200) {
                    params.onComplete(request)
                } else {
                    params.onComplete(request)
                }
            }
        }
        request.open(params.method, params.url)
        request.send(null)
    }


})();
