# --
# Copyright (c) 2008-2018 Net-ng.
# All rights reserved.
#
# This software is licensed under the BSD License, as described in
# the file LICENSE.txt, which you should have received as part of
# this distribution.
# --

"""This module implements the component model of the framework.

This model is inspired by the Seaside one. With the possibility to embed,
replace and call a component. It's described in `ComponentModel`
"""

import sys
import types

from peak.rules import when

from nagare import presentation, callbacks, continuation, partial

_marker = object()


class AnswerWithoutCall(BaseException):
    pass


class Component(object):
    """This class transforms any Python object into a component

    A component has views, can the embedded, replaced, called and can answsered a value.
    """
    def __init__(self, o=None, model=0, url=None):
        """Initialisation

        In:
          - ``o`` -- the python object (or component) to transform into a component
          - ``model`` -- the name of the view to use (``None`` for the default view)
          - ``url`` -- the url fragment to add before all the links generated by
            views of this component
        """
        self._becomes(o, model, url)

        self._cont = None
        self._on_answer = None

    def __call__(self):
        """Return the inner object
        """
        return self.o

    def register_callback(self, model, priority, callback, with_request, render):
        """Register a callback for this component

        In:
          - ``model`` -- name of the view which registers this callback (``None`` for the default view)
          - ``priority`` -- type and priority of the callback
          - ``callback`` -- the action function or method
          - ``with_request`` -- will the request and response objects be passed to the action?
          - ``render`` -- the render function or method
        """
        self.__dict__.setdefault('_new_callbacks', {})
        return callbacks.register(model, priority, callback, with_request, render, self._new_callbacks)

    def serialize_callbacks(self, clean_callbacks):
        """Return the callbacks to serialize

        In:
          - ``clean_callbacks`` -- do we have to forget all the old callbacks?

        Return:
          - the callbacks of this component
        """
        old = self.__dict__.pop('_callbacks', {})
        new = self.__dict__.pop('_new_callbacks', {})

        if not clean_callbacks:
            # Selectively keep some old callbacks

            # --
            # Policy #1: keep the old callbacks only if no view was rendered
            # => simple cleanup but can lead to ``CallbackLookup`` errors
            # if not new:
            #     new = old
            # --

            # --
            # Policy #2: no cleanup, always keep all the old callbacks
            # => no ``CallbackLookup`` errors but the sessions can keep growing
            #    in size as they are only reset when a new complete synchronous
            #    page rendering is done
            # new.update(old)
            # --

            # --
            # Policy #3: keep the old callbacks registered by a view only if
            # this view was not rendered again
            new.update(callbacks.clean(old, new))
            # --

        if new:
            self._callbacks = new

        return new

    def render(self, renderer, model=0):
        """Rendering method of a component

        Forward the call to the generic method of the ``presentation`` service
        """
        return presentation.render(self, renderer, self, model)

    def init(self, url, http_method, request):
        """Initialisation from an url

        Forward the call to the generic method of the ``presentation`` service
        """
        return presentation.init(self, url, self, http_method, request)

    def _becomes(self, o, model, url):
        """Replace a component by an object or an other component

        In:
          - ``o`` -- object to be replaced by
          - ``model`` -- the name of the view to use (``None`` for the default view)
          - ``url`` -- the url fragment to add before all the links generated by
            views of this component

        Return:
          - ``self``
        """
        o = self if o.__class__ is object else o
        if isinstance(o, Component):
            o = o()

        self.o = o

        self.model = model
        self.url = url

    def becomes(self, o=_marker, model=0, url=None):
        """Replace a component by an object or an other component

        In:
          - ``o`` -- object to be replaced by
          - ``model`` -- the name of the view to use (``None`` for the default view)
          - ``url`` -- the url fragment to add before all the links generated by
            views of this component

        Return:
          - ``self``
        """
        self._becomes(o, model, url or self.url)
        self._cont = None

        return self

    def call(self, o=_marker, model=0, url=None):
        """Call an other object or component

        The current component is replaced and will be back when the object
        will do an ``answer()``

        In:
          - ``o`` -- the object to call
          - ``model`` -- the name of the view to use (``None`` for the default view)
          - ``url`` -- the url fragment to add before all the links generated by
            views of this component

        Return:
          - the answer of the called object
        """
        sys.exc_clear()

        # Keep my configuration
        previous_o = self.o
        previous_model = self.model
        previous_url = self.url
        previous_cont = self._cont
        previous_on_answer = self._on_answer

        # Set the new configuration
        self._on_answer = None

        # Replace me by the object and wait its answer
        self.becomes(o, model, url)

        self._cont = continuation.get_current()

        try:
            r = self._cont.switch()
        except NotImplementedError:
            raise NotImplementedError('Stackless Python or PyPy is needed for `comp.call()`')

        # Restore my configuration
        self._on_answer = previous_on_answer
        self._cont = previous_cont
        self._becomes(previous_o, previous_model, previous_url)

        # Return the answer
        return r

    def answer(self, r=None):
        """Answer to a call

        In:
          - the value to answer
        """
        # If a function is listening to my answer, calls it
        if self._on_answer is not None:
            return self._on_answer(r)

        # Else, check if I was called by a component
        if self._cont is None:
            raise AnswerWithoutCall(self)

        # Returns my answer to the calling component
        self._cont.switch(r)

    def on_answer(self, f, *args, **kw):
        """
        Register a function to listen to my answer

        In:
          - ``f`` -- function to call with my answer
          - ``args``, ``kw`` -- ``f`` parameters
        """
        self._on_answer = partial.Partial(f, *args, **kw) if args or kw else f
        return self

    def __repr__(self):
        return '<%s with %s at 0x%x on object %r>' % (
            self.__class__.__name__.lower(),
            "model '%s'" % self.model if self.model else 'default model',
            id(self),
            self.o
        )


@when(presentation.render, (Component, object, object, int))
@when(presentation.render, (Component, object, object, types.NoneType))
@when(presentation.render, (Component, object, object, str))
def render(self, renderer, comp, model):
    """Rendering of a ``Component``

    In:
      - ``renderer`` -- the renderer
      - ``comp`` -- the component
      - ``model`` -- the name of the view

    Return:
      - the view of the component object
    """
    renderer = renderer.new()   # Create a new renderer of the same class than the current renderer
    renderer.start_rendering(self, model)

    if model == 0:
        model = self.model

    output = presentation.render(self(), renderer, self, model)
    return renderer.end_rendering(output)


@presentation.init_for(Component)
def init_for(self, url, comp, http_method, request):
    """Initialisation from an url

    In:
      - ``url`` -- rest of the url to process
      - ``comp`` -- the component
      - ``http_method`` -- the HTTP method
      - ``request`` -- the complete Request object

    Return:
      - ``presentation.NOT_FOUND`` if the url is invalid, else ``None``
    """
    presentation.init(self(), url, self, http_method, request)


# -----------------------------------------------------------------------------------------------------

class Task(object):
    """A ``Task`` encapsulated a simple method. A ``task`` is typically used to
    manage other components by calling them.

    .. warning::

       A ``Task`` is an object, not a component: you must wrap it into a ``Component()`` to use it.
    """

    def _go(self, comp):
        # If I was not called by an other component and nobody is listening to
        # my answer,  I'm the root component. So I call my ``go()`` method forever
        if comp._cont is comp._on_answer is None:
            while True:
                self.go(comp)

        # Else, answer with the return of the ``go`` method
        comp.answer(self.go(comp))

    def go(self, comp):
        raise BaseException('AbstractMethod')


@presentation.render_for(Task)
def render_task(self, renderer, comp, *args):
    return presentation.render(self._go, renderer, comp, *args)


# -----------------------------------------------------------------------------------------------------

@when(presentation.render, (types.FunctionType,))
@when(presentation.render, (types.MethodType,))
def render_function(f, renderer, comp, *args):
    continuation.Continuation(f, comp)
    return comp.render(renderer.parent)
