/**
 * 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("sdk/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, notification) {
  
  if (hash in callbackMap) {
    // console.log("callback", callbackMap[hash]);
    notification.close();
	  throw Error("already being checked.");	
  } else {
	  callbackMap[hash] = callback;
  }
  //console.log("callback is type: ", callback.constructor);
  //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.
    //console.log("Error with", x);
	  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,
  notification:null,
  walkTreeCancelled: false,
  shortText:null,
  hash: null,

  /**
   * searchJs
   *
   * Takes in some javascript code (as string).
   * Uses Narcissus parser to build an abstract syntax tree.
   * Checks for trivialness.
   *
   */
  searchJs: function (jsCode, resultReady, url) {
	  var that = this;

	  this.resultReady = resultReady;
	  this.jsCode = jsCode;
    this.shortText = jsCode.replace('this.narcissusBugFixLibreJS', '').substring(0,100);
    this.notification = require("ui/notification").createNotification(this.shortText).notification;
	  this.hash = scriptsCached.getHash(this.jsCode);	
    //console.log("hash is", this.hash, "for url:", url);
	  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.result.type);
      //console.log("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;
	  }

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

      // 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.
	    narcissusWorker.parse(this.jsCode, this.hash);
	    
	  } catch (x) {
	    console.log('error', x);
      this.handleTree(false, x);
      this.removeNotification();

	  }
	  
  },

  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);
      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.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);
    
	  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) {
	  var check = freeChecker.freeCheck.checkNodeFreeLicense(n, this.jsCode);
    return check;
  },

  /**
   * 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++;

        //console.log("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 (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.
   */
  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) {
    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.
   * 
   */
  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.log("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.log("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;
	    }

	  }


  },
  removeNotification: function () {
    var that = this;
    if (this.notification && this.notification.close) {
      //console.log("removing", this.shortText);
      // remove notification early on.
      this.notification.close();
      this.notification = null;
    }
  }

};

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