//
//  Copyright (c) 2012-2013 Stefan Bolte <portix@gmx.net>
//
//  This program is free software; you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation; either version 3 of the License, or
//  (at your option) any later version.
//  
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//  
//  You should have received a copy of the GNU General Public License along
//  with this program; if not, write to the Free Software Foundation, Inc.,
//  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//


/*
 * TODO not finished yet
 *
 * userscripts extension, load userscripts and injects them into websites, this
 * extension is mostly greasemonkey compatible. Scripts can be loaded by either
 * specifying a path with using the configuration or putting them into
 * $XDG_CONFIG_HOME/dwb/scripts/. 
 *
 *
 * To use this extension load it with a userscript in 
 * $HOME/.config/dwb/userscripts/, e.g. 
 *
 * ------------------------------------------------------------------------------
 * |#!javascript                                                                | 
 * |                                                                            | 
 * |extensions.load("userscripts");                                             | 
 * ------------------------------------------------------------------------------
 *
 *
 * Config: An array of paths to userscripts
 *
 * Example (loading with extensions load):
 * 
 * ------------------------------------------------------------------------------
 * |extensions.load("userscripts", [ "/path/to/script1", "/path/to/script2" ]); |                    |
 * ------------------------------------------------------------------------------
 *
 * Example (extensionsrc): 
 *
 * ------------------------------------------------------------------------------
 * |return {                                                                    |
 * |  ... : {  ...  },  // other config objects                                 |
 * |                                                                            |
 * |  userscripts : [ "/path/to/script1", "/path/to/script2" ],                 |
 * |                                                                            |
 * |  ... : {  ...  }  // other config objects                                  |
 * |}                                                                           |
 * ------------------------------------------------------------------------------
 *
 * */

/*<INFO
Load userscripts and inject them into websites, mostly greasemonkey compatible
INFO>*/

var me = "userscripts";
var onStart = []; 
var onEnd = []; 
/*
//<DEFAULT_CONFIG
  // paths to userscripts, this extension will also load all scripts in 
  // $XDG_CONFIG_HOME/dwb/greasemonkey, it will also load all scripts in
  // $XDG_CONFIG_HOME/dwb/scripts but this is deprecated and will be
  // disabled in future versions.
  scripts : []
//>DEFAULT_CONFIG
*/


var metaData = {};

const DATA_DIR = data.userDataDir + "/extension_data/userscripts/resources";
const META_DATA = DATA_DIR + "/.metadata";

UserScript.prototype = new Function();
UserScript.prototype.constructor = UserScript;
function UserScript() 
{
    this.description = null;
    this.downloadURL = null;
    this.exclude = [];
    this.icon = null;
    this.include = [];
    this.match = [];
    this.name = null;
    this.namespace = null;
    this.require = [];
    this.resource = [];
    this.runAt = "document-end";
    this.unwrap = false;
    this.updateURL = null;
    this.version = null;
    this.script = null;
    this.scriptId = null;
    // scriptish
    this.delay = 0;
    this.noframes = false;
    this.priority = 0;
    this.offset = 0;
}

// Reused regular expressions
var regexes = {
    isTld : /\.tld(?:\/|:|$)/, 
    isRegExp : /^\/.*\/$/
};

var GM_compatability = function () 
{

    var unsafeWindow = window;
    var DWB_scriptPrefix = "dwb_userscript_";
    if (DWB_scriptId !== undefined) 
    {
        DWB_scriptPrefix = DWB_scriptPrefix + DWB_scriptId + "_";
    }
    function GM_addStyle (styles) 
    {
        var style = document.createElement("style");
        style.setAttribute("type", "text/css");
        style.appendChild(document.createTextNode(styles));
        document.getElementsByTagName("head")[0].appendChild(style);
    };
    function GM_log (text) 
    {
        console.log(text);
    };
    function GM_setValue (key, value) 
    {
        if (localStorage !== null && (typeof value === "string" || typeof value === "number" || typeof value == "boolean") )
            localStorage.setItem(DWB_scriptPrefix + key, value);
        else 
            GM_log("GM_setValue only works with enabled localStorage and only for strings, numbers and booleans");
    };
    function GM_getValue (key, def) 
    {
        if (localStorage !== null) 
            return localStorage.getItem(DWB_scriptPrefix + key) || def;
        else 
            GM_log("GM_getValue only works with enabled localStorage");
        return undefined;
    };
    function GM_deleteValue (key) 
    {
        if (localStorage !== null) 
            localStorage.removeItem(DWB_scriptPrefix + key);
        else 
            GM_log("GM_deleteValue only works with enabled localStorage");
    };
    function GM_listValues () 
    {
        var i;
        var a = [];
        for (i=0; i<localStorage.length; i++) {
            a.push(localStorage.key(i).replace(DWB_scriptPrefix, ""));
        }
        return a;
    };
    function GM_info () { return undefined; };
    function GM_registerMenuCommand () { return undefined; };
    function GM_openInTab (url) { return null; };
};

function matchIncludeExclude(frame, items) //{{{
{
    var uri = frame.uri;
    var domain = frame.domain;
    var i;
    for (i=0; i<items.length; i++) 
    {
        try 
        {
            if (items[i].isTld && domain !== null) 
            {
                var reg = new RegExp("(?=.)" + domain + "($|/|:)");
                var newDomain = domain.substring(0, domain.indexOf(".")) + ".tld$1";
                uri = uri.replace(reg, newDomain);
            }
            if (items[i].regExp.test(uri)) 
                return true;
        }
        catch(e) 
        {
            script.debug(e);
        }
    }
    return false;
}//}}}

function matchMatches(frame, items) //{{{
{
    var i, item;
    var o = net.parseUri(frame.uri);
    if (o === null)
        return false;
    for (i=0; i<items.length; i++) 
    {
        item = items[i];
        if (item.allUrls || 
            (item.scheme.test(o.scheme)  &&  
             item.host.test(o.host)      &&  
             (o.path === null  || item.path.test(o.path)))) 
        {
            return true;
        }

    }
  return false;
}//}}}

function doInject(frame, item) 
{
  if (item.delay > 0) 
  {
      timer.start(item.delay, function() {
          frame.inject(item.script, null, item.offset, item.unwrap);
          return false;
      });
  }
  else 
      frame.inject(item.script, null, item.offset, item.unwrap);
}

function handle(frame, array, isMainFrame) //{{{
{
    var i, item;
    for (i=0; i<array.length; i++) 
    {
        item = array[i];
        if (item.noframes && !isMainFrame)
            continue;
        try 
        {
            if (matchIncludeExclude(frame, item.exclude)) 
                continue;

            if (matchIncludeExclude(frame, item.include)) 
                doInject(frame, item);
            else if (matchMatches(frame, item.match)) 
                doInject(frame, item);
        }
        catch (e) 
        {
            script.debug(e);
        }
    }
}//}}}

function loadFinishedCallback(wv, frame) //{{{
{
    handle(frame, onEnd, wv.mainFrame === frame);
}//}}}

function loadCommittedCallback(wv) //{{{
{
    handle(wv.mainFrame, onStart, true);
}//}}}

function parseIncludeExclude(array) //{{{
{
    var i, rule;
    for (i=0; i<array.length; i++) 
    {
        rule = array[i];
        if (regexes.isRegExp.test(rule)) 
        {
            array[i] = { 
                regExp : new RegExp(rule.substring(1, rule.length-1)), 
                isTld : regexes.isTld.test(rule) 
            };
        }
        else 
        {
            array[i] = { 
                regExp : new RegExp(rule.replace(/\*/g, ".*")), 
                isTld : regexes.isTld.test(rule) 
            };
        }
    }
}//}}}

function parseMatch(m) 
{
    var i, scheme, host, path, parts, j; 
    if (m === "<all_urls>") {
        return { allUrls : true };
    }

    var o = net.parseUri(m);
    if (o === null) 
    {
        extensions.warning(me, "Invalid or unsupported match rule: " + m);
        return null;
    }
    if (!(/\*|http|https|file/.test(o.scheme))) 
    {
        extensions.warning(me, "Invalid scheme pattern: " + m);
        return null;
    }
    else 
        o.scheme = new RegExp(o.scheme.replace("*", ".*"));
    
    if (! (/^(?:\*\.[^*\/]*|[^*]*|\*)$/.test(o.host))) 
    {
        extensions.warning(me, "Invalid host pattern: " + m);
        return null;
    }
    else 
        o.host = new RegExp(o.host.replace(/([.?+^$[\]\\(){}|-])/g, "\\$1").replace("*", ".*"));
    
    if (! (/^\/.*/.test(o.path))) 
    { 
        extensions.warning(me, "Invalid path pattern: " + m);
        return null;
    }
    else if (o.path !== null) 
        o.path = new RegExp(o.path.replace(/([.?+^$[\]\\(){}|-])/g, "\\$1").replace("*", ".*"));
    
    return { host : o.host, scheme : o.scheme, path : o.path, allUrls : false };
}

function parseScript(path) //{{{
{
    var userscript;
    var i, items, key, value, idx, m, matches = [], numVal;

    var curScript = io.read(path);
    if (!curScript || (/^\s*$/).test(curScript)) 
      return;

    var metaStart = curScript.search(/(^|\n)\/\/\s*==UserScript==/);
    var metaEnd = curScript.search(/\n\/\/\s*==\/UserScript==/);

    userscript = new UserScript();

    if (metaStart == -1 || metaEnd == -1) 
    {
        userscript.script = curScript; 
        userscript.offset = 1;
        userscript.include = [ { regExp : /.*/, isTld : false} ];
        onEnd.push(userscript);
        return;
    }

    var meta = curScript.substring(metaStart, metaEnd).split("\n");
    var scriptStart = curScript.substring(metaEnd+1).indexOf("\n") + metaEnd + 1;

    var regValue = /\s[^\/@]\S?/;
    var regIsRule = /^\s*\/\/\s*@/;
    for (i=1; i<meta.length; i++) 
    {
        if (! (regIsRule.test(meta[i])) )
            continue;
        try 
        {
            items = meta[i].split(/\s+/, 2);
            key = items[1].substring(1).trim();
            idx = meta[i].search(regValue);
            value = idx >= 0 ? meta[i].substring(idx+1).trim() : null;
            if (key == "description"  || 
                key == "downloadURL"  || 
                key == "icon"         || 
                key == "name"         || 
                key == "namespace"    || 
                key == "updateURL"    || 
                key == "version") 
            {
                userscript[key] = value;
            }
            else if (typeof userscript[key] == "number") 
            {
                try 
                {
                    numVal = parseInt(value, 10);
                    if (!isNaN(numVal)) 
                        userscript[key] = numVal;
                }
                catch (e) {
                    script.debug(e);
                }
            }
            else if (key == "unwrap") 
                userscript.unwrap = true;
            else if (key == "noframes") 
                userscript.noframes = true;
            else if (key == "run-at")
                userscript.runAt = value;
            else if (userscript[key] instanceof Array) 
                    userscript[key] = userscript[key].concat(value.match(/\S+/g));
        }
        catch(e) 
        {
            script.debug(e);
        }

    }
    if (userscript.include.length === 0) 
        userscript.include.push({regExp : /.*/, isTld : false});
    else 
        parseIncludeExclude(userscript.include);
    parseIncludeExclude(userscript.exclude);
    // TODO resources

    var scriptId = new String();
    if (userscript.namespace === null || userscript.name === null) 
        userscript.scriptId = path;
    else
        userscript.scriptId = userscript.namespace + "::" + userscript.name;
    userscript.scriptId = userscript.scriptId.replace(/\s+/g, "_");
    userscript.script = "var DWB_scriptId = '" + userscript.scriptId + "';" 
        + util.getBody(GM_compatability) 
        + curScript.substring(0, metaStart) 
        + curScript.substring(scriptStart);
    userscript.offset = meta.length + 1;

    for (i=0; i<userscript.match.length; i++) 
    {
        m = parseMatch(userscript.match[i]);
        if (m !== null) 
            matches.push(m);
    }
    userscript.match = matches;
    if (userscript.runAt == "document-start")
        onStart.push(userscript);
    else 
        onEnd.push(userscript);
}//}}}

function userscriptsStart() 
{
    var ret = false;
    if (onStart.length > 0) 
    {
        onStart.sort(function(a, b) { return b.priority - a.priority; });
        Signal.connect("loadCommitted", loadCommittedCallback);
        ret = true;
    }
    if (onEnd.length > 0) 
    {
        onEnd.sort(function(a, b) { return b.priority - a.priority; });
        Signal.connect("documentLoaded", loadFinishedCallback);
        ret = true;
    }
    if (ret == false)
    {
        extensions.error(me, "No scripts found, place scripts in $XDG_CONFIG_HOME/.config/dwb/greasemonkey");
    }
    //metaData = {};
    //onStart.concat(onEnd).forEach(function (v, k, obj) {
    //  var o = { 
    //    require     : obj[k].require,
    //    downloadURL : obj[k].downloadURL,
    //    updateURL   : obj[k].updateURL,
    //    version     : obj[k].version,
    //    resource    : obj[k].resource
    //  };
    //  metaData[obj[k].scriptId] = o;
    //});
    //io.write(META_DATA, "w", JSON.stringify(metaData));
    return ret;
}
function parseScripts(scripts) //{{{
{
    var i, path;
    for (i=0; i<scripts.length; i++) 
    {
        if (system.fileTest(scripts[i], FileTest.regular | FileTest.symlink)) 
            parseScript(scripts[i]);
    }
    [ "scripts", "greasemonkey" ].forEach(function(path) {
        var scriptDir = data.configDir + "/" + path;
        if (system.fileTest(scriptDir, FileTest.dir)) 
        {
            if (path == "scripts")
                extensions.warning(me, "Using $XDG_CONFIG_HOME/.config/dwb/scripts is deprecated, use $XDG_CONFIG_HOME/.config/dwb/greasemonkey instead");
            var lines = io.dirNames(scriptDir);
            for (i=0; i<lines.length; i++) 
            {
                if (lines[i].charAt(0) == ".")
                    continue;
                path = scriptDir + "/" + lines[i];
                if (!(/^\s*$/.test(lines[i])) && system.fileTest(path, FileTest.regular | FileTest.symlink)) 
                    parseScript(path);
            }
        }
    });
    return userscriptsStart();
}//}}}

var userscripts = {
    init : function (c) {
        return parseScripts(c ? c.scripts || [] : []);
    },
    end : function () {
        Signal.disconnect(loadFinishedCallback);
        Signal.disconnect(loadCommittedCallback);
    }
};

return userscripts;

// vim: set ft=javascript:
