import escodegen = require ('escodegen');
import estraverse = require ('estraverse');
import _ = require ('underscore');

import {BranchType, CoverageBranch} from './coverage-branch';
import CoverageStatement = require ('./coverage-statement');


interface Location {
  line : number;
  column : number;
}

interface FunctionLocation {
  start : Location;
  end : Location;
}

class CoverageFunction {
  // Key of the function
  public key : number;

  // Name of the function
  public name : string;

  // Path of the file where the function is declared
  public pathFile : string;

  // Name of the coverage object generated by Istanbul
  public coverageObjectName : string;

  // AST of the function
  public functionAST : any;

  // Parameters of the function. Each parameter is of the form:
  //    {
  //      "type": "Identifier",
  //      "name": <param name> : String
  //    }
  public parameters : Array<any>;

  // Starting location of the function
  public locationStartFunctionDeclaration : FunctionLocation;

  // Ending location of the function
  public locationEndFunctionDeclaration : FunctionLocation;

  // Array of branches declared inside the function
  public branches : Array<CoverageBranch>;

  // Array of statements declared inside the function
  public statements : Array<CoverageStatement>;


  constructor (key : number, name : string, originalAST : any,
               pathFile : string, coverageObjectName : string) {
    this.key = key;
    this.name = name;
    this.pathFile = pathFile;
    this.coverageObjectName = coverageObjectName;
    this.setParameters (originalAST);
  }

  private setParameters (originalAST : any) : void {
    var that = this;

    this.parameters = null;
    estraverse.traverse (originalAST, {
      enter: function (node) {
        if (that.isFunctionToTest (node)) {
          that.parameters = node.params;

          this.break ();
        }
      }
    });

    if (this.parameters === null) {
      throw new Error (
        'Unable to get parameters of function "' + this.name + '"'
      );
    }
  }

  public setStartLocation (locationStart : Location, locationEnd : Location) {
    this.locationStartFunctionDeclaration = <FunctionLocation> {};
    this.locationStartFunctionDeclaration.start = locationStart;
    this.locationStartFunctionDeclaration.end = locationEnd;
  }

  public setEndLocation (originalAST : any) {
    var that = this;

    this.locationEndFunctionDeclaration = <FunctionLocation> {};

    estraverse.traverse (originalAST, {
      enter: function (node) {
        if (that.isFunctionToTest (node)) {
          if (node.body !== undefined && node.body.loc !== undefined) {
            that.locationEndFunctionDeclaration.start = {
              line: node.body.loc.start.line,
              column: node.body.loc.start.column
            };
            that.locationEndFunctionDeclaration.end = {
              line: node.body.loc.end.line,
              column: node.body.loc.end.column
            };

            that.functionAST = node;

            this.break ();
          }
        }
      }
    });
  }

  public addStatements (statement: any, statementMap : any) : void {
    var startLocation : Location;
    var endLocation : Location;
    var stmtInstance : CoverageStatement;
    this.statements = [];

    for (var key in statementMap) {
      if (statementMap.hasOwnProperty (key)) {
        startLocation = <Location> {};
        startLocation.line   = statementMap[key].start.line;
        startLocation.column = statementMap[key].start.column;

        endLocation = <Location> {};
        endLocation.line   = statementMap[key].end.line;
        endLocation.column = statementMap[key].end.column;

        // Statement is declared inside 'this' function
        if (this.objectMapIsInsideFunction (startLocation, endLocation)) {
          try {
            stmtInstance = new CoverageStatement (
              key,
              startLocation,
              endLocation,
              this.functionAST
            );
            // Initialize values of the statement
            stmtInstance.initializeValues (statement);

            this.statements.push (stmtInstance);
          } catch (e) {
            throw e;
          }
        }
      }
    }
  }

  public addBranches (branch : any, branchMap : any) : void {
    var startLocation : Location;
    var endLocation : Location;
    var branchInstance : CoverageBranch;
    var handleBranches = [
      'if',       // Expression node is avaiable in 'test' node
      'switch',   // Expression node is avaiable in 'discriminant' node
      'cond-expr' // Expression node is avaiable in 'test' node
    ];
    this.branches = [];

    for (var key in branchMap) {
      if (branchMap.hasOwnProperty (key)) {
        if (handleBranches.indexOf (branchMap[key].type) === -1) {
          continue;
        }
        // We must handle this branch
        startLocation = <Location> {};
        startLocation.line = branchMap[key].locations[0].start.line;
        startLocation.column = branchMap[key].locations[0].start.column;

        endLocation = <Location> {};
        endLocation.line = branchMap[key].locations[0].end.line;
        endLocation.column = branchMap[key].locations[0].end.column;

        // Statement is declared inside 'this' function
        if (this.objectMapIsInsideFunction (startLocation, endLocation)) {
          try {
            branchInstance = new CoverageBranch (
              key,
              startLocation,
              endLocation,
              this.functionAST
            );
            // Initialize values of the branch
            try {
              branchInstance.initializeValues (branch);

              this.branches.push (branchInstance);
            } catch (e) {
              throw e;
            }
          } catch (e) {
            throw e;
          }
        }
      }
    }
  }

  public execute (coverageObject : any, cb : (err : Error, res) => void) : void {
    var statements_ = coverageObject.s;
    var statementsMap = coverageObject.statementMap;
    var branches_ = coverageObject.b;
    var branchesMap = coverageObject.branchMap;
    var key;
    var found : boolean;
    var error : Error;

    // Update statements
    for (var k = 0; k < this.statements.length; k++) {
      found = false;
      for (key in statementsMap) {
        if (statementsMap.hasOwnProperty (key) && key == this.statements[k].key) {
          try {
            this.statements[k].updateValues (statements_);
            found = true;
            break;
          } catch (e) {
            error = new Error (
              'Unable to update values of statement ' + key + '. Reason: ' + e.message
            );

            cb (error, null);
          }
        }
      }
      if (!found) {
        error = new Error (
          'Unable to update values of statement ' + key + '. Object not found.'
        );

        cb (error, null);
      }
    }

    // Update branches
    var indexSwitchStatement : number = 0;
    var numSwitchs : Object = {
      'branches':   0,  // Counter for 'this.branches'
      'statements': 0   // Counter for 'this.statements'
    };

    for (var k = 0; k < this.branches.length; k++) {
      found = false;
      for (key in branchesMap) {
        if (branchesMap.hasOwnProperty (key) && key == this.branches[k].key) {
          try {
            this.branches[k].updateValues (branches_);
            found = true;

            // The update of switch is different from the 'if' and 'cond-expr'
            // If the condition is checked, the branch always changes its value.
            // The 'switch', instead, is different. For example:
            //   switch (a) {
            //     case 1: break;
            //   }
            // In the code above, the array generated by Istanbul will be composed
            // by one element. If a is different from 1, it's not enough to use
            // the attribute 'isExecuted'. For this reason, when we encounter
            // the 'switch' we must find the branch inside the 'statementsMap' and
            // check if the statement that map the branch is updated
            // 1 -> Switch
            if (this.branches[k].branchType === BranchType.Switch) {
              numSwitchs['branches']++;

              for (var j = indexSwitchStatement; j < this.statements.length; j++) {
                if (this.statements[j].type === 'SwitchStatement') {
                  indexSwitchStatement = j + 1;
                  //this.branches[k].isExecuted = this.statements[j].isExecuted;
                  numSwitchs['statements']++;
                  break;
                }
              }
            }

            break;
          } catch (e) {
            error = new Error (
              'Unable to update values of branches ' + key + '. Reason: ' + e.message
            );

            cb (error, null);
          }
        }
      }
      if (!found) {
        error = new Error (
          'Unable to update values of branches ' + key + '. Object not found.'
        );

        cb (error, null);
      }
    }

    if (numSwitchs['branches'] !== numSwitchs['statements']) {
      error = new Error (
        'Unable to update values of branches/statements ' +
        '(numSwitchs["branches"] !== numSwitchs["statements"])'
      );

      cb (error, null);
    }

    cb (null, true);
  }

  private objectMapIsInsideFunction (sl : Location, el : Location) : boolean {
    // Function declaration (function keyword, function name, parameters) starts
    // in 'locationStartFunctionDeclaration'. Its body, instead, terminates
    // in 'locationEndFunctionDeclaration'.
    // If one 'instruction' declaration (branch or statement) is in this range
    // then it's declared inside this function
    // ** Function declaration is also a statement! We ignore it! **
    var locStartFunc = <Location> {};
    var locEndFunc = <Location> {};

    // Location where function is declared (we ignore the column)
    locStartFunc.line = this.locationStartFunctionDeclaration.start.line;

    // Location where function 'terminates'
    locEndFunc.line = this.locationEndFunctionDeclaration.end.line;
    locEndFunc.column = this.locationEndFunctionDeclaration.end.column;

    if (sl.line >= locStartFunc.line) {
      if (el.line < locEndFunc.line) {
        return true;
      } else if (el.line === locEndFunc.line) {
        return el.column < locEndFunc.column;
      }
    }

    return false;
  }

  private isFunctionToTest (node) : boolean {
    var isFunctionToTest_ : boolean;

    isFunctionToTest_ = node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration';
    isFunctionToTest_ = isFunctionToTest_ && node.id !== null;
    isFunctionToTest_ = isFunctionToTest_ && node.id.type === 'Identifier' && node.id.name === this.name;

    return isFunctionToTest_;
  }
}

export = CoverageFunction;
