/**
 * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
 * *
 * Copyright (C) 2011, 2012, 2013, 2014 Loic J. Duros
 * Copyright (C) 2014, 2015 Nik Nyby
 * Copyright (C) 2015 Ruben Rodriguez
 *
 * This file is part of GNU LibreJS.
 *
 * GNU LibreJS 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.
 *
 * GNU LibreJS 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 GNU LibreJS.  If not, see <http://www.gnu.org/licenses/>.
 */

var {Cc, Ci, Cu, Cm, Cr} = require("chrome");

var narcissusWorker = require("parser/narcissus_worker")
    .narcissusWorker;

const nonTrivialModule = require("js_checker/nontrivial_checker");
const freeChecker = require("js_checker/free_checker");
const relationChecker = require("js_checker/relation_checker");
const types = require("js_checker/constant_types");

const scriptsCached = require("script_entries/scripts_cache").scriptsCached;
var isDryRun = require("addon_management/prefchange").isDryRun;

var checkTypes = types.checkTypes;

const token = types.token;

// for setTimeout.
const timer = require("sdk/timers");

var callbackMap = {};

/**
 * 
 * Pairs a hash with a given callback 
 * method from an object.
 * 
 */
var setHashCallback = function(hash, callback, notification) {
    console.debug('setHashCallback', hash);
    if (hash in callbackMap && isDryRun()) {
        // workaround for issue with dryrun after checking box.
        // do nothing.
        callbackMap[hash] = callback;    
    } else if (hash in callbackMap) {
        console.debug("callback", callbackMap[hash]);
        if (notification && typeof notification.close === 'function') {
            notification.close();
        }
        throw Error("already being checked.");	
    } else {
        console.debug('setting callbackMap for', hash, 'to', callback);
        callbackMap[hash] = callback;
    }
    console.debug("callback is type: ", callback.constructor);
    //callbackMap[hash] = callback;
};

var removeHashCallback = function(hash) {
    if (hash in callbackMap) {
        delete callbackMap[hash];
    }
};

/**
 * find callback and return result (parse tree).
 * 
 */
exports.callbackHashResult = function(hash, result) {
    console.debug('typeof callbackMap function:', typeof callbackMap[hash]);
    console.debug('for hash', hash);
    try {
        callbackMap[hash](result, hash);
    } catch (x) {
        console.debug('error in jsChecker', x, 'hash:', hash);
        // return tree as false.
        console.debug("Error with", x);
        if (typeof callbackMap[hash] === 'function') {
            callbackMap[hash](false, hash);
        } else {
            console.debug('callbackHashResult Error', x);
        }
    }
    // remove callback after it's been called.
    console.debug('JsChecker.callbackHashResult: calling removeHashCallback');
    removeHashCallback(hash);
};

var JsChecker = function() {
    this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    this.nonTrivialChecker = null;
    this.freeToken = false;
    this.nontrivialness = false;
    this.parseTree = null;
    this.relationChecker = null;
    this.jsCode = null;
    this.resultReady = null;
    this.notification = null;
    this.walkTreeCancelled = false;
    this.shortText = null;
    this.hash = null;
    this.queue = null; // will contain the nodes of the script.
};

/**
 * isFreeLicensed
 *
 * This function returns true if the input script is licensed under
 * a free license. Otherwise, it returns false.
 */
JsChecker.prototype.isFreeLicensed = function(script) {
    var magnets = '(' +
        'magnet:\\?xt=urn:btih:90dc5c0be029de84e523b9b3922520e79e0e6f08&dn=cc0.txt|' +
        'magnet:\\?xt=urn:btih:cf05388f2679ee054f2beb29a391d25f4e673ac3&dn=gpl-2.0.txt|' +
        'magnet:\\?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt|' +
        'magnet:\\?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt|' +
        'magnet:\\?xt=urn:btih:5de60da917303dbfad4f93fb1b985ced5a89eac2&dn=lgpl-2.1.txt|' +
        'magnet:\\?xt=urn:btih:0ef1b8170b3b615170ff270def6427c317705f85&dn=lgpl-3.0.txt|' +
        'magnet:\\?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt|' +
        'magnet:\\?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt|' +
        'magnet:\\?xt=urn:btih:3877d6d54b3accd4bc32f8a48bf32ebc0901502a&dn=mpl-2.0.txt|' +
        'magnet:\\?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt|' +
        'magnet:\\?xt=urn:btih:5305d91886084f776adcf57509a648432709a7c7&dn=x11.txt|' +
        'magnet:\\?xt=urn:btih:12f2ec9e8de2a3b0002a33d518d6010cc8ab2ae9&dn=xfree86.txt|' +
        'magnet:\\?xt=urn:btih:87f119ba0b429ba17a44b4bffcab33165ebdacc0&dn=freebsd.txt|' +
        'magnet:\\?xt=urn:btih:b8999bbaf509c08d127678643c515b9ab0836bae&dn=ISC.txt|' +
        'magnet:\\?xt=urn:btih:54fd2283f9dbdf29466d2df1a98bf8f65cafe314&dn=artistic-2.0.txt|' +
        'magnet:\\?xt=urn:btih:e95b018ef3580986a04669f1b5879592219e2a7a&dn=public-domain.txt' +
        ')';

    // Remove licensed parts
    var re = new RegExp(
        '@license +' + magnets + '.*([\n\r].*?)*@license-end', 'g');
    script = script.replace(re, '');

    // Remove comments and empty lines
    re = new RegExp('\/\/.*|/\\*.*?\\*/|^\s*[\n\r]*', 'gm');

    script = script.replace(re, "");

    // If only spaces remain, the file has a free license
    return (script.match(/\S/) === null);
};

/**
 * searchJs
 *
 * Takes in some javascript code (as string).
 * Uses Narcissus parser to build an abstract syntax tree.
 * Checks for trivialness.
 */
JsChecker.prototype.searchJs = function(jsCode, resultReady, url) {
    var that = this;
    var bugfix = require('html_script_finder/bug_fix').narcissusBugFixLibreJS;
    console.debug('JsChecker.searchJs for script url:', url);
    this.url = url;
    this.resultReady = resultReady;
    this.jsCode = jsCode;
    this.shortText = jsCode.replace(bugfix, '').substring(0,100);
    this.notification = require("ui/notification")
        .createNotification(this.shortText).notification;

    var verbatimCode = this.jsCode.replace(bugfix, '');
    this.hash = scriptsCached.getHash(verbatimCode);    
    var isCached = scriptsCached.isCached(verbatimCode, this.hash);
    if (isCached) {
        console.debug("We have it cached indeed!");
        // there is an existing entry for this exact copy
        // of script text.
        console.debug('this script result is cached', this.hash,
                      isCached.result.type);
        console.debug("Return right away");
        // we are not generating a parse tree.
        this.parseTree = {};
        // fake the result is from parse tree.
        this.parseTree.freeTrivialCheck = isCached.result;

        this.relationChecker = isCached.relationChecker;
        // leave without doing parsing/analysis part.
        this.resultReady();
        this.removeNotification();
        return;
    }

    console.debug('url is not cached:', url);

    try {
        // no cache, continue.
        this.relationChecker = relationChecker.relationChecker();
        this.freeToken = types.emptyTypeObj();
        this.nontrivialness = types.emptyTypeObj();

        if (typeof url === 'undefined' || url === null) {
            // use this.hash to keep track of comments made by the nontrivial
            // checker code about why/how the code is found to be nontrivial.
            this.nonTrivialChecker =
                nonTrivialModule.nonTrivialChecker(this.hash);

            // register callback and hash. So that result
            // can be passed.
            setHashCallback(
                this.hash, this.handleTree.bind(this), this.notification);

            // parse using ChromeWorker.
            console.debug(
                'JsChecker.searchJs(): starting narcissusWorker.parse()');
            narcissusWorker.parse(this.jsCode, this.hash);
        } else {
            // Handle external scripts
            console.debug('Analyzing external script: ' + url);
            this.parseTree = {};
            if (this.isFreeLicensed(this.jsCode)) {
                this.parseTree.freeTrivialCheck = type.trivialWithComment(
                    'This script is under free software licenses: ' + url);
            } else {
                this.parseTree.freeTrivialCheck = type.nontrivialWithComment(
                    'This external script is not under free software ' +
                        'licenses: ' + url);
            }

            // Cache result with hash of script for future checks
            scriptsCached.addEntry(
                this.jsCode, this.parseTree.freeTrivialCheck,
                this.relationChecker, true, this.url);
        }
    } catch (x) {
        console.debug('error', x);
        this.handleTree(false, x);
        this.removeNotification();
    }
};

JsChecker.prototype.handleTree = function(tree, errorMessage) {
    var that = this;

    if (tree == false || tree == undefined) {
        // error parsing tree. Just return nonfree nontrivial.
        this.parseTree = {};
        this.parseTree.freeTrivialCheck = types.nontrivialWithComment(
            'error parsing: ' + errorMessage);

        // cache result with hash of script for future checks.
        scriptsCached.addEntry(this.jsCode, this.parseTree.freeTrivialCheck,
                this.relationChecker, true, this.url);
        this.resultReady();
    } else {
        try {
            // no need to keep parseTree in property
            this.parseTree = {}; //tree;
            //console.debug(tree);
            this.walkTree(tree);
        } catch (x) {
            console.debug(x, x.lineNumber, x.fileName);
        }
    }
};

/**
 * getCheckerResult
 * 
 * Callback to Assign result from walkTree to property.
 * reset parse tree. create cache entry.
 * 
 */
JsChecker.prototype.getCheckerResult = function(result) {
    // done with parse tree. Get rid of it.
    this.parseTree = {}; 
    this.removeNotification();

    this.parseTree.nonTrivialChecker = this.nonTrivialChecker;

    // actual result stored here. hack since we used parseTree before.
    this.parseTree.freeTrivialCheck = result;

    // cache result with hash of script for future checks.
    scriptsCached.addEntry(this.jsCode, this.parseTree.freeTrivialCheck,
            this.relationChecker, true, this.url);

    this.resultReady();
};

/**
 * trivialCheck
 *
 * Runs nodes through a series of conditional statements 
 * to find out whether it is trivial or not.
 *
 * @param {object} n. The current node being studied.
 * @param {string} t. The type of node being studied 
 * (initializer, functionbody, try block, ...)
 * 
 */
JsChecker.prototype.trivialCheck = function(n) {
    return this.nonTrivialChecker.checkNontrivial(n);
};

/**
 * freeCheck
 *
 * Check if comments above current node could be a free licence. 
 * If it is, then the script will be flagged as free.
 *
 * @param {object} n. The current node being studied.
 * (initializer, functionbody, try block, ...)
 * 
 */
JsChecker.prototype.freeCheck = function(n, ntype) {
    var check = freeChecker.freeCheck.checkNodeFreeLicense(n, this.queue);
    return check;
};

/**
 * walkTree
 * 
 * An iterative functionwalking the parse tree generated by 
 * Narcissus.
 *
 * @param {object} node. The original node.
 *
 */
JsChecker.prototype.walkTree = function(node) {
    var queue = [node];
    var i, 
        len,
        n, counter = 0, 
        result, 
        processQueue,
        that = this;

    this.queue = queue;  // set as property.

    // set top node as visited.
    node.visited = true;

    /**
     *  functionwalking the tree for a given
     *  amount of time, before calling itself again.
     */
    processQueue = function() {
        var nodeResult, end;

        // record start time of functionexecution.
        var start = Date.now();

        if (that.walkTreeCancelled) {
            // tree walking already completed.
            return;
        }

        while (queue.length) {
            n = queue.shift();
            n.counter = counter++;
            console.debug("Under review", n.type);
            if (n.children != undefined) {
                // fetch all the children.
                len = n.children.length;
                for (i = 0; i < len; i++) {
                    if (n.children[i] != undefined && 
                        n.children[i].visited == undefined
                       ) {
                        // figure out siblings.
                        if (i > 0) {
                            n.children[i].previous = n.children[i-1];
                        }

                        if (i < len) {
                            n.children[i].next = n.children[i+1];
                        }
                        // set parent property.
                        n.children[i].parent = n;
                        n.children[i].visited = true;
                        queue.push(n.children[i]);
                    }
                }
            }

            if (n.type != undefined) {
                // fetch all properties that may have nodes.
                for (var item in n) {
                    if (item != 'tokenizer' && 
                            item != 'children' &&
                            item != 'length' && 
                            n[item] != null && 
                            typeof n[item] === 'object' && 
                            n[item].type != undefined &&
                            n[item].visited == undefined
                       ) {
                        n[item].visited = true;
                        // set parent property
                        n[item].parent = n;
                        queue.push(n[item]);
                    }
                }
            }

            that.checkNode(n);

            if (that.freeToken.type === checkTypes.FREE ||
                that.freeToken.type === checkTypes.FREE_SINGLE_ITEM
               ) {
                // nothing more to look for. We are done.
                that.walkTreeComplete(that.freeToken);
                return;
            } else if (that.nontrivialness.type === checkTypes.NONTRIVIAL) {
                // nontrivial
                // we are done.
                that.walkTreeComplete(that.nontrivialness);
                return;
            }
            // call processQueue again if needed.
            end = Date.now();

            if (queue.length) {
                // there are more nodes in the queue.

                if ((end - start) > 30) {

                    // been running more than 20ms, pause
                    // for 10 ms before calling processQueue 
                    // again.
                    timer.setTimeout(processQueue, 8);
                    return;
                }
            } else {
                // we are done.
                that.removeNotification();
                that.walkTreeComplete();
                return;
            }
        }
    };

    if (node.type === token.SCRIPT) {
        // this is the global scope.
        node.global = true;
        node.parent = null;

        this.relationChecker.storeGlobalDeclarations(node);

        queue.push(node);
        processQueue();
    }
};

/**
 * set walk tree cancelled bool as true.
 * the walk tree method won't run after the variable
 * is set to true.
 */
JsChecker.prototype.cancelWalkTree = function() {
    // prevent any further work on node codes.
    this.walkTreeCancelled = true;
};

/**
 * walkTreeComplete
 * 
 * Trigger when the walkTree has been completed or
 * when it has been cut short.
 * 
 */
JsChecker.prototype.walkTreeComplete = function(result) {
    var that = this;
    this.removeNotification();

    if (this.walkTreeCancelled) {
        // we already triggered complete.
        return;
    }

    // we set the token to cancel further processing.
    this.cancelWalkTree();

    if (result != undefined) {
        // walkTree was returned faster, use it instead.
        this.getCheckerResult(result);

        // we are done.
        return;
    }

    // if all code was fully analyzed.
    if (this.nontrivialness.type === checkTypes.NONTRIVIAL) {
        this.getCheckerResult(this.nontrivialness);
    } else if (this.freeToken.type === checkTypes.FREE) {
        // this is free and may or may not define functions, we don't care.
        this.getCheckerResult(this.freeToken);
    } else if (this.nontrivialness.type ===
            checkTypes.TRIVIAL_DEFINES_FUNCTION) {
        // trivial scripts should become nontrivial if an external script.
        // it may or may not be trivial if inline.
        this.getCheckerResult(this.nontrivialness);
    } else {
        // found no nontrivial constructs or free license, so it's
        // trivial.

        this.getCheckerResult(
                types.trivialFuncWithComment("This script is trivial"));
    }
};


/**
 * checkNode
 * 
 * checks a single node.
 * 
 */
JsChecker.prototype.checkNode = function(n) {
    var sub;
    var fc = this.freeCheck(n);
    var tc = this.trivialCheck(n);

    var nodeResult;

    // check if identifier may be window property (assumption).
    this.relationChecker.checkIdentifierIsWindowProperty(n);

    /*if (fc) {
    console.debug("FC is", fc, "type is", fc.type);
    }*/
    if (fc && fc.type == checkTypes.FREE) {
        // this is free!
        // freeToken is persistent across nodes analyzed and valid
        // for an entire script.
        this.freeToken = types.freeWithComment(
                "Script appears to be free under the following license: " +
                fc.licenseName);
        return;
    } else if (fc && fc.type == checkTypes.FREE_SINGLE_ITEM) {
        console.debug("free single item");
        this.freeToken = types.singleFreeWithComment(
                "Script appears to be free under the following license: " +
                fc.licenseName);
        return;
    }

    if (tc) {
        if (tc.type === checkTypes.NONTRIVIAL) {
            // nontrivial_global is deprecated
            this.nontrivialness = tc;
            return;
        } else if (tc.type === checkTypes.TRIVIAL_DEFINES_FUNCTION) {
            this.nontrivialness = tc;
            return;
        }
    }
};

JsChecker.prototype.removeNotification = function() {
    console.debug('JsChecker.removeNotification()');
    if (this.notification &&
        typeof this.notification.close === 'function'
       ) {
        console.debug('removing', this.shortText);
        // remove notification early on.
        this.notification.close();
        this.notification = null;
    }
};

exports.jsChecker = function() {
    return new JsChecker();
};

exports.removeHashCallback = removeHashCallback;
