"use strict";
var Promise = require('bluebird');
var _ = require('underscore');
var ChromeTesterClient = require('../tester/chrome-tester-client');
var ConcreteMemory = require('./memory/concrete-memory');
var cUtils = require('../context/coverage/coverage-utils');
var loop_record_1 = require('./loop-summarization/loop-record');
var ParserExpression = require('./smt-wrapper/parser-expression');
var SMTSolver = require('./smt-wrapper/smt-solver');
var sUtils = require('./symbolic-execution-utils');
var SymEval = require('./symbolic-evaluation');
var SymbolicMemory = require('./memory/symbolic-memory');
var SymbolicExecution = (function () {
    function SymbolicExecution(functionName, options) {
        this.functionName = functionName;
        this.chromeClient = new ChromeTesterClient({
            hostname: options.hostname,
            port: options.port
        });
        this.M = new ConcreteMemory();
        this.S = new SymbolicMemory();
    }
    SymbolicExecution.prototype.inspectFunction = function (uParameters, cb) {
        var that = this;
        this.response = {};
        this.response.errors = [];
        this.response.testCases = [];
        this.response.results = [];
        this.uParameters = uParameters;
        this.chromeClient.getConfiguration(function (err, res) {
            if (err) {
                that.response.errors.push('Unable to get the global configuration');
                cb(that.response);
            }
            else {
                that.leenaConfig = res;
                try {
                    that.smtSolver = new SMTSolver(that.leenaConfig.solver.name, that.leenaConfig.solver.path, that.leenaConfig.browserSync.webServer.server);
                    that.initializeFunction(function () {
                        cb(that.response);
                    });
                }
                catch (e) {
                    that.response.errors.push(e.message);
                    cb(that.response);
                }
            }
        });
    };
    SymbolicExecution.prototype.initializeFunction = function (cb) {
        var that = this;
        var fName = this.functionName;
        this.chromeClient.getFunctionInstance(fName, function (err, res) {
            if (err) {
                that.response.errors.push('Unable to get instance of function "' + fName + '"');
                cb();
            }
            else {
                var retParseSignature = sUtils.parseFunctionSignature(res.name, res.parameters, that.uParameters);
                if (retParseSignature.errors.length > 0) {
                    that.response.errors = retParseSignature.errors;
                    cb();
                }
                else {
                    that.parsedParameters = retParseSignature.parameters;
                    that.runDART(retParseSignature.parameters, function () {
                        cb();
                    });
                }
            }
        });
    };
    SymbolicExecution.prototype.runDART = function (parametersFirstExecution, cb) {
        var that = this;
        var stackBranch = [];
        this.instrumentedProgram(stackBranch, parametersFirstExecution, function () {
            cb();
        });
    };
    SymbolicExecution.prototype.instrumentedProgram = function (stackBranch, parameters, cb) {
        var that = this;
        this.response.testCases.push(sUtils.getTestCase(parameters));
        this.lParameters = _.clone(parameters);
        for (var pName in parameters) {
            if (parameters.hasOwnProperty(pName)) {
                this.M.add(pName, parameters[pName].value);
                this.S.add(pName, pName);
            }
        }
        this.chromeClient.executeFunctionWithDebugger(this.functionName, sUtils.getActualParameters(parameters), function (err, res) {
            if (err) {
                that.response.errors.push('Unable to execute function with "executeFunctionWithDebugger"');
                cb();
            }
            else {
                that.response.results.push(res.result);
                that.analyzeStatements(stackBranch, res, function (err, res) {
                    if (err) {
                        var errorMessage = (err instanceof Error)
                            ? err.message
                            : 'Uknown error';
                        that.response.errors.push(errorMessage);
                        cb();
                    }
                    else {
                        var rs = res.directed;
                        if (rs === 1) {
                            that.instrumentedProgram(res.stackBranch, res.parameters, cb);
                        }
                        else {
                            if (rs !== 0) {
                                that.response.errors.push('Uknown directed search value (' + rs + ')');
                            }
                            cb();
                        }
                    }
                });
            }
        });
    };
    SymbolicExecution.prototype.analyzeStatements = function (stackBranch, resExecFunction, cb) {
        var functionI = resExecFunction.function;
        var execBranches;
        var pathConstraint = [];
        var LR = new loop_record_1.LoopRecord();
        var statementTable = [];
        var that = this;
        execBranches = _.filter(functionI.branches, function (b) {
            return (b.nExecutions > 0);
        });
        this.analyzeStatement(0, resExecFunction.cfgStatements, functionI.statements, 0, execBranches, 0, stackBranch, pathConstraint, LR, resExecFunction.scope, statementTable, function (err, res) {
            if (err) {
                cb(err, null);
            }
            else {
                var executedConditions = res['executedConditions'];
                that.solvePathConstraint(executedConditions, pathConstraint, stackBranch, function (err, res) {
                    cb(err, res);
                });
            }
        });
    };
    SymbolicExecution.prototype.analyzeStatement = function (indexCFG, cfgStatements, statements, indexB, branches, executedConditions, stackBranch, pathConstraint, LR, scope, statementTable, cb) {
        var s;
        var statementAST;
        var ptData = [];
        var promiseUpdateGT = Promise.resolve();
        var that = this;
        if (indexCFG < cfgStatements.length) {
            s = _.find(statements, function (s) {
                return (s.key === cfgStatements[indexCFG]);
            });
            if (s === undefined) {
                cb(new Error('[analyzeStatement] Exception. Reason: unable to find statement with key "' +
                    cfgStatements[indexCFG] + '"'), null);
            }
            try {
                statementAST = sUtils.getAST(s.instruction);
                if (statementAST.hasOwnProperty('type') && cUtils.nodeIsLoop(statementAST.type)) {
                    try {
                        LR.addLoop(s.key, statementAST);
                        LR.createTables();
                        LR.incrementIteration();
                    }
                    catch (e) {
                        cb(e, null);
                    }
                }
                var statementKeyInTable = -1;
                if ((statementKeyInTable = sUtils.statementInsideTable(s.key, statementTable)) !== -1) {
                    indexB = statementTable[statementKeyInTable].branchesIndexes[0];
                }
                SymEval.evaluateSymbolic(statementAST, indexB, branches, this.M, this.S, ptData, LR, function (err, res) {
                    if (err) {
                        cb(err, null);
                    }
                    else {
                        if (indexCFG >= scope.length) {
                            cb(new Error('indexCFG >= scope.length'), null);
                        }
                        SymEval.evaluateConcrete(scope[indexCFG], that.M);
                        if (LR.isActive()) {
                            if (LR.isStatementDeclaredInsideLoop(s.key)) {
                                if (statementAST.hasOwnProperty('type') && cUtils.nodeIsBranch(statementAST.type)) {
                                    if (ptData.length === 0) {
                                        cb(new Error('Unable to update GT table ' + indexB + ', ' + ptData.length + ', ' + s.instruction), null);
                                    }
                                    else if (sUtils.conditionIsSymbolic(ptData[0].ast, that.S, that.parsedParameters)) {
                                        promiseUpdateGT = Promise.promisify(LR.updateGT.bind(LR))(s.key, ptData[0].ast, ptData[0].resultCondition, pathConstraint, stackBranch, executedConditions, that.M, that.S);
                                    }
                                }
                                var nextStatementKey;
                                var isLastStatementInsideLoop;
                                nextStatementKey = (indexCFG + 1 < cfgStatements.length)
                                    ? cfgStatements[indexCFG + 1]
                                    : null;
                                isLastStatementInsideLoop = (nextStatementKey === null ||
                                    LR.isLastStatementDeclaredInsideLoop(s.key, nextStatementKey));
                                if (isLastStatementInsideLoop) {
                                    try {
                                        LR.incrementIteration();
                                        LR.updateIVT(that.M, that.S);
                                        LR.guessPostconditions(that.S);
                                    }
                                    catch (e) {
                                        cb(e, null);
                                    }
                                    if (LR.isLastIteration(s.key, nextStatementKey)) {
                                        try {
                                            LR.deleteLoop();
                                        }
                                        catch (e) {
                                            cb(e, null);
                                        }
                                    }
                                }
                            }
                            else {
                                try {
                                    LR.deleteLoop();
                                }
                                catch (e) {
                                    cb(e, null);
                                }
                            }
                        }
                        promiseUpdateGT.then(function (newExecutedConditions) {
                            newExecutedConditions = executedConditions;
                            for (var k = 0; k < ptData.length; k++) {
                                if (sUtils.conditionIsSymbolic(ptData[k].ast, that.S, that.parsedParameters)) {
                                    try {
                                        that.updateStack(ptData[k].condition, ~~ptData[k].resultCondition, newExecutedConditions++, stackBranch);
                                    }
                                    catch (e) {
                                        cb(e, null);
                                    }
                                    pathConstraint.push(ptData[k].condition);
                                }
                                if (statementKeyInTable === -1) {
                                    sUtils.addStatementInTable(s.key, ptData[k].indexBranch, statementTable);
                                }
                            }
                            if (statementKeyInTable === -1) {
                                if (statementTable.length > 0) {
                                    indexB = statementTable[statementTable.length - 1].branchesIndexes[0] + 1;
                                }
                                else {
                                    indexB = 0;
                                }
                            }
                            if (indexCFG === (cfgStatements.length - 1)) {
                                cb(null, { 'executedConditions': newExecutedConditions });
                                if (LR.isActive()) {
                                    LR.echoIVT();
                                    LR.echoGT();
                                }
                            }
                            else {
                                that.analyzeStatement(++indexCFG, cfgStatements, statements, indexB, branches, newExecutedConditions, stackBranch, pathConstraint, LR, scope, statementTable, cb);
                            }
                        }).catch(function (err) {
                            cb(err, null);
                        });
                    }
                });
            }
            catch (e) {
                cb(e, null);
            }
        }
    };
    SymbolicExecution.prototype.updateStack = function (condition, branch, index, stackBranch) {
        if (index < stackBranch.length) {
            if (stackBranch[index].branch !== branch) {
                throw new Error('Unable to update the stack. Condition: "' + condition + '"');
            }
            else if (index === (stackBranch.length - 1)) {
                stackBranch[index].branch = branch;
                stackBranch[index].done = true;
            }
        }
        else {
            var stackE = {};
            stackE.branch = branch;
            stackE.done = false;
            stackE.M = _.clone(this.M);
            stackE.S = _.clone(this.S);
            stackBranch.push(stackE);
        }
    };
    SymbolicExecution.prototype.solvePathConstraint = function (kTry, pathConstraint, stackBranch, cb) {
        var that = this;
        var j = -1;
        for (var k = (kTry - 1); k >= 0; k--) {
            if (!stackBranch[k].done) {
                j = k;
                break;
            }
        }
        if (j === -1) {
            cb(null, {
                'directed': 0,
                'parameters': {}
            });
        }
        else {
            pathConstraint[j] = '!(' + pathConstraint[j] + ')';
            stackBranch[j].branch = (stackBranch[j].branch === 0)
                ? 1
                : 0;
            var newPathConstraint = [];
            for (var k = 0; k <= j; k++) {
                newPathConstraint.push({
                    'constraint': pathConstraint[k],
                    'M': stackBranch[k].M,
                    'S': stackBranch[k].S
                });
            }
            var s = [];
            for (var k = 0; k < newPathConstraint.length; k++) {
                s.push(newPathConstraint[k].constraint);
            }
            this.getPathConstraintSolution(newPathConstraint, function (err, res) {
                if (err) {
                    cb(new Error('Unable to get the solution of the path constraint. Reason: ' + err.message), null);
                }
                else {
                    if (res.isSAT) {
                        var newParams = _.clone(that.parsedParameters);
                        for (var pName in newParams) {
                            if (newParams.hasOwnProperty(pName)) {
                                newParams[pName].value = (res.values[pName] !== undefined)
                                    ? res.values[pName]
                                    : (that.lParameters[pName] !== undefined)
                                        ? that.lParameters[pName].value
                                        : sUtils.getDefaultValue(that.lParameters[pName].type);
                            }
                        }
                        var newStackBranch = [];
                        for (var k = 0; k <= j; k++) {
                            newStackBranch.push(stackBranch[k]);
                        }
                        cb(null, {
                            'directed': 1,
                            'parameters': newParams,
                            'stackBranch': newStackBranch
                        });
                    }
                    else {
                        that.solvePathConstraint(j, pathConstraint, stackBranch, cb);
                    }
                }
            });
        }
    };
    SymbolicExecution.prototype.getPathConstraintSolution = function (pathConstraint, cb) {
        var params = [];
        for (var pName in this.uParameters) {
            if (this.uParameters.hasOwnProperty(pName)) {
                params.push({
                    'id': pName,
                    'type': this.uParameters[pName].type,
                    'value': this.uParameters[pName].value,
                    'symbolicallyExecute': true
                });
            }
        }
        var parserExpression = new ParserExpression(pathConstraint, params, this.smtSolver.getName(), this.chromeClient, null);
        var that = this;
        try {
            parserExpression.parse(function (err, smtExpression) {
                if (err) {
                    var errorMessage;
                    if (err instanceof Error) {
                        errorMessage = err.message;
                    }
                    else {
                        errorMessage = 'Error while parsing expression';
                    }
                    that.response.errors.push(errorMessage);
                    cb(errorMessage, null);
                }
                else {
                    that.smtSolver.run(smtExpression, function (err, res) {
                        if (err) {
                            that.response.errors.push('Unable to run SMT expression');
                            console.log(smtExpression);
                            cb(true, null);
                        }
                        else {
                            var smtResponse = that.smtSolver.parseResponse(res);
                            cb(false, smtResponse);
                        }
                    });
                }
            });
        }
        catch (e) {
            that.response.errors.push(e.message);
            cb(true, null);
        }
    };
    return SymbolicExecution;
}());
module.exports = SymbolicExecution;
