""" Stage API

    This work is funded through NLnet under Grant 2019-02-012

    License: LGPLv3+


    Associated development bugs:
    * http://bugs.libre-riscv.org/show_bug.cgi?id=148
    * http://bugs.libre-riscv.org/show_bug.cgi?id=64
    * http://bugs.libre-riscv.org/show_bug.cgi?id=57

    Stage API:
    ---------

    stage requires compliance with a strict API that may be
    implemented in several means, including as a static class.

    Stages do not HOLD data, and they definitely do not contain
    signalling (ready/valid).  They do however specify the FORMAT
    of the incoming and outgoing data, and they provide a means to
    PROCESS that data (from incoming format to outgoing format).

    Stage Blocks really should be combinatorial blocks (Moore FSMs).
    It would be ok to have input come in from sync'd sources
    (clock-driven, Mealy FSMs) however by doing so they would no longer
    be deterministic, and chaining such blocks with such side-effects
    together could result in unexpected, unpredictable, unreproduceable
    behaviour.

    So generally to be avoided, then unless you know what you are doing.
    https://en.wikipedia.org/wiki/Moore_machine
    https://en.wikipedia.org/wiki/Mealy_machine

    the methods of a stage instance must be as follows:

    * ispec() - Input data format specification.  Takes a bit of explaining.
                The requirements are: something that eventually derives from
                nmigen Value must be returned *OR* an iterator or iterable
                or sequence (list, tuple etc.) or generator must *yield*
                thing(s) that (eventually) derive from the nmigen Value class.

                Complex to state, very simple in practice:
                see test_buf_pipe.py for over 25 worked examples.

    * ospec() - Output data format specification.
                format requirements identical to ispec.

    * process(m, i) - Optional function for processing ispec-formatted data.
                returns a combinatorial block of a result that
                may be assigned to the output, by way of the "nmoperator.eq"
                function.  Note that what is returned here can be
                extremely flexible.  Even a dictionary can be returned
                as long as it has fields that match precisely with the
                Record into which its values is intended to be assigned.
                Again: see example unit tests for details.

    * setup(m, i) - Optional function for setting up submodules.
                may be used for more complex stages, to link
                the input (i) to submodules.  must take responsibility
                for adding those submodules to the module (m).
                the submodules must be combinatorial blocks and
                must have their inputs and output linked combinatorially.

    Both StageCls (for use with non-static classes) and Stage (for use
    by static classes) are abstract classes from which, for convenience
    and as a courtesy to other developers, anything conforming to the
    Stage API may *choose* to derive.  See Liskov Substitution Principle:
    https://en.wikipedia.org/wiki/Liskov_substitution_principle

    StageChain:
    ----------

    A useful combinatorial wrapper around stages that chains them together
    and then presents a Stage-API-conformant interface.  By presenting
    the same API as the stages it wraps, it can clearly be used recursively.

    StageHelper:
    ----------

    A convenience wrapper around a Stage-API-compliant "thing" which
    complies with the Stage API and provides mandatory versions of
    all the optional bits.
"""

from nmigen import Elaboratable
from abc import ABCMeta, abstractmethod
import inspect

from nmutil import nmoperator


def _spec(fn, name=None):
    """ useful function that determines if "fn" has an argument "name".
        if so, fn(name) is called otherwise fn() is called.

        means that ispec and ospec can be declared with *or without*
        a name argument.  normally it would be necessary to have
        "ispec(name=None)" to achieve the same effect.
    """
    if name is None:
        return fn()
    varnames = dict(inspect.getmembers(fn.__code__))['co_varnames']
    if 'name' in varnames:
        return fn(name=name)
    return fn()


class StageCls(metaclass=ABCMeta):
    """ Class-based "Stage" API.  requires instantiation (after derivation)

        see "Stage API" above..  Note: python does *not* require derivation
        from this class.  All that is required is that the pipelines *have*
        the functions listed in this class.  Derivation from this class
        is therefore merely a "courtesy" to maintainers.
    """
    @abstractmethod
    def ispec(self): pass       # REQUIRED
    @abstractmethod
    def ospec(self): pass       # REQUIRED
    #@abstractmethod
    #def setup(self, m, i): pass # OPTIONAL
    #@abstractmethod
    #def process(self, i): pass  # OPTIONAL


class Stage(metaclass=ABCMeta):
    """ Static "Stage" API.  does not require instantiation (after derivation)

        see "Stage API" above.  Note: python does *not* require derivation
        from this class.  All that is required is that the pipelines *have*
        the functions listed in this class.  Derivation from this class
        is therefore merely a "courtesy" to maintainers.
    """
    @staticmethod
    @abstractmethod
    def ispec(): pass

    @staticmethod
    @abstractmethod
    def ospec(): pass

    #@staticmethod
    #@abstractmethod
    #def setup(m, i): pass

    #@staticmethod
    #@abstractmethod
    #def process(i): pass


class StageHelper(Stage):
    """ a convenience wrapper around something that is Stage-API-compliant.
        (that "something" may be a static class, for example).

        StageHelper happens to also be compliant with the Stage API,
        it differs from the stage that it wraps in that all the "optional"
        functions are provided (hence the designation "convenience wrapper")
    """
    def __init__(self, stage):
        self.stage = stage
        self._ispecfn = None
        self._ospecfn = None
        if stage is not None:
            self.set_specs(self, self)

    def ospec(self, name=None):
        assert self._ospecfn is not None
        return _spec(self._ospecfn, name)

    def ispec(self, name=None):
        assert self._ispecfn is not None
        return _spec(self._ispecfn, name)

    def set_specs(self, p, n):
        """ sets up the ispecfn and ospecfn for getting input and output data
        """
        if hasattr(p, "stage"):
            p = p.stage
        if hasattr(n, "stage"):
            n = n.stage
        self._ispecfn = p.ispec
        self._ospecfn = n.ospec

    def new_specs(self, name):
        """ allocates new ispec and ospec pair
        """
        return (_spec(self.ispec, "%s_i" % name),
                _spec(self.ospec, "%s_o" % name))

    def process(self, i):
        if self.stage and hasattr(self.stage, "process"):
            return self.stage.process(i)
        return i

    def setup(self, m, i):
        if self.stage is not None and hasattr(self.stage, "setup"):
            self.stage.setup(m, i)

    def _postprocess(self, i): # XXX DISABLED
        return i # RETURNS INPUT
        if hasattr(self.stage, "postprocess"):
            return self.stage.postprocess(i)
        return i


class StageChain(StageHelper):
    """ pass in a list of stages (combinatorial blocks), and they will
        automatically be chained together via their input and output specs
        into a combinatorial chain, to create one giant combinatorial
        block.

        the end result conforms to the exact same Stage API.

        * input to this class will be the input of the first stage
        * output of first stage goes into input of second
        * output of second goes into input into third
        * ... (etc. etc.)
        * the output of this class will be the output of the last stage

        NOTE: whilst this is very similar to ControlBase.connect(), it is
        *really* important to appreciate that StageChain is pure
        combinatorial and bypasses (does not involve, at all, ready/valid
        signalling OF ANY KIND).

        ControlBase.connect on the other hand respects, connects, and uses
        ready/valid signalling.

        Arguments:

        * :chain: a chain of combinatorial blocks conforming to the Stage API
                  NOTE: StageChain.ispec and ospect have to have something
                  to return (beginning and end specs of the chain),
                  therefore the chain argument must be non-zero length

        * :specallocate: if set, new input and output data will be allocated
                         and connected (eq'd) to each chained Stage.
                         in some cases if this is not done, the nmigen warning
                         "driving from two sources, module is being flattened"
                         will be issued.

        NOTE: DO NOT use StageChain with combinatorial blocks that have
        side-effects (state-based / clock-based input) or conditional
        (inter-chain) dependencies, unless you really know what you are doing.
    """
    def __init__(self, chain, specallocate=False):
        assert len(chain) > 0, "stage chain must be non-zero length"
        self.chain = chain
        StageHelper.__init__(self, None)
        if specallocate:
            self.setup = self._sa_setup
        else:
            self.setup = self._na_setup
        self.set_specs(self.chain[0], self.chain[-1])

    def _sa_setup(self, m, i):
        for (idx, c) in enumerate(self.chain):
            if hasattr(c, "setup"):
                c.setup(m, i)               # stage may have some module stuff
            ofn = self.chain[idx].ospec     # last assignment survives
            cname = 'chainin%d' % idx
            o = _spec(ofn, cname)
            if isinstance(o, Elaboratable):
                setattr(m.submodules, cname, o)
            m.d.comb += nmoperator.eq(o, c.process(i)) # process input into "o"
            if idx == len(self.chain)-1:
                break
            ifn = self.chain[idx+1].ispec   # new input on next loop
            i = _spec(ifn, 'chainin%d' % (idx+1))
            m.d.comb += nmoperator.eq(i, o) # assign to next input
        self.o = o
        return self.o                       # last loop is the output

    def _na_setup(self, m, i):
        for (idx, c) in enumerate(self.chain):
            if hasattr(c, "setup"):
                c.setup(m, i)               # stage may have some module stuff
            i = o = c.process(i)            # store input into "o"
        self.o = o
        return self.o                       # last loop is the output

    def process(self, i):
        return self.o # conform to Stage API: return last-loop output


