import sys
import modelverse_kernel.primitives as primitive_functions
import modelverse_jit.runtime as jit_runtime

class KnownRequestHandled(Exception):
    """An exception that signifies that a known request was handled."""
    pass

class GeneratorStackEntry(object):
    """An entry in the generator stack of a request handles."""
    def __init__(self, generator):
        self.generator = generator
        self.function_name = None
        self.source_map = None
        self.function_origin = None
        self.pending_requests = None
        self.finished_requests = True
        self.replies = []
        self.has_reply = False

    def append_reply(self, new_reply):
        """Appends a reply to the this entry's list of pending replies."""
        self.replies.append(new_reply)
        self.has_reply = True

    def extend_replies(self, new_replies):
        """Appends a list of replies to this entry's list of pending replies."""
        if new_replies is not None:
            self.replies.extend(new_replies)
            self.has_reply = True

    def step(self):
        """Performs a single step: accumulated replies are fed to the generator,
           which then produces requests."""
        # Send the replies to the generator, and ask for new requests.
        self.pending_requests = self.generator.send(self.replies if self.has_reply else None)

        # Reset some data structures.
        self.finished_requests = False
        self.replies = []
        self.has_reply = False

def format_stack_trace(stack_trace):
    """Formats a list of (function name, debug info, origin) triples."""
    return '\n'.join([jit_runtime.format_stack_frame(*triple) for triple in stack_trace])

class UnhandledRequestHandlerException(Exception):
    """The type of exception that is thrown when the request handler encounters an
       unhandled exception."""
    def __init__(self, inner_exception, stack_trace):
        Exception.__init__(
            self,
            """The request handler encountered an unknown exception.\n
               Inner exception: %s\n
               Stack trace:\n%s\n""" % (inner_exception, format_stack_trace(stack_trace)))
        self.inner_exception = inner_exception
        self.stack_trace = stack_trace

class RequestHandler(object):
    """A type of object that intercepts logic-related Modelverse requests, and
       forwards Modelverse state requests."""
    def __init__(self):
        # generator_stack is a stack of GeneratorStackEntry values.
        self.generator_stack = []
        # exception_handlers is a stack of
        # (generator_stack index, [(exception type, handler function)])
        # tuples.
        self.exception_handlers = []
        self.produce_stack_trace = True
        self.handlers = {
            'CALL' : self.execute_call,
            'CALL_ARGS' : self.execute_call_args,
            'CALL_KWARGS' : self.execute_call_kwargs,
            'TAIL_CALL' : self.execute_tail_call,
            'TAIL_CALL_ARGS' : self.execute_tail_call_args,
            'TAIL_CALL_KWARGS' : self.execute_tail_call_kwargs,
            'TRY' : self.execute_try,
            'CATCH' : self.execute_catch,
            'END_TRY' : self.execute_end_try,
            'DEBUG_INFO' : self.execute_debug_info
        }

    def is_active(self):
        """Tests if this request handler has a top-of-stack generator."""
        return len(self.generator_stack) > 0

    def handle_request(self, reply):
        """Replies to a request from the top-of-stack generator, and returns a new request."""
        if not self.is_active():
            raise ValueError('handle_request cannot be called with an empty generator stack.')

        # Append the server's replies to the list of replies.
        self.extend_replies(reply)
        while 1:
            # Silence pylint's warning about catching Exception.
            # pylint: disable=I0011,W0703
            try:
                if self.has_pending_requests():
                    try:
                        # Try to pop a request for the modelverse state.
                        return self.pop_requests()
                    except KnownRequestHandled:
                        # Carry on.
                        pass

                if not self.has_pending_requests():
                    # Perform a single generator step.
                    self.step()
            except StopIteration:
                # Done, so remove the generator
                self.pop_generator()
                if self.is_active():
                    # This generator was called from another generator.
                    # Append 'None' to the caller's list of replies.
                    self.append_reply(None)
                else:
                    # Looks like we're done here.
                    return None
            except primitive_functions.PrimitiveFinished as ex:
                # Done, so remove the generator
                self.pop_generator()
                if self.is_active():
                    # This generator was called from another generator.
                    # Append the callee's result to the caller's list of replies.
                    self.append_reply(ex.result)
                else:
                    # Looks like we're done here.
                    return None
            except Exception as ex:
                # Maybe get an exception handler to do this.
                stack_trace = self.handle_exception(ex)
                if stack_trace is not None:
                    if self.produce_stack_trace:
                        raise UnhandledRequestHandlerException(ex, stack_trace)
                    else:
                        raise

    def set_finished_requests_flag(self):
        """Sets the finished_requests flag in the top-of-stack tuple."""
        self.generator_stack[-1].finished_requests = True

    def has_pending_requests(self):
        """Tests if the top-of-stack generator has pending requests."""
        return not self.generator_stack[-1].finished_requests

    def push_generator(self, gen):
        """Pushes a new generator onto the stack."""
        self.generator_stack.append(GeneratorStackEntry(gen))
        # print('Pushed generator %s. Generator count: %d' % (gen, len(self.generator_stack)))

    def pop_generator(self):
        """Removes the top-of-stack generator from the generator stack."""
        # Pop the generator itself.
        self.generator_stack.pop()
        # print('Popped generator %s. Generator count: %d' % (gen, len(self.generator_stack)))
        # Pop any exception handlers defined by the generator.
        top_of_stack_index = len(self.generator_stack)
        while len(self.exception_handlers) > 0:
            stack_index, _ = self.exception_handlers[-1]
            if stack_index == top_of_stack_index:
                # Pop exception handlers until exception_handlers is empty or until
                # we find an exception handler that is not associated with the popped
                # generator.
                self.exception_handlers.pop()
            else:
                # We're done here.
                break

    def append_reply(self, new_reply):
        """Appends a reply to the top-of-stack generator's list of pending replies."""
        self.generator_stack[-1].append_reply(new_reply)

    def extend_replies(self, new_replies):
        """Appends a list of replies to the top-of-stack generator's list of pending replies."""
        self.generator_stack[-1].extend_replies(new_replies)

    def step(self):
        """Performs a single step: accumulated replies are fed to the generator,
           which then produces requests."""
        self.generator_stack[-1].step()

    def handle_exception(self, exception):
        """Handles the given exception. A Boolean is returned that tells if
           the exception was handled."""
        # print('Exception thrown from %s: %s' % (str(self.generator_stack[-1]), str(exception)))
        while len(self.exception_handlers) > 0:
            # Pop the top-of-stack exception handler.
            stack_index, handlers = self.exception_handlers.pop()

            # Try to find an applicable handler.
            applicable_handler = None
            for handled_type, handler in handlers:
                if isinstance(exception, handled_type):
                    applicable_handler = handler

            if applicable_handler is not None:
                # We handle exceptions by first clearing the current stack frame and
                # all of its children. Then, we place a dummy frame on the stack with
                # a single 'TAIL_CALL_ARGS' request. The next iteration will replace
                # the dummy frame by an actual frame.
                del self.generator_stack[stack_index:]
                stack_entry = GeneratorStackEntry(None)
                stack_entry.pending_requests = [
                    ('TAIL_CALL_ARGS', [applicable_handler, (exception,)])]
                stack_entry.finished_requests = False
                self.generator_stack.append(stack_entry)
                return None

        # We couldn't find an applicable exception handler, even after exhausting the
        # entire exception handler stack. All is lost.
        # Also, clean up after ourselves by unwinding the stack.
        return self.unwind_stack()

    def unwind_stack(self):
        """Unwinds the entirety of the stack. All generators and exception handlers are
           discarded. A list of (function name, debug information, source) statements is
           returned."""
        class UnwindStackException(Exception):
            """A hard-to-catch exception that is used to make generators crash.
               The exceptions they produce can then be analyzed for line numbers."""
            pass

        # First throw away all exception handlers. We won't be needing them any more.
        self.exception_handlers = []

        # Then pop every generator from the stack and make it crash.
        stack_trace = []
        while len(self.generator_stack) > 0:
            top_entry = self.generator_stack.pop()
            if top_entry.function_origin is None:
                # Skip this function.
                continue

            try:
                # Crash the generator.
                top_entry.generator.throw(UnwindStackException())
            except UnwindStackException:
                # Find out where the exception was thrown.
                _, _, exc_traceback = sys.exc_info()
                line_number = exc_traceback.tb_lineno
                source_map = top_entry.source_map
                if source_map is not None:
                    debug_info = source_map.get_debug_info(line_number)
                else:
                    debug_info = None

                function_name = top_entry.function_name
                stack_trace.append((function_name, debug_info, top_entry.function_origin))

        return stack_trace[::-1]

    def pop_requests(self):
        """Tries to pop a batch of Modelverse _state_ requests from the
           current list of requests. Known requests are executed immediately.

           A list of requests and a Boolean are returned. The latter is True
           if there are no more requests to process, and false otherwise."""
        requests = self.generator_stack[-1].pending_requests
        if requests is None or len(requests) == 0:
            # Couldn't find a request for the state to handle.
            self.set_finished_requests_flag()
            return requests

        for i, elem in enumerate(requests):
            if elem[0] in self.handlers:
                # The kernel should handle known requests.
                if i > 0:
                    # Handle any requests that precede the known request first.
                    pre_requests = requests[:i]
                    del requests[:i]
                    return pre_requests

                # The known request must be the first element in the list. Pop it.
                requests.pop(0)

                # The list of requests might be empty now. If so, then flag this
                # batch of requests as finished.
                if len(requests) == 0:
                    self.set_finished_requests_flag()

                # Handle the request.
                _, request_args = elem
                self.handlers[elem[0]](request_args)
                raise KnownRequestHandled()

        # We couldn't find a known request in the batch of requests, so we might as well
        # handle them all at once then.
        self.set_finished_requests_flag()
        return requests

    def execute_call(self, request_args):
        """Executes a CALL-request with the given argument list."""
        # Format: ("CALL", [gen])
        gen, = request_args
        self.push_generator(gen)

    def execute_call_kwargs(self, request_args):
        """Executes a CALL_KWARGS-request with the given argument list."""
        # Format: ("CALL_KWARGS", [func, kwargs])
        # This format is useful because it also works for functions that
        # throw an exception but never yield.
        func, kwargs = request_args
        # We need to be extra careful here, because func(**kwargs) might
        # not be a generator at all: it might simply be a method that
        # raises an exception. To cope with this we need to push a dummy
        # entry onto the stack if a StopIteration or PrimtiveFinished
        # exception is thrown. The logic in execute_yields will then pop
        # that dummy entry.
        try:
            self.push_generator(func(**kwargs))
        except StopIteration:
            self.push_generator(None)
            raise
        except primitive_functions.PrimitiveFinished:
            self.push_generator(None)
            raise

    def execute_call_args(self, request_args):
        """Executes a CALL_ARGS-request with the given argument list."""
        # Format: ("CALL_ARGS", [gen, args])
        func, args = request_args
        # We need to be extra careful here, because func(*args) might
        # not be a generator at all: it might simply be a method that
        # raises an exception. To cope with this we need to push a dummy
        # entry onto the stack if a StopIteration or PrimtiveFinished
        # exception is thrown. The logic in execute_yields will then pop
        # that dummy entry.
        try:
            self.push_generator(func(*args))
        except StopIteration:
            self.push_generator(None)
            raise
        except primitive_functions.PrimitiveFinished:
            self.push_generator(None)
            raise

    def execute_tail_call(self, request_args):
        """Executes a TAIL_CALL-request with the given argument list."""
        # Format: ("TAIL_CALL", [gen])
        self.pop_generator()
        self.execute_call(request_args)

    def execute_tail_call_args(self, request_args):
        """Executes a TAIL_CALL_ARGS-request with the given argument list."""
        # Format: ("TAIL_CALL_ARGS", [gen, args])
        self.pop_generator()
        self.execute_call_args(request_args)

    def execute_tail_call_kwargs(self, request_args):
        """Executes a TAIL_CALL_KWARGS-request with the given argument list."""
        # Format: ("TAIL_CALL_KWARGS", [gen, kwargs])
        self.pop_generator()
        self.execute_call_kwargs(request_args)

    def execute_try(self, request_args):
        """Executes a TRY-request with the given argument list."""
        # TRY pushes an exception handler onto the exception handler stack.
        # Format: ("TRY", [])
        if len(request_args) != 0:
            raise ValueError(
                ("TRY was given argument list '%s', " +
                 "expected exactly zero arguments.") % repr(request_args))
        self.exception_handlers.append((len(self.generator_stack) - 1, []))
        self.append_reply(None)

    def execute_catch(self, request_args):
        """Executes a CATCH-request with the given argument list."""
        if len(request_args) != 2:
            raise ValueError(
                ("CATCH was given argument list '%s', "
                 "expected exactly two arguments: an exception "
                 "type and an exception handler.") % repr(request_args))
        exception_type, handler = request_args
        stack_index, handlers = self.exception_handlers[-1]
        if stack_index != len(self.generator_stack) - 1:
            raise ValueError(
                'Cannot comply with CATCH because there is no exception handler for the '
                'current generator.')

        handlers.append((exception_type, handler))
        self.append_reply(None)

    def execute_end_try(self, request_args):
        """Executes an END_TRY-request with the given argument list."""
        # END_TRY pops a value from the exception handler stack. The
        # popped value must reference the top-of-stack element in the
        # generator stack. END_TRY takes no arguments.
        # Format: ("END_TRY", [])
        if len(request_args) != 0:
            raise ValueError(
                "END_TRY was given argument list '%s', expected '%s'." % (
                    repr(request_args), repr([])))

        if len(self.exception_handlers) == 0:
            raise ValueError(
                'Cannot comply with END_TRY because the exception handler stack is empty.')

        stack_index, _ = self.exception_handlers[-1]
        if stack_index != len(self.generator_stack) - 1:
            raise ValueError(
                'Cannot comply with END_TRY because there is no exception handler for the '
                'current generator.')

        # Everything seems to be in order. Pop the exception handler.
        self.exception_handlers.pop()
        self.append_reply(None)

    def execute_debug_info(self, request_args):
        """Executes a DEBUG_INFO-request with the given argument list."""
        # DEBUG_INFO updates the function name and source map for the top-of-stack generator.
        # These two things allow us to unwind the stack neatly if an unhandled exception is
        # encountered.
        # Format: ("DEBUG_INFO", [function_name, source_map])
        top_entry = self.generator_stack[-1]
        top_entry.function_name, top_entry.source_map, top_entry.function_origin = request_args
        top_entry.append_reply(None)

