import modelverse_kernel.primitives as primitive_functions

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

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 (generator, pending requests, request replies, has-reply)
        # tuples.
        self.generator_stack = []
        # exception_handlers is a stack of
        # (generator_stack index, [(exception type, handler function)])
        # tuples.
        self.exception_handlers = []
        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
        }

    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.
                        result = self.pop_requests()
                        return result
                    except KnownRequestHandled:
                        # Carry on.
                        pass

                # 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.
                if not self.handle_exception(ex):
                    raise

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

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

    def push_generator(self, gen):
        """Pushes a new generator onto the stack."""
        self.generator_stack.append((gen, None, True, [], False))
        # 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."""
        current_generator, requests, requests_done, replies, has_reply = self.generator_stack[-1]
        replies.append(new_reply)
        has_reply = True
        self.generator_stack[-1] = (current_generator, requests, requests_done, replies, has_reply)

    def extend_replies(self, new_replies):
        """Appends a list of replies to the top-of-stack generator's list of pending replies."""
        current_generator, requests, requests_done, replies, has_reply = self.generator_stack[-1]
        if new_replies is not None:
            replies.extend(new_replies)
            has_reply = True
            self.generator_stack[-1] = (
                current_generator, requests, requests_done, replies, has_reply)

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

        # Send the replies to the generator, and ask for new requests.
        requests = current_generator.send(replies if has_reply else None)

        # Update the entry on the stack.
        self.generator_stack[-1] = (current_generator, requests, False, [], False)

    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:]
                self.generator_stack.append(
                    (None,
                     [('TAIL_CALL_ARGS', [applicable_handler, (exception,)])],
                     False,
                     [],
                     False))
                return True

        # We couldn't find an applicable exception handler, even after exhausting the
        # entire exception handler stack. All is lost.
        # Also, clean up after ourselves.
        self.generator_stack = []
        self.exception_handlers = []
        return False

    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]
        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, []))

    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))

    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()

