# encoding: utf-8

"""Central interpreter object for an IPython engine.

The interpreter is the object whose job is to process lines of user input and
actually execute them in the user's namespace.
"""

__docformat__ = "restructuredtext en"

#-------------------------------------------------------------------------------
#  Copyright (C) 2008  The IPython Development Team
#
#  Distributed under the terms of the BSD License.  The full license is in
#  the file COPYING, distributed as part of this software.
#-------------------------------------------------------------------------------

#-------------------------------------------------------------------------------
# Imports
#-------------------------------------------------------------------------------

# Standard library imports.
from types import FunctionType

import __builtin__
import codeop
import compiler
import sys
import traceback

# Local imports.
from IPython import ultraTB
from IPython.kernel.core.display_trap import DisplayTrap
from IPython.kernel.core.macro import Macro
from IPython.kernel.core.prompts import CachedOutput
from IPython.kernel.core.traceback_trap import TracebackTrap
from IPython.kernel.core.util import Bunch, system_shell
from IPython.external.Itpl import ItplNS

# Global constants
COMPILER_ERROR = 'error'
INCOMPLETE_INPUT = 'incomplete'
COMPLETE_INPUT = 'complete'

##############################################################################
# TEMPORARY!!! fake configuration, while we decide whether to use tconfig or
# not

rc = Bunch()
rc.cache_size = 100
rc.pprint = True
rc.separate_in = '\n'
rc.separate_out = '\n'
rc.separate_out2 = ''
rc.prompt_in1 = r'In [\#]: '
rc.prompt_in2 = r'   .\\D.: '
rc.prompt_out = ''
rc.prompts_pad_left = False

##############################################################################

# Top-level utilities
def default_display_formatters():
    """ Return a list of default display formatters.
    """

    from display_formatter import PPrintDisplayFormatter, ReprDisplayFormatter
    return [PPrintDisplayFormatter(), ReprDisplayFormatter()]

def default_traceback_formatters():
    """ Return a list of default traceback formatters.
    """

    from traceback_formatter import PlainTracebackFormatter
    return [PlainTracebackFormatter()]

# Top-level classes
class NotDefined(object):    pass

class Interpreter(object):
    """ An interpreter object.

    fixme: needs to negotiate available formatters with frontends.

    Important: the interpeter should be built so that it exposes a method
    for each attribute/method of its sub-object. This way it can be
    replaced by a network adapter.
    """

    def __init__(self, user_ns=None, global_ns=None,translator=None,
                 magic=None, display_formatters=None,
                 traceback_formatters=None, output_trap=None, history=None,
                 message_cache=None, filename='<string>', config=None):

        # The local/global namespaces for code execution
        local_ns = user_ns  # compatibility name
        if local_ns is None:
            local_ns = {}
        self.user_ns = local_ns
        # The local namespace
        if global_ns is None:
            global_ns = {}
        self.user_global_ns = global_ns

        # An object that will translate commands into executable Python.
        # The current translator does not work properly so for now we are going
        # without!
        # if translator is None:
        #             from IPython.kernel.core.translator import IPythonTranslator
        #             translator = IPythonTranslator()
        self.translator = translator

        # An object that maintains magic commands.
        if magic is None:
            from IPython.kernel.core.magic import Magic
            magic = Magic(self)
        self.magic = magic

        # A list of formatters for the displayhook.
        if display_formatters is None:
            display_formatters = default_display_formatters()
        self.display_formatters = display_formatters

        # A list of formatters for tracebacks.
        if traceback_formatters is None:
            traceback_formatters = default_traceback_formatters()
        self.traceback_formatters = traceback_formatters

        # The object trapping stdout/stderr.
        if output_trap is None:
            from IPython.kernel.core.output_trap import OutputTrap
            output_trap = OutputTrap()
        self.output_trap = output_trap

        # An object that manages the history.
        if history is None:
            from IPython.kernel.core.history import InterpreterHistory
            history = InterpreterHistory()
        self.history = history
        self.get_history_item = history.get_history_item
        self.get_history_input_cache = history.get_input_cache
        self.get_history_input_after = history.get_input_after

        # An object that caches all of the return messages.
        if message_cache is None:
            from IPython.kernel.core.message_cache import SimpleMessageCache
            message_cache = SimpleMessageCache()
        self.message_cache = message_cache

        # The "filename" of the code that is executed in this interpreter.
        self.filename = filename

        # An object that contains much configuration information.
        if config is None:
            # fixme: Move this constant elsewhere!
            config = Bunch(ESC_MAGIC='%')
        self.config = config

        # Hook managers.
        # fixme: make the display callbacks configurable. In the meantime,
        # enable macros.
        self.display_trap = DisplayTrap(
            formatters=self.display_formatters,
            callbacks=[self._possible_macro],
        )
        self.traceback_trap = TracebackTrap(
            formatters=self.traceback_formatters)

        # This is used temporarily for reformating exceptions in certain
        # cases.  It will go away once the ultraTB stuff is ported
        # to ipython1
        self.tbHandler = ultraTB.FormattedTB(color_scheme='NoColor',
                                                 mode='Context',
                                                 tb_offset=2)

        # An object that can compile commands and remember __future__
        # statements.
        self.command_compiler = codeop.CommandCompiler()

        # A replacement for the raw_input() and input() builtins. Change these
        # attributes later to configure them.
        self.raw_input_builtin = raw_input
        self.input_builtin = input

        # The number of the current cell.
        self.current_cell_number = 1

        # Initialize cache, set in/out prompts and printing system
        self.outputcache = CachedOutput(self,
                                        rc.cache_size,
                                        rc.pprint,
                                        input_sep = rc.separate_in,
                                        output_sep = rc.separate_out,
                                        output_sep2 = rc.separate_out2,
                                        ps1 = rc.prompt_in1,
                                        ps2 = rc.prompt_in2,
                                        ps_out = rc.prompt_out,
                                        pad_left = rc.prompts_pad_left)

        # Need to decide later if this is the right approach, but clients
        # commonly use sys.ps1/2, so it may be best to just set them here
        sys.ps1 = self.outputcache.prompt1.p_str
        sys.ps2 = self.outputcache.prompt2.p_str

        # This is the message dictionary assigned temporarily when running the
        # code.
        self.message = None

        self.setup_namespace()


    #### Public 'Interpreter' interface ########################################

    def formatTraceback(self, et, ev, tb, message=''):
        """Put a formatted version of the traceback into value and reraise.
        
        When exceptions have to be sent over the network, the traceback 
        needs to be put into the value of the exception in a nicely
        formatted way.  The method takes the type, value and tb of an 
        exception and puts a string representation of the tb into the 
        value of the exception and reraises it.
        
        Currently this method uses the ultraTb formatter from IPython trunk.
        Eventually it should simply use the traceback formatters in core
        that are loaded into self.tracback_trap.formatters.
        """
        tbinfo = self.tbHandler.text(et,ev,tb)
        ev._ipython_traceback_text = tbinfo
        return et, ev, tb
        
    def execute(self, commands, raiseException=True):
        """ Execute some IPython commands.

        1. Translate them into Python.
        2. Run them.
        3. Trap stdout/stderr.
        4. Trap sys.displayhook().
        5. Trap exceptions.
        6. Return a message object.

        Parameters
        ----------
        commands : str
            The raw commands that the user typed into the prompt.
        
        Returns
        -------
        message : dict
            The dictionary of responses. See the README.txt in this directory
            for an explanation of the format.
        """

        # Create a message dictionary with all of the information we will be
        # returning to the frontend and other listeners.
        message = self.setup_message()

        # Massage the input and store the raw and translated commands into
        # a dict.
        user_input = dict(raw=commands)
        if self.translator is not None:
            python = self.translator(commands, message)
            if python is None:
                # Something went wrong with the translation. The translator
                # should have added an appropriate entry to the message object.
                return message
        else:
            python = commands
        user_input['translated'] = python
        message['input'] = user_input

        # Set the message object so that any magics executed in the code have
        # access.
        self.message = message

        # Set all of the output/exception traps.
        self.set_traps()
        
        # Actually execute the Python code.
        status = self.execute_python(python)

        # Unset all of the traps.
        self.unset_traps()

        # Unset the message object.
        self.message = None

        # Update the history variables in the namespace.
        # E.g. In, Out, _, __, ___
        if self.history is not None:
            self.history.update_history(self, python)

        # Let all of the traps contribute to the message and then clear their
        # stored information.
        self.output_trap.add_to_message(message)
        self.output_trap.clear()
        self.display_trap.add_to_message(message)
        self.display_trap.clear()
        self.traceback_trap.add_to_message(message)
        # Pull out the type, value and tb of the current exception
        # before clearing it.
        einfo = self.traceback_trap.args
        self.traceback_trap.clear()
        
        # Cache the message.
        self.message_cache.add_message(self.current_cell_number, message)

        # Bump the number.
        self.current_cell_number += 1
        
        # This conditional lets the execute method either raise any
        # exception that has occured in user code OR return the message
        # dict containing the traceback and other useful info.
        if raiseException and einfo:
            raise einfo[0],einfo[1],einfo[2]
        else:
            return message

    def generate_prompt(self, is_continuation):
        """Calculate and return a string with the prompt to display.

        :Parameters:
          is_continuation : bool
            Whether the input line is continuing multiline input or not, so
          that a proper continuation prompt can be computed."""
        
        if is_continuation:
            return str(self.outputcache.prompt2)
        else:
            return str(self.outputcache.prompt1)

    def execute_python(self, python):
        """ Actually run the Python code in the namespace.

        :Parameters:

        python : str
            Pure, exec'able Python code. Special IPython commands should have
            already been translated into pure Python.
        """

        # We use a CommandCompiler instance to compile the code so as to keep
        # track of __future__ imports.
        try:
            commands = self.split_commands(python)
        except (SyntaxError, IndentationError), e:
            # Save the exc_info so compilation related exceptions can be
            # reraised
            self.traceback_trap.args = sys.exc_info()
            self.pack_exception(self.message,e)
            return None

        for cmd in commands:
            try:
                code = self.command_compiler(cmd, self.filename, 'single')
            except (SyntaxError, OverflowError, ValueError), e:
                self.traceback_trap.args = sys.exc_info()
                self.pack_exception(self.message,e)
                # No point in continuing if one block raised
                return None
            else:
                self.execute_block(code)

    def execute_block(self,code):
        """Execute a single block of code in the user namespace.

        Return value: a flag indicating whether the code to be run completed
        successfully:

          - 0: successful execution.
          - 1: an error occurred.
          """
        
        outflag = 1 # start by assuming error, success will reset it
        try:
            exec code in self.user_ns
            outflag = 0
        except SystemExit:
            self.resetbuffer()
            self.traceback_trap.args = sys.exc_info()
        except:
            self.traceback_trap.args = sys.exc_info()

        return outflag

    def execute_macro(self, macro):
        """ Execute the value of a macro.

        Parameters
        ----------
        macro : Macro
        """

        python = macro.value
        if self.translator is not None:
            python = self.translator(python)
        self.execute_python(python)

    def getCommand(self, i=None):
        """Gets the ith message in the message_cache.
        
        This is implemented here for compatibility with the old ipython1 shell
        I am not sure we need this though.  I even seem to remember that we
        were going to get rid of it.
        """
        return self.message_cache.get_message(i)

    def reset(self):
        """Reset the interpreter.
        
        Currently this only resets the users variables in the namespace.
        In the future we might want to also reset the other stateful
        things like that the Interpreter has, like In, Out, etc.
        """
        self.user_ns.clear()
        self.setup_namespace()

    def complete(self,line,text=None, pos=None):
        """Complete the given text.

        :Parameters:

          text : str
            Text fragment to be completed on.  Typically this is
        """
        # fixme: implement
        raise NotImplementedError

    def push(self, ns):
        """ Put value into the namespace with name key.
        
        Parameters
        ----------
        **kwds
        """

        self.user_ns.update(ns)

    def push_function(self, ns):
        # First set the func_globals for all functions to self.user_ns
        new_kwds = {}
        for k, v in ns.iteritems():
            if not isinstance(v, FunctionType):
                raise TypeError("function object expected")
            new_kwds[k] = FunctionType(v.func_code, self.user_ns)
        self.user_ns.update(new_kwds)        

    def pack_exception(self,message,exc):
        message['exception'] = exc.__class__
        message['exception_value'] = \
            traceback.format_exception_only(exc.__class__, exc)

    def feed_block(self, source, filename='<input>', symbol='single'):
        """Compile some source in the interpreter.

        One several things can happen:

        1) The input is incorrect; compile_command() raised an
        exception (SyntaxError or OverflowError).

        2) The input is incomplete, and more input is required;
        compile_command() returned None.  Nothing happens.

        3) The input is complete; compile_command() returned a code
        object.  The code is executed by calling self.runcode() (which
        also handles run-time exceptions, except for SystemExit).

        The return value is:

          - True in case 2

          - False in the other cases, unless an exception is raised, where
          None is returned instead.  This can be used by external callers to
          know whether to continue feeding input or not.

        The return value can be used to decide whether to use sys.ps1 or
        sys.ps2 to prompt the next line."""

        self.message = self.setup_message()

        try:
            code = self.command_compiler(source,filename,symbol)
        except (OverflowError, SyntaxError, IndentationError, ValueError ), e:
            # Case 1
            self.traceback_trap.args = sys.exc_info()
            self.pack_exception(self.message,e)
            return COMPILER_ERROR,False

        if code is None:
            # Case 2: incomplete input.  This means that the input can span
            # multiple lines.  But we still need to decide when to actually
            # stop taking user input.  Later we'll add auto-indentation support
            # somehow.  In the meantime, we'll just stop if there are two lines
            # of pure whitespace at the end.
            last_two = source.rsplit('\n',2)[-2:]
            print 'last two:',last_two  # dbg
            if len(last_two)==2 and all(s.isspace() for s in last_two):
                return COMPLETE_INPUT,False
            else:
                return INCOMPLETE_INPUT, True
        else:
            # Case 3
            return COMPLETE_INPUT, False
        
    def pull(self, keys):
        """ Get an item out of the namespace by key.
        
        Parameters
        ----------
        key : str

        Returns
        -------
        value : object

        Raises
        ------
        TypeError if the key is not a string.
        NameError if the object doesn't exist.
        """

        if isinstance(keys, str):
            result = self.user_ns.get(keys, NotDefined())
            if isinstance(result, NotDefined):
                raise NameError('name %s is not defined' % keys)        
        elif isinstance(keys, (list, tuple)):
            result = []
            for key in keys:
                if not isinstance(key, str):
                    raise TypeError("objects must be keyed by strings.")
                else:
                    r = self.user_ns.get(key, NotDefined())
                    if isinstance(r, NotDefined):
                        raise NameError('name %s is not defined' % key)
                    else:
                        result.append(r)
                    if len(keys)==1:
                        result = result[0]
        else:
            raise TypeError("keys must be a strong or a list/tuple of strings")
        return result

    def pull_function(self, keys):
        return self.pull(keys)

    #### Interactive user API ##################################################

    def ipsystem(self, command):
        """ Execute a command in a system shell while expanding variables in the
        current namespace.

        Parameters
        ----------
        command : str
        """

        # Expand $variables.
        command = self.var_expand(command)

        system_shell(command,
            header='IPython system call: ',
            verbose=self.rc.system_verbose,
        )

    def ipmagic(self, arg_string):
        """ Call a magic function by name.

        ipmagic('name -opt foo bar') is equivalent to typing at the ipython
        prompt:

        In[1]: %name -opt foo bar

        To call a magic without arguments, simply use ipmagic('name').

        This provides a proper Python function to call IPython's magics in any
        valid Python code you can type at the interpreter, including loops and
        compound statements.  It is added by IPython to the Python builtin
        namespace upon initialization.

        Parameters
        ----------
        arg_string : str
            A string containing the name of the magic function to call and any
            additional arguments to be passed to the magic.

        Returns
        -------
        something : object
            The return value of the actual object.
        """

        # Taken from IPython.
        raise NotImplementedError('Not ported yet')

        args = arg_string.split(' ', 1)
        magic_name = args[0]
        magic_name = magic_name.lstrip(self.config.ESC_MAGIC)

        try:
            magic_args = args[1]
        except IndexError:
            magic_args = ''
        fn = getattr(self.magic, 'magic_'+magic_name, None)
        if fn is None:
            self.error("Magic function `%s` not found." % magic_name)
        else:
            magic_args = self.var_expand(magic_args)
            return fn(magic_args)


    #### Private 'Interpreter' interface #######################################

    def setup_message(self):
        """Return a message object.

        This method prepares and returns a message dictionary.  This dict
        contains the various fields that are used to transfer information about
        execution, results, tracebacks, etc, to clients (either in or out of
        process ones).  Because of the need to work with possibly out of
        process clients, this dict MUST contain strictly pickle-safe values.
        """

        return dict(number=self.current_cell_number)
    
    def setup_namespace(self):
        """ Add things to the namespace.
        """

        self.user_ns.setdefault('__name__', '__main__')
        self.user_ns.setdefault('__builtins__', __builtin__)
        self.user_ns['__IP'] = self
        if self.raw_input_builtin is not None:
            self.user_ns['raw_input'] = self.raw_input_builtin
        if self.input_builtin is not None:
            self.user_ns['input'] = self.input_builtin

        builtin_additions = dict(
            ipmagic=self.ipmagic,
        )
        __builtin__.__dict__.update(builtin_additions)

        if self.history is not None:
            self.history.setup_namespace(self.user_ns)

    def set_traps(self):
        """ Set all of the output, display, and traceback traps.
        """

        self.output_trap.set()
        self.display_trap.set()
        self.traceback_trap.set()

    def unset_traps(self):
        """ Unset all of the output, display, and traceback traps.
        """

        self.output_trap.unset()
        self.display_trap.unset()
        self.traceback_trap.unset()

    def split_commands(self, python):
        """ Split multiple lines of code into discrete commands that can be
        executed singly.

        Parameters
        ----------
        python : str
            Pure, exec'able Python code.

        Returns
        -------
        commands : list of str
            Separate commands that can be exec'ed independently.
        """

        # compiler.parse treats trailing spaces after a newline as a
        # SyntaxError.  This is different than codeop.CommandCompiler, which
        # will compile the trailng spaces just fine.  We simply strip any
        # trailing whitespace off.  Passing a string with trailing whitespace
        # to exec will fail however.  There seems to be some inconsistency in
        # how trailing whitespace is handled, but this seems to work.
        python = python.strip()
        
        # The compiler module does not like unicode. We need to convert
        # it encode it:
        if isinstance(python, unicode):
            # Use the utf-8-sig BOM so the compiler detects this a UTF-8
            # encode string.
            python = '\xef\xbb\xbf' + python.encode('utf-8')
        
        # The compiler module will parse the code into an abstract syntax tree.
        # This has a bug with str("a\nb"), but not str("""a\nb""")!!!
        ast = compiler.parse(python)
        
        # Uncomment to help debug the ast tree
        # for n in ast.node:
        #     print n.lineno,'->',n
        
        # Each separate command is available by iterating over ast.node. The
        # lineno attribute is the line number (1-indexed) beginning the commands
        # suite.
        # lines ending with ";" yield a Discard Node that doesn't have a lineno
        # attribute.  These nodes can and should be discarded.  But there are
        # other situations that cause Discard nodes that shouldn't be discarded.
        # We might eventually discover other cases where lineno is None and have
        # to put in a more sophisticated test.
        linenos = [x.lineno-1 for x in ast.node if x.lineno is not None]
        
        # When we finally get the slices, we will need to slice all the way to
        # the end even though we don't have a line number for it. Fortunately,
        # None does the job nicely.
        linenos.append(None)
        
        # Same problem at the other end: sometimes the ast tree has its
        # first complete statement not starting on line 0. In this case
        # we might miss part of it.  This fixes ticket 266993.  Thanks Gael!
        linenos[0] = 0
        
        lines = python.splitlines()
        
        # Create a list of atomic commands.
        cmds = []
        for i, j in zip(linenos[:-1], linenos[1:]):
            cmd = lines[i:j]
            if cmd:
                cmds.append('\n'.join(cmd)+'\n')
        
        return cmds

    def error(self, text):
        """ Pass an error message back to the shell.

        Notes
        -----
        This should only be called when self.message is set. In other words,
        when code is being executed.

        Parameters
        ----------
        text : str
        """

        errors = self.message.get('IPYTHON_ERROR', [])
        errors.append(text)

    def var_expand(self, template):
        """ Expand $variables in the current namespace using Itpl.

        Parameters
        ----------
        template : str
        """

        return str(ItplNS(template, self.user_ns))

    def _possible_macro(self, obj):
        """ If the object is a macro, execute it.
        """

        if isinstance(obj, Macro):
            self.execute_macro(obj)

