"use strict"

var esprima = require("esprima")
var uniq = require("uniq")

var PREFIX_COUNTER = 0

function CompiledArgument(name, lvalue, rvalue) {
  this.name = name
  this.lvalue = lvalue
  this.rvalue = rvalue
  this.count = 0
}

function CompiledRoutine(body, args, thisVars, localVars) {
  this.body = body
  this.args = args
  this.thisVars = thisVars
  this.localVars = localVars
}

function isGlobal(identifier) {
  if(identifier === "eval") {
    throw new Error("cwise-parser: eval() not allowed")
  }
  if(typeof window !== "undefined") {
    return identifier in window
  } else if(typeof global !== "undefined") {
    return identifier in global
  } else if(typeof self !== "undefined") {
    return identifier in self
  } else {
    return false
  }
}

function getArgNames(ast) {
  var params = ast.body[0].expression.callee.params
  var names = new Array(params.length)
  for(var i=0; i<params.length; ++i) {
    names[i] = params[i].name
  }
  return names
}

function preprocess(func) {
  var src = ["(", func, ")()"].join("")
  var ast = esprima.parse(src, { range: true })
  
  //Compute new prefix
  var prefix = "_inline_" + (PREFIX_COUNTER++) + "_"
  
  //Parse out arguments
  var argNames = getArgNames(ast)
  var compiledArgs = new Array(argNames.length)
  for(var i=0; i<argNames.length; ++i) {
    compiledArgs[i] = new CompiledArgument([prefix, "arg", i, "_"].join(""), false, false)
  }
  
  //Create temporary data structure for source rewriting
  var exploded = new Array(src.length)
  for(var i=0, n=src.length; i<n; ++i) {
    exploded[i] = src.charAt(i)
  }
  
  //Local variables
  var localVars = []
  var thisVars = []
  var computedThis = false
  
  //Retrieves a local variable
  function createLocal(id) {
    var nstr = prefix + id.replace(/\_/g, "__")
    localVars.push(nstr)
    return nstr
  }
  
  //Creates a this variable
  function createThisVar(id) {
    var nstr = "this_" + id.replace(/\_/g, "__")
    thisVars.push(nstr)
    return nstr
  }
  
  //Rewrites an ast node
  function rewrite(node, nstr) {
    var lo = node.range[0], hi = node.range[1]
    for(var i=lo+1; i<hi; ++i) {
      exploded[i] = ""
    }
    exploded[lo] = nstr
  }
  
  //Remove any underscores
  function escapeString(str) {
    return "'"+(str.replace(/\_/g, "\\_").replace(/\'/g, "\'"))+"'"
  }
  
  //Returns the source of an identifier
  function source(node) {
    return exploded.slice(node.range[0], node.range[1]).join("")
  }
  
  //Computes the usage of a node
  var LVALUE = 1
  var RVALUE = 2
  function getUsage(node) {
    if(node.parent.type === "AssignmentExpression") {
      if(node.parent.left === node) {
        if(node.parent.operator === "=") {
          return LVALUE
        }
        return LVALUE|RVALUE
      }
    }
    if(node.parent.type === "UpdateExpression") {
      return LVALUE|RVALUE
    }
    return RVALUE
  }
  
  //Handle visiting a node
  (function visit(node, parent) {
    node.parent = parent
    if(node.type === "MemberExpression") {
      //Handle member expression
      if(node.computed) {
        visit(node.object, node)
        visit(node.property, node)
      } else if(node.object.type === "ThisExpression") {
        rewrite(node, createThisVar(node.property.name))
      } else {
        visit(node.object, node)
      }
    } else if(node.type === "ThisExpression") {
      throw new Error("cwise-parser: Computed this is not allowed")
    } else if(node.type === "Identifier") {
      //Handle identifier
      var name = node.name
      var argNo = argNames.indexOf(name)
      if(argNo >= 0) {
        var carg = compiledArgs[argNo]
        var usage = getUsage(node)
        if(usage & LVALUE) {
          carg.lvalue = true
        }
        if(usage & RVALUE) {
          carg.rvalue = true
        }
        ++carg.count
        rewrite(node, carg.name)
      } else if(isGlobal(name)) {
        //Don't rewrite globals
      } else {
        rewrite(node, createLocal(name))
      }
    } else if(node.type === "Literal") {
      if(typeof node.value === "string") {
        rewrite(node, escapeString(node.value))
      }
    } else if(node.type === "WithStatement") {
      throw new Error("cwise-parser: with() statements not allowed")
    } else {
      //Visit all children
      var keys = Object.keys(node)
      for(var i=0, n=keys.length; i<n; ++i) {
        if(keys[i] === "parent") {
          continue
        }
        var value = node[keys[i]]
        if(value) {
          if(value instanceof Array) {
            for(var j=0; j<value.length; ++j) {
              if(value[j] && typeof value[j].type === "string") {
                visit(value[j], node)
              }
            }
          } else if(typeof value.type === "string") {
            visit(value, node)
          }
        }
      }
    }
  })(ast.body[0].expression.callee.body, undefined)
  
  //Remove duplicate variables
  uniq(localVars)
  uniq(thisVars)
  
  //Return body
  var routine = new CompiledRoutine(source(ast.body[0].expression.callee.body), compiledArgs, thisVars, localVars)
  return routine
}

module.exports = preprocess