var inBrowser = typeof window !== 'undefined' && this === window;
var parseAndModify = (inBrowser ? window.falafel : require("falafel"));

(inBrowser ? window : exports).blanket = (function(){
    var linesToAddTracking = [
        "ExpressionStatement",
        "BreakStatement"   ,
        "ContinueStatement" ,
        "VariableDeclaration",
        "ReturnStatement"   ,
        "ThrowStatement"   ,
        "TryStatement"     ,
        "FunctionDeclaration"    ,
        "IfStatement"       ,
        "WhileStatement"    ,
        "DoWhileStatement"   ,
        "ForStatement"   ,
        "ForInStatement"  ,
        "SwitchStatement"  ,
        "WithStatement"
    ],
    linesToAddBrackets = [
        "IfStatement"       ,
        "WhileStatement"    ,
        "DoWhileStatement"     ,
        "ForStatement"   ,
        "ForInStatement"  ,
        "WithStatement"
    ],
    __blanket,
    copynumber = Math.floor(Math.random()*1000),
    coverageInfo = {},options = {
        reporter: null,
        adapter:null,
        filter: null,
        customVariable: null,
        loader: null,
        ignoreScriptError: false,
        existingRequireJS:false,
        autoStart: false,
        timeout: 180,
        ignoreCors: false,
        branchTracking: false,
        sourceURL: false,
        debug:false,
        engineOnly:false,
        testReadyCallback:null,
        commonJS:false,
        instrumentCache:false,
        modulePattern: null,
        ecmaVersion: 5
    };

    if (inBrowser && typeof window.blanket !== 'undefined'){
        __blanket = window.blanket.noConflict();
    }

    _blanket = {
        noConflict: function(){
            if (__blanket){
                return __blanket;
            }
            return _blanket;
        },
        _getCopyNumber: function(){
            //internal method
            //for differentiating between instances
            return copynumber;
        },
        extend: function(obj) {
            //borrowed from underscore
            _blanket._extend(_blanket,obj);
        },
        _extend: function(dest,source){
          if (source) {
            for (var prop in source) {
              if ( dest[prop] instanceof Object && typeof dest[prop] !== "function"){
                _blanket._extend(dest[prop],source[prop]);
              }else{
                  dest[prop] = source[prop];
              }
            }
          }
        },
        getCovVar: function(){
            var opt = _blanket.options("customVariable");
            if (opt){
                if (_blanket.options("debug")) {console.log("BLANKET-Using custom tracking variable:",opt);}
                return inBrowser ? "window."+opt : opt;
            }
            return inBrowser ?   "window._$blanket" : "_$jscoverage";
        },
        options: function(key,value){
            if (typeof key !== "string"){
                _blanket._extend(options,key);
            }else if (typeof value === 'undefined'){
                return options[key];
            }else{
                options[key]=value;
            }
        },
        // instrument the file synchronously
        // `next` is optional callback which will be called
        // with instrumented code when present
        instrumentSync: function(config, next){
            //check instrumented hash table,
            //return instrumented code if available.
            var inFile = config.inputFile,
                inFileName = config.inputFileName;
            //check instrument cache
           if (_blanket.options("instrumentCache") && sessionStorage && sessionStorage.getItem("blanket_instrument_store-"+inFileName)){
                if (_blanket.options("debug")) {console.log("BLANKET-Reading instrumentation from cache: ",inFileName);}
                if (next) {
                    next(sessionStorage.getItem("blanket_instrument_store-"+inFileName));
                } else {
                    return(sessionStorage.getItem("blanket_instrument_store-"+inFileName));
                }
            }else{
                var sourceArray = _blanket._prepareSource(inFile);
                _blanket._trackingArraySetup=[];
                //remove shebang
                inFile = inFile.replace(/^\#\!.*/, "");
                var instrumented =  parseAndModify(inFile,{locations:true,comment:true,ecmaVersion:_blanket.options("ecmaVersion")}, _blanket._addTracking(inFileName));
                instrumented = _blanket._trackingSetup(inFileName,sourceArray)+instrumented;
                if (_blanket.options("sourceURL")){
                    instrumented += "\n//@ sourceURL="+inFileName.replace("http://","");
                }
                if (_blanket.options("debug")) {console.log("BLANKET-Instrumented file: ",inFileName);}
                if (_blanket.options("instrumentCache") && sessionStorage){
                    if (_blanket.options("debug")) {console.log("BLANKET-Saving instrumentation to cache: ",inFileName);}
                    sessionStorage.setItem("blanket_instrument_store-"+inFileName,instrumented);
                }
                if (next) {
                    next(instrumented);
                } else {
                    return(instrumented);
                }
            }
        },
        instrument: function(config, next){
            _blanket.instrumentSync(config, next);
        },
        _trackingArraySetup: [],
        _branchingArraySetup: [],
        _useStrictMode: false,
        _prepareSource: function(source){
            return source.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(/(\r\n|\n|\r)/gm,"\n").split('\n');
        },
        _trackingSetup: function(filename,sourceArray){
            var branches = _blanket.options("branchTracking");
            var sourceString = sourceArray.join("',\n'");
            var intro = "";
            var covVar = _blanket.getCovVar();

            if(_blanket._useStrictMode) {
                intro += "'use strict';\n";
            }

            intro += "if (typeof "+covVar+" === 'undefined') "+covVar+" = {};\n";
            if (branches){
                intro += "var _$branchFcn=function(f,l,c,r){ ";
                intro += "if (!!r) { ";
                intro += covVar+"[f].branchData[l][c][0] = "+covVar+"[f].branchData[l][c][0] || [];";
                intro += covVar+"[f].branchData[l][c][0].push(r); }";
                intro += "else { ";
                intro += covVar+"[f].branchData[l][c][1] = "+covVar+"[f].branchData[l][c][1] || [];";
                intro += covVar+"[f].branchData[l][c][1].push(r); }";
                intro += "return r;};\n";
            }
            intro += "if (typeof "+covVar+"['"+filename+"'] === 'undefined'){";

            intro += covVar+"['"+filename+"']=[];\n";
            if (branches){
                intro += covVar+"['"+filename+"'].branchData=[];\n";
            }
            intro += covVar+"['"+filename+"'].source=['"+sourceString+"'];\n";
            //initialize array values
            _blanket._trackingArraySetup.sort(function(a,b){
                return parseInt(a,10) > parseInt(b,10);
            }).forEach(function(item){
                intro += covVar+"['"+filename+"']["+item+"]=0;\n";
            });
            if (branches){
                _blanket._branchingArraySetup.sort(function(a,b){
                    return a.line > b.line;
                }).sort(function(a,b){
                    return a.column > b.column;
                }).forEach(function(item){
                    if (item.file === filename){
                        intro += "if (typeof "+ covVar+"['"+filename+"'].branchData["+item.line+"] === 'undefined'){\n";
                        intro += covVar+"['"+filename+"'].branchData["+item.line+"]=[];\n";
                        intro += "}";
                        intro += covVar+"['"+filename+"'].branchData["+item.line+"]["+item.column+"] = [];\n";
                        intro += covVar+"['"+filename+"'].branchData["+item.line+"]["+item.column+"].consequent = "+JSON.stringify(item.consequent)+";\n";
                        intro += covVar+"['"+filename+"'].branchData["+item.line+"]["+item.column+"].alternate = "+JSON.stringify(item.alternate)+";\n";
                    }
                });
            }
            intro += "}";

            return intro;
        },
        _blockifyIf: function(node){
            if (linesToAddBrackets.indexOf(node.type) > -1){
                var bracketsExistObject = node.consequent || node.body;
                var bracketsExistAlt = node.alternate;
                if( bracketsExistAlt && bracketsExistAlt.type !== "BlockStatement") {
                    bracketsExistAlt.update("{\n"+bracketsExistAlt.source()+"}\n");
                }
                if( bracketsExistObject && bracketsExistObject.type !== "BlockStatement") {
                    bracketsExistObject.update("{\n"+bracketsExistObject.source()+"}\n");
                }
            }
        },
        _trackBranch: function(node,filename){
            //recursive on consequent and alternative
            var line = node.loc.start.line;
            var col = node.loc.start.column;

            _blanket._branchingArraySetup.push({
                line: line,
                column: col,
                file:filename,
                consequent: node.consequent.loc,
                alternate: node.alternate.loc
            });

            var updated = "_$branchFcn"+
                          "('"+filename+"',"+line+","+col+","+node.test.source()+
                          ")?"+node.consequent.source()+":"+node.alternate.source();
            node.update(updated);
        },
        _addTracking: function (filename) {
            //falafel doesn't take a file name
            //so we include the filename in a closure
            //and return the function to falafel
            var covVar = _blanket.getCovVar();

            return function(node){
                _blanket._blockifyIf(node);

                if (linesToAddTracking.indexOf(node.type) > -1 && node.parent.type !== "LabeledStatement") {
                    _blanket._checkDefs(node,filename);
                    if (node.type === "VariableDeclaration" &&
                        (node.parent.type === "ForStatement" || node.parent.type === "ForInStatement")){
                        return;
                    }
                    if (node.loc && node.loc.start){
                        node.update(covVar+"['"+filename+"']["+node.loc.start.line+"]++;\n"+node.source());
                        _blanket._trackingArraySetup.push(node.loc.start.line);
                    }else{
                        //I don't think we can handle a node with no location
                        throw new Error("The instrumenter encountered a node with no location: "+Object.keys(node));
                    }
                }else if (_blanket.options("branchTracking") && node.type === "ConditionalExpression"){
                    _blanket._trackBranch(node,filename);
                }else if (node.type === "Literal" && node.value === "use strict" && node.parent && node.parent.type === "ExpressionStatement" && node.parent.parent && node.parent.parent.type === "Program"){
                    _blanket._useStrictMode = true;
                }
            };
        },
        _checkDefs: function(node,filename){
            // Make sure developers don't redefine window. if they do, inform them it is wrong.
            if (inBrowser){
                if (node.type === "VariableDeclaration" && node.declarations) {
                    node.declarations.forEach(function(declaration) {
                        if (declaration.id.name === "window") {
                            throw new Error("Instrumentation error, you cannot redefine the 'window' variable in  " + filename + ":" + node.loc.start.line);
                        }
                    });
                }
                if (node.type === "FunctionDeclaration" && node.params) {
                    node.params.forEach(function(param) {
                        if (param.name === "window") {
                            throw new Error("Instrumentation error, you cannot redefine the 'window' variable in  " + filename + ":" + node.loc.start.line);
                        }
                    });
                }
                //Make sure developers don't redefine the coverage variable
                if (node.type === "ExpressionStatement" &&
                    node.expression && node.expression.left &&
                    node.expression.left.object && node.expression.left.property &&
                    node.expression.left.object.name +
                        "." + node.expression.left.property.name === _blanket.getCovVar()) {
                    throw new Error("Instrumentation error, you cannot redefine the coverage variable in  " + filename + ":" + node.loc.start.line);
                }
            }else{
                //Make sure developers don't redefine the coverage variable in node
                if (node.type === "ExpressionStatement" &&
                    node.expression && node.expression.left &&
                    !node.expression.left.object && !node.expression.left.property &&
                    node.expression.left.name === _blanket.getCovVar()) {
                    throw new Error("Instrumentation error, you cannot redefine the coverage variable in  " + filename + ":" + node.loc.start.line);
                }
            }
        },
        setupCoverage: function(){
            coverageInfo.instrumentation = "blanket";
            coverageInfo.stats = {
                "suites": 0,
                "tests": 0,
                "passes": 0,
                "pending": 0,
                "failures": 0,
                "start": new Date()
            };
        },
        _checkIfSetup: function(){
            if (!coverageInfo.stats){
                throw new Error("You must call blanket.setupCoverage() first.");
            }
        },
        onTestStart: function(){
            if (_blanket.options("debug")) {console.log("BLANKET-Test event started");}
            this._checkIfSetup();
            coverageInfo.stats.tests++;
            coverageInfo.stats.pending++;
        },
        onTestDone: function(total,passed){
            this._checkIfSetup();
            if(passed === total){
                coverageInfo.stats.passes++;
            }else{
                coverageInfo.stats.failures++;
            }
            coverageInfo.stats.pending--;
        },
        onModuleStart: function(){
            this._checkIfSetup();
            coverageInfo.stats.suites++;
        },
        onTestsDone: function(){
            if (_blanket.options("debug")) {console.log("BLANKET-Test event done");}
            this._checkIfSetup();
            coverageInfo.stats.end = new Date();

            if (inBrowser){
                this.report(coverageInfo);
            }else{
                if (!_blanket.options("branchTracking")){
                    delete (inBrowser ? window : global)[_blanket.getCovVar()].branchFcn;
                }
                this.options("reporter").call(this,coverageInfo);
            }
        }
    };
    return _blanket;
})();
