/*
 * Copyright (c) 2012 Samsung Electronics Co., Ltd. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*jslint regexp: true, evil: true, unparam: true, ass: true, nomen: true*/
/*global window, console, App, document*/

/**
 * Template manager module.
 */
App.Ui.Template = null;
App.Ui.Template = function Template() {
    'use strict';

    /**
     * Compiled template cache
     */
    this.cache = {};
    this.document = window.document;
    this.whitespaceRegex = /[\r\t\n]/g;
    this.loopTemplateRegex = /\{\{#([\w]*)\}\}(.*)\{\{\/(\1)\}\}/ig;
    this.conditionTemplateRegex = null;
    this.variablesRegex = /\{\{(.+?)\}\}/g;

    /**
     * Regular expression matching template conditions.
     *
     * \{\{ -opening braces
     * \? -mandatory question mark at the start
     * (typeof\s|[^a-zA-Z0-9]*) -special stuff eg. !, typeof
     * (.+?) -any sequence of chars in the condition
     * \}\} - closing braces
     * (.*) -body
     * \{\{ - opening braces
     * \/ -backslash
     * (\1)(\2) -the same sequence as found before
     * \}\} -closing braces
     *
     * /ig -global, case-insensitive
     *
     */
    this.conditionTemplateRegex =
        /\{\{\?(typeof\s|[^a-zA-Z0-9]*)(.+?)\}\}(.*)\{\{\/(\1)(\2)\}\}/ig;
    return;
};
(function template() {
    'use strict';

    App.Ui.Template.prototype = {
        /**
         * Generates code for template loops.
         * @param {string} match The matched substring.
         * @param {string} $1 First submatch (array variable).
         * @param {string} $2 Second submatch (body).
         * @return {string}
         */
        templateLoop: function templateLoop(match, $1, $2) {
            var loopVariablesRegex = /\{\{this(\..+?)?\}\}/g,
                loopIterationRegex = /\{\{i\}\}/g;

            return '\';var i=0,l=data.' + $1 + '.length,d=data.' + $1 +
                ';for(;i<l;i++){s+=\'' + $2.replace(loopVariablesRegex,
                    '\'+d[i]$1+\'').replace(loopIterationRegex,
                    '\'+i+\'') + '\'}s+=\'';
        },

        /**
         * Generates code for template conditions.
         * @param {string} match The matched substring.
         * @param {string} $1 First submatch (typeof, negation).
         * @param {string} $2 Second submatch (condition).
         * @param {string} $3 Third submatch (body).
         * @return {string}
         */
        templateCondition: function templateCondition(match, $1, $2, $3) {
            var pref = 'data.';

            // Check property condition
            if ($2.match(/this(\.)/g)) {
                pref = 'd[i].';
                $2 = $2.replace(/this(\.)/g, '');
            }

            return '\';if(' + $1 + pref + $2 + '){s+=\'' + $3 + '\'}s+=\'';
        },

        /**
         * Saves compiled template to cache.
         * @param {string} tplName Template name.
         * @param {function} tplData Template data.
         */
        save: function save(tplName, tplData) {
            this.cache[tplName] = tplData;
        },

        /**
         * Compiles a template.
         * @param {string} tplName Template name.
         * @param {string} template Template.
         * @return {function} Compiled template.
         * @throws {Error} Error in template syntax.
         */
        compile: function compile(tplName, template) {
            var content = this.cache[tplName];

            if (!content) {
                // initialize empty string
                content = 'try { var s=\'\';s+=\'' +
                    template.replace(this.whitespaceRegex, ' ')
                        .split('\'').join('\\\'') // escape quotes
                        .replace(/**
                         * Handle loops.
                         *
                         * In the loop, 'i' is the key and 'this' is the value
                         *
                         * Example:
                         *     {{#arr}}<li>key:{{i}} value:{{this}}</li>{{/arr}}
                         */
                        this.loopTemplateRegex,
                        this.templateLoop
                    ).replace(
                        /**
                         * Handle conditions.
                         *
                         * Example:
                         *     {{?logged}}Logged{{/logged}}
                         * becomes:
                         *     if(data.logged){s+='Logged'}
                         *
                         * Some of other possible conditions:
                         *     {{?!variable}}test{{/!variable}}
                         *     {{?typeof variable !== "undefined"}}
                         *         test
                         *     {{/typeof variable !== "undefined"}}
                         *     {{?variable === "value"}}
                         *         test
                         *     {{/variable === "value"}}
                         */
                        this.conditionTemplateRegex,
                        this.templateCondition
                    )
                    /**
                     * Handle other references by adding 'data.'.
                     *
                     * Example:
                     *     {{user.name}}
                     * becomes:
                     *     s+=data.user.name
                     *
                     * \{\{ -opening braces
                        * (.+?) -any sequence of characters
                        * \}\} -closing braces
                     *
                     * /g -global
                     *
                     */.replace(this.variablesRegex, '\'+data.$1+\'') +
                    '\';return s;' + // return the string
                    '} catch (e) {' + ' throw Error(\'Error in template ' +
                    tplName + ': \' + e.message); }';

                content = new Function('data', content);
                this.save(tplName, content);
            }

            return content;
        },

        /**
         * Creates and sends request
         * @param {object} options Options.
         * @param {string} options.url Url.
         * @param {boolean} [options.async=false] Async mode.
         * @param {function} [options.success] Success callback.
         * @param {function} [options.progress] Progress callback.
         * @param {function} [options.error] Error callback.
         * @return {XMLHttpRequest} req Request object.
         */
        request: function request(options) {
            var req = null, async = null, url = null;

            options = options || {};
            async = typeof options.async === 'boolean' ? options.async : false;
            url = options.url !== undefined ? options.url : null;

            if (url === null) {
                console.error('Url is empty, please provide correct url.');
                return;
            }

            req = new window.XMLHttpRequest();
            req.open('GET', url, async);

            req.addEventListener('load', function load() {
                if (typeof options.success === 'function') {
                    options.success(req.response);
                }
            }, false);

            req.addEventListener('error', function error(evt) {
                if (typeof options.error === 'function') {
                    options.error(evt.target.status);
                }
            }, false);

            req.addEventListener('progress', function progress(evt) {
                if (typeof options.progress === 'function') {
                    options.progress(evt);
                }
            }, false);

            req.send();

            return req;
        },

        /**
         * Loads a template using ajax.
         * @param {string} tplName Template name.
         * @param {object} [options] Options.
         * @param {boolean} [options.async=false] Async mode.
         * @param {function} [onSuccess] Success callback.
         * @return {function|undefined}
         */
        loadOne: function loadOne(tplName, options, onSuccess) {
            var tplPath = [
                    'templates', [tplName, '.tpl'].join('')
                ].join('/'),
                tplCompiled = null,
                async = null,
                onReqSuccess = null,
                self = this;

            options = options || {};
            async = typeof options.async === 'boolean' ? options.async : false;

            onReqSuccess = function onReqSuccess(data) {
                tplCompiled = self.compile(tplName, data);
                if (async === false) {
                    if (typeof onSuccess === 'function') {
                        onSuccess();
                    }
                }
            };

            this.request({
                url: tplPath,
                async: async,
                success: onReqSuccess,
                error: function error(textStatus) {
                    console.error(tplPath + ' loading error: ' + textStatus);
                }
            });

            if (async === false) {
                return tplCompiled;
            }
            return undefined;
        },

        /**
         * Loads templates.
         * @memberof core/template
         * @param {string[]} tplNames Template names.
         * @param {object} [options] Options.
         * @param {boolean} [options.async=false] Async mode.
         */
        load: function load(tplNames, options) {
            var i = 0;

            options = options || {};
            options.async =
                    typeof options.async === 'boolean' ? options.async : false;

            if (Array.isArray(tplNames)) {
                for (i = 0; i < tplNames.length; i += 1) {
                    this.loadOne(tplNames[i], options);
                }
            }
        },

        /**
         * Returns template completed by specified params.
         * @param {function} tplCompiled Compiled template.
         * @param {array|object} [tplParams] Template parameters.
         * @return {string} Completed template.
         */
        getCompleted: function getCompleted(tplCompiled, tplParams) {
            /*jshint validthis:true*/
            return tplCompiled.call(this, tplParams);
        },

        /**
         * Returns template in html format.
         * @memberof core/template
         *
         * @example
         * // Variable
         * // test.tpl content: {{foo}}
         * get('test', {foo: '123'}) // returns '123'
         *
         * @example
         * // Variables
         * // test.tpl content: {{foo}} {{bar}}
         * get('test', {foo: '123', bar: 456}) // returns '123 456'
         *
         * @example
         * // Object property
         * // test.tpl content: {{obj.prop}}
         * get('test', {obj: {prop: 'test'}}) // returns 'test'
         *
         * @example
         * // Array element
         * // test.tpl content: {{arr[0]}}
         * get('test', {arr: ['test']}) // returns 'test'
         *
         * @example
         * // Array loop
         * // test.tpl content: {{#arr}}{{i}}-{{this}} {{/arr}}
         * get('test', {arr: ['test', 'test2']}) // returns '0-test 1-test2 '
         *
         * @example
         * // Array loop with prop
         * // test.tpl content: {{#arr}}{{this.prop}} {{/arr}}
         * get('test', {arr: [{'prop': 'test'}, {'prop': 'test2'}]})
         * // returns 'test test2 '
         *
         * @example
         * // Array loop with prop condition
         * // test.tpl content: {{#arr}}{{?this.prop}}test{{/this.prop}}{{/arr}}
         * get('test', {arr: [{'prop': true}, {'prop': false}]})
         * // returns 'test'
         *
         * @example
         * // Condition true
         * // test.tpl content: {{?variable}}test{{/variable}}
         * get('test', {variable: true}) // returns 'test'
         *
         * @example
         * // Condition false
         * // test.tpl content: {{?!variable}}test{{/!variable}}
         * get('test', {variable: false}) // returns 'test'
         *
         * @example
         * // Condition typeof
         * // test.tpl content:
         * // {{?typeof variable !== "undefined"}}
         * //     test
         * // {{/typeof variable !== "undefined"}}
         * get('test', {variable: 'value'}) // returns 'test'
         *
         * @example
         * // Condition variable
         * // test.tpl content:
         * // {{?variable === "value"}}test{{/variable === "value"}}
         * get('test', {variable: 'value'}) // returns 'test'
         *
         * @param {string} tplName Template name.
         * @param {string} [tplParams] Template parameters.
         * @return {string} Completed template.
         */
        get: function get(tplName, tplParams) {
            var tplCompiled = this.cache[tplName] || this.loadOne(tplName);
            return this.getCompleted(tplCompiled, tplParams);
        },

        /**
         * Returns first HTML element from completed template.
         * @memberof core/template
         * @param {string} tplName Template name.
         * @param {string} [tplParams] Template parameters.
         * @return {HTMLElement} First element from the completed template.
         */
        getElement: function getElement(tplName, tplParams) {
            var html = this.get(tplName, tplParams),
                tempElement = document.createElement('div');

            tempElement.innerHTML = html;
            return tempElement.firstChild;
        },

        /**
         * Returns completed template as DocumentFragment.
         * @memberof core/template
         * @param {string} tplName Template name.
         * @param {string} [tplParams] Template parameters.
         * @return {DocumentFragment} First element from the completed template.
         */
        getAsFragment: function getAsFragment(tplName, tplParams) {
            var html = this.get(tplName, tplParams),
                tempElement = document.createElement('div'),
                fragment = document.createDocumentFragment();

            tempElement.innerHTML = html;

            while (tempElement.firstChild) {
                fragment.appendChild(tempElement.firstChild);
            }
            return fragment;
        },

        /**
         * Returns the compiled template.
         * @memberof core/template
         * @param {string} tplName Template name.
         * @return {function} Compiled template.
         */
        getCompiled: function getCompiled(tplName) {
            return this.cache[tplName] || this.loadOne(tplName);
        }
    };
}());
