/**
 * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
 * *
 * Copyright (C) 2011, 2012 Loic J. Duros
 *
 * 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, see  <http://www.gnu.org/licenses/>.
 *
 */

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

const narcissusWorker = require("narcissus_parser/narcissus_worker");

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 checkTypes = types.checkTypes;

const token = types.token;

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

var callbackMap = {};

/**
 *  This module needs to be updated so that it is asynchronous
 *  and to reduce CPU usage --
 * 
 *  Partly already done in:
 *  https://gitorious.org/librejs/librejs/blobs/async/lib/js_checker/js_checker.js
 *  data/complain/contact_finder.js on how to proceed.
 * 
 */

/**
 * 
 * Pairs a hash with a given callback 
 * method from an object.
 * 
 */
var setHashCallback = function (hash, callback) {
    
    if (hash in callbackMap) {
	throw Error("already being checked.");	
    } else {
	callbackMap[hash] = callback;

    }

    //callbackMap[hash] = callback;
};

var removeHashCallback = function (hash) {

  if (hash in callbackMap) {

     callbackMap[hash] = null;

  }

};

/**
 * find callback and return result (parse tree).
 * 
 */
exports.callbackHashResult = function (hash, result) {
    try {

	callbackMap[hash](result);

    } catch (x) {
	//console.log('error in jsChecker', x, 'hash:', hash);
	// return tree as false.
	callbackMap[hash](false);
    }

    // remove callback after it's been called.
    removeHashCallback(hash);

};

var jsCheckerObject = {

    timer: Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer),
    nonTrivialChecker: null,
    freeToken: false,
    nontrivialness:false,
    parseTree: null,
    relationChecker: null,
    jsCode: null,
    resultReady: null,

    walkTreeCancelled: false,

    hash: null,

    /**
     * searchJs
     *
     * Takes in some javascript code (as string).
     * Uses Narcissus parser to build an abstract syntax tree.
     * Checks for trivialness.
     *
     * @param {string} jsCode. A string with js content.
     * @return {object} parseTree. The abstract syntax tree from Narcissus
     * with an extra "trivialness" and "free" property.
     * 
     */
    searchJs: function (jsCode, resultReady) {
	var that = this;
	
	this.resultReady = resultReady;
	this.jsCode = jsCode;

	this.hash = scriptsCached.getHash(this.jsCode);	
	var isCached = scriptsCached.isCached(this.jsCode, this.hash);

	if (isCached) {
	    
	    // there is an existing entry for this exact copy
	    // of script text.
	    //console.log('this script result is cached', this.hash, isCached.filename);

	    // 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();
	    return;
	}

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

	    this.nonTrivialChecker = nonTrivialModule.nonTrivialChecker();

	    // register callback and hash. So that result
	    // can be passed.
	    setHashCallback(this.hash, this.handleTree.bind(this));
	    
	    // parse using ChromeWorker.
	    narcissusWorker.parse(this.jsCode, this.hash);
	    
	} catch (x) {
	    //console.log('error', x);
	    this.handleTree(false);
	}
	

    },

    handleTree: function (tree) {
	var that = this;

	if (tree == false || tree == undefined) {

	    // error parsing tree. Just return nonfree nontrivial global.
	    this.parseTree = {};
	    this.parseTree.freeTrivialCheck = checkTypes.NONTRIVIAL_GLOBAL; 

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

	    this.resultReady();

	} else {
	    
	    try {

		// no need to keep parseTree in property
		this.parseTree = {}; //tree;

		//console.log(tree);

		this.walkTree(tree);

		

	    } catch (x) {
		console.log(x, x.lineNumber, x.fileName);
	    }
	}

    },

    /**
     * getCheckerResult
     * 
     * Callback to Assign result from walkTree to property.
     * reset parse tree. create cache entry.
     * 
     */
    getCheckerResult: function (result) {

	// done with parse tree. Get rid of it.
	this.parseTree = {}; 

	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);

	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, function body, try block, ...)
     * 
     */
    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, function body, try block, ...)
     * 
     */
    freeCheck: function (n, ntype) {
	return freeChecker.freeCheck.checkNodeFreeLicense(n);
    },

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

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

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

	    // record start time of function execution.
	    var start = Date.now();
	    
	    if (that.walkTreeCancelled) {
		// tree walking already completed.
		return;
	    }

	    
	    while (queue.length) {
		
		n = queue.shift();

		n.counter = counter++;

		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 (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 === checkTypes.FREE && 
		    that.nontrivialness === checkTypes.NONTRIVIAL_GLOBAL) {
		    // nothing more to look for. We are done.
		    that.walkTreeComplete(checkTypes.FREE_NONTRIVIAL_GLOBAL);
		    return;
		} 
		
		else if (that.nontrivialness === checkTypes.NONTRIVIAL_GLOBAL) {
		    // we already know there's no free license at the top,
		    // and this is nontrivial global nothing more to look
		    // for. We are done.
		    that.walkTreeComplete(checkTypes.NONTRIVIAL_GLOBAL);
		    return;
		}

		// call processQueue again if needed.
		end = Date.now();

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

		    if ((end - start) > 20) {

			// been running more than 20ms, pause
			// for 10 ms before calling processQueue 
			// again.
			timer.setTimeout(processQueue, 10);
			return;

		    }

		} else {
		    // we are done.
		    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.
     */
    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.
     * 
     */
    walkTreeComplete: function (result) {


	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 === checkTypes.NONTRIVIAL_GLOBAL) {
	    this.getCheckerResult(checkTypes.NONTRIVIAL_GLOBAL);
	}
	

	else if (this.freeToken === checkTypes.FREE) {
	    // this is free and may or may not define functions, we don't care.
	    this.getCheckerResult(checkTypes.FREE);
	}
	
	else if (this.nontrivialness === checkTypes.TRIVIAL_DEFINES_FUNCTION) {

	    // trivial scripts should become nontrivial if an external script.
	    // it may or may not be trivial if inline.
	    this.getCheckerResult(checkTypes.TRIVIAL_DEFINES_FUNCTION);
	    
	}

	else {
	    // found no nontrivial constructs or free license, so it's
	    // trivial.
	    this.getCheckerResult(checkTypes.TRIVIAL);
	}

    },


    /**
     * checkNode
     * 
     * checks a single node.
     * 
     */
    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) {
	    // this is free!
	    // freeToken is persistent across nodes analyzed and valid
            // for an entire script.
	    this.freeToken = checkTypes.FREE;
	}

	if (tc) {
	    
	    if (tc === checkTypes.NONTRIVIAL_GLOBAL) {

		// this is free and a construct that makes all scripts
		// globally nontrivial.
		this.nontrivialness = checkTypes.NONTRIVIAL_GLOBAL;
		return;
	    }

  	    else if (tc === checkTypes.TRIVIAL_DEFINES_FUNCTION &&
		     this.nontrivialness != checkTypes.NONTRIVIAL_GLOBAL) {
		this.nontrivialness =  checkTypes.TRIVIAL_DEFINES_FUNCTION;
		return;
	    }

	}


    }

};

// create an instance of jsCheckerObject.
exports.jsChecker = function () {
    var checker = Object.create(jsCheckerObject);
    return checker;
};