/*
 * Copyright (c) 2014 Samsung Electronics Co., Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
///<reference path='../ts-declarations/node.d.ts' />
///<reference path='../ts-declarations/jalangi.d.ts' />
///<reference path='../ts-declarations/websocket.d.ts' />
///<reference path='../ts-declarations/rewriting-proxy.d.ts' />
///<reference path='../ts-declarations/mkdirp.d.ts' />
/**
 * Created by m.sridharan on 5/29/14.
 */
var websocket = require('websocket');
var http = require('http');
var fs = require('fs');
var path = require('path');
var cp = require('child_process');
var proxy = require('rewriting-proxy');
var urlparser = require('url');
var instUtil = require('jalangi/src/js/instrument/instUtil');
var memTracer = require('./../analysis/memTraceAPI');
var bufUtil = require('./../analysis/bufferUtil');
var mkdirp = require('mkdirp');
var lifetimeAnalysis = require('./../gui/lifetimeAnalysisAPI');
var serveStatic = require('serve-static');
var finalhandler = require('finalhandler');
var WebSocketServer = websocket.server;
var PROTOCOL_NAME = 'mem-trace-protocol';
var javaProc;
var outputStream;
/**
 * utility function used by the websocket server.
 * currently does nothing
 */
function originIsAllowed(origin) {
    // put logic here to detect whether the specified origin is allowed.
    return true;
}

var argparse = require('argparse');
var parser = new argparse.ArgumentParser({
    addHelp: true,
    description: "Integrated server for memory profiler"
});
parser.addArgument(['--proxy'], { help: "run as a proxy server, instrumenting code on-the-fly", action: 'storeTrue' });
parser.addArgument(['--proxyOutput'], { help: "in proxy server mode, directory under which to store instrumented code", defaultValue: '/tmp/proxyOut' });
parser.addArgument(['--noHTTPServer'], { help: "don't start up a local HTTP server", action: 'storeTrue' });
parser.addArgument(['--outputFile'], { help: "write generated trace to a file, instead of sending to lifetime analysis" });
parser.addArgument(['--clientMode'], { help: "in client mode, try to connect to the tinyweb server in the target", action: 'storeTrue' });
parser.addArgument(['--sdbPath'], { help: "the sdb path included in the installed tizen sdk" });
parser.addArgument(['--cliPath'], { help: "The CLI is located in the $<TIZEN_SDK_HOME>/tools/ide/bin directory" });
parser.addArgument(['--serial'], { help: "Sets the serial, which installs the Tizen package" });
parser.addArgument(['--port'], { help: "port on which websocket server should listen", defaultValue: '8082' });
parser.addArgument(['app'], { help: "the app to serve.  in proxy mode, the app should be uninstrumented.", nargs: 1 });
var args = parser.parseArgs();
var app = args.app[0];
// default to app directory; we'll change it in proxy server mode
var outputDir = app;
var sdbPath=args.sdbPath;
var serial=args.serial;
var cliPath = args.cliPath +"/tizen.sh";
/**
 * create a fresh directory in which to dump instrumented scripts
 */
function initProxyOutputDir() {
    outputDir = args.proxyOutput;
    var scriptDirToTry = "";
    for (var i = 0; i < 100; i++) {
        scriptDirToTry = path.join(outputDir, "/site" + i);
        if (!fs.existsSync(scriptDirToTry)) {
            break;
        }
    }
    // create the directory, including parents
    mkdirp.sync(scriptDirToTry);
    console.log("writing output to " + scriptDirToTry);
    outputDir = scriptDirToTry;
}
/**
 * initializes the output target, either the Java process or a WriteStream for a file
 */
function initOutputTarget() {

    if (args.outputFile) {
        if (outputStream) {
            // already initialized
            return;
        }
        outputStream = fs.createWriteStream(args.outputFile);
    }
    else {

        if (javaProc) {
            // already initialized
            return;
        }


        javaProc = lifetimeAnalysis.runLifetimeAnalysis(outputDir);

        javaProc.stdout.on("data", function (chunk) {

			
        });
        javaProc.stderr.on("data", function (chunk) {
            console.log("lifetime analysis error!");
            console.log(chunk.toString());
        });
        javaProc.on("exit", function () {
            
            console.log("done");
            process.exit(0);
            //            showGui();
        });

    }
}

function withSdb(subProc, sdbInSdk, needStartServer){	
	//console.log("get available sdb...");
	var sdbInJsa='tools/sdb';
	var sdbInPath='sdb';
	
	var startSDB=function(callback, sdbPath, needStartServer){	
		//console.log("SDB Path : "+sdbPath);
		if(!needStartServer) return callback(sdbPath);

		var sdbProc=cp.spawn(sdbPath, ['start-server']);		
		sdbProc.stdout.on('data', function (data) {
			process.stdout.write(String(data));
		});
		sdbProc.stderr.on('data', function (data) {
			process.stderr.write(String(data));
		});
		sdbProc.on('exit', function(code, signal){
			//console.log("exit sdb start-server process with code:"+code);
			sdbProc.stdout.end();
			sdbProc.stderr.end();
		});
		sdbProc.on('close', function (code) {
			//console.log("close sdb start-server process.");			
			callback(sdbPath);			
		});
	}
	
	if(sdbInSdk==null || sdbInSdk=="null"){		

		try{
			var sdbCheckProc = cp.spawn(sdbInPath);
			var checked=false;
			sdbCheckProc.on('error', function(err){	
				if(!checked){	
					checked=true;	
					startSDB(subProc, sdbInJsa, needStartServer);
				}
			});
			sdbCheckProc.on('close', function (code) {	
				if(!checked){	
					checked=true;					
					startSDB(subProc, sdbInPath, needStartServer);
				}			
			});	
			
			sdbCheckProc.stdout.on('data', function (data) {
				if(!checked){	
					checked=true;
					startSDB(subProc, sdbInPath, needStartServer);
				}
			});
			sdbCheckProc.stderr.on('data', function (data) {
				if(!checked){
					checked=true;
					startSDB(subProc, sdbInPath, needStartServer);
				}
			});		
			
		}catch(e){
			if(!checked){
				checked=true;
				startSDB(subProc, sdbInJsa, needStartServer);
			}
		}
		
	}else{		
		startSDB(subProc, sdbInSdk, needStartServer);
	}	
}


var recordServer;
function record(sdbpath) {
	var sdb=sdbpath;
    if (args.clientMode) {
        var WebSocket = require('ws');
        var log;
		
		//console.log("==>Begin to spawn checking tinyWeb process");
		//var tinyProc = cp.spawn('python', [process.cwd()+'/web-tct/tools/config_device_tinyweb.py', '--check', '--sdb-path', sdb]);

        console.log("-----------------------------------------------------");
        console.log("Satring 'TinyWeb' process on Target");
        console.log("-----------------------------------------------------");

		var tinyProc = cp.spawn('node', [path.join(process.cwd(),"lib","server","tinyweb.js"),'--check', '--sdbPath',sdb,"--targetId",serial,"--baseDir",path.join(process.cwd(),"web-tct","resource","tinyweb")]);
		tinyProc.stdout.on('data', function (data) {
			process.stdout.write(String(data));				
		});
			
		tinyProc.stderr.on('data', function (data) {
			process.stderr.write(String(data));			
		});
			
		tinyProc.on('exit', function(code, signal){
			//console.log("tinyweb check process exit with code:"+code);	
			tinyProc.stdout.end();
			tinyProc.stderr.end();			
		});
			
		tinyProc.on('close', function (code) {				
			if (code !== 0) {
				console.log("Error : TinyWeb check failed.");
			}	
			//console.log("tinyweb check process closed.");
			//if((String(data)).indexOf(CHECK_TINYWEB_FINISHED)>=0){
			console.log("TinyWeb check completed.");
			
			var connection = new WebSocket('ws://127.0.0.1:8082');
            console.log("-----------------------------------------------------");
            console.log("WebSocket connection completed\n");
            //console.log("connection complete");
			//console.log("start app : " + getApplicationID());

			var runProc = cp.spawn(cliPath, ['run', "--serial",serial,"--pkgid",getApplicationID()]);

			runProc.stdout.on('data', function (data) {
				process.stdout.write(String(data));
			});
			runProc.stderr.on('data', function (data) {

				process.stderr.write(String(data));
			});
		
			connection.on('message', function (message, flags) {
	            //console.log("[SJ] message = " + message + ", message type = "+ message.type + ", message.data = " + message.utf8Data + ", flags.binary = " + flags.binary);
			    if (!flags.binary) {
					//console.log("[SJ] utf8 message:"+message);
					var stringMsg = message;

					if (stringMsg === 'startup') {
						console.log((new Date().toUTCString()) + ' Connection accepted.');
						initOutputTarget();
					} else {								
					}
				} else if (flags.binary) {
					//console.log("[SJ] binary message");
					var binaryMsg = message;
					if (args.outputFile) {
						outputStream.write(binaryMsg);
					} else {
						javaProc.stdin.write(binaryMsg);
					}
					connection.send("done");
				}
			});
			connection.on('close', function (reasonCode, description) {
				//console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected. ' + reasonCode + " " + description);
				if (args.outputFile) {
					outputStream.end("", function () {
						console.log("done writing log");
					});
				} else {
					process.stdout.write("Completing Stale object analysis...");
					if (javaProc) javaProc.stdin.end();
					if (tinyProc) tinyProc.stdin.end();														
				}
			});
			connection.on('error', function(err){
				console.log("connection error:"+String(err));
			});

        });
		
    }
    else {
        var port = args.port;
        var outputDir = '.';
        recordServer = http.createServer(function (request, response) {
            console.log((new Date()) + ' Received request for ' + request.url);
            response.writeHead(404);
            response.end();
        });
        recordServer.listen(port, function () {
            console.log((new Date().toUTCString()));
            console.log('Server is listening on port ' + port);
        });
        var wsServer = new WebSocketServer({
            httpServer: recordServer
        });
        wsServer.on('request', function (request) {
            if (!originIsAllowed(request.origin)) {
                // Make sure we only accept requests from an allowed origin
                request.reject();
                console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.');
                return;
            }

            var connection = request.accept(PROTOCOL_NAME, request.origin);
            //console.log((new Date()) + ' Connection accepted.  Type \'end\' and press enter to stop trace generation in the app.');
            process.stdin.setEncoding('utf8');
            process.stdin.on('readable', function () {

                var chunk = process.stdin.read();
                if (chunk !== null) {
                    if (String(chunk).indexOf('end') !== -1) {
                        process.stdout.write("stopping tracing");
                        connection.sendUTF("endTracing");
                    }
                }
            });
            connection.on('message', function (message) {

                if (message.type === 'utf8') {

                    var stringMsg = message.utf8Data;

                    if (stringMsg === 'startup') {
                        initOutputTarget();
                    }
                    else {
                    }
                }
                else if (message.type === 'binary') {
                    // node.js will buffer in RAM if data isn't written yet
                    // TODO do our own buffering via the callback?
                    var binaryMsg = message.binaryData;
                    if (args.outputFile) {
                        outputStream.write(binaryMsg);
                    }
                    else {
                        javaProc.stdin.write(binaryMsg);
                    }
                    connection.sendUTF("done");
                }
            });
            connection.on('close', function (reasonCode, description) {
                //console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected. ' + reasonCode + " " + description);
                if (args.outputFile) {
                    outputStream.end("", function () {
                        console.log("done writing log");
                        process.exit(0);
                    });
                }
                else {
                    //console.time("lifetime finish");
                    process.stdout.write("completing lifetime analysis...");
                    javaProc.stdin.end();
                }
            });
        });
    }
}
var appServer;
function showApp() {
    var serve = serveStatic(app, { 'index': ['index.html', 'index.htm'] });
    appServer = http.createServer(function (req, res) {
        var done = finalhandler(req, res);
        serve(req, res, done);
    });
    appServer.listen(8888, function () {
        console.log('Serving ' + app + ' on http://localhost:8888');
    });
}
var runGUIServer = function () {
    //process.chdir("../memory-analysis");
    var nodeArgs = ["lib/gui/guiServer.js", path.join(app, "enhanced-trace"), path.join(app, 'staleness.json')];
    cp.spawn("node", nodeArgs, {
        cwd: process.cwd(),
        env: process.env,
        stdio: 'inherit'
    });
    console.log("Showing the results on http://localhost:9000/ Kill with Ctrl-C");
};
var showGui = function () {
    recordServer.close(function () {
        console.log("recorder closed");
    }); //Close the trace writer
    runGUIServer();
    appServer.close(function () {
    });
    //TODO Running the GUI from with an API is not possible
    //Just spawn a subprocess for it.
};
////////
// PROXY SERVER CODE
///////
var jalangiRuntimePrefix = "jalangiRuntime/";
var memTracerRuntimePrefix = "memTracerRuntime/";
var memTracerInitCode = "__memTracer__init__.js";
// TODO generalize this
var initCode = "J$.initParams = {};";
var memTracerDir = path.resolve(path.join(__dirname, '..', '..'));
var jalangiDir = path.resolve(path.join(memTracerDir, 'node_modules', 'jalangi'));
function getHeaderURLs() {
    var result = [];
    // set up headers for analysis2
    instUtil.setHeaders(true);
    instUtil.headerSources.forEach(function (src) {
        result.push(jalangiRuntimePrefix + src);
    });
    // some initialization code via  a fake URL
    result.push(memTracerInitCode);
    // we also need our own analysis files
    memTracer.browserAnalysisFiles.forEach(function (src) {
        result.push(memTracerRuntimePrefix + path.relative(path.join(__dirname, "..", ".."), src));
    });
    return result;
}
// copied from LoggingAnalysis; yuck
// TODO switch to require.js in the browser so we don't need this copy-pasting
// IID special values: -1 is unknown, -2 corresponds to the initial
// DOM traversal to attach mutation observers
var LogEntryType;
(function (LogEntryType) {
    LogEntryType[LogEntryType["DECLARE"] = 0] = "DECLARE";
    LogEntryType[LogEntryType["CREATE_OBJ"] = 1] = "CREATE_OBJ";
    LogEntryType[LogEntryType["CREATE_FUN"] = 2] = "CREATE_FUN";
    LogEntryType[LogEntryType["PUTFIELD"] = 3] = "PUTFIELD";
    LogEntryType[LogEntryType["WRITE"] = 4] = "WRITE";
    LogEntryType[LogEntryType["LAST_USE"] = 5] = "LAST_USE";
    LogEntryType[LogEntryType["FUNCTION_ENTER"] = 6] = "FUNCTION_ENTER";
    LogEntryType[LogEntryType["FUNCTION_EXIT"] = 7] = "FUNCTION_EXIT";
    LogEntryType[LogEntryType["TOP_LEVEL_FLUSH"] = 8] = "TOP_LEVEL_FLUSH";
    LogEntryType[LogEntryType["UPDATE_IID"] = 9] = "UPDATE_IID";
    LogEntryType[LogEntryType["DEBUG"] = 10] = "DEBUG";
    LogEntryType[LogEntryType["RETURN"] = 11] = "RETURN";
    LogEntryType[LogEntryType["CREATE_DOM_NODE"] = 12] = "CREATE_DOM_NODE";
    LogEntryType[LogEntryType["ADD_DOM_CHILD"] = 13] = "ADD_DOM_CHILD";
    LogEntryType[LogEntryType["REMOVE_DOM_CHILD"] = 14] = "REMOVE_DOM_CHILD";
    LogEntryType[LogEntryType["ADD_TO_CHILD_SET"] = 15] = "ADD_TO_CHILD_SET";
    LogEntryType[LogEntryType["REMOVE_FROM_CHILD_SET"] = 16] = "REMOVE_FROM_CHILD_SET";
    LogEntryType[LogEntryType["DOM_ROOT"] = 17] = "DOM_ROOT";
    LogEntryType[LogEntryType["CALL"] = 18] = "CALL";
    LogEntryType[LogEntryType["SCRIPT_ENTER"] = 19] = "SCRIPT_ENTER";
    LogEntryType[LogEntryType["SCRIPT_EXIT"] = 20] = "SCRIPT_EXIT";
    LogEntryType[LogEntryType["FREE_VARS"] = 21] = "FREE_VARS";
    LogEntryType[LogEntryType["SOURCE_MAPPING"] = 22] = "SOURCE_MAPPING";
})(LogEntryType || (LogEntryType = {}));
function sendMetadata(iidSourceInfo, freeVars) {
    var result = [];
    var totalLength = 0;
    Object.keys(iidSourceInfo).forEach(function (iid) {
        var sourceInfo = iidSourceInfo[iid];
        var len = 1 + 4 * 4 + sourceInfo[0].length * 2;
        var tmpBuf = new bufUtil.BufferManager(len);
        tmpBuf.writeByte(22 /* SOURCE_MAPPING */).writeInt(parseInt(iid)).writeString(sourceInfo[0]).writeInt(sourceInfo[1]).writeInt(sourceInfo[2]);
        result.push(tmpBuf.buffer);
        totalLength += len;
    });
    Object.keys(freeVars).forEach(function (iid) {
        var curVarNames = freeVars[iid];
        var tmpBuf, offset = 0;
        var len;
        if (typeof curVarNames === 'string') {
            len = 9 + curVarNames.length * 2;
            tmpBuf = new bufUtil.BufferManager(len);
            tmpBuf.writeByte(21 /* FREE_VARS */).writeInt(parseInt(iid)).writeInt(-1).writeString(curVarNames);
        }
        else {
            var arrayByteLength = 4;
            for (var i = 0; i < curVarNames.length; i++) {
                arrayByteLength += 4 + curVarNames[i].length * 2;
            }
            len = 5 + arrayByteLength;
            tmpBuf = new bufUtil.BufferManager(len);
            tmpBuf.writeByte(21 /* FREE_VARS */).writeInt(parseInt(iid)).writeInt(curVarNames.length);
            for (var i = 0; i < curVarNames.length; i++) {
                tmpBuf.writeString(curVarNames[i]);
            }
        }
        result.push(tmpBuf.buffer);
        totalLength += len;
    });
    var finalBuffer = Buffer.concat(result, totalLength);
    if (args.outputFile) {
        outputStream.write(finalBuffer);
    }
    else {
        javaProc.stdin.write(finalBuffer);
    }
}
function startProxy() {
    initProxyOutputDir();
    // just get the Java process running so it's ready when we start instrumenting
    initOutputTarget();
    var headerURLs = getHeaderURLs();
    throw new Error("this code needs to be fixed");
    // blow away source map files if they exist in temp dir
    //["jalangi_sourcemap.js", "jalangi_sourcemap.json", "jalangi_initialIID.json"].forEach((file) => {
    //    var thePath = path.join(outputDir,file);
    //    if (fs.existsSync(thePath)) {
    //        fs.unlinkSync(thePath);
    //    }
    //});
    var rewriter = function (src, metadata) {
        var url = metadata.url;
        console.log("instrumenting " + url);
        var basename = instUtil.createFilenameForScript(url);
        var filename = path.join(outputDir, basename);
        // TODO check for file conflicts and handle appropriately
        fs.writeFileSync(filename, src);
        var instFileName = basename.replace(new RegExp(".js$"), "_jalangi_.js");
        var options = {
            inputFileName: basename,
            outputFile: instFileName,
            dirIIDFile: outputDir
        };
        var instResult = memTracer.instScriptAndGetMetadata(src, options);
        fs.writeFileSync(path.join(outputDir, instFileName), instResult.instCode);
        sendMetadata(instResult.iidSourceInfo, instResult.freeVars);
        return instResult.instCode;
    };
    var intercept = function (url) {
        var parsedPath = urlparser.parse(url).path;
        if (parsedPath.indexOf(memTracerInitCode) !== -1) {
            return initCode;
        }
        var filePath = null;
        if (parsedPath.indexOf(jalangiRuntimePrefix) !== -1) {
            // serve from Jalangi directory
            filePath = path.join(jalangiDir, parsedPath.substring(jalangiRuntimePrefix.length + 1));
        }
        else if (parsedPath.indexOf(memTracerRuntimePrefix) !== -1) {
            // serve from parent's parent
            filePath = path.join(memTracerDir, parsedPath.substring(memTracerRuntimePrefix.length + 1));
        }
        if (filePath !== null) {
            console.log("serving " + filePath);
        }
        return filePath ? String(fs.readFileSync(filePath)) : null;
    };
    var port = 8501;
    proxy.start({ headerURLs: headerURLs, rewriter: rewriter, intercept: intercept, port: port, noInstRegExp: new RegExp("localhost:9000") });
    console.log("proxy server running on port " + port);
}

function getApplicationID(){
	var parseString = require('xml2js').parseString;
	var config = path.join(outputDir, '/', 'config.xml');
	var xml = fs.readFileSync(config);
	var id;
	parseString(xml, function (err, result) {
		id = result.widget['tizen:application'][0].$.package;
	});
	return id;
}
if (args.proxy) {
    startProxy();
}
if (!args.noHTTPServer) {
    showApp();
}
withSdb(record, sdbPath, false);
//# sourceMappingURL=server.js.map
