/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

"use strict";

// Constants
var DEBUG = false;
var DEBUG_WWW_URL = 'http://localhost/~ghis/momo/www/';
var ANIMATION_ENABLED = true;
var ANIMATION_OUT_CLASS  = 'pt-page-moveToLeftEasing pt-page-ontop';
var ANIMATION_IN_CLASS = 'pt-page-moveFromRight';
var ANIMATION_BACK_OUT_CLASS  = 'pt-page-moveToRightEasing pt-page-ontop';
var ANIMATION_BACK_IN_CLASS = 'pt-page-moveFromLeft';
var ON_PULL = 'checkForUpdate'; // || 'update'
var CHECK_FOR_CONNECTION_INTERVAL = 3000;
var AJAX_TIMEOUT = 5000;

// Application
var app = {

    // Pages Registry
    pages: {},

    // Minimal Default Manifest
    manifest: {
        meta: {
            'title': 'Momo Application',
            'icon': 'icon.png',
            'contact': 'contact@cadoles.com',
            'manifestUrl': 'index.json',
            'assetsUrl': 'assets.zip',
            'updateFreq': 60000,
            'default': true,
            'titlePersitent': true,
            'titleSeparator': "<br>",
            'menu': [],
            'stylesheets': [],
            'javascripts': []
        },
        'menu': [],
        'stylesheets': [],
        'javascripts': [],
        'content': tmpl('momo-first-launch-tmpl', {}),
        'footer': undefined
    },

    // Default page attributes
    defaultPage: {
        colxs: 4,
        colsm: 3,
        colmd: 2,
        collg: 1,
        menu: [],
        javascripts: [],
        stylesheets: []
    },

    // Misc Data
    online        : false,
    current       : 0,
    isAnimating   : false,
    endCurrPage   : false,
    endNextPage   : false,
    pageIndex     : 0,
    pageHistory   : [/*window.location.hash.slice(1)*/],
    historyLength : window.history.length,
    hasStarted    : false,
    assetsMtime   : null,
    manifestMtime : null,
    updateTimeout : null,
    ignoreHash    : false,
    previousPage  : 'home',
    currentPage   : 'home',
    parentPage    : 'home',
    javascripts   : [],
    stylesheets   : [],
    rootPath      : DEBUG_WWW_URL, //TODO Remove useless calls to fileSystem

    // Application Constructor
    initialize: function() {
        this.bindEvents();
    },

    // Application events
    bindEvents: function() {

        // Navigation Handler
        window.addEventListener('hashchange', this.onHashChange, false);

        // Device Handler
        window.isphone = false;
        if(document.URL.indexOf("http://") === -1 && document.URL.indexOf("https://") === -1) {
            window.isphone = true;
        }

        // Phone Context
        if( window.isphone ) { 
            document.addEventListener("deviceready", this.onDeviceReady, false);
        // Testing Context
        } else { 
            this.onDeviceReady();
        }
    },

    // Device ready callback
    onDeviceReady: function() {
        var request = new XMLHttpRequest();
        request.open('GET', './index.json');
        request.onload = function() {
            if (request.status >= 200 && request.status < 400 || request.status === 0 /* iOS OhMyBuddha!! */) {
                var new_manifest = JSON.parse(this.responseText);
                for (var attr in new_manifest.meta) {
                    if (new_manifest.meta.hasOwnProperty(attr)) {
                        app.manifest.meta[attr] = new_manifest.meta[attr];
                    }
                }
                app.manifest.title = new_manifest.title;
                app.manifest.content = new_manifest.content;
                app.manifest.pages = new_manifest.pages;
                app.manifest.footer = new_manifest.footer;
                app.onDefaultManifestLoaded();
            } else {
                /* this should never happen */
                app.utils.setLoadingMsg("Initialisation de l'application : Erreur de chargement");
            }
        };
        request.timeout = AJAX_TIMEOUT;
        request.ontimeout = function(){
            app.utils.setLoadingMsg("Initialisation de l'application : Temps imparti dépassé");
        };
        request.send();
    },

    onDefaultManifestLoaded: function() {
        app.initApplication();
    },

    initApplication: function() {
        // Init fileSystem
        app.initFileSystem();

        // Backup assets
        app.backupAssets();

        // Init search engine index
        app.initIndex();

        // Load manifest from localStorage
        app.loadLocalManifest();

        // Check for new updates
        //app.checkForUpdate(app.start, app.start);

        // Regulary check for connection
        setInterval( app.checkConnection, CHECK_FOR_CONNECTION_INTERVAL );

        // Update Reminder
        app.updateTimeout = setTimeout( app.checkForLastUpdateCheck, app.manifest.meta.updateFreq );

        // Touch events faster response patch
        FastClick.attach(document.body);

        // Start Application
        app.start();
    },

    initFileSystem: function(){
        // Chrome patch
        window.requestFileSystem  = window.requestFileSystem || window.webkitRequestFileSystem;

        // Register rootPath
        if(typeof FileTransfer !== 'undefined' && typeof zip !== 'undefined' && typeof window.requestFileSystem === 'function'){
            if(DEBUG){ console.log('FileSystem access'); }
            app.rootPath = cordova.file.dataDirectory;
        } else {
            if(DEBUG){ console.log('FileSystem unavaible'); }
            app.rootPath = DEBUG_WWW_URL;
        }
    },

    // Backup permanent assets
    backupAssets: function(){
        var els, i, l;
        els = document.getElementsByTagName("script");
        for(i = 0, l = els.length; i < l; i++) {
            app.javascripts.push(els[i].src);
        }
        els = document.getElementsByTagName("link");
        for(i = 0, l = els.length; i < l; i++) {
            app.stylesheets.push(els[i].href);
        }
    },

    // Initialize search engine index
    initIndex: function(){
        app.index = lunr(function () {
            this.use(lunr.fr);
            this.field('title', {boost: 10});
            this.field('keywords', {boost: 5});
            this.field('content');
            this.ref('id');
            this.pipeline.add(function (token, tokenIndex, tokens) {
                if(token.length > 2){
                    return app.utils.replaceAccents(token);
                }
            });
        });
    },

    checkConnection: function(resolve, reject){
        try {
            switch(navigator.network.connection.type){
                case Connection.ETHERNET:
                case Connection.WIFI:
                case Connection.CELL_4G:
                case Connection.CELL_3G:
                    app.online = true;
                    if(typeof resolve === "function"){
                        resolve();  
                    }
                    break;
                case Connection.CELL_2G:
                case Connection.NONE:
                case Connection.UNKNOWN:
                default:
                    app.online = false;
                    if(typeof reject === "function"){
                        reject();  
                    }
                    break;
            }
        } catch(e) {
            if(navigator.onLine){
                app.online = true;
                if(typeof resolve === "function"){
                    resolve();  
                }
            } else {
                app.online = false;
                if(typeof reject === "function"){
                    reject();  
                }
            }
        }
        // Hide offline specific elements
        var i, elements;
        elements = document.getElementsByClassName("offline-hidden");
        for (i = 0; i < elements.length; i++){
            if(app.online){
                elements[i].classList.remove('hidden');
            } else {
                elements[i].classList.add('hidden');
            }
        }
        // Show offline specific elements
        var i, elements;
        elements = document.getElementsByClassName("offline-visible");
        for (i = 0; i < elements.length; i++){
            if(app.online){
                elements[i].classList.add('hidden');
            } else {
                elements[i].classList.remove('hidden');
            }
        }
        // Disable offline specific elements
        elements = document.getElementsByClassName("offline-disable");
        for (i = 0; i < elements.length; i++){
            if(app.online){
                elements[i].classList.remove('disabled');
            } else {
                elements[i].classList.add('disabled');
            }
        }
        return app.online;
    },

    // Check for last update check
    checkForLastUpdateCheck: function(resolve, reject){
        // Checklast Update
        var lastUpdate   = localStorage.getItem("momo-timestamp") ? new Date(localStorage.getItem("momo-timestamp")) : new Date(0);
        var timeDiff     = ((new Date()).getTime() - lastUpdate.getTime());
        var updateRequired   = timeDiff > app.manifest.meta.updateFreq;

        if(updateRequired){
            app.flash("Vérifiez si une mise à jour est disponible en tirant la page vers le bas", "info");
        }

        if(typeof resolve === 'function'){
            resolve(updateRequired);
        }
    },

    checkForUpdate: function(resolve, reject) {
        app.utils.setLoadingMsg("Vérification des mises à jour");
        app.checkConnection();

        var manifestReady = false;
        var assetsReady = false;
        var updateAvailable = false;
        var updateError = false;

        if(!app.online){
            var msg = "Impossible de détecter une mise à jour car votre appareil n'est pas connecté à Internet. ";
            var updatedAt = localStorage.getItem('momo-updated-at');
            if(updatedAt){
               msg += "Pour information, la dernière mise à jour date du " + app.utils.formatDate(new Date(updatedAt)) + ".";
            }
            app.utils.setLoadingMsg("Application hors-ligne");
            app.flash(msg, 'danger');
            if(typeof resolve === 'function'){
                resolve(false);
            }
            return;
        }

        var onGetMtime = function(key, mtime, ready){
            var old_mtime = localStorage.getItem("momo-"+key+"-mtime");
            if (mtime) {
                if(mtime != old_mtime) {
                    updateAvailable = true;
                }
            } else {
                updateError = true;
            }
            if(ready){
                if(updateError){
                    if(DEBUG){ console.error('Error checking for updates'); }
                    app.flash("Impossible de détecter des nouvelles mises à jour. Veuillez vérifier votre connexion internet.", 'danger');
                    if(typeof resolve === 'function'){
                        resolve(false);
                    }
                } else {
                    /* record that a check for update was succesfully done */
                    localStorage.setItem("momo-timestamp",  (new Date()).toString());

                    if(updateAvailable){
                        app.flash(tmpl('momo-update-available-tmpl', { mtime: app.utils.formatDate(mtime) }), 'success');
                        app.utils.setLoadingMsg("Mise à jour disponible !");
                    } else {
                        app.utils.setLoadingMsg("Aucune nouvelle mises à jour");
                    }
                    if(typeof resolve === 'function'){
                        resolve(updateAvailable);
                    }
                }
            }
        };

        app.utils.getModifiedTime(app.manifest.meta.manifestUrl, function(mtime) {
            app.manifestMtime = mtime;
            onGetMtime('manifest', mtime, assetsReady);
            manifestReady = true;
        });

        app.utils.getModifiedTime(app.manifest.meta.assetsUrl, function(mtime) {
            app.assetsMtime = mtime;
            onGetMtime('assets', mtime, manifestReady);
            assetsReady = true;
        });
    },

    loadLocalManifest: function() {
        // In case the url is incorrect, we get the backup manifest
        app.safeManifest = app.manifest;

        var manifest = localStorage.getItem("momo-manifest");
        if(manifest){
            try {
                app.manifest = JSON.parse(manifest);
                // Override meta
                app.manifest.meta = app.utils.extend(app.safeManifest.meta, app.manifest.meta);
            } catch(e) {
                app.manifest = app.safeManifest;
            }
        }
    },

    // JSON Manifest loading function
    loadManifest: function(resolve, reject){
        if(DEBUG){ console.log('load '+JSON.stringify(app.manifest)); }
        app.utils.setLoadingMsg("Mise à jour du manifest - 0%");

        // Get manifest from localStorage if it exists
        app.loadLocalManifest();

        // Start AJAX
        var url          = app.manifest.meta.manifestUrl;
        var request      = new XMLHttpRequest();

        // Force reload with timestamp injected in URL
        request.open('GET', app.utils.addParameter(url, 'timestamp', (+new Date()), true), true);

        // AJAX Callback
        request.onload = function() {

            // AJAX success
            if (request.status >= 200 && request.status < 400 || request.status === 0 /* iOS OhMyBuddha!! */) {

                app.utils.setLoadingMsg("Mise à jour du manifest - 99%");

                // Patch raw text response for fileSystem relative paths
                app.patchResponse(request.responseText, function(manifestResponse){

                    app.utils.setLoadingMsg("Mise à jour du manifest - 100%");

                    try {
                        // Override current manifest
                        app.manifest = JSON.parse(manifestResponse);

                        // Override meta
                        app.manifest.meta = app.utils.extend(app.safeManifest.meta, app.manifest.meta);
                        
                        // Store manifest if parsable
                        localStorage.setItem("momo-manifest", manifestResponse);

                        // Reload if new manifest url
                        if(url !== app.manifest.meta.manifestUrl){
                            app.loadManifest(resolve, reject);
                        } else  {
                            if(typeof resolve === "function"){
                                resolve();
                            }
                        }
                    } catch(e) {
                        if(DEBUG){ console.log('Cannot parse application manifest '+url); }
                        app.flash('Le manifest JSON comporte des erreurs', 'danger');
                        app.manifest = app.safeManifest;
                        if(typeof reject === "function"){
                            reject();
                        }
                    }
                });

            // Handle AJAX Error
            } else {
                app.onAjaxError(url, request);
                if(typeof reject === "function"){
                    reject();
                }
            }
        };

        // Handle AJAX Error
        request.onerror = function() {
            if(typeof reject === "function"){
                reject();
            }
            app.onAjaxError(url);
        };

        // Set AJAX Timeout
        request.timeout = AJAX_TIMEOUT;

        // Handle AJAX Timeout
        request.ontimeout = function() {
            if(typeof reject === "function"){
                reject();
            }
            app.onAjaxError(url);
        };

        // Send AJAX
        request.send();
    },

    // Start Application with safe manifest
    onAjaxError: function(url, request){
        if(DEBUG){ console.log("Cannot load "+url+" [Error "+(request ? request.status : 'Unknown')+"]. Loading local manifest instead."); }

        // Store proper manifest
        localStorage.setItem("momo-manifest", JSON.stringify(app.safeManifest));

        // Restore safe manifest 
        app.manifest = app.safeManifest;
    },

    // Patch manifest response to set fileSystem's relative paths (offline)
    patchResponse: function(response, cb){

        var patch = function(jsonText, path) {
            return jsonText.replace(/(['"\(])\/?(assets\/[^'"\)]*)(['"\)])/g, function(match, q1, p, q2){
                return q1+path+p+q2;
            });
        };

        // Phone context requires 'FileTransfer' & 'Zip' plugins
        if(typeof FileTransfer !== 'undefined' && typeof zip !== 'undefined' && typeof window.requestFileSystem === 'function'){
            if(DEBUG){ console.log('FileSystem access'); }

            var onFileSystemGet = function(fileSystem){
                // Get fileSystem's relative cache folder
                var rootPath = app.rootPath = cordova.file.dataDirectory;

                // Callback
                cb(patch(response, rootPath));
            };

            try {
                window.requestFileSystem(LocalFileSystem.TEMPORARY, 0, onFileSystemGet, function(err){
                    if(DEBUG){ console.log('FileSystem unreachable'); }
                    app.flash("Impossible d'écrire sur le périphérique", 'danger');
                    cb(patch(response, DEBUG_WWW_URL));
                });
            } catch(e) {
                if(DEBUG){ console.log('FileSystem error'+e.message); }
                cb(patch(response, DEBUG_WWW_URL));
            }
        } else {
            if(DEBUG){ console.log('FileSystem unavaible'); }
            cb(patch(response, DEBUG_WWW_URL));
        }
    },

    // Get distant zip asset archive and update local cache
    loadAssets: function(resolve, reject){

        if(DEBUG){ console.log('fetch assets'); }
        app.utils.setLoadingMsg("Téléchargement des assets - 0%");

        var onFileSystemGet = function(fileSystem){

            var rootPath = app.rootPath = cordova.file.dataDirectory;
            var fileTransfer = new FileTransfer();
            var uri = encodeURI(app.manifest.meta.assetsUrl);
            var filePath = fileSystem.root.toURL() + uri.substr(uri.lastIndexOf("/") + 1);

            // Fetch Assets Zip Archive
            fileTransfer.download(
                // Source
                uri, 
                // Destination
                filePath, 
                // Success callback 
                function(entry) {
                    app.utils.setLoadingMsg("Extraction de la mise à jour");
                    // Unzip Assets
                    zip.unzip(filePath, rootPath,
                        // Success callback
                        function(ret){
                            if(DEBUG){ console.log('unzip success'); }
                            app.utils.setLoadingMsg("Téléchargement des assets - 100%");
                            if(ret === 0) {
                                if(typeof resolve === 'function') {
                                    resolve();
                                }
                            } else 
                            if(ret === -1) {
                                if(typeof reject === 'function') {
                                    reject();
                                }
                            }
                        },
                        // Progress callback
                        function(e){
                            var progress = Math.round((e.loaded / e.total) * 100);
                            app.utils.setLoadingMsg("Téléchargement des assets - "+progress+"%");
                        }
                    );
                },
                // Error callback
                function(error) {
                    if(DEBUG){ console.log("download error source " + error.source); }
                    if(DEBUG){ console.log("download error target " + error.target); }
                    if(DEBUG){ console.log("upload error code" + error.code); }
                    if(typeof reject === 'function'){
                       reject();
                    }
                },
                // Misc
                false,
                {
                    headers: {}
                }
            );
        };


        if(typeof FileTransfer !== 'undefined' && typeof zip !== 'undefined' && typeof window.requestFileSystem === 'function'){
            try {
                window.requestFileSystem(LocalFileSystem.TEMPORARY, 0, onFileSystemGet, function(err){
                    if(DEBUG){ console.error('Filesystem error'); }
                    app.utils.setLoadingMsg("Erreur du système de fichier");
                    app.flash("Erreur du système de fichier", 'danger');
                    if(typeof reject === 'function'){
                        reject();
                    }
                });
            } catch(e) {
                if(DEBUG){ console.log('FileSystem error : '+e.message); }
                if(typeof reject === 'function'){
                    reject();
                }
            }
        } else {
            if(DEBUG){ console.error('Plugins "zip" & "file-transfer" not available (local mode ?)'); }
            app.utils.setLoadingMsg("Plugin 'zip' indisponible");
            app.flash("Plugin 'zip' indisponible", 'danger');
            if(typeof reject === 'function'){
                reject();
            }
        }
    },

    refreshAssets: function(page){
        var i, j, l, m, file, found, els, el;

        // Refresh scripts
        for(i = 0, l = page.javascripts.length; i < l; i++){
            file = page.javascripts[i];
            found = false;
            els = document.getElementsByTagName("script");
            for (j = 0, m = els.length; j < m; j++) {
                el = els[j];
                if (el.src === file) {
                    found = true;
                } else if(page.javascripts.indexOf(el.src) < 0 && app.javascripts.indexOf(el.src) < 0){
                    el.parentNode.removeChild(el);
                    m = els.length; j--;
                }
            }
            if(!found){
                // Script tag
                var script = document.createElement("script");
                script.type = "text/javascript";
                script.src = file;
                document.getElementsByTagName("head")[0].appendChild(script);
            }
            
        }

        // Refresh stylesheets
        for(i = 0, l = page.stylesheets.length; i < l; i++){
            file = page.stylesheets[i];
            found = false;
            els = document.getElementsByTagName("link");
            for (j = 0, m = els.length; j < m; j++) {
                el = els[j];
                if (el.href === file) {
                    found = true;
                } else if(page.stylesheets.indexOf(el.href) < 0 && app.stylesheets.indexOf(el.href) < 0){
                    el.parentNode.removeChild(el);
                    m = els.length; j--;
                }
            }
            if(!found){
                // Link Tag
                var link = document.createElement("link");
                link.type = "text/css";
                link.rel = "stylesheet";
                link.href = file;
                document.getElementsByTagName("head")[0].appendChild(link);
            }
            
        }
    },

    appendAssets: function(resolve, reject){
        if(typeof resolve === 'function'){
            resolve();
        }
        return;
        //var append = function(rootPath){

        //    // Link Tag
        //    var link = document.createElement("link");
        //    link.type = "text/css";
        //    link.rel = "stylesheet";
        //    link.href = rootPath+"assets/index.css";

        //    // Script tag
        //    var script = document.createElement("script");
        //    script.type = "text/javascript";
        //    script.src = rootPath+"assets/index.js";

        //    // Delete previous link tags
        //    var els = document.getElementsByTagName("link"),
        //      els_length = els.length;
        //    for (var i = 0, l = els_length; i < l; i++) {
        //        var el = els[i];
        //        if (el.href === link.href) {
        //            delete el;
        //        }
        //    }

        //    // Delete previous scripts tags
        //    els = document.getElementsByTagName("script");
        //    els_length = els.length;
        //    for (var i = 0, l = els_length; i < l; i++) {
        //        var el = els[i];
        //        if (el.src === script.src) {
        //            delete el;
        //        }
        //    }

        //    // Reinsert
        //    document.getElementsByTagName("head")[0].appendChild(link);
        //    document.getElementsByTagName("head")[0].appendChild(script);

        //    if(typeof resolve === 'function')
        //        resolve();
        //};

        //if(typeof FileTransfer !== 'undefined' && typeof zip !== 'undefined'){
        //    window.requestFileSystem(LocalFileSystem.TEMPORARY, 0, function (fileSystem) {

        //        // Get fileSystem's relative cache folder
        //        var rootPath = app.rootPath = fileSystem.root.toURL();
        //        append(rootPath);
        //    });
        //} else {
        //    append(DEBUG_WWW_URL);
        //}
    },

    // Application starter
    start: function(resolve, reject){
        if(DEBUG){ console.log('start '+JSON.stringify(app.manifest)); }
        app.utils.setLoadingMsg("Démarrage de l'application");
        app.hasStarted = true;

        /* if there's no footer, fallback to the default */
        if (typeof app.manifest.footer === 'undefined') {
            app.manifest.footer = tmpl('momo-default-footer-tmpl', app.manifest);
        }

        // Default route to home
        app.manifest.id = app.currentPage = 'home';
        
        // Dev page refresh : redirect to home
        window.location.replace('#home');
        // Regiter pages tree
        app.registerPage(app.manifest, app.defaultPage);
    
        // Render Homepage
        app.render(app.manifest);

        // Navigate to home
        app.navigate('home', false, function(){

            // Reset history
            app.pageIndex = 0;

            // Append assets
            app.appendAssets(resolve, reject);
        });

        // Listen for search form submission
        var $form = document.getElementById('momo-search');
        $form.addEventListener('submit', app.onSearchSubmit, false);
    },

    // Recursive function to index page from JSON Manifest
    registerPage: function(data, parentPage){

        if(data instanceof Object){

            // Cache orginal page
            data.original = JSON.parse(JSON.stringify(data).replace(new RegExp(app.rootPath, 'g'), ""));

            // Extends default page
            data = app.utils.extend(app.defaultPage, data);
            
            // Set page's parent
            data.parent = parentPage;

            // Generate a page ID
            var id = data.id = data.id ? data.id : (data.title ? app.utils.hyphenate(data.title.stripTags()) : '_' + Math.random().toString(36).substr(2, 9));

            // Make sure id is unique
            var i = 0, page;
            while(app.pages.hasOwnProperty(data.id)){
                data.id = id+'_'+(i++).toString();
            }

            // Register page
            app.pages[data.id] = data;

            // Register menu items
            if(data.menu instanceof Array && data.menu.length > 0){
                for(i = 0; i < data.menu.length; i++){
                    page = data.menu[i];
                    if(page instanceof Object){
                        app.pages[data.id].menu[i] = page.id;
                    }
                    app.pages[data.id].menu[i] = app.registerPage(page, app.pages[data.id]);
                }
            } else {
                app.pages[data.id].menu = parentPage.menu || [];
            }
            app.pages[data.id].menu = app.manifest.meta.menu.concat(app.pages[data.id].menu).unique();

            // Register javascript items
            if(data.javascripts instanceof Array && data.javascripts.length > 0){
                app.pages[data.id].javascripts = data.javascripts;
            } else {
                app.pages[data.id].javascripts = parentPage.javascripts || [];
            }
            app.pages[data.id].javascripts = app.manifest.meta.javascripts.concat(app.pages[data.id].javascripts).unique();

            // Register stylesheets items
            if(data.stylesheets instanceof Array && data.stylesheets.length > 0){
                app.pages[data.id].stylesheets = data.stylesheets;
            } else {
                app.pages[data.id].stylesheets = parentPage.stylesheets || [];
            }
            app.pages[data.id].stylesheets = app.manifest.meta.stylesheets.concat(app.pages[data.id].stylesheets).unique();

            // Register hidden pages childrens
            if(data._pages instanceof Array){
                for(i = 0; i < data._pages.length; i++){
                    page = data._pages[i];
                    if(page instanceof Object){
                        app.pages[data.id]._pages[i] = page.id;
                    }
                    app.pages[data.id]._pages[i] = app.registerPage(page, app.pages[data.id]);
                }
            }

            // Register page childrens
            if(data.pages instanceof Array){
                var images = [];
                for(i = 0; i < data.pages.length; i++){
                    page = data.pages[i];
                    if(page instanceof Object){
                        app.pages[data.id].pages[i] = page.id;
                    }
                    page = app.pages[data.id].pages[i] = app.registerPage(page, app.pages[data.id]);
                    if(app.pages[page] && app.pages[page].url && app.pages[page].url.isImage() && !app.pages[page].external){
                        images.push(page);
                    }
                }
                // Build Gallery
                if(images.length){
                    if(images.length > 1 && images[0] === images[images.length - 1]){
                        app.pages[data.id].pages.pop();
                    }
                    for(i = 0; i < images.length; i++){
                        if(i > 0){
                            app.pages[images[i]].prev = images[i-1];
                        }
                        if(i < images.length-1){
                            app.pages[images[i]].next = images[i+1];
                        }
                    }
                }
            }

            // Register seealso childrens
            if(data.seealso instanceof Array){
                for(i = 0; i < data.seealso.length; i++){
                    page = data.seealso[i];
                    if(page instanceof Object){
                        app.pages[data.id].seealso[i] = page.id;
                    }
                    app.pages[data.id].seealso[i] = app.registerPage(page, app.pages[data.id]);
                }
            }

            // Index page for search engine
            app.index.add({
                id: data.id,
                title: data.title,
                body: data.content,
                keywords: data.keywords
            });
            
            // Return ID string
            return data.id;
        } else
        if(typeof data === 'string' || data instanceof String || data instanceof Number){
            // Return ID string
            return data;
        }
    },

    // Render Application
    render: function(data){

        // Render Main section
        var $main = tmpl("momo-main-tmpl", data);
        document.getElementById('momo-main').innerHTML = $main;

        // Render every page
        for(var page in app.pages){
            if(app.pages.hasOwnProperty(page)){
                var $page = document.createElement('div');
                $page.id = app.pages[page].id;
                $page.className = "momo-page";
                //if($page.id == 'home'){
                //    $page.classList.add('momo-page-current');
                //    $page.innerHTML = app.renderPage(app.pages[page]);
                //    document.title = app.pages[page].title;
                //}
                document.getElementById('momo-pages').appendChild($page);
            }
        }

    },

    // Render Page
    renderPage: function(page, forceMenu){
        if(page instanceof Object){
            if(DEBUG){ console.log('render page '+JSON.stringify(page)); }

            // Refresh assets
            app.refreshAssets(page);

            // Render every menu items
            document.getElementById('momo-menu').innerHTML = "";


            for(var i = 0, l = page.menu.length; i < l; i++){
                var $menuItem = document.createElement('li');
                var _data = app.utils.extend(app.pages[page.menu[i]], { header: true });
                $menuItem.innerHTML = tmpl("momo-list-item-tmpl", _data);
                document.getElementById('momo-menu').appendChild($menuItem);
            }

            // Render navigation
            if(page.menu !== app.pages[app.currentPage].menu || !app.nav || forceMenu){
                if(app.nav){
                    app.nav.destroy();
                }
                app.nav = responsiveNav(".momo-nav-collapse", { // Selector
                    animate: true, // Boolean: Use CSS3 transitions, true or false
                    transition: 284, // Integer: Speed of the transition, in milliseconds
                    //label: "Menu", // String: Label for the navigation toggle
                    //insert: "before", // String: Insert the toggle before or after the navigation
                    customToggle: "momo-menu-toggle", // Selector: Specify the ID of a custom toggle
                    closeOnNavClick: true, // Boolean: Close the navigation when one of the links are clicked
                    openPos: "relative", // String: Position of the opened nav, relative or static
                    navClass: "nav-collapse", // String: Default CSS class. If changed, you need to edit the CSS too!
                    navActiveClass: "active", // String: Class that is added to <html> element when nav is active
                    jsClass: "js", // String: 'JS enabled' class which is added to <html> element
                    init: function(){}, // Function: Init callback
                    open: function(){}, // Function: Open callback
                    close: function(){} // Function: Close callback
                });
            }
            // Cleanup
            app.nav.unbindEvents();
            app.nav.bindEvents();

            // Get data to display
            var data = app.utils.extend(page, {meta: app.manifest.meta});

            // Change page header
            document.getElementById("momo-header").innerHTML = tmpl("momo-header-tmpl", data);

            // Render inner template
            if(data.template){
                var script = document.createElement("script");
                script.type = "text/x-tmpl";
                script.id = data.id+"-tmpl";
                script.innerHTML = data.template;
                document.getElementById('momo-templates').innerHTML = "";
                document.getElementById('momo-templates').appendChild(script);
                data.content = tmpl(data.id+"-tmpl", data) + (data.content || "");
            }

            // Render Page
            return tmpl("momo-page-tmpl", data);
        } else
        if(typeof page === 'string' || page instanceof String || page instanceof Number){
            if(DEBUG){ console.log('render page '+page); }
            return app.renderPage(app.pages[page]);
        }
    },

    // Navigate to page
    navigate: function(page, back, cb, force){
        var page_obj = app.pages[page];

        if(page_obj.external && back){
            window.history.back();
            return false;
        } else
        if(page_obj.external){
            app.utils.openExternalURL(page_obj.url, page_obj.inAppBrowser);
            return false;
        }

        // Keep try to navigate until previous animation is done.
        if(app.isAnimating) {
            setTimeout(function(){
                app.navigate(page, back, cb);
            }, 100);
            return false;
        }

        app.isAnimating = true;
        document.title = page_obj.title;

        var $outpage = document.getElementById(app.currentPage);
        var $inpage  = document.getElementById(page);

        // Render page (with small hack, so it doesn't mess up the display)
        $inpage.innerHTML = app.renderPage(page_obj, force);

        // Check for connection
        app.checkConnection();

        // Pull to update binder
        app.bindPagePull($inpage);

        if(!ANIMATION_ENABLED || app.currentPage === page){
            $inpage.classList.add('momo-page-current');
            app.onAnimationEnd($outpage, $inpage, back, cb);
        } else {

            var outCb = function(){
                $outpage.removeEventListener('animationend',       outCb);
                $outpage.removeEventListener('webkitAnimationEnd', outCb);
                $outpage.removeEventListener('oAnimationEnd',      outCb);
                $outpage.removeEventListener('MSAnimationEnd',     outCb);
                app.endCurrPage = true;
                if(app.endNextPage){
                    app.onAnimationEnd($outpage, $inpage, back, cb);
                }
            };

            var inCb = function(){
                $inpage.removeEventListener('animationend',       inCb);
                $inpage.removeEventListener('webkitAnimationEnd', inCb);
                $inpage.removeEventListener('oAnimationEnd',      inCb);
                $inpage.removeEventListener('MSAnimationEnd',     inCb);
                app.endNextPage = true;
                if(app.endCurrPage){
                    app.onAnimationEnd($outpage, $inpage, back, cb);
                }
            };

            var i;
            var out_classes = (back ? ANIMATION_BACK_OUT_CLASS : ANIMATION_OUT_CLASS).split(' ');
            for(i = 0; i < out_classes.length; i++){
                $outpage.classList.add(out_classes[i]);
            }
            $outpage.addEventListener('animationend',       outCb, false);
            $outpage.addEventListener('webkitAnimationEnd', outCb, false);
            $outpage.addEventListener('oAnimationEnd',      outCb, false);
            $outpage.addEventListener('MSAnimationEnd',     outCb, false);

            $inpage.classList.add('momo-page-current');

            var in_classes = (back ? ANIMATION_BACK_IN_CLASS : ANIMATION_IN_CLASS).split(' ');
            for(i = 0; i < in_classes.length; i++){
                $inpage.classList.add(in_classes[i]);
            }
            $inpage.addEventListener('animationend',       inCb, false);
            $inpage.addEventListener('webkitAnimationEnd', inCb, false);
            $inpage.addEventListener('oAnimationEnd',      inCb, false);
            $inpage.addEventListener('MSAnimationEnd',     inCb, false);
        }

        app.currentPage = page;
    },

    bindPagePull: function($page) {
        WebPullToRefresh.destroy();
        WebPullToRefresh.init( {
            loadingFunction: function(){
                if(ON_PULL === "checkForUpdate"){
                    return new Promise( app.checkForUpdate );
                } else
                if(ON_PULL === "update"){
                    return new Promise( app.update );
                }
            },
            contentEl: $page
        } );
    },

    // Animation Callback
    onAnimationEnd: function($outpage, $inpage, back, cb) {
        app.endCurrPage = false;
        app.endNextPage = false;
        var i;
        var out_classes = (back ? ANIMATION_BACK_OUT_CLASS : ANIMATION_OUT_CLASS).split(' ');
        for(i = 0; i < out_classes.length; i++){
            $outpage.classList.remove(out_classes[i]);
        }
        if($outpage !== $inpage){
            $outpage.classList.remove('momo-page-current');
        }
        var in_classes = (back ? ANIMATION_BACK_IN_CLASS : ANIMATION_IN_CLASS).split(' ');
        for(i = 0; i < in_classes.length; i++){
            $inpage.classList.remove(in_classes[i]);
        }
        app.isAnimating = false;
        if(typeof cb === "function"){
            cb();
        }
    },

    onTouchStart: function(e) {
        e = e || window.event;
        var targ = e.target || e.srcElement;
        if (targ.nodeType === 3){ targ = targ.parentNode; }
        //return targ.onclick();
    },

    // Location Hash change event (sorry & good luck)
    onHashChange: function(e) {
        var hash = window.location.hash, 
            length = window.history.length,
            page = window.location.hash.slice(1),
            prev, next;


        //console.warn(app.previousPage + " -> " + page);

        // Hack of the century ?
        if(app.ignoreHash){ return false; }
        if(document.body.classList.contains( 'ptr-back' )){ return false; }
        if(document.body.classList.contains( 'ptr-forward' )){ return false; }

        if(page === 'momo-update'){
            //window.location.replace('#'+app.pageHistory[app.pageHistory.length-1]);
            window.replaceHash(app.pageHistory[app.pageHistory.length-1]);
            document.body.classList.add('ptr-loading');
            var cb = function() {
                setTimeout(function(){
                    document.body.classList.remove('ptr-loading');
                    document.body.classList.add('ptr-reset');
                    setTimeout(function() {
                        document.body.classList.remove('ptr-reset');
                    }, 250);
                }, 1000);
            };
            app.update(cb, cb);
            app.ignoreHash = true;
            window.history.go(-1);
            app.ignoreHash = false;
            app.previousPage = page;
            return false;
        } else
        if(page === 'momo-blank'){
            //return window.location.replace(app.pageHistory[app.pageHistory.length-1]);
            //return window.history.back();
            app.ignoreHash = true;
            window.history.go(-1);
            app.ignoreHash = false;
            app.previousPage = page;
            return false;
        } 
        else
        if(page === 'momo-back'){
            prev = app.pages[app.currentPage].prev;
            if(prev){
                page = prev;
                window.replaceHash(app.parentPage);
                window.history.go(-1);
                app.navigate(page, true);
                //app.ignoreHash = true;
                //window.location.hash = "#"+page;
                //app.ignoreHash = false;
            } else {
                if(app.previousPage === 'momo-forward'){
                    window.history.go(-1);
                } else {
                    window.history.go(-2);
                }
            }
            //window.replaceHash(page);
            app.previousPage = page;
            return false;
        }
        else
        if(page === 'momo-forward'){
            prev = app.pages[app.currentPage].prev;
            next = app.pages[app.currentPage].next;
            if(next){
                page = next;
                window.replaceHash(app.parentPage);
                window.history.go(-1);
                app.navigate(page, false);
                //window.replaceHash(page);
                //app.ignoreHash = true;
                //window.location.hash = "#"+page;
                //app.ignoreHash = false;
            } else if(prev) {
                window.replaceHash(app.parentPage);
                window.history.go(-1);
            } else {
                if(app.previousPage === 'momo-back'){
                    window.history.go(-1);
                } else {
                    page = app.pageIndex+1 < app.pageHistory.length ? app.pageHistory[app.pageIndex+1] : app.pageHistory[app.pageHistory.length - 1];
                    if(page !== app.currentPage){
                        window.location.hash = "#"+app.pageHistory[app.pageHistory.length-1];
                    } else {
                        window.history.go(-1);
                    }
                }
            }
            //window.replaceHash(page);
            app.previousPage = 'momo-forward';
            return false;
        }

        if(!app.pages.hasOwnProperty(page)){
            page = 'home';
        } 

        var back = page === 'home';

        //console.log(app.pageHistory.join(',')+ " (index = "+app.pageIndex+")");

        if (app.pageHistory.length/* && app.historyLength == length*/) {
            // Goind Back
            if (app.pageHistory[app.pageIndex - 1] === page) {
                //app.pageHistory = app.pageHistory.slice(0, app.pageIndex);
                back = true; 
                app.pageIndex--;
            } else
            // Going Forward
            if(app.pageIndex+1 < app.pageHistory.length && app.pageHistory[app.pageIndex + 1] === page) {
                //app.pageHistory = app.pageHistory.slice(0, app.pageIndex);
                app.pageIndex++;
            } else {
            // Going to new page
            app.pageHistory = app.pageHistory.slice(0, app.pageIndex+1);
            if(page !== app.pageHistory[app.pageHistory.length-1]){
                app.pageHistory.push(page);
            }
            app.pageIndex = app.pageHistory.length - 1;
            app.historyLength = length;
            }
        } else {
            app.pageHistory.push(page);
        }

        if(app.currentPage !== page){
            if(!app.pages[page].external || app.pages[page].external && app.online){
                app.navigate(page, back);
            } else {
                window.history.go(-1);
            }
        }
        app.parentPage = page;
        app.previousPage = page;
        return false;
    },

    onSearchSubmit: function(e){
        // Stop form default action
        e.preventDefault();
        //e.stopPropagation();

        // Get search input
        var $searchInput = document.getElementById('momo-search-input');
        var searchInput = $searchInput.value;

        // Close navigation + Keyboard
        app.nav.close();
        $searchInput.value = '';
        $searchInput.blur();

        // Search
        app.search(searchInput);

        return false;
    },

    search: function(query){
        var id = "search-"+app.utils.hyphenate(query);

        // Store current query
        app.current_query = query;

        // Get search results
        var results = app.index.search(app.utils.replaceAccents(query));

        // Register new search results page
        app.pages[id] = app.utils.extend(app.defaultPage, {
            id: id,
            search: query,
            icon: "fa fa-search",
            title: 'Recherche "'+query+'"',
            content: results.length ? null : '<div class="well text-center text-muted">Aucun résultat</div>',
            pages: results.map(function(item){
                return item.ref;
            }),
            menu: app.pages[app.currentPage].menu
        });

        var $page = document.getElementById(id);

        // If search query doesn't exist
        if(!$page){
            // Generate result page view
            $page = document.createElement('div');
            $page.id = id;
            $page.className = "momo-page";
            document.getElementById('momo-pages').appendChild($page);
        }

        // Render page
        $page.innerHTML = app.renderPage(app.pages[id], app.pages[app.currentPage]);

        // Navigate to search result page
        window.location.hash = "#"+id;
    },

    flash: function(message, type) {
        var elements = document.getElementsByClassName("momo-flash-messages");
        var i;
        for (i = 0; i < elements.length; i++){
            elements[i].innerHTML = "";
        }
        setTimeout(function(){
            var elements = document.getElementsByClassName("momo-flash-messages");
            for (i = 0; i < elements.length; i++){
                elements[i].innerHTML += tmpl('momo-flash-message-tmpl', { 
                    message: message,
                    type: type ? type : 'info'  
                });
            }
        }, 200);
    },
    
    update: function( resolve, reject ) {
        var els = document.getElementsByTagName("a"),
          els_length = els.length;
        for (var i = 0, l = els_length; i < l; i++) {
            var el = els[i];
            if (el.href.split("#")[1] === 'momo-update') {
                el.classList.add('disabled');
            }
        }

        var appendAssets = function(_resolve, _reject){
            app.appendAssets(
                function(){
                    app.reset(_resolve, _reject);
                }, 
                function(){
                    app.reset(_reject, _reject);
                }
            );
        };

        var loadAssets = function(_resolve, _reject){
            app.loadAssets(
                function(){
                    appendAssets(_resolve, _reject);       
                },
                function(){
                    app.reset(_reject, _reject);
                }
            );
        };

        app.loadManifest(
            function(){
                localStorage.setItem('momo-manifest-mtime', app.manifestMtime);
                localStorage.setItem('momo-assets-mtime', app.assetsMtime);
                localStorage.setItem('momo-updated-at', new Date());
                loadAssets(resolve, reject);
            },
            function(){
                loadAssets(resolve, reject);
            }
        );
    },

    reset: function(resolve, reject){
        // Update Reminder
        clearTimeout(app.updateTimeout);
        app.updateTimeout = setTimeout( app.checkForLastUpdateCheck, app.manifest.meta.updateFreq );

        var page, $page;

        for(page in app.pages){
            if(app.pages.hasOwnProperty(page)){
                if(typeof app.pages[page].search === 'undefined'){
                    delete app.pages[page];
                    $page = document.getElementById(page);
                    if($page){
                        $page.parentNode.removeChild($page);
                    }
                }
            }
        }
        // Regiter pages tree
        app.manifest.id = 'home';
        app.registerPage(app.manifest, app.defaultPage);

        // Render every page
        for(page in app.pages){
            if(app.pages.hasOwnProperty(page)){
                $page = document.getElementById(page);
                if(!$page){
                    $page = document.createElement('div');
                    $page.id = app.pages[page].id;
                    $page.className = "momo-page";
                    document.getElementById('momo-pages').appendChild($page);
                }
            }
        }

        // Check for url change in manifest
        //app.checkForUpdate(function(updateAvailable){
        //    if(updateAvailable)
        //        app.update(resolve, reject);
        //    else
                app.utils.setLoadingMsg("Mise à jour effectuée !");
            if(typeof resolve === 'function'){
                resolve();
            }
        //}, function(){
        //    if(typeof reject === 'function')
        //        reject();
        //});

        if(typeof app.pages[app.currentPage].search !== 'undefined'){
            app.search(app.current_query);
        } else {
            app.navigate(app.currentPage, false, null, true);
        }
    },


    // Various Javascript Helpers
    utils: {

        removeElement: function($el){
            $el.parentElement.removeChild($el);
            return false;
        },

        setLoadingMsg: function(text){
            var elements = document.getElementsByClassName("momo-loading-text");
            for (var i = 0; i < elements.length; i++){
                elements[i].innerHTML = text;
            }
        },

        onExternalLinkClick: function(e){
            e = e || window.event;
            var targ = e.target || e.srcElement;
            if (targ.nodeType === 3){ targ = targ.parentNode; }
            var url = targ.getAttribute("href");
            app.utils.openExternalURL(url);
            return false;
        },

        openExternalURL: function(url, inAppBrowser){
            //if(inAppBrowser){
            //    cordova.InAppBrowser.open(url);
            //} else {
                if(navigator.app){ // Android
                    navigator.app.loadUrl(encodeURI(url), { openExternal:true });
                } else { // iOS and others
                    window.open(encodeURI(url), "_system", 'location=yes'); // opens in the app, not in safari
                }
            //}
            return false;
        },

        extend: function ( defaults, options ) {
            var extended = {};
            var prop;
            for (prop in defaults) {
                if (Object.prototype.hasOwnProperty.call(defaults, prop)) {
                    extended[prop] = defaults[prop];
                }
            }
            for (prop in options) {
                if (Object.prototype.hasOwnProperty.call(options, prop)) {
                    extended[prop] = options[prop];
                }
            }
            return extended;
        },

        trim: function(str){
            return (str || '').replace(/^\s+|\s+$/g, '');
        },

        removeNonWord: function(str){
            return (str || '').replace(/[^0-9a-zA-Z\xC0-\xFF \-]/g, ''); //remove non-word chars
        },

        replaceAccents: function(str){
            str = str || '';
            // verifies if the String has accents and replace them
            if (str.search(/[\xC0-\xFF]/g) > -1) {
                str = str
                .replace(/[\xC0-\xC5]/g, "A")
                .replace(/[\xC6]/g, "AE")
                .replace(/[\xC7]/g, "C")
                .replace(/[\xC8-\xCB]/g, "E")
                .replace(/[\xCC-\xCF]/g, "I")
                .replace(/[\xD0]/g, "D")
                .replace(/[\xD1]/g, "N")
                .replace(/[\xD2-\xD6\xD8]/g, "O")
                .replace(/[\xD9-\xDC]/g, "U")
                .replace(/[\xDD]/g, "Y")
                .replace(/[\xDE]/g, "P")
                .replace(/[\xE0-\xE5]/g, "a")
                .replace(/[\xE6]/g, "ae")
                .replace(/[\xE7]/g, "c")
                .replace(/[\xE8-\xEB]/g, "e")
                .replace(/[\xEC-\xEF]/g, "i")
                .replace(/[\xF1]/g, "n")
                .replace(/[\xF2-\xF6\xF8]/g, "o")
                .replace(/[\xF9-\xFC]/g, "u")
                .replace(/[\xFE]/g, "p")
                .replace(/[\xFD\xFF]/g, "y");
            }
            return str;
        },

        slugify: function(str, delimeter){
            if (delimeter === null) {
                delimeter = "-";
            }
            str = app.utils.replaceAccents(str);
            str = app.utils.removeNonWord(str);
            str = app.utils.trim(str) //should come after removeNonWord
            .replace(/ +/g, delimeter) //replace spaces with delimeter
            .toLowerCase();
            return str;
        },

        unCamelCase: function(str){
            return (str || '').replace(/([a-z\xE0-\xFF])([A-Z\xC0\xDF])/g, '$1 $2').toLowerCase(); //add space between camelCase text
        },

        hyphenate: function(str){
            str = app.utils.unCamelCase(str);
            return app.utils.slugify(str, "-");
        },

        addParameter: function(url, parameterName, parameterValue, atStart/*Add param before others*/){
            var replaceDuplicates = true;
            var cl, urlhash, sourceUrl;
            if(url.indexOf('#') > 0){
                cl = url.indexOf('#');
                urlhash = url.substring(url.indexOf('#'),url.length);
            } else {
                urlhash = '';
                cl = url.length;
            }
            sourceUrl = url.substring(0,cl);

            var urlParts = sourceUrl.split("?");
            var newQueryString = "";

            if (urlParts.length > 1)
            {
                var parameters = urlParts[1].split("&");
                for (var i=0; (i < parameters.length); i++)
                {
                    var parameterParts = parameters[i].split("=");
                    if (!(replaceDuplicates && parameterParts[0] === parameterName))
                    {
                        if (newQueryString === ""){
                            newQueryString = "?";
                        } else {
                            newQueryString += "&";
                        }
                        newQueryString += parameterParts[0] + "=" + (parameterParts[1]?parameterParts[1]:'');
                    }
                }
            }
            if (newQueryString === ""){
                newQueryString = "?";
            }

            if(atStart){
                newQueryString = '?'+ parameterName + "=" + parameterValue + (newQueryString.length>1?'&'+newQueryString.substring(1):'');
            } else {
                if (newQueryString !== "" && newQueryString !== '?'){
                    newQueryString += "&";
                }
                newQueryString += parameterName + "=" + (parameterValue?parameterValue:'');
            }
            return urlParts[0] + newQueryString + urlhash;
        },

        getModifiedTime: function(url, callback) {
            var xhr = new XMLHttpRequest();
            xhr.open('HEAD', url, true); // use HEAD - we only need the headers
            // AJAX Callback
            xhr.onload = function() {

                // AJAX success
                if (xhr.status >= 200 && xhr.status < 400 || xhr.status === 0 /* iOS OhMyBuddha!! */) {
                    var mtime = new Date(xhr.getResponseHeader('Last-Modified'));
                    if (mtime.toString() === 'Invalid Date') {
                      callback(); // dont want to return a bad date
                    } else {
                      callback(mtime);
                    }
                // AJAX error
                } else {
                  callback(); // dont want to return a bad date
                }
            };

            // Handle AJAX Error
            xhr.onerror = function() {
              //app.onAjaxError(url);
              callback();
            };

            // Set AJAX Timeout
            xhr.timeout = AJAX_TIMEOUT;

            // Handle AJAX Timout
            xhr.ontimeout = function(){
                callback(); // dont want to return a bad date
            }
            xhr.send();
        },

        formatDate: function(date){
            var d = date;
            var months = ['janvier', 'février', 'mars', 'avril', 'mai', 'juin',
                          'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'];
            var days = ['Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'];
            return days[d.getDay()] + ' ' + d.getDate() + ' ' +
                    months[d.getMonth()] + ' ' + d.getFullYear() + ' ' +
                    d.toLocaleTimeString();
        },

        clone: function (obj){
            var copy;
        
            // Handle the 3 simple types, and null or undefined
            if (null === obj || "object" !== typeof obj){ return obj; }
        
            // Handle Date
            if (obj instanceof Date) {
                copy = new Date();
                copy.setTime(obj.getTime());
                return copy;
            }
        
            // Handle Array
            if (obj instanceof Array) {
                copy = [];
                for (var i = 0, len = obj.length; i < len; i++) {
                    copy[i] = app.utils.clone(obj[i]);
                }
                return copy;
            }
        
            // Handle Object
            if (obj instanceof Object) {
                copy = {};
                for (var attr in obj) {
                    if (obj.hasOwnProperty(attr)){ copy[attr] = app.utils.clone(obj[attr]); }
                }
                return copy;
            }
        
            throw new Error("Unable to copy obj! Its type isn't supported.");
        },

        prettyJSON: function(json) {
            if (typeof json !== 'string') {
                 json = JSON.stringify(json, undefined, 2);
            }
            json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
            json = json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
                var cls = 'number';
                if (/^"/.test(match)) {
                    if (/:$/.test(match)) {
                        cls = 'key';
                    } else {
                        cls = 'string';
                    }
                } else if (/true|false/.test(match)) {
                    cls = 'boolean';
                } else if (/null/.test(match)) {
                    cls = 'null';
                }
                return '<span class="' + cls + '">' + match + '</span>';
            });
            return '<pre class="code">'+json+'</pre>';
        }
    }
};

(function(namespace) { // Closure to protect local variable "var hash"
    if ('replaceState' in history) { // Yay, supported!
        namespace.replaceHash = function(newhash) {
            if ((''+newhash).charAt(0) !== '#'){ newhash = '#' + newhash; }
            history.replaceState('', '', newhash);
        };
    } else {
        var hash = location.hash;
        namespace.replaceHash = function(newhash) {
            if (location.hash !== hash){ history.back(); }
            location.hash = newhash;
        };
    }
})(window);

if (typeof Object.prototype.toHTML !== 'function') {
    Object.prototype.toHTML = function (){
        return app.utils.prettyJSON(this);
    };
}

if (typeof String.prototype.startsWith !== 'function') {
    String.prototype.startsWith = function (str){
        return this.slice(0, str.length) === str;
    };
}
if (typeof String.prototype.endsWith !== 'function') {
    String.prototype.endsWith = function (str){
        return this.slice(-str.length) === str;
    };
}
if (typeof String.prototype.stripTags !== 'function') {
    String.prototype.stripTags = function (){
        return this.replace(/(<([^>]+)>)/ig,"");
    };
}
if (typeof String.prototype.isImage !== 'function') {
    String.prototype.isImage = function (){
        return this.endsWith('.png') ||
            this.endsWith('.svg') ||
            this.endsWith('.ico') ||
            this.endsWith('.jpeg') ||
            this.endsWith('.jpg');
    };
}
if (typeof String.prototype.isUrl !== 'function') {
    String.prototype.isUrl = function (){
        return this.startsWith('http://') ||
            this.startsWith('https://') ||
            this.startsWith('file://');
    };
}
if (typeof String.prototype.isFramable != 'function') {
    String.prototype.isFramable = function (){
        return this.isImage() || this.isUrl();
    };
}

if (typeof String.prototype.unique !== 'function') {
    Array.prototype.unique = function() {
        var a = this.concat();
        for(var i=0; i<a.length; ++i) {
            for(var j=i+1; j<a.length; ++j) {
                if(a[i] === a[j]){
                    a.splice(j--, 1);
                }
            }
        }
        return a;
    };
}
