# -----------------------------------------------------------------------------
# typecheck.py
#
# Semantic analysis on the ast generated by the parser
# -----------------------------------------------------------------------------
import sys
from ast import *
from datatypes import *
from symbol import *
import adalex
import adaparse



class typecheck():
  Int = Integer()
  Float = Float()
  Str = String()
  Bool = Boolean()
  Enum = Enumeration()
  Char = Character()
  Arr = Array()
  types = {
   "INTEGER": Int,
   "FLOAT": Float,
   "CHARACTER": Char,
   "BOOLEAN": Bool,
   "STRING": Str,
   "ARRAY": Arr,
   "ENUMERATION": Enum
  }

  type_check_errors = 0
  scope_level = 0
  def check_goal_symbol(self,node):
    self.table_current = symtable(None, node)
    if hasattr(node, "compilation"):
      self.check_compilation(node.compilation)
    else:
      print "Please fix all the syntax errors and compile again"
      sys.exit(0)

  def check_compilation(self, node):
    for comp_unit in node.comp_unit:
      self.check(comp_unit)

  # def check_comp_unit(self,node):
  #   print node
  #   node.table = symtable(self.table_current, node)
  #   self.table_current = node.table
  #   for statement in node.statements.statements:
  #     self.check(statement)
  #   if isinstance(statement, AssignmentStatement):
  #     self.table_current[statement.location.name] = statement.expr


  def check_AssignmentStatement(self,node):
    table_entry = self.table_current.lookup(node.location.name)
    #check if variable is declared
    if not table_entry:
      self.error(node.lineno, "name"+node.location.name+" not defined")
      return
    #check if value belongs in range if type is enum
    if table_entry.typename.check_type == Enum :
      if node.expr.location.name not in table_entry.length.enum_id :
        self.error(node.lineno,"Not an enum values")
    else :
        #removed because records not implemented
      if isinstance(node.expr,list):
        for e in node.expr :
          self.check(e)
              # if len(node.expr) != len((table_entry.length.record_def)[0].comp_decls) :
              #     error(node.lineno, "Length does not match")
              # else :
              #     for i in range(0,len(node.expr)):
              #         if ((table_entry.length.record_def)[0].comp_decls)[i].typename.check_type != (node.expr)[i].check_type :
              #             error(node.lineno, "Type doesn't match")
      else :
        self.check(node.expr)
      #if assignment while declaration
    if isinstance(table_entry, VariableDeclaration):
      if hasattr(table_entry, "check_type") and hasattr(node.expr, "check_type"):
        declared_type = table_entry.check_type
        value_type = node.expr.check_type
        if declared_type != value_type:
          self.error(node.lineno, "Cannot assign"+str(value_type)+" to "+str(declared_type))
          return
    #otherwise
    if hasattr(node.location, "check_type") and hasattr(node.expr, "check_type"):
      declared_type = node.location.check_type
      value_type = node.expr.check_type
      if declared_type != value_type:
        self.error(node.lineno, "Cannot assign "+value_type.typename+" to "+declared_type.typename)

  def check_ArrayAssignmentStatement(self,node):
    table_entry = self.table_current.lookup(node.location.name)
    if not table_entry:
      self.error(node.lineno, "name "+node.location.name+" not defined")
    else :
      self.check(node.args)
      self.check(node.expr)
      if isinstance(table_entry, VariableDeclaration):
        if hasattr(table_entry.length.subtypeind, "check_type") and hasattr(node.expr, "check_type"):    
          declared_type = table_entry.length.subtypeind.check_type
          value_type = node.expr.check_type
          if declared_type != value_type:
            self.error(node.lineno, "Cannot assign "+value_type+" to "+declared_type)
        else:
          if hasattr(table_entry.length.subtypeind, "check_type") and hasattr(node.expr, "check_type"):
            declared_type = table_entry.length.subtypeind.check_type
            value_type = node.expr.check_type
            if declared_type != value_type:
              self.error(node.lineno, "Cannot assign "+value_type+" to "+declared_type)


  def check_ExitStatement(self,node):
    self.check(node.name)
    if node.expr != None :
      self.check(node.expr)
      if node.expr.check_type != self.Bool:
        self.error(node.lineno, "Condition in a exit statement must be a boolean")
  
  def check_ReturnStatement(self, node):
    self.check(node.expr)
    if self.table_current.return_type() != node.expr.check_type:
      self.error(node.lineno, "Type of the return statement does not match the return type of the function")


  def check_GotoStatement(self,node):
    self.check(node.name)

  def check_ProcCall(self, node):
    self.check(node.name)

  def check_IfStatement(self,node):
    self.check(node.expr)
    if node.expr.check_type != self.Bool:
      self.error(node.lineno, "Expression in if statement must evaluate to bool")
    if node.truebranch is not None:
      self.check(node.truebranch)
    if node.falsebranch is not None:
      self.check(node.falsebranch)
    
  def check_CaseStatement(self,node):
    self.check(node.condition)
    for alternative in node.alternatives.alternatives :
      if alternative.choices.choices[0] != 'OTHERS':
        self.check(alternative)
        if hasattr(node.condition,'check_type') and hasattr(alternative,'check_type') :
          if node.condition.check_type  != alternative.check_type :
            self.error(node.lineno, "Type of condition and choice should match")
        else :
          self.error(node.lineno, "No type assigned to node")
      else:
          self.check(alternative.statements)


  def  check_Alternative(self,node) :
    self.check(node.choices.choices[0])
    if hasattr(node.choices.choices[0],'check_type') :
      temp=node.choices.choices[0].check_type
    else :
      self.error(node.lineno, "Type error") 
    for ch in range(1,(len(node.choices.choices))) :
      self.check(node.choices.choices[ch])
      if hasattr(node.choices.choices[ch],'check_type')==False or node.choices.choices[ch].check_type!=temp :
        self.error(node.lineno, "Type error")
    if 'temp' in locals():
      node.check_type = temp
    self.check(node.statements)


  def check_Forloop(self,node):
    node.scope_level = self.scope_level
    self.table_current = symtable(self.table_current, node)
    self.scope_level += 1
    self.check(node.name)
    self.table_current.parent.symbols[node.name.name] = node.name
    self.check(node.discreterange)
    if hasattr(node.name, "check_type") and hasattr(node.discreterange, "check_type"):
      if node.name.check_type == node.discreterange.check_type :
        node.check_type = self.Bool
      else :
        self.error(node.lineno, "Expression in for statement must evaluate to bool")
    self.table_current = self.table_current.parent
    self.scope_level -=1

  def check_WhileStatement(self, node):
    flag = 1
    if node.label !=  None :
      if table_current.lookup(node.label) is not None:
        self.error(node.lineno, "Redefining label "+node.label+" not allowed")
        flag = 0
      else:
        if node.id!=None and node.label != node.id :
          self.error(node.lineno, "Label "+node.label+" does not match")
    if flag != 0 :
      self.table_current.symbols[node.label] = node
    self.table_current = symtable(self.table_current, node)
    self.scope_level += 1
    self.check(node.expr)
    if node.expr != None :
      if node.expr.check_type != self.Bool:
        self.error(node.lineno, "Expression in while statement must evaluate to bool")
    self.check(node.truebranch)
    self.table_current = self.table_current.parent
    self.scope_level -= 1

  def check_Block(self,node):
    flag = 1
    if node.label != None:
      if self.table_current.lookup(node.label) is not None:
        self.error(node.lineno, "Redefining label "+node.label+" not allowed")
        flag = 0
      else:
        if node.id!=None and node.label != node.id :
          self.error(node.lineno, "Label "+node.label+" does not match")
    self.table_current = symtable(self.table_current, node)
    self.scope_level += 1
    if flag != 0 :
       self.table_current.symbols[node.label] = node
    for declarations in node.decl:
      self.check(declarations)
    self.check(node.block)
    self.table_current = self.table_current.parent
    self.scope_level -= 1


  def check_VariableDeclaration(self,node):
    if self.table_current.lookup(node.name) is not None:
      self.error(node.lineno, "redefining variable "+node.name+" is not allowed")
    else :
      p=self.table_current.lookup(node.typename.name)
      if isinstance(p,TypeDeclaration) :
        node.expr = p.expr
        node.length = p.length
        node.typename = p.typename
      else :
        self.check(node.typename)
        if node.length is not None :
            self.check(node.length)
        if node.expr is None:
          default = node.typename.check_type.default
          node.expr = Literal(default,lineno=node.lineno)
          node.expr.check_type = node.typename.check_type
        self.check(node.expr)
        if node.typename.check_type != node.expr.check_type:
          self.error(node.lineno, "Cannot assign "+str(node.typename.check_type)+" to "+str(node.expr.check_type))
      self.table_current.symbols[node.name] = node
      if hasattr(node.typename, "check_type"):
        node.check_type = node.typename.check_type
    node.scope_level = self.scope_level

  # def check_TypeDeclaration(self,node):
  #   if self.table_current.lookup(node.name) is not None:
  #     self.error(node.lineno, "redefining variable "+node.name+" is not allowed")
  #   else :
  #     self.check(node.typename)
  #     if node.length is not None :
  #       self.check(node.length)
  #     if node.expr is not None :
  #       self.check(node.expr)
  #     if isinstance(node.length,Float_type) and hasattr(node.length,'check_type') and hasattr(node.typename,'check_type'):
  #       if node.length.check_type != node.typename.check_type :
  #         self.error(node.lineno,'Range has wrong type')
  #     self.table_current.symbols[node.name] = node
  #     if hasattr(node.typename, "check_type"):
  #       node.check_type = node.typename.check_type

  # def check_SubTypeDeclaration(self,node):
  #   if self.table_current.lookup(node.name) is not None:
  #     self.error(node.lineno, "redefining variable "+node.name+" is not allowed")
  #   else :
  #     p=self.table_current.lookup(node.typename.name)
  #     if isinstance(p,TypeDeclaration) :
  #       node.expr = p.expr
  #       node.typename = p.typename
  #       self.check(node.typename)
  #       if node.length is not None :
  #         self.check(node.length)
  #       if node.expr is not None :
  #         self.check(node.expr)
  #       self.table_current.symbols[node.name] = node
  #       if hasattr(node.typename, "check_type"):
  #         node.check_type = node.typename.check_type
  #     else :
  #       self.error(node.lineno, "Type is not valid")


  def check_Integertype(self, node):
    if node.rangespec is not None:
      self.check(node.rangespec)
    else:
      self.check(node.expression)

  def check_Floattype(self, node):
    node.scope_level = self.scope_level
    if node.rangespecopt is not None:
      self.check(node.rangespecopt)
      if hasattr(node.rangespecopt,'check_type'):
        node.check_type = node.rangespecopt.check_type
    self.check(node.expression)

  def checkt_Fixedtype(self, node):
    node.scope_level = self.scope_level
    if node.rangespecopt is not None:
      self.check(node.rangespecopt)
    self.check(node.expression1)
    self.check(node.expression2)

  def check_Unconstrarray(self,node):
    for index in node.indexs.indexs :
      self.check(index)
    self.check(node.subtypeind)
    node.check_type = node.subtypeind.check_type

  def check_Constrarray(self,node):
    for drange in node.indexconstraint :
      if drange[0] is not None :
        self.check(drange[0])
      self.check(drange[1])
      if drange[0]!=None :
        if hasattr(drange[0],'check_type') and hasattr(drange[1],'check_type'):
          if drange[0].check_type != drange[1].check_type :
            self.error(node.lineno, "Type Mismatch "+node.location.name)
    self.check(node.subtypeind)
    node.check_type = node.subtypeind.check_type

  def check_Typename(self,node):
    if node.name not in self.types.keys():
      self.error(node.lineno, str(node.name)+" is not a valid type")
      datatype = None
    else:
      datatype = self.types[node.name]
    node.check_type = datatype
      
  def check_Location(self,node):
      table_entry = self.table_current.lookup(node.name)
      if not table_entry:
        self.error(node.lineno, "name "+node.name+" not found")
      node.check_type = table_entry.check_type

  def check_LoadLocation(self, node):
    if node.location.name == 'NEWLINE':
      return
    elif node.location.name[0] == '"' and node.location.name[-1] == '"':
      node.check_type = self.Str
      return
    table_entry = self.table_current.lookup(node.location.name)
    if not table_entry:
      self.error(node.lineno, "name "+node.location.name+" not found")
      node.check_type = Undeclared()
    else:
      # if not isinstance(table_entry, Integer) and not isinstance(table_entry, Float) and not isinstance(table_entry, Boolean) and not isinstance(table_entry, Array) and not isinstance(table_entry, String) and not isinstance(table_entry, Enumeration) and not isinstance(table_entry, Character):
      #   self.error(node.lineno, "cannot use "+table_entry.typename.name+" outside of variable declarations")
      #   return
      if isinstance(table_entry,FuncStatement):
        node.check_type = None
        pass
      else:
        check_type = table_entry.check_type
        if check_type is None:
          self.error(node.lineno, "Using unrecognized type "+valtype)
        node.check_type = check_type

  def check_Literal(self,node):
    typemap={int:self.Int, float:self.Float, str:self.Str, bool:self.Bool, list:self.Arr}
    valtype = type(node.value)
    check_type = typemap.get(valtype, None)
    if check_type is None:
      self.error(node.lineno, "Using unrecognized type {}".format(valtype))
    if check_type==self.Str and len(node.value)==1 :
      check_type = self.Char
    node.check_type = check_type


  def check_Values(self, node):
    for argument in node.arguments:
      if isinstance(argument,tuple):
        if argument[0] is not None :
          self.check(argument[0])
        if argument[1] is not None :
          self.check(argument[1])
        if hasattr(argument[0],'check_type') :
          node.check_type = argument[0].check_type
        elif hasattr(argument[1],'check_type') :
          node.check_type = argument[1].check_type
      else :
        self.check(argument)
        if hasattr(node,'check_type') :
          node.check_type = argument.check_type

  def check_Doubledotrange(self,node):
    node.scope_level = self.scope_level
    self.check(node.left)
    self.check(node.right)
    if hasattr(node.left, "check_type") and hasattr(node.right, "check_type"):
      if node.left.check_type == node.right.check_type :
        node.check_type = node.left.check_type
      else :
        self.error(node.lineno,"Type does not match")

  def check_Nametick(self,node):
    self.check(node.name)
    if node.expression != None :
      self.check(node.expression)
    if hasattr(node.name,'check_type'):
      node.check_type = node.name.check_type


  def check_FuncStatement(self, node):
    node.scope_level = self.scope_level
    self.table_current = symtable(self.table_current, node)
    self.scope_level += 1

    if self.table_current.lookup(node.name) is not None:
      self.error(node.lineno, "Redefining function "+node.name+"is not allowed")
    else :
      if node.id!=None and node.name != node.id :
        self.error(node.lineno, "Label does not match")
      self.table_current.parent.symbols[node.name] = node
    if node.returntype is not None :
      self.check(node.returntype)
      if hasattr(node.returntype, "check_type"):
        node.check_type = node.returntype.check_type
    self.check(node.parameters)
    for declarations in node.declpart:
      self.check(declarations)
    self.check(node.statements)
    self.table_current = self.table_current.parent
    self.scope_level -= 1

  def check_Statements(self, node):
    for statement in node.statements:
      self.check(statement)

  def check_FuncParameterList(self, node):
    for parameter in node.parameters:
      self.check(parameter)

  def check_FuncParameter(self, node):
    node.scope_level = self.scope_level
    self.table_current.symbols[node.name] = node
    self.check(node.typename)
    node.check_type = node.typename.check_type 

  def check_ProcCall(self, node):
    self.check(node.name)

  def check_FuncCall(self, node):
    table_entry = self.table_current.lookup(node.name)
    if not table_entry:
      if node.name == 'PUT':
        if node.arguments is not None:
          if len(node.arguments.arguments) > 1:
              self.error(node.lineno, "Function PUT accepts only one argument")
          else:
#                        if hasattr(node.arguments.arguments[0],'location') and node.arguments.arguments[0].location.name != 'newline':
            self.check(node.arguments)
      else:
        self.error(node.lineno, "Function name "+node.name+" not found")
    else :
      if hasattr(table_entry,'check_type'):
        node.check_type = table_entry.check_type
        if node.check_type == self.Arr: 
          node.check_type = self.Int
          node.isArray = True
        elif not isinstance(table_entry, FuncStatement):
          self.error(node.lineno, "Tried to call non-function "+node.name)
        else:
          if node.arguments is not None:
            if len(table_entry.parameters.parameters) != len(node.arguments.arguments):
              self.error(node.lineno, "Number of arguments for function call "+node.name+" do not match function declaration")
            self.check(node.arguments)
            argerrors = False
            for arg, parm in zip(node.arguments.arguments, table_entry.parameters.parameters):
              if arg.check_type != parm.typename.check_type:
                self.error(node.lineno, "Argument type "+arg.check_type.name+" does not match parameter type "+parm.check_type.name+" in function call to "+ node.name)
                argerrors = True
              if argerrors:
                return
              arg.parm = parm

  def check_Unaryop(self,node):
    self.check(node.expr)
    check_type = self.check_type_unary(node, node.op, node.expr)
    node.check_type = check_type

  def check_Binop(self,node):
    self.check(node.left)
    self.check(node.right)
    check_type = self.check_type_binary(node, node.op, node.left, node.right)
    node.check_type = check_type

  def check_Relop(self,node):
    self.check(node.left)
    self.check(node.right)
    check_type = self.check_type_rel(node, node.op, node.left, node.right)
    node.check_type = check_type

  def check_type_unary(self, node, op, val):
    if hasattr(val, "check_type"):
      if op not in val.check_type.unary:
        error(node.lineno, "Unary operator "+op+" not supported")
      return val.check_type

  def check_type_binary(self, node, op, left, right):
    if hasattr(left, "check_type") and hasattr(right, "check_type"):
      if left.check_type != right.check_type:
        self.error(node.lineno, "Binary operator "+op+" does not have matching LHS/RHS types")
        return left.check_type
      errside = None
      if op not in left.check_type.binary:
        errside = "LHS"
      if op not in right.check_type.binary:
        errside = "RHS"
      if errside is not None:
        self.error(node.lineno, "Binary operator "+op+" not supported on "+errside+" of expression")
      return left.check_type

  def check_type_rel(self, node, op, left, right):
    if hasattr(left, "check_type") and hasattr(right, "check_type"):
      if left.check_type != right.check_type:
        self.error(node.lineno, "Relational operator "+op+" does not have matching LHS/RHS types")
        return left.check_type
      errside = None
      if op not in left.check_type.rel:
        errside = "LHS"
      if op not in right.check_type.rel:
        errside = "RHS"
      if errside is not None:
        self.error(node.lineno, "Relational operator "+op+" not supported on "+errside+" of expression")
      return self.Bool





  def error(self, lineno, msg):
    self.type_check_errors = self.type_check_errors + 1
    print("type error on line number "+str(lineno)+": "+msg)

  def get_error_count(self):
    return self.type_check_errors

  def check(self, node):
    if node is not None:
      function_name = 'check_' + node.__class__.__name__
      function = getattr(self, function_name)
      function(node)


def tracefunc(frame, event, arg, indent=[0]):
      if event == "call":
          indent[0] += 2
          print "-" * indent[0] + "> call function", frame.f_code.co_name
      elif event == "return":
          print "<" + "-" * indent[0], "exit function", frame.f_code.co_name
          indent[0] -= 2
      return tracefunc

import sys
#sys.settrace(tracefunc)


def main():
  global DEBUG
  if(len(sys.argv) > 2):
    DEBUG = 1
  fileName = sys.argv[1]
  f = open(fileName, 'r')
  lexer = adalex.make_lexer()
  tokens = adalex.tokens
  parser = adaparse.make_parser()
  tree = parser.parse(f.read())
  typechecker = typecheck()
  typechecker.check_goal_symbol(tree)

DEBUG = 0
lexer = adalex.make_lexer()
tokens = adalex.tokens
if __name__ == '__main__':
  main()