#!/usr/bin/python
## -*- coding: utf-8 -*-
## (C) 2007, 2008, 2013-2015 Muthiah Annamalai
## Licensed under GPL Version 3
##
## This module is the runtime elements for the exprs language.
## It contains classes 
## 

from .errors import RuntimeException
import keyword
import sys
import time
import traceback
from .profile import Profiler

PYTHON3 = (sys.version[0] == '3')
if PYTHON3:
    str = str

# customize I/O like functions for external GUI
class tamilCustomFunction(object):
    _instance = None
    def __init__(self):
        object.__init__(self)
        self.input_fcn = input
    @staticmethod
    def get_instance():
        if not tamilCustomFunction._instance:
            tamilCustomFunction._instance = tamilCustomFunction()
        return tamilCustomFunction._instance
    
    @staticmethod
    def input(args):
        return tamilCustomFunction.get_instance().input_fcn(args)
    
    @staticmethod
    def reset():
        if PYTHON3:
            tamilCustomFunction.get_instance().input_fcn = input
        else:
            tamilCustomFunction.get_instance().input_fcn = input
    
    @staticmethod
    def set(newfcn):
        tamilCustomFunction.get_instance().input_fcn = newfcn
    
## RUNTIME / LIBRARY etc
class DebugUtils:
    def __init__(self,dbg):
        self.debug = dbg

    def dbg_msg(self, msg):
        """ handy to print debug messages """
        if ( self.debug ):
            print(u"## ",msg)
        return

class BuiltinFunction:
    """ a templatized way to invoke bulitin functions."""
    def __init__(self,fn,name,padic=1,dbg = False):
        self.fn = fn
        self.name = name
        self.use_adicity = True
        self.padic = padic
        self.debug = dbg
        self.aslist = False
        
    def __str__(self):
        return u"[BuiltinFunction[%s,nargs=%d]]"%(self.name,self.padic)
    
    def evaluate(self,env):
        try:
            return self.do_evaluate(env)
        except AssertionError as assert_excep:
            env.unroll_stack()
            raise RuntimeException( str(assert_excep) + u' Assertion failed!')
        except Exception as excep:
            env.unroll_stack()            
            print(u"failed dispatching function ",str(self),u"with exception",str(excep))
            if ( self.debug ):
                traceback.print_tb(sys.exc_info()[2])
            raise excep
        
    def do_evaluate(self,env):
        ## push stuff into the call-stack
        env.call_function(self.name)
        
        ## check arguments match, otherwise raise error
        args = env.get_args()#.get_list()
        env.set_local({})

        if ( self.use_adicity and len(args) < self.padic ):
            raise RuntimeException("Too few args to bulitin function " + self.name)

        # keep evaluating for as many evaluate object-methods are available
        # because Python libraries don't recognize tamil AST objects.
        while( any(filter(lambda x: hasattr(x,'evaluate'),args)) ):            
            args_ = []
            for a in args:
                if hasattr(a,'evaluate'):
                    a = a.evaluate(env)
                args_.append(a)
            args = args_
        
        if ( self.use_adicity ):
            if ( self.debug ): print(self.fn, args, self.padic)
            rval = self.fn(*args)
        else:
            try:                
                if ( self.aslist ):
                    rval = self.fn(*[args])
                else:
                    rval = self.fn(*args)
            except AssertionError as assert_excep:
                raise assert_excep
            except Exception as excep:
                #print u"catch exception", excep
                raise RuntimeException( str(excep) )
        env.clear_call(copyvars= self.name in ["execute","eval",u'இயக்கு',u'மதிப்பீடு'])
        ## pop stuff into the call-stack
        env.return_function(self.name)
        return  rval

class BlindBuiltins(BuiltinFunction):
    """ also blindly include all functions from the 
        os, sys, curses.ascii, math etc. donot check arguments 
        here.    """
    def __init__(self,fn,name,dbg = False,aslist=False,padic=-1):
        BuiltinFunction.__init__(self,fn,name,padic,dbg)
        self.use_adicity = False
        self.aslist = aslist
        
    def __str__(self):
        return BuiltinFunction.__str__(self)

class CountDownTimer(object):
    def __init__(self):
        object.__init__(self)
        self._engaged = False
        self._safeModeTimeout = 13  # seconds of wall clock timeout for tamil codes in SAFE MODE
        self._startTime = 0

    # all times are wall clock times
    def is_timeout(self):
        if not self._engaged:
            return False
        now = time.time()
        rval = (now - self._startTime) > self._safeModeTimeout
        if rval:
            self._engaged = False
        return rval

    def record_start_time(self):
        self._startTime = time.time()
        self._engaged = True
        return

    # update safe mode timeout
    def set_timeout(self, seconds):
        assert seconds > 0
        self._safeModeTimeout = seconds


# <<Side-Effects>>: computation is side-effect of programming.
class Environment(CountDownTimer):
    """ used to manage the side-effects of an interpreter """
    def __init__(self, call_stack, function_map, builtin_map, dbg = False, MAX_REC_DEPTH  = 128 ):
        CountDownTimer.__init__(self)
        self.do_profiling = False
        self.profiler = None
        self.max_recursion_depth = MAX_REC_DEPTH #keep it smaller than Python stack
        self.call_stack = call_stack#list
        self.function_map = function_map#dicts
        self.builtin_map = builtin_map#dicts
        self.local_vars = []#list of dicts.
        self.arg_stack = []#list of lists
        self.ret_stack = [] #list
        self.debug = dbg #use to turn debugging on.
        self.readonly_global_vars = {'True':True, 'False':False,u"மெய்":True, u"பொய்":False} #dict of global vars
        self.clear_break_return_continue()

    def validate_timer(self):
        # time to be reset everytime the interpreter calls evaluate
        if self.is_timeout():
            raise RuntimeException("Evaluation of your program is taking too long!\n"
                                   "\tTimeout exceeded %g (seconds)"%self._safeModeTimeout)
        return

    def __del__(self):
        if (self.debug): print(u"deleting environment")
        
    def enable_profiling(self):
        self.validate_timer()
        self.do_profiling =True
        self.profiler = Profiler()
        return
    
    def is_profiling(self):
        self.validate_timer()
        return self.do_profiling
    
    def disable_profiling(self):
        self.validate_timer()
        self.do_profiling = False
        
    def report_profiling(self):
        self.validate_timer()
        self.disable_profiling()
        self.profiler.report_stats()
        
    def reset_profiling(self):
        self.validate_timer()
        self.disable_profiling()
        self.profiler = None
        
    def unroll_stack(self):
        self.validate_timer()
        # we skip BOS since it is a __toplevel__
        if (self.debug): print("clearing locals")
        if len(self.call_stack) > 0:
            if self.call_stack[-1][0] != "__toplevel__":
                self.clear_call()
        
        for i in range(1,len(self.call_stack)):
            print(u"%sError in function '%s'%s:"%(u" "*(i-1),self.call_stack[i][0],self.call_stack[i][1]))
        while len(self.call_stack) > 0:
            tos,_ = self.call_stack.pop()
            #print(u"Error in location %s"%str(tos))
        return
    
    def get_break_return(self):
        self.validate_timer()
        """ get if break or return was set for use in loops """
        val = self.Break or self.Return
        return val

    def clear_break(self):
        self.validate_timer()
        """ reset after a break statement """
        self.Break = False
        return False

    def clear_continue(self):
        self.validate_timer()
        """ reset after a continue statement """
        self.Continue = False
        return False

    def set_break(self):
        self.validate_timer()
        """ execute a continue statement """
        self.Break = True
        return True

    def set_continue(self):
        self.validate_timer()
        """ execute a continue statement """
        self.Continue = True
        return True
    
    def break_return_continue(self):
        self.validate_timer()
        val = ( self.Break or 
                 self.Return or 
                 self.Continue )
        ## must clear continue flag right away.
        if ( self.Continue ):
            self.Continue = False
        return val
    
    def dbg_msg(self, msg):
        """ handy to print debug messages """
        if ( self.debug ):
            print(msg)
        return
    
    def __str__(self):
        if (self.debug):
            return repr(self)
        return u"<env>"
    
    def __repr__(self):
        retval = u"CallStack =>"+str(self.call_stack) + u"\n" \
            u"LocalVars =>"+ str(self.local_vars) + u"\n" \
            u"ArgStack =>"+ str(self.arg_stack) + u"\n" 
        return retval

    def set_retval( self, rval ):
        self.validate_timer()
        self.ret_stack.append( rval )
        self.Return = True
        return
    
    def get_retval( self ):
        self.validate_timer()
        rval = None
        if ( len(self.ret_stack ) >= 1  ):
            rval = self.ret_stack.pop()
        return rval

    def clear_break_return_continue(self):
        self.validate_timer()
        self.Break = False
        self.Return = False
        self.Continue = False

    def clear_call(self,copyvars=False):
        self.validate_timer()
        """ utility to cleanup the stacks etc.. """
        self.clear_local( copyvars ) 
        self.clear_args ( )
        self.clear_break_return_continue()
        
    def clear_args(self):
        self.validate_timer()
        """ cleanup the stack """
        self.arg_stack.pop()
        return

    def get_args(self):
        self.validate_timer()
        """ manage a global argument stack """
        return self.arg_stack[-1]

    def set_args(self,val):
        self.validate_timer()
        """ manage a global argument stack """
        self.dbg_msg( "setting args " + str( val ) )
        return self.arg_stack.append(val)
    
    def set_local(self, vars):
        self.validate_timer()
        self.local_vars.append(vars)
        self.dbg_msg( "setting locals " + str( vars ) )
        self.clear_break_return_continue()
        return
    
    def clear_local(self,copyvars=False):
        self.validate_timer()
        prev_values = self.local_vars.pop()
        if copyvars:
            if len(self.local_vars) < 1:
                self.local_vars.append({})
            self.local_vars[-1].update(prev_values)
        return
    
    def has_id(self, idee):
        """ check various 'scopes' for ID variable """
        self.validate_timer()
        rval = False
        if idee in self.readonly_global_vars:
            return True
        if ( len( self.local_vars ) == 0 ):
            return False
        variables = self.local_vars[-1]
        rval = idee in variables
        return rval

    def set_id(self, idee, val, global_id = False):
        """ someday do global_id """
        self.validate_timer()
        if idee in self.readonly_global_vars:
            raise Exception(u"Error: Attempt to reassign constant %s"%idee)
        if ( len(self.local_vars) > 0 ):
            d=self.local_vars[-1]
        else:
            d=dict()
            self.local_vars.append(d)
        #if not (idee in d) and (idee in self.global_vars):
        #    self.global_vars[idee] = val
        # else:
        d[idee]=val
        self.dbg_msg("set_id: " + str(idee) +" = "+str(val))
        return

    def get_id(self, idee):
        self.validate_timer()
        val = None
        if idee in self.readonly_global_vars:
            return self.readonly_global_vars[idee]
        if not self.has_id(idee):
            note = ''
            if idee in keyword.kwlist:
                note = 'Did you possibly confuse the Python english keyword %s for tamil keyword ?'%idee
            raise RuntimeException("Identifier %s not found"%idee)
        variables = self.local_vars[-1]
        val = variables[idee]
        self.dbg_msg("get_id: val = "+str(val))
        return val

    def call_function(self, fn,callsite=u""):
        """ set call stack, used in function calls. Also check overflow"""
        self.validate_timer()
        if ( len(self.call_stack) >= self.max_recursion_depth ):
            raise RuntimeException( "Maximum recursion depth [ " + 
                                    str(self.max_recursion_depth) + 
                                    " ] exceeded; stack overflow." )
        self.dbg_msg(u"calling function"+str(fn)+callsite)
        if ( self.is_profiling() ):
            self.profiler.add_function( fn )
        self.call_stack.append( (fn,callsite) ) 
    
    def return_function(self, fn):
        self.validate_timer()
        va,_ = self.call_stack.pop( )
        if ( fn != va ):
            raise RuntimeException("function %s doesnt match Top-of-Stack"%fn)
        if ( self.is_profiling() ):
            self.profiler.update_function( va )
        return va
    
    def has_function(self, fn):
        self.validate_timer()
        if ( fn in self.builtin_map ):
            return True
        if ( fn in self.function_map ):
            return True
        return False
    
    def get_function(self, fn):
        self.validate_timer()
        if not self.has_function(fn):
            raise RuntimeException("undefined function: "+fn)

        if ( fn in self.builtin_map ):
            return self.builtin_map[fn]
        
        if ( fn in self.function_map ):
            return self.function_map[fn] 
        
        raise RuntimeException("Environment error on fetching function "+fn)
