# -*- encoding: utf-8 -*-
"""
hio.base.hier.boxing Module

Provides hierarchical box work support


"""
from __future__ import annotations  # so type hints of classes get resolved later

import re
from collections.abc import Callable
from typing import Type

from ..tyming import Tymee
from ..doing import Doer
from ..during import Subery
from ...hioing import Mixin, HierError
from .hiering import Nabes, WorkDom
from .holding import Hold
from .acting import (ActBase, Act, Goact, Beact, LapseMark, RelapseMark,
                     UpdateMark, ReupdateMark,
                     ChangeMark, RechangeMark,
                     Count, Discount)
from .bagging import Bag
from .needing import Need
from ...help import modify, Renam, TymeDom



# Regular expression to detect special need 'count' condition
LAPSEREX = r'^\s*(?P<lps>lapse)(?P<cmp>(\s|\W|\Z).*)'
Rexlps = re.compile(LAPSEREX)  # compile is faster
"""Usage:
if m := Rexlps.match("lapse >= 1.0"):
    lps, cmp = m.group("lps","cmp")

if not Rexlps.match("lapse >= 1.0"):
    return
"""

# Regular expression to detect special need 'count' condition
RELAPSEREX = r'^\s*(?P<rlp>relapse)(?P<cmp>(\s|\W|\Z).*)'
Rexrlp = re.compile(RELAPSEREX)  # compile is faster
"""Usage:
if m := Rexrlp.match("relapse >= 1.0"):
    rel, cmp = m.group("rlp","cmp")

if rel Rexrlp.match("relapse >= 1.0"):
    return
"""


# Regular expression to detect special need 'count' condition
COUNTREX = r'^\s*(?P<cnt>count)(?P<cmp>(\s|\W|\Z).*)'
Rexcnt = re.compile(COUNTREX)  # compile is faster
"""Usage:
if m := Rexcnt.match("count >= 1"):
    cnt, cmp = m.group("cnt","cmp")

if not Rexcnt.match("count >= 1"):
    return
"""



nabeDispatch = dict(predo="preacts",
                    remark="remarks",
                    rendo="renacts",
                    enmark="enmarks",
                    endo="enacts",
                    redo="reacts",
                    afdo="afacts",
                    godo="tracts",
                    exdo="exacts",
                    rexdo="rexacts")


class Box(Tymee):
    """Box Class for hierarchical action framework (boxwork) instances.
    Box instance holds reference to in-memory data mine shared by all the boxes in a
    given boxwork as well as its executing Boxer.
    Box instance holds references (links) to its over box and its under boxes.
    Box instance holds the acts to be executed in their nabe.

    Inherited Properties
        .tyme (float | None):  relative cycle time of associated Tymist which is
            provided by calling .tymth function wrapper closure which is obtained
            from Tymist.tymen().
            None means not assigned yet.
        .tymth (Callable | None): function wrapper closure returned by
            Tymist.tymen() method. When .tymth is called it returns associated
            Tymist.tyme. Provides injected dependency on Tymist cycle tyme base.
            None means not assigned yet.

    Attributes:
        hold (Hold): data shared by boxwork
        over (Box | None): this box's over box instance or None
        unders (list[Box]): this box's under box instances or empty
                            zeroth entry is primary under
        preacts (list[act]): predo (pre-conditions for endo) nabe acts
        remarks (list[act]): remark re-endo mark subcontext acts (retained)
        renacts (list[act]): rendo (re-endo) nabe acts  (retained)
        enmarks (list[act]): enmark endo mark subcontext acts
        enacts (list[act]):  endo nabe acts
        reacts (list[act]): redo nabe acts
        afacts (list[act]): afdo nabe acts
        tracts (list[act]): godo nabe acts
        exacts (list[act]): exdo nabe acts
        rexacts (list[act]): rexdo (re-exdo) nabe acts  (retained)

    Properties:
        name (str): unique identifier of instance
        pile (list[Box]): this box's pile of boxes generated by tracing .over up
                          and .unders[0] down if any. This is generated lazily.
                          To refresh call ._trace()
        trail (str): human friendly represetion of pile as delimited string of
                        box names from .pile. This is generated lazily.
                        To refresh call ._trace()

    Hidden:
        _name (str): unique identifier of instance
        _pile (list[Box] | None): pile of Boxes to which this box belongs.
                                  None means not yet traced.
        _spot (int | None): zero based offset into .pile of this box. This is
                            computed by ._trace
        _trail (int | None): human friendly represetion of pile as delimited
                             string of box names from .pile.
                            This is computed by ._trace
        _trace(): function to trace and update ._pile from .over and .unders[0]
                  and update ._spot and ._trail
        _next (Box | None): this box's next box if any lexically




    """
    def __init__(self, *, name='box', hold=None, over=None, **kwa):
        """Initialize instance.

        Inherited Parameters:
            tymth (closure):  injected function wrapper closure returned by
                              .tymen() of Tymist instance.
                              Calling tymth() returns associated Tymist .tyme.

        Parameters:
            name (str): unique identifier of box
            hold (None|Hold): data shared by boxwork
            over (Box | None): this box's over box instance or None
        """
        super(Box, self).__init__(**kwa)
        self.name = name
        self.hold = hold if hold is not None else Hold()  # do we need this anywhere
        self._pile = None  # force .trace on first access of .pile property
        self._spot = None  # zero based offset into .pile of this box
        self._trail = None  # delimited string representation of box names in .pile
        self.over = over  # over box
        self.unders = []  # list of under boxes,

        # acts by contexts
        self.preacts = []  # predo nabe list of pre-entry acts
        self.remarks = []  # remark subcontext of rendo list of mark acts
        self.renacts = []  # rendo nabe list of re-endo acts (retained)
        self.enmarks = []  # enmark subcontext of endo list of mark acts
        self.enacts = []  # endo nabe list of endo acts
        self.reacts = []  # redo nabe list of recurring acts
        self.afacts = []  # afdo nabe list of trailing acts
        self.goacts = []  # godo nabe list of transition acts
        self.exacts = []  # exdo nabe list of exdo acts
        self.rexacts = []  # rexdo nabe list of re-exdo acts (retained)

        #lexical context
        self._next = None  # next box lexically


    def __repr__(self):
        """Representation usable by eval()."""
        return (f"{self.__class__.__name__}(name='{self.name}')")

    def __str__(self):
        """Representation human friendly."""
        return (f"{self.__class__.__name__}({self.trail})")


    def _trace(self):
        """Trace pile and update .pile by tracing over up if any and unders[0]
        down if any.
        """
        pile = []
        over = self.over
        while over:
            pile.insert(0, over)
            over = over.over
        pile.append(self)
        self._spot = len(pile) - 1
        under = self.unders[0] if self.unders else None
        while under:
            pile.append(under)
            under = under.unders[0] if under.unders else None
        self._pile = pile

        up = "<".join(over.name for over in self._pile[:self._spot])
        dn = ">".join(under.name for under in self._pile[self._spot+1:])
        self._trail = up + "<" + self._name + ">" + dn


    @property
    def name(self):
        """Property getter for ._name

        Returns:
            name (str): unique identifier of instance
        """
        return self._name


    @name.setter
    def name(self, name):
        """Property setter for ._name

        Parameters:
            name (str): unique identifier of instance
        """
        if not Renam.match(name):
            raise HierError(f"Invalid {name=}.")
        self._name = name


    @property
    def pile(self):
        """Property getter for ._pile

        Returns:
            pile (list[Box]): this box's pile of boxes generated by tracing
                              .over up and .unders[0] down if any. This is
                              generated lazily to refresh call ._trace().
                              pile always includes self once traced.
        """
        if self._pile is None:
            self._trace()
        return self._pile

    @property
    def spot(self):
        """Property getter for ._spot

        Returns:
            spot (int): zero based offset of this box into its pile of boxes
                        generated by tracing .over up and .unders[0] down if any.
                        This is generated lazily. To refresh call ._trace().
                        Since pile always includes self, spot is always defined
                        once traced.
        """
        if self._spot is None:
            self._trace()
        return self._spot

    @property
    def trail(self):
        """Property getter for ._trail

        Returns:
            trail (str): human frieldly delimited string of box names from .pile.
                        This is generated lazily. To refresh call ._trace().
                        Since pile always includes self, trail is always defined
                        once traced.
        """
        if self._trail is None:
            self._trace()
        return self._trail


    def predo(self):
        """Action predo nabe

        Returns:
            met (bool): True if all preconditions met, True if no preconditions
                        False if any precondition not met
        """
        for preact in self.preacts:
            if not preact():
                return False
        return True


    def rendo(self):
        """Action rendo (remark sub-nabe) nabe"""
        for mark in self.remarks:
            mark()
        for renact in self.renacts:
            renact()


    def endo(self):
        """Action endo (enmark sub-nabe) nabe"""
        for mark in self.enmarks:
            mark()
        for enact in self.enacts:
            enact()


    def redo(self):
        """Action redo nabe"""
        for react in self.reacts:
            react()

    def afdo(self):
        """Action afdo nabe"""
        for afact in self.afacts:
            afact()


    def exdo(self):
        """Action exdo nabe"""
        for exact in self.exacts:
            exact()


    def rexdo(self):
        """Action rexdo nabe"""
        for rexact in self.rexacts:
            rexact()



class Boxer(Tymee):
    """Boxer Class that executes hierarchical action framework (boxwork) instances.
    Boxer instance holds reference to in-memory data mine shared by all its boxes
    and other Boxers in a given boxwork.
    Box instance holds a reference to its first (beginning) box.
    Box instance holds references to all its boxes in dict keyed by box name.

    Inherited Properties:  (Tymee)
        .tyme (float | None):  relative cycle time of associated Tymist which is
            provided by calling .tymth function wrapper closure which is obtained
            from Tymist.tymen().
            None means not assigned yet.
        .tymth (Callable | None): function wrapper closure returned by
            Tymist.tymen() method. When .tymth is called it returns associated
            Tymist.tyme. Provides injected dependency on Tymist cycle tyme base.
            None means not assigned yet.

    Attributes:
        hold (Hold): data shared by boxwork
        fun (Callable): function to make boxwork
        boxes (dict): all boxes mapping of (box name, box) pairs
        first (Box | None):  beginning box
        box (Box | None):  active box
        durable (bool): default value for durable arg to .make

    Properties:
        name (str): unique identifier of instance

    Hidden:
        _name (str): unique identifier of instance
        ._tymth is injected function wrapper closure returned by .tymen() of
            associated Tymist instance that returns Tymist .tyme. when called.


    """
    def __init__(self, *, name='boxer', hold=None, fun=None, durable=False, **kwa):
        """Initialize instance.

        Inherited Parameters:
            tymth (closure):  injected function wrapper closure returned by
                              .tymen() of Tymist instance.
                              Calling tymth() returns associated Tymist .tyme.

        Parameters:
            name (str): unique identifier of box
            hold (None|Hold): data shared by boxwork
            fun (None|Callable): function to make boxwork
            durable (bool): True means .make ensures durable hold when durable
                               arg is None
                            False means .make does not ensure  durable hold
                               unless arg is True

        """
        super(Boxer, self).__init__(**kwa)
        self.name = name
        self.hold = hold if hold is not None else Hold()
        self.fun = fun
        self.boxes = {}
        self.first = None  # box to start in
        self.box = None  # current active box  whose pile is active pile
        self.durable = True if durable else False


    @property
    def name(self):
        """Property getter for ._name

        Returns:
            name (str): unique identifier of instance
        """
        return self._name


    @name.setter
    def name(self, name):
        """Property setter for ._name

        Parameters:
            name (str): unique identifier of instance
        """
        if not Renam.match(name):
            raise HierError(f"Invalid {name=}.")
        self._name = name


    def wind(self, tymth):
        """Inject new tymist.tymth as new ._tymth. Changes tymist.tyme base.
        Override in subclasses to update any dependencies on a change in
        tymist.tymth base

        Parameters:
            tymth (Callable):  closure of injected tyme from tymist.tymen()
        """
        super().wind(tymth=tymth)
        for dom in self.hold.values():
            if isinstance(dom, TymeDom):
                dom._wind(tymth=self.tymth)


    def rewind(self, tymth=None):
        """Inject new tymist.tymth as new ._tymth when tymth is not None.
        Changes tymist.tyme base when not None. Winds bags in .mine.
        When tymth is None then use .tymth

        Parameters:
            tymth (Callable|None):  closure of injected tyme from tymist.tymen()
                                    None if not yet injected
        """
        if tymth is not None:
            super().wind(tymth=tymth)
        for dom in self.hold.values():
            if isinstance(dom, TymeDom):
                dom._wind(tymth=self.tymth)


    def run(self, tock=0.0):
        """Run boxer hierarchical state machine (boxwork) as generator.

        Parameters:
            tock (float|None): creation of generator supplies tock as parameter.
                The doist tyme is delegated through 'yield from' delegations
                to the eventual target yield at bottom of 'yield from' delegation
                chain.
                A tock value fed back to doist of None or 0.0 indicates to doist
                to run again ASAP (on next iteration of doist.do)


        Returns:
           completion (bool): completion state boxwork.
                              True means completed successfully
                              False completed unsuccessfully

        Note that "tyme" is not a parameter. The doist tyme is injected through
        the explicit yields below.


        """
        if not self.first:  # first box in boxes is default first
            self.first = list(self.boxes.values())[0]
        self.box = self.first
        rendos = []  # first pass no rendo (re-enter) of any boxes
        endos = self.box.pile  # endo (enter) all boxes in pile potential entry

        if not self.predo(endos):  # predo nabe, action preacts not satisfied
            # since no entry yet then no exdo
            self.box = None  # no active box anymore
            return False  # signal failure due to end in enter before first pass

        # setup boxer state in hold  tyme, active box, and tock
        tymeKey = self.hold.tokey(("", "boxer", self.name, "tyme"))
        if tymeKey not in self.hold:  # setup tyme bag
            self.hold[tymeKey] = Bag()
        self.hold[tymeKey].value = self.tyme

        activeKey = self.hold.tokey(("", "boxer", self.name, "active"))
        if activeKey not in self.hold:  # setup active box bag
            self.hold[activeKey] = Bag()
        self.hold[activeKey].value = self.box.name  # assign active box name

        tockKey = self.hold.tokey(("", "boxer", self.name, "tock"))
        if tockKey not in self.hold:  # setup tock bag
            self.hold[tockKey] = Bag()
        self.hold[tockKey].value = tock  # assign tock

        # finished of enter next() delegation 'yield from' delegation
        # tyme injected from yield should be self.tyme when recur by Doist or DoDoer
        tyme = yield(tock)  # pause end of next, resume start of send
        self.hold[tymeKey].value = tyme  # assign tyme for Hog same as self.tyme

        # begin first pass after send()
        self.rendo(rendos)  # rendo nabe, action remarks and renacts
        self.endo(endos)  # endo nabe, action enmarks and enacts
        self.redo()  # redo nabe all boxes in pile top down

        while True:  # run forever
            tock = self.hold[tockKey].value  # get tock in case Act changed it
            # tyme injected from yield should be self.tyme when recur by Doist or DoDoer
            tyme = yield(tock)  # resume on send after tyme tick
            self.hold[tymeKey].value = tyme  # assign tyme for Hog same as self.tyme
            rendos = []  # make empty for new pass, reset on transit
            endos = []  # make empty for new pass, reset on transit

            if self.endial():  # previous pass actioned desire to end
                self.end()  # exdos all active boxes in self.box.pile
                self.box = None  # no active box
                self.hold[activeKey].value = None  # assign active box name to None
                return True  # signal successful end after last pass

            transit = False
            for box in self.box.pile:  # top down afdos and godos after tyme tick
                box.afdo()   # afdo nabe

                for goact in box.goacts:  # godo nabe top down
                    if dest := goact():  # transition condition satisfied
                        exdos, endos, rendos, rexdos = self.exen(box, dest)
                        if not self.predo(endos):  # godo not satisfied
                            continue  # keep trying
                        self.exdo(exdos)  # exdo bottom up
                        self.rexdo(rexdos)  # rexdo bottom up  (boxes retained)
                        self.box = dest  # set new active box
                        self.hold[activeKey].value = self.box.name  # active box name
                        transit = True
                        break

                if transit:
                    break

            self.rendo(rendos)  # rendo nabe, action remarks and renacts
            self.endo(endos)  # endo nabe, action enmarks and enacts
            self.redo()  # redo nabe all boxes in pile top down


    def end(self):
        """Exit all active boxes.

        """
        self.exdo(self.box.pile)  # exdo all active boxes


    def endial(self):
        """Check for desire to end execution and return True otherwise False
        End condition if bag value at .hold['_boxer_boxername_end'].value == True

        Returns:
            end (bool): True means end condition satisfied
                        False otherwise
        """
        keys = ("", "boxer", self.name, "end")  # _boxer_boxername_end
        if keys in self.hold and self.hold[keys].value:
            return True

        return False


    def predo(self, predos):
        """Evaluate preconditions for entry of boxes in endos in top down order

        Parameters:
            predos (list[Box]): boxes to be predo (checked for entry)
                         given all their preacts are  satisfied

        Returns:
            met (bool): True means all preconditions are satisfied for predos
                        False otherwise
                    When no preconditions then returns True.

        """
        met = True
        for box in predos:
            met = box.predo()
            if not met:
                break
        return met


    def rendo(self, rendos):
        """Action re-mark (remarks) and re-endo (renacts) acts of boxes in
        .rendos in top down order. Boxes retained in hierarchical state.
        Re-enter box

        Parameters:
            rendos (list[Box]): boxes to be rendo (re-entered)
        """
        for box in rendos:
            box.rendo()


    def endo(self, endos):
        """Action e-mark (emacts) and endo (enacts) acts of boxes in .endos
        in top down order. Enter box.

        Parameters:
            endos (list[Box]): boxes to be endo (entered)

        """
        for box in endos:
            box.endo()

    def redo(self):
        """Action redo nabe for current .box.pile in top down order"""
        for box in self.box.pile:
            box.redo()


    def exdo(self, exdos):
        """Action exacts of boxes in exdos in bottom up order. Exit box.

        Parameters:
            exdos (list[Box]): boxes to be exdo in bottom up order

        """
        for box in exdos:
            box.exdo()


    def rexdo(self, rexdos):
        """Action rexacts of boxes in rexdos (re-exdos) in bottom up order.
        Boxes retained in hierarchical state. Re-exit box.

        Parameters:
            rexdos (list[Box]): boxes to be re-exdo in bottom up order
        """
        for box in rexdos:
            box.rexdo()


    def resolve(self):
        """Resolve both over box names and goact dest box names into boxes for
        all boxes in .boxes

        """
        for name, box in self.boxes.items():
            if isinstance(box.over, str):
                try:
                    over = self.boxes[over]  # resolve
                except KeyError as ex:
                    raise HierError(f"Unresolvable over box name={over} for"
                                           f"box {name=}.") from ex
                box.over = over  # resolve over as a box
                box.over.unders.append(box)  # add box to its over.unders list

            for goact in box.goacts:
                if isinstance(goact.dest, str):
                    if goact.dest == 'next':  # next
                        if not box._next:
                            HierError(f"Unresolvable dest 'next' for goact in "
                                      f"box{name=}")
                        dest = box._next
                    else:
                        try:
                            dest = self.boxes[goact.dest]  # resolve
                        except KeyError as ex:
                            raise HierError(f"Unresolvable dest box={goact.dest}"
                                f" for goact in box{name=}") from ex

                    goact.dest = dest  # resolve dest as box

        if isinstance(self.first, str):  # resolve first box
            try:
                self.first = self.boxes[self.first]  # resolve
            except KeyError as ex:
                raise HierError(f"Unresolvable over box name={self.first} for"
                                       f"boxer {self.name}.") from ex


    def make(self, fun=None, *, durable=None, temp=False):
        """Make box work for this boxer from function fun
        Parameters:
            fun (Callable|None):  employs be, go, do, on, at, be, verb functions with
                injected mods of boxwork state vars
                When None use self.fun
                Signature:
                    fun(H: (Hold),
                        bx: (Callable),
                        go: (Callable),
                        do: (Callable),
                        on: (Callable),
                        at: (Callable),
                        be: (Callable))
            durable (None|bool):  True means ensure .hold has opened subery
                                  False do nothing
                                  None means use .durable
            temp (bool): when reopen .hold.subery open with temp.
                         True means use temp directory False use default

        Injects mods as WorkDom dataclass instance whose attributes are used to
        construct boxwork.

        """
        fun = fun if fun is not None else self.fun
        durable = durable if durable is not None else self.durable
        if durable:
            if self.hold.subery and not self.hold.subery.opened:
                self.hold.subery.reopen(temp=temp)
            else:
                subery = Subery(name=self.name, reopen=True, temp=temp)
                self.hold['_hold_subery'] = subery


        works = WorkDom()  # standard defaults
        works.acts = ActBase.Registry
        bx = modify(mods=works)(self.bx)
        go = modify(mods=works)(self.go)
        do = modify(mods=works)(self.do)
        on = modify(mods=works)(self.on)
        at = modify(mods=works)(self.at)
        be = modify(mods=works)(self.be)

        # calling fun will build boxer.boxes
        fun(H=self.hold, bx=bx, go=go, do=do, on=on, at=at, be=be)
        self.resolve()
        return works  # for debugging analysis



    def bx(self, name: None|str=None, over: None|str|Box="", first: bool=False,
                *, mods: WorkDom|None=None)->Box:
        """Make a box and add to box work

        Parameters:
            name (None | str): when None then create name from bepre and beidx
                               items in works.
                               if non-empty string then use provided
                               otherwise raise exception

            over (None | str | Box): over box for new box.
                                    when str then name of new over box
                                    when box then actual over box
                                    when None then no over box (top level)
                                    when empty then same level use _over
            first (bool): True means set this box as boxer.first
                          False (default) to not change boxer.first

            mods (None | WorkDom):  state variables used to construct box work
                None is just to allow definition as keyword arg. Assumes in
                actual usage that mods is always provided as WorkDom instance of
                form:

                    box (Box|None): current box in box work. None if not yet a box
                    over (Box|None): current over Box in box work. None if top level
                    bxpre (str): default name prefix used to generate unique box
                        name relative to boxer.boxes
                    bxidx (int): default box index used to generate unique box
                        name relative to boxer.boxes



        """
        m = mods  # alias more compact

        if not name:  # empty or None
            if name is None:
                name = m.bxpre + str(m.bxidx)
                m.bxidx += 1
                while name in self.boxes:
                    name = m.bxpre + str(m.bxidx)
                    m.bxidx += 1

            else:
                raise HierError(f"Missing name.")

        if name in self.boxes:  # duplicate name
            raise HierError(f"Non-unique box {name=}.")

        if over is not None:  # not at top level
            if isinstance(over, str):
                if not over:  # empty string
                    over = m.over  # same level
                else:  # resolvable string
                    try:
                        over = self.boxes[over]  # resolve
                    except KeyError as ex:
                        raise HierError(f"Under box={name} defined before"
                                               f"its {over=}.") from ex

            elif over.name not in self.boxes:  # stray over box
                self.boxes[over.name] = over  # add to boxes

        box = Box(name=name, over=over, hold=self.hold, tymth=self.tymth)
        self.boxes[box.name] = box  # update box work
        if box.over is not None:  # not at top level
            box.over.unders.append(box)  # add to over.unders list

        m.over = over  # update current level
        if m.box:  # update last boxes lexical ._next to this box
            m.box._next = box
        m.box = box  # update current box

        m.nabe = Nabes.native  # reset nabe to native at creation of new box

        if first:
            self.first = box

        return box


    def go(self, dest: None|str=None, expr: None|str|Need=None,
                 *, mods: WorkDom|None=None, **kwa)->Goact:
        """Make a Goact and add it to the tracts nabe of the current box.

        Returns:
            goact (Goact):  newly created goact

        Parameters:
            dest (None|str|Box): destination box its name for transition.
                When None use next box if any
                When str then resolve name to box if possible else save for
                    later resolution
                When Box instance that already resolved

            expr (None|str|Need): need for transition to dest.
                When None then conditional always True. Always godo.
                When str then evalable python boolean expression to be
                    resolved into a Need instance for eval at run time
                When Need instance then use as is

            mods (None | WorkDom):  state variables used to construct box work
                None is just to allow definition as keyword arg. Assumes in
                actual usage that mods is always provided as WorkDom instance of
                form:

                    box (Box|None): current box in box work. None if not yet a box
                    over (Box|None): current over Box in box work. None if top level
                    bxpre (str): default name prefix used to generate unique box
                        name relative to boxer.boxes
                    bxidx (int): default box index used to generate unique box
                        name relative to boxer.boxes



        """
        m = mods  # alias more compact

        if not dest or dest in ('next', 'Next', 'NEXT'):  # empty or None or next
            if m.box._next:
                dest = m.box._next
            else:
                dest = 'next'  # to be resolved later
        elif isinstance(dest, str):
            if not Renam.match(dest):
                raise HierError(f"Invalid {dest=}.")
            if dest in self.boxes:
                dest = self.boxes[dest]

        if isinstance(expr, Need):
            need = expr
        else:   # assumes evalable expr str
            need = Need(expr=expr, hold=self.hold)

        goact = Goact(dest=dest, need=need)
        m.box.goacts.append(goact)
        return goact



    def do(self, deed: None|str|Type[ActBase]|Callable=None, nabe=None, *,
                 name: str|None=None, mods: WorkDom|None=None, **iops)->str:
        """Make an act and add to box work

        Parameters:
            deed (None|str|Type[ActBase]|Callable):
                    When None use Act with default lambda and iops as parameters
                    When str name of class in ActBase registry.
                    When issubclass of ActBase then create instance
                    When Callable use Act with iops as parameters.
            name (None|str): name of act instance created by do.
                    When None use default indexed name created by Act.
            nabe (None|str): action nabe (context) for act instance created by do.
                    None means use nabe from mods.
            mods (None | WorkDom):  state variables used to construct box work
                None is just to allow definition as keyword arg. Assumes in
                actual usage that mods is always provided as WorkDom instance.

            iops (dict): input-output-parms for Act

        """
        m = mods  # alias more compact

        parms = dict(name=name, hold=self.hold)
        if nabe is None:
            nabe = m.nabe
        if nabe != Nabes.native:
            parms.update(nabe=nabe)   # override native nabe for klas

        iops = dict(_boxer=self.name, _box=m.box.name, **iops)
        parms.update(iops=iops)

        deed = deed if deed is not None else "Act"

        try:  # is deed registered act class name or alias
            klas = m.acts[deed]  # get registered klas
        except KeyError:  # not registered act class name
            if isinstance(deed, type) and issubclass(deed, ActBase):
                act = deed(**parms)  # ActBase class so just instantiate
            else:  # handles deed is None or callable(deed) or executable str
                act = Act(deed=deed, **parms)  # executable statement(s) or callable
        else:  # deed is registered act class name or alias
            act = klas(**parms)  # create act from klas with **parms

        nabe = act.nabe  # act init may ignore nabe parameter passed to init

        try:
            getattr(m.box, nabeDispatch[nabe]).append(act)
        except (KeyError, AttributeError) as ex:
            raise HierError("Unrecognized nabe='{nabe}'") from ex

        return act


    def on(self, cond: None|str=None, key: None|str=None, expr: None|str=None,
                 *, mods: WorkDom|None=None, **iops)->Need:
        """Make a Need with support for special Need conditions and return it.
        Use inside go verb as need argument for special need condition
        Use inside do verb as deed argument for preact

        Returns:
            need (Need):  newly created special need

        Parameters:
            cond (None|str): special need condition to be satisfied. This is
                resolved in evalable boolean expression.
                When None then ignore
                When str then special need condition to be resolved into evalable
                boolean expression

            key (None|str): key to mine item ref for special need cond when
                applicable, i.e. cond is with respect to mine at key that is
                not predetermined solely by cond. Otherwise None.
                When None use default for cond
                When str then resolve key to mine at key


            expr (None|str): evalable boolean expression as additional constraint(s)
                ANDed with result of cond.
                When None or empty then ignore
                When str then evalable python boolean expression to be ANDed with
                    the result of cond resolution.

            mods (None | WorkDom):  state variables used to construct box work
                None is just to allow definition as keyword arg. Assumes that
                mods is always provided as WorkDom instance of form:
                    box (Box| None): current box in box work. None if not yet a box
                    over (Box | None): current over Box in box work. None if top level
                    bxpre (str):  default box name prefix used to generate unique box name
                                relative to boxer.boxes
                    bxidx (int): default box name index used to generate unique box name
                                relative to boxer.boxes
                    acts (dict):  registry of ActBase subclasses by name (including aliases)
                    nabe (str): action nabe (context) for act

            iops (dict): input-output-parms for Act

        """
        m = mods  # alias more compact
        iops = dict(_boxer=self.name, _box=m.box.name, **iops)


        _expr = None

        if not cond:
            if not expr:
                cond = "updated"  # default
            else:  # no cond but with expr
                _expr = expr  # use expr instead of resolved cond
                expr = None  # can't have both _expr and expr same below

        if not _expr:  # cond above so need to resolve cond into _expr
            if cond == "update":
                if not key:
                    raise HierError(f"Missing bag key for special need '{cond=}'")
                iops.update(_key=key)
                mks =  ("", "boxer", self.name, "box", m.box.name, "update", key)
                mk = self.hold.tokey(mks)  # mark bag key
                name = UpdateMark.__name__ + key
                found = False
                for mark in m.box.enmarks:  # check if already has mark for key
                    if mark.name == name:
                        found = True
                        break
                if not found:  # no preexisting UpdateMark for this key
                    mark = UpdateMark(name=name, iops=iops, hold=self.hold)
                    m.box.enmarks.append(mark)  # update is always enmark

                _expr = (f"(H.{mk}.value is None and H.{key}._tyme is not None) or "
                         f"(H.{mk}.value is not None and H.{key}._tyme > H.{mk}.value)")

            elif cond == "reupdate":
                if not key:
                    raise HierError(f"Missing bag key for special need '{cond=}'")
                iops.update(_key=key)
                mks =  ("", "boxer", self.name, "box", m.box.name, "reupdate", key)
                mk = self.hold.tokey(mks)  # mark bag key
                name = ReupdateMark.__name__ + key
                found = False
                for mark in m.box.remarks:  # check if already has mark for key
                    if mark.name == name:
                        found = True
                        break
                if not found:  # no preexisting UpdateMark for this key
                    mark = ReupdateMark(name=name, iops=iops, hold=self.hold)
                    m.box.remarks.append(mark)  # update is always enmark

                _expr = (f"(H.{mk}.value is None and H.{key}._tyme is not None) or "
                         f"(H.{mk}.value is not None and H.{key}._tyme > H.{mk}.value)")

            elif cond == "change":
                if not key:
                    raise HierError(f"Missing bag key for special need '{cond=}'")
                iops.update(_key=key)
                mks =  ("", "boxer", self.name, "box", m.box.name, "change", key)
                mk = self.hold.tokey(mks)  # mark bag key
                name = ChangeMark.__name__ + key
                found = False
                for mark in m.box.enmarks:  # check if already has mark for key
                    if mark.name == name:
                        found = True
                        break
                if not found:  # no preexisting ChangeMark for this key
                    mark = ChangeMark(name=name, iops=iops, hold=self.hold)
                    m.box.enmarks.append(mark)  # update is always enmark

                _expr = (f"H.{mk}.value != H.{key}._astuple()")

            elif cond == "rechange":
                if not key:
                    raise HierError(f"Missing bag key for special need '{cond=}'")
                iops.update(_key=key)
                mks =  ("", "boxer", self.name, "box", m.box.name, "rechange", key)
                mk = self.hold.tokey(mks)  # mark bag key
                name = RechangeMark.__name__ + key
                found = False
                for mark in m.box.remarks:  # check if already has mark for key
                    if mark.name == name:
                        found = True
                        break
                if not found:  # no preexisting ChangeMark for this key
                    mark = RechangeMark(name=name, iops=iops, hold=self.hold)
                    m.box.remarks.append(mark)  # update is always enmark

                _expr = (f"H.{mk}.value != H.{key}._astuple()")

            elif match := Rexlps.match(cond):  # lapse special need
                mks =  ("", "boxer", self.name, "box", m.box.name, "lapse")
                mk = self.hold.tokey(mks)  # mark bag key
                name = LapseMark.__name__
                found = False
                for mark in m.box.enmarks:  # check if already has mark for key
                    if mark.name == name:
                        found = True
                        break
                if not found:  # no preexisting ElapseMark for this box
                    mark = LapseMark(name=name, iops=iops, hold=self.hold)
                    m.box.enmarks.append(mark)  # lapse is always enmark

                _, cmp = match.group("lps", "cmp")

                _expr = (f"((H.{mk}.value is not None) and (H.{mk}._now is not None)"
                         f" and (H.{mk}._now - H.{mk}.value)" + cmp + ")")

            elif match := Rexrlp.match(cond):  # relapse special need
                mks =  ("", "boxer", self.name, "box", m.box.name, "relapse")
                mk = self.hold.tokey(mks)  # mark bag key
                name = RelapseMark.__name__
                found = False
                for mark in m.box.remarks:  # check if already has mark for key
                    if mark.name == name:
                        found = True
                        break
                if not found:  # no preexisting ElapseMark for this box
                    mark = RelapseMark(name=name, iops=iops, hold=self.hold)
                    m.box.remarks.append(mark)  # relapse is always remark

                _, cmp = match.group("rlp", "cmp")

                _expr = (f"((H.{mk}.value is not None) and (H.{mk}._now is not None)"
                         f" and (H.{mk}._now - H.{mk}.value)" + cmp + ")")

            elif match := Rexcnt.match(cond):  # count special need
                mks =  ("", "boxer", self.name, "box", m.box.name, "count")
                mk = self.hold.tokey(mks)  # count mark bag key
                if mk not in self.hold:
                    self.hold[mk] = Bag()  # create bag default value = None

                _, cmp = match.group("cnt", "cmp")
                _expr = (f"H.{mk}.value" + cmp)


            else:
                raise HierError(f"Invalid special need {cond=}")

        # now _expr is valid
        if expr:  # both resolved cond as _expr and expr so AND together
            _expr = "(" + _expr + ") and (" + expr + ")"

        need = Need(expr=_expr, hold=self.hold)
        return need


    def at(self, nabe: str=Nabes.native, *, mods: WorkDom|None=None)->str:
        """Make set mods.nabe to nabe

        Parameters:
            nabe (str): action nabe (context) for mods. Defualt is native

            mods (None | WorkDom):  state variables used to construct box work
                None is just to allow definition as keyword arg. Assumes in
                actual usage that mods is always provided as WorkDom instance.

        """
        m = mods  # alias more compact
        if nabe not in Nabes._fields:
            raise HierError("Invalid {nabe=}")

        m.nabe = nabe
        return nabe


    def be(self, lhs: str|tuple[str], rhs: None|str|Callable=None, nabe=None, *,
                 name: str|None=None, mods: WorkDom|None=None, **iops)->str:
        """Make Beact instance that assigns mine bag at lhs to value from rhs.
        lhs is of form "key.field"
        rhs may be either a Callable or an evalable expression or None.
        Resulting act performs on of:
        H.key.field = None   when rhs is None
        H.key.field = eval(rhs)  when rhs is evalable str
        H.key.field = rhs(**parms)  when rhs is callable

        Usage:
            be(lhs, rhs)

        Parameters:
            lhs (str|tuple[str]): key.field in mine to be assigned.
                Resolves lhs to (key, field)
            rhs (None|str|Callable):
                When None assign directly
                When str compile to evable expression
                When Callable then call directly with iops
            nabe (None|str): action nabe (context) for act instance created by do.
                    None means use nabe from mods.
            name (None|str): name of act instance created by do.
                    When None use default indexed name created by Act.
            mods (None | WorkDom):  state variables used to construct box work
                None is just to allow definition as keyword arg. Assumes in
                actual usage that mods is always provided as WorkDom instance.
            iops (dict): input-output-parms for Beact

        """
        m = mods  # alias more compact

        parms = dict(name=name, hold=self.hold)
        if nabe is None:
            nabe = m.nabe
        if nabe != Nabes.native:
            parms.update(nabe=nabe)   # override native nabe for klas

        iops = dict(_boxer=self.name, _box=m.box.name, **iops)
        parms.update(iops=iops)

        # Create instance of Beact
        act = Beact(rhs=rhs, lhs=lhs, **parms)
        nabe = act.nabe  # act init may override passed in nabe

        try:
            getattr(m.box, nabeDispatch[nabe]).append(act)
        except (KeyError, AttributeError) as ex:
            raise HierError("Unrecognized nabe='{nabe}'") from ex

        return act


    @staticmethod
    def exen(near, far):
        """Computes the relative differences (uncommon  and common parts) between
        the box pile lists nears passed in and fars from box far.pile

        Parameters:
            near (Box): near box giving nears =near.pile in top down order
            far (Box): far box giving fars = far.pile in top down order.

        Assumes piles nears and fars are in top down order

        Returns:
            quadruple (tuple[list]): quadruple of lists of form:
                (exdos, endos, rendos, rexdos) where:
                exdos is list of uncommon boxes in nears but not in fars to be exited.
                    Reversed to bottom up order.
                endos is list of uncommon boxes in fars but not in nears to be entered
                rexdos is list of common boxes in both nears and fars to be re-exited
                    Reversed to bottom up order. These are boxes retained in pile.
                rendos is list of common boxes in both nears and fars to be re-entered
                    These are boxes retained in pile.
                The sets of boxes in rexdos and rendos are the same set but rexdos
                is reversed to bottum up order. These are boxes retained in the
                pile before and after the transition. This is where common exdo/endo
                actions for the non-common boxes can be actioned non-redundantly.


        Supports forced reentry transitions when far is in nears. This means fars
            == nears. In this case:
            The common part of nears/fars from far down is force exited
            The common part of nears/fars from far down is force entered

        When far in nears then forced endo at far so far is nears[i]
        catches that case for forced endo at some far in nears. Since
        far is in fars, then when far == nears[i] then fars == nears.

        Since a given box's pile is always traced up via its .over if any and down via
        its primary under i.e. .unders[0] if any, when far is in nears the anything
        below far is same in both fars and nears.

        Otherwise when far not in nears then i where fars[i] is not nears[i]
        indicates first box where fars down and nears down is uncommon i.e. the pile
        tree branches at i. This is the normal non-forced endo case for transition.

        Two different topologies are accounted for with this code.
        Recall that python slice of list is zero based where:
           fars[i] not in fars[:i] and fars[i] in fars[i:]
           nears[i] not in nears[:i] and nears[i] in nears[i:]
           this means fars[:0] == nears[:0] == [] empty list

        1.0 near and far in same tree either on same branch or different branches
            1.1 on same branch forced endo where nears == fars so far in nears.
               Walk down from shared root to find where far is nears[i]. Boxes above
               far given by fars[:i] == nears[:i] are re-exdo re-endo set of boxes.
               Boxes at far and below are forced exdo endo.
            1.2 on different branch to walk down from root until find fork where
               fars[i] is not nears[i]. So fars[:i] == nears[:i] above fork at i,
               and are re-exdo and re-endo set of boxes. Boxes at i and below in
               nears are exdo and boxes at i and below in fars are endo
        2.0 near and far not in same tree. In this case top of nears at nears[0] is
            not top of fars ar fars[0] i.e. different tree roots, far[0] != near[0]
            and fars[:0] == nears[:0] = [] means empty re-exdos and re-endos and
            all nears are exdo and all fars are endo.

        """
        nears = near.pile  # top down order
        fars = far.pile  # top down order
        l = min(len(nears), len(fars))  # l >= 1 since far in fars & near in nears
        for i in range(l):  # start at the top of both nears and fars
            if (far is nears[i]) or (fars[i] is not nears[i]): #first effective uncommon member
                # (exdos, endos, rexdos, rendos)
                return (list(reversed(nears[i:])), fars[i:],
                        list(reversed(nears[:i])), fars[:i])




class Boxery(Mixin):
    """Boxery Class builds multiple boxworks of Boxer and Box instances.
    Holds reference to in-memory mine shared by all boxers and their boxes in
    multi-boxer boxworks
    Holds reference to current Boxer and Box being built

    ****Placeholder for now. Future to be able to make multiple boxers from
    single fun or in multiple iterations making.****

    Attributes:
        hold (Hold): data shared by boxwork
        boxer (Boxer | None): current boxer
        box (Box | None): cureent box

    Properties:
        name (str): unique identifier of instance

    Hidden:
        _name (str): unique identifier of instance

    """
    def __init__(self, *, name='maker', hold=None, **kwa):
        """Initialize instance.

        Parameters:
            name (str): unique identifier of instance
            hold (None|Hold): data shared by boxwork
        """
        super(Boxery, self).__init__(**kwa)
        self.name = name
        self.hold = hold if hold is not None else Hold()
        self.boxer = None
        self.box = None

    @property
    def name(self):
        """Property getter for ._name

        Returns:
            name (str): unique identifier of instance
        """
        return self._name


    @name.setter
    def name(self, name):
        """Property setter for ._name

        Parameters:
            name (str): unique identifier of instance
        """
        if not Renam.match(name):
            raise HierError(f"Invalid {name=}.")

        self._name = name

    def make(self, fun, hold=None, boxes=None):
        """Make box work from function fun
        Parameters:
            fun (function):  employs be, do, on, go maker functions with
                              globals
            hold (None|Hold):  data shared by boxwork
            boxes (None|dict): shared boxes map

        bags, boxes, and boxers can be referenced by fun in its nonlocal
        enclosing scope. collections references so do not need to be global
        """
        hold = hold if hold is not None else Hold()  # create new if not provided
        boxes = boxes if boxes is not None else {}  # create new if not provided
        boxers = []  # list of made boxers

        # create a default boxer
        boxer = Boxer(name='boxer', hold=hold, boxes=boxes)
        boxers.append(boxer)

        fun()


class BoxerDoer(Doer):
    """BoxerDoer .recur method is a generator method that runs its boxer.run()
    generator method with 'yield from' delegation. A Doer subclass whose recur
    is a generator method is in turn run by its .do method using 'yield from'
    delegation which is in turn run by a Doist or DoDoer using next() and generator
    .send().


    Inherited Attributes:
        done (bool | None): completion state: True means completed
            Otherwise incomplete. Incompletion maybe due to close or abort.
        opts (dict): injected options into its .do generator by scheduler
        temp (bool | None): use temporary file resources if any

    Inherited Properties:
        tyme (float): relative cycle time of associated Tymist .tyme obtained
            via injected .tymth function wrapper closure.
        tymth (closure): function wrapper closure returned by Tymist.tymen() method.
            When .tymth is called it returns associated Tymist.tyme.
            .tymth provides injected dependency on Tymist tyme base.
        tock (float): desired time in seconds between runs or until next run,
                 non negative, zero means run asap

    Inherited Methods:
        .wind  injects ._tymth dependency from associated Tymist to get its .tyme
        .__call__ makes instance callable
            Appears as generator function that returns generator
        .do is generator method that returns generator
        .enter is enter context action method
        .recur is recur context action method or generator method
        .exit is exit context method
        .close is close context method
        .abort is abort context method

    Attributes:
        boxer (Boxer): boxwork instance this doer runs

    Overidden Methods:
        .recur

    Hidden:
       ._tymth is injected function wrapper closure returned by .tymen() of
            associated Tymist instance that returns Tymist .tyme. when called.
       ._tock is hidden attribute for .tock property

    """

    def __init__(self, boxer, **kwa):
        """
        Initialize instance.

        Parameters:
            boxer (Boxer): instance of boxer to run

        """
        super(BoxerDoer, self).__init__(**kwa)
        self.boxer = boxer



    def wind(self, tymth):
        """
        Inject new tymist.tymth as new ._tymth. Changes tymist.tyme base.
        Updates winds .tymer .tymth
        """
        super(BoxerDoer, self).wind(tymth)
        self.boxer.rewind(tymth)


    def enter(self, *, temp=None):
        """Do 'enter' context actions. Not a generator method.
        Set up resources. Comparable to context manager enter.

        Parameters:
            temp (bool | None): True means use temporary file resources if any
                                None means ignore parameter value. Use self.temp

        Inject temp or self.temp into file resources here if any
        """
        mods = self.boxer.make(temp=temp)
        if not self.tymth:
            raise HierError(f"Unable to wind boxer with doer's tymth")
        self.boxer.wind(self.tymth)  # ensures are bags in boxer.mine are wound


    def recur(self, tock=None):
        """Do 'recur' context actions as a generator method.

        Parameters:
            tock (float|None): this doer when creating this generator in recur
                section of its .do method supplies its .tock as this method's
                tock parameter.
                Note, the doist tyme is delegated through the 'yield from'
                to the eventual target yield at the  bottom of delegation chain.
                when tock fed back to doist is None or 0.0 it indicates to
                run again ASAP (on next iteration of doist.do)


        Returns:
           completion (bool): completion state of recurrence actions.
                              True means completed successfully
                              False completed unsuccessfully

        Note that "tyme" is not a parameter when recur is a generator method
        since doist tyme is injected into the explicit yield below by the
        Doist or DoDoer send(tyme) in their recur method for generator Doers.
        The recur method itself returns a generator so parameters
        to this method are to setup the generator not to be used at recur time.

        Assumes resource setup in .enter() and resource takedown in .exit()
        """
        tock = tock if tock is not None else self.tock
        done = yield from self.boxer.run(tock=tock)
        return done


    def exit(self):
        """"""
        if self.boxer.hold.subery:
            self.boxer.hold.subery.close(clear=self.boxer.hold.subery.temp)
