"""
# Search by relational patterns between nodes
"""

from __future__ import annotations

import collections
import types
import re
from itertools import chain
import array
from typing import TYPE_CHECKING, Any, Callable, Iterator

if TYPE_CHECKING:
    from cfabric.core.api import Api
    from cfabric.search.searchexe import SearchExe

from cfabric.core.config import OTYPE, OSLOTS, OMAP
from cfabric.utils.helpers import makeIndex, safe_rank_key
from cfabric.utils.logging import DEEP
from cfabric.search.syntax import reTp

# LOW-LEVEL NODE RELATIONS SEMANTICS ###


def _l_em(n: int) -> tuple[()]:
    return ()


def _l_eq(n: int) -> tuple[int]:
    return (n,)


def _l_uneq(n: int, m: int) -> bool:
    return n != m


def _l_lt(n: int, m: int) -> bool:
    return n < m


def _l_gt(n: int, m: int) -> bool:
    return n > m


def _l_ranklt(Crank: array.array[int]) -> Callable[[int, int], bool]:
    rank_key = safe_rank_key(Crank)

    def func(n: int, m: int) -> bool:
        return rank_key(n) < rank_key(m)

    return func


def _l_rankgt(Crank: array.array[int]) -> Callable[[int, int], bool]:
    rank_key = safe_rank_key(Crank)

    def func(n: int, m: int) -> bool:
        return rank_key(n) > rank_key(m)

    return func


def basicRelations(searchExe: SearchExe, api: Api) -> None:
    C = api.C
    F = api.F
    Fs = api.Fs
    E = api.E
    Crank = C.rank.data
    ClevDown = C.levDown.data
    ClevUp = C.levUp.data
    (CfirstSlots, ClastSlots) = C.boundary.data
    Eoslots = E.oslots.data
    slotType = F.otype.slotType
    maxSlot = F.otype.maxSlot
    maxSlotP = maxSlot + 1
    sets = searchExe.sets
    setInfo = searchExe.setInfo
    searchExe.featureValueIndex = {}
    Sindex = searchExe.featureValueIndex

    def isSlotType(nType):
        if nType == ".":
            return None
        if sets is not None and nType in sets:
            if nType in setInfo:
                return setInfo[nType]
            nodes = sets[nType]
            allSlots = all(n < maxSlotP for n in nodes)
            if allSlots:
                setInfo[nType] = True
                return True
            allNonSlots = all(n > maxSlot for n in nodes)
            if allNonSlots:
                setInfo[nType] = False
                return False
            setInfo[nType] = None
            return None
        return nType == slotType

    # EQUAL

    def spinEqual(fTp, tTp):
        def doyarns(yF, yT):
            x = set(yF) & set(yT)
            return (x, x)

        return doyarns

    def equalR(fTp, tTp):
        return _l_eq

    # UNEQUAL

    def unequalR(fTp, tTp):
        return _l_uneq

    # CANONICAL BEFORE

    def canonicalBeforeR(fTp, tTp):
        isSlotF = isSlotType(fTp)
        isSlotT = isSlotType(tTp)
        if isSlotF and isSlotT:
            return _l_lt

        return _l_ranklt(Crank)

    # CANONICAL AFTER

    def canonicalAfterR(fTp, tTp):
        isSlotF = isSlotType(fTp)
        isSlotT = isSlotType(tTp)
        if isSlotF and isSlotT:
            return _l_gt

        return _l_rankgt(Crank)

    # SAME SLOTS

    def spinSameSlots(fTp, tTp):
        isSlotF = isSlotType(fTp)
        isSlotT = isSlotType(tTp)
        if isSlotF and isSlotT:

            def doyarns(yF, yT):
                x = set(yF) & set(yT)
                return (x, x)

            return doyarns

        elif isSlotF or isSlotT:

            def doyarns(yS, y2):
                sindex = {}
                for m in y2:
                    ss = Eoslots[m - maxSlotP] if m > maxSlot else (m,)
                    if len(ss) == 1:
                        sindex.setdefault(ss[0], set()).add(m)
                nyS = yS & set(sindex.keys())
                ny2 = set(chain.from_iterable(sindex[s] for s in nyS))
                return (nyS, ny2)

            if isSlotF:
                return doyarns

            else:

                def xx(yF, yT):
                    (nyT, nyF) = doyarns(yT, yF)
                    return (nyF, nyT)

                return xx

        else:

            def doyarns(yF, yT):
                sindexF = {}
                for n in yF:
                    s = frozenset(Eoslots[n - maxSlotP] if n > maxSlot else (n,))
                    sindexF.setdefault(s, set()).add(n)
                sindexT = {}
                for m in yT:
                    s = frozenset(Eoslots[m - maxSlotP] if m > maxSlot else (m,))
                    sindexT.setdefault(s, set()).add(m)
                nyS = set(sindexF.keys()) & set(sindexT.keys())
                nyF = set(chain.from_iterable(sindexF[s] for s in nyS))
                nyT = set(chain.from_iterable(sindexT[s] for s in nyS))
                return (nyF, nyT)

            return doyarns

    def sameSlotsR(fTp, tTp):
        isSlotF = isSlotType(fTp)
        isSlotT = isSlotType(tTp)
        if isSlotF and isSlotT:
            return _l_eq
        else:

            def xx(n):
                nmin = n - 1
                if n < maxSlotP:
                    nA = array.array("I", (n,))
                    # nA = (n,)
                    yield n
                    for m in ClevUp[nmin]:
                        if Eoslots[m - maxSlotP] == nA:
                            yield m
                    return
                nSlots = Eoslots[n - maxSlotP]
                if len(nSlots) == 1:
                    slot1 = nSlots[0]
                    nA = array.array("I", (slot1,))
                    # nA = tuple(slot1,)
                    yield n
                    yield slot1
                    for m in ClevUp[nmin]:
                        if Eoslots[m - maxSlotP] == nA:
                            yield m
                    return
                yield n
                for m in ClevUp[nmin]:
                    if n in ClevUp[m - 1]:
                        yield m

            return xx

    # OVERLAP

    def spinOverlap(fTp, tTp):
        isSlotF = isSlotType(fTp)
        isSlotT = isSlotType(tTp)

        if isSlotF and isSlotT:

            def doyarns(yF, yT):
                x = set(yF) & set(yT)
                return (x, x)

            return doyarns

        elif isSlotF or isSlotT:

            def doyarns(yS, y2):
                sindex = {}
                for m in y2:
                    for s in Eoslots[m - maxSlotP] if m > maxSlot else (m,):
                        sindex.setdefault(s, set()).add(m)
                nyS = yS & set(sindex.keys())
                ny2 = set(chain.from_iterable(sindex[s] for s in nyS))
                return (nyS, ny2)

            if isSlotF:
                return doyarns

            else:

                def xx(yF, yT):
                    (nyT, nyF) = doyarns(yT, yF)
                    return (nyF, nyT)

                return xx
        else:

            def doyarns(yF, yT):
                REDUCE_FACTOR = 0.4
                SIZE_LIMIT = 10000
                sindexF = {}
                for n in yF:
                    for s in Eoslots[n - maxSlotP] if n > maxSlot else (n,):
                        sindexF.setdefault(s, set()).add(n)
                sindexT = {}
                for m in yT:
                    for s in Eoslots[m - maxSlotP] if m > maxSlot else (m,):
                        sindexT.setdefault(s, set()).add(m)
                nyS = set(sindexF.keys()) & set(sindexT.keys())

                lsF = len(sindexF)
                lsT = len(sindexT)
                lsI = len(nyS)
                if lsF == lsT:  # spinning is completely useless
                    return (yF, yT)
                if lsI > REDUCE_FACTOR * lsT and lsT > SIZE_LIMIT:
                    # spinning is not worth it
                    return (yF, yT)

                nyF = set(chain.from_iterable(sindexF[s] for s in nyS))
                nyT = set(chain.from_iterable(sindexT[s] for s in nyS))
                return (nyF, nyT)

            return doyarns

    def overlapR(fTp, tTp):
        isSlotF = isSlotType(fTp)
        isSlotT = isSlotType(tTp)
        if isSlotF and isSlotT:
            return _l_eq
        elif isSlotT:

            def func(n):
                return Eoslots[n - maxSlotP] if n > maxSlot else (n,)

            return func
        elif isSlotF:

            def func(n):
                return chain(ClevUp[n - 1], (n,))

            return func
        else:

            def xx(n):
                nSlots = Eoslots[n - maxSlotP] if n > maxSlot else (n,)
                return chain(
                    nSlots, set(chain.from_iterable(ClevUp[s - 1] for s in nSlots))
                )

            return xx

    # DIFFERENT SLOTS

    def diffSlotsR(fTp, tTp):
        isSlotF = isSlotType(fTp)
        isSlotT = isSlotType(tTp)
        if isSlotF and isSlotT:
            return _l_uneq
        elif isSlotT:

            def func(n, m):
                return (Eoslots[m - maxSlotP] if m > maxSlot else (m,)) != (n,)

            return func
        elif isSlotF:

            def func(n, m):
                return (Eoslots[n - maxSlotP] if n > maxSlot else (n,)) != (m,)

            return func
        else:

            def func(n, m):
                return frozenset(
                    Eoslots[n - maxSlotP] if n > maxSlot else (n,)
                ) != frozenset(Eoslots[m - maxSlotP] if m > maxSlot else (m,))

            return func

    # DISJOINT SLOTS

    def disjointSlotsR(fTp, tTp):
        isSlotF = isSlotType(fTp)
        isSlotT = isSlotType(tTp)
        if isSlotF and isSlotT:
            return _l_uneq
        elif isSlotT:

            def func(n, m):
                return m not in frozenset(
                    Eoslots[n - maxSlotP] if n > maxSlot else (n,)
                )

            return func
        elif isSlotF:

            def func(n, m):
                return n not in frozenset(
                    Eoslots[m - maxSlotP] if m > maxSlot else (m,)
                )

            return func
        else:

            def func(n, m):
                return (
                    len(
                        frozenset(Eoslots[n - maxSlotP] if n > maxSlot else (n,))
                        & frozenset(Eoslots[m - maxSlotP] if m > maxSlot else (m,))
                    )
                    == 0
                )

            return func

    # EMBEDDED IN

    def inR(fTp, tTp):
        isSlotF = isSlotType(fTp)
        isSlotT = isSlotType(tTp)
        if isSlotF and isSlotT:
            return _l_em
        elif isSlotT:
            return _l_em
        elif isSlotF:

            def func(n):
                return ClevUp[n - 1]

            return func
        else:

            def func(n):
                return ClevUp[n - 1]

            return func

    # EMBEDS

    def hasR(fTp, tTp):
        isSlotF = isSlotType(fTp)
        isSlotT = isSlotType(tTp)
        if isSlotF and isSlotT:
            return _l_em
        elif isSlotF:
            return _l_em
        elif isSlotT:

            def func(n):
                return Eoslots[n - maxSlotP] if n > maxSlot else ()

            return func
        else:
            if isSlotT is None:

                def func(n):
                    return (
                        chain(ClevDown[n - maxSlotP], Eoslots[n - maxSlotP])
                        if n > maxSlot
                        else ()
                    )

                return func
            else:

                def func(n):
                    return ClevDown[n - maxSlotP] if n > maxSlot else ()

                return func

    # BEFORE WRT SLOTS

    def slotBeforeR(fTp, tTp):
        isSlotF = isSlotType(fTp)
        isSlotT = isSlotType(tTp)
        if isSlotF and isSlotT:
            return _l_lt
        elif isSlotF:

            def func(n, m):
                return n < (Eoslots[m - maxSlotP][0] if m > maxSlot else m)

            return func
        elif isSlotT:

            def func(n, m):
                return (Eoslots[n - maxSlotP][-1] if n > maxSlot else n) < m

            return func
        else:

            def func(n, m):
                return (Eoslots[n - maxSlotP][-1] if n > maxSlot else n) < (
                    Eoslots[m - maxSlotP][0] if m > maxSlot else m
                )

            return func

    # AFTER WRT SLOTS

    def slotAfterR(fTp, tTp):
        isSlotF = isSlotType(fTp)
        isSlotT = isSlotType(tTp)
        if isSlotF and isSlotT:
            return _l_gt
        elif isSlotF:

            def func(n, m):
                return n > (Eoslots[m - maxSlotP][-1] if m > maxSlot else m)

            return func
        elif isSlotT:

            def func(n, m):
                return (Eoslots[n - maxSlotP][0] if n > maxSlot else n) > m

            return func
        else:

            def func(n, m):
                return (Eoslots[n - maxSlotP][0] if n > maxSlot else n) > (
                    Eoslots[m - maxSlotP][-1] if m > maxSlot else m
                )

            return func

    # START AT SAME SLOT

    def sameFirstSlotR(fTp, tTp):
        isSlotF = isSlotType(fTp)
        isSlotT = isSlotType(tTp)
        if isSlotF and isSlotT:
            return _l_eq
        elif isSlotF:
            if isSlotT is None:

                def func(n):
                    return chain(CfirstSlots[n - 1], (n,))

                return func
            else:

                def func(n):
                    return CfirstSlots[n - 1]

                return func
        elif isSlotT:

            def func(n):
                return ((Eoslots[n - maxSlotP][0] if n > maxSlot else n),)

            return func
        else:

            def xx(n):
                fn = Eoslots[n - maxSlotP][0] if n > maxSlot else n
                fnmin = fn - 1
                if isSlotT is None:
                    return chain(CfirstSlots[fnmin], (fn,))
                else:
                    return CfirstSlots[fnmin]

            return xx

    # ENDS AT SAME SLOT

    def sameLastSlotR(fTp, tTp):
        isSlotF = isSlotType(fTp)
        isSlotT = isSlotType(tTp)
        if isSlotF and isSlotT:
            return _l_eq
        elif isSlotF:
            if isSlotT is None:

                def func(n):
                    return chain(ClastSlots[n - 1], (n,))

                return func
            else:

                def func(n):
                    return ClastSlots[n - 1]

                return func
        elif isSlotT:

            def func(n):
                return ((Eoslots[n - maxSlotP][-1] if n > maxSlot else n),)

            return func
        else:

            def xx(n):
                ln = Eoslots[n - maxSlotP][-1] if n > maxSlot else n
                lnmin = ln - 1
                if isSlotT is None:
                    return chain(ClastSlots[lnmin], (ln,))
                else:
                    return ClastSlots[lnmin]

            return xx

    # START AND END AT SAME SLOT

    def sameBoundaryR(fTp, tTp):
        isSlotF = isSlotType(fTp)
        isSlotT = isSlotType(tTp)
        if isSlotF and isSlotT:
            return _l_eq
        elif isSlotF:
            if isSlotT is None:

                def xx(n):
                    nmin = n - 1
                    fok = set(chain(CfirstSlots[nmin], (n,)))
                    lok = set(chain(ClastSlots[nmin], (n,)))
                    return fok & lok

            else:

                def xx(n):
                    nmin = n - 1
                    fok = set(CfirstSlots[nmin])
                    lok = set(ClastSlots[nmin])
                    return fok & lok

            return xx

        elif isSlotT:

            def xx(n):
                slots = Eoslots[n - maxSlotP] if n > maxSlot else (n,)
                fs = slots[0]
                ls = slots[-1]
                return (fs,) if fs == ls else ()

            return xx

        else:
            if isSlotT is None:

                def xx(n):
                    fn = Eoslots[n - maxSlotP][0] if n > maxSlot else n
                    ln = Eoslots[n - maxSlotP][-1] if n > maxSlot else n
                    fnmin = fn - 1
                    lnmin = ln - 1
                    fok = set(chain(CfirstSlots[fnmin], (fn,)))
                    lok = set(chain(ClastSlots[lnmin], (ln,)))
                    return fok & lok

            else:

                def xx(n):
                    fn = Eoslots[n - maxSlotP][0] if n > maxSlot else n
                    ln = Eoslots[n - maxSlotP][-1] if n > maxSlot else n
                    fnmin = fn - 1
                    lnmin = ln - 1
                    fok = set(CfirstSlots[fnmin])
                    lok = set(ClastSlots[lnmin])
                    return fok & lok

            return xx

    # FIRST SLOTS ARE k-CLOSE

    def nearFirstSlotR(k):
        def zz(fTp, tTp):
            isSlotF = isSlotType(fTp)
            isSlotT = isSlotType(tTp)
            if isSlotF and isSlotT:

                def func(n):
                    return range(max((1, n - k)), min((maxSlot, n + k)) + 1)

                return func
            elif isSlotF:
                if isSlotT is None:

                    def xx(n):
                        near = range(max((1, n - k)), min((maxSlot, n + k)) + 1)
                        return chain(
                            near,
                            chain.from_iterable(CfirstSlots[lf - 1] for lf in near),
                        )

                else:

                    def xx(n):
                        near = range(max((1, n - k)), min((maxSlot, n + k)) + 1)
                        return chain.from_iterable(CfirstSlots[lf - 1] for lf in near)

                return xx
            elif isSlotT:

                def xx(n):
                    fn = Eoslots[n - maxSlotP][0] if n > maxSlot else n
                    return range(max((1, fn - k)), min((maxSlot, fn + k)) + 1)

                return xx

            else:
                if isSlotT is None:

                    def xx(n):
                        fn = Eoslots[n - maxSlotP][0] if n > maxSlot else n
                        near = range(max((1, fn - k)), min((maxSlot, fn + k)) + 1)
                        return chain(
                            near,
                            chain.from_iterable(CfirstSlots[lf - 1] for lf in near),
                        )

                else:

                    def xx(n):
                        fn = Eoslots[n - maxSlotP][0] if n > maxSlot else n
                        near = range(max((1, fn - k)), min((maxSlot, fn + k)) + 1)
                        return chain.from_iterable(CfirstSlots[lf - 1] for lf in near)

                return xx

        return zz

    # LAST SLOTS ARE k-CLOSE

    def nearLastSlotR(k):
        def zz(fTp, tTp):
            isSlotF = isSlotType(fTp)
            isSlotT = isSlotType(tTp)
            if isSlotF and isSlotT:

                def func(n):
                    return range(max((1, n - k)), min((maxSlot, n + k)) + 1)

                return func
            elif isSlotF:
                if isSlotT is None:

                    def xx(n):
                        near = range(max((1, n - k)), min((maxSlot, n + k)) + 1)
                        return chain(
                            near, chain.from_iterable(ClastSlots[lf - 1] for lf in near)
                        )

                else:

                    def xx(n):
                        near = range(max((1, n - k)), min((maxSlot, n + k)) + 1)
                        return chain.from_iterable(ClastSlots[lf - 1] for lf in near)

                return xx
            elif isSlotT:

                def xx(n):
                    ln = Eoslots[n - maxSlotP][-1] if n > maxSlot else n
                    return range(max((1, ln - k)), min((maxSlot, ln + k)) + 1)

                return xx
            else:
                if isSlotT is None:

                    def xx(n):
                        ln = Eoslots[n - maxSlotP][-1] if n > maxSlot else n
                        near = range(max((1, ln - k)), min((maxSlot, ln + k)) + 1)
                        return chain(
                            near, chain.from_iterable(ClastSlots[lf - 1] for lf in near)
                        )

                else:

                    def xx(n):
                        ln = Eoslots[n - maxSlotP][-1] if n > maxSlot else n
                        near = range(max((1, ln - k)), min((maxSlot, ln + k)) + 1)
                        return chain.from_iterable(ClastSlots[lf - 1] for lf in near)

                return xx

        return zz

    # FIRST SLOTS ARE k-CLOSE and LAST SLOTS ARE k-CLOSE

    def nearBoundaryR(k):
        def zz(fTp, tTp):
            isSlotF = isSlotType(fTp)
            isSlotT = isSlotType(tTp)
            if isSlotF and isSlotT:

                def func(n):
                    return range(max((1, n - k)), min((maxSlot, n + k)) + 1)

                return func
            elif isSlotF:
                if isSlotT is None:

                    def xx(n):
                        near = set(range(max((1, n - k)), min((maxSlot, n + k)) + 1))
                        fok = set(
                            chain.from_iterable(CfirstSlots[lf - 1] for lf in near)
                        )
                        lok = set(
                            chain.from_iterable(ClastSlots[lf - 1] for lf in near)
                        )
                        return near | (fok & lok)

                else:

                    def xx(n):
                        near = range(max((1, n - k)), min((maxSlot, n + k)) + 1)
                        fok = set(
                            chain.from_iterable(CfirstSlots[lf - 1] for lf in near)
                        )
                        lok = set(
                            chain.from_iterable(ClastSlots[lf - 1] for lf in near)
                        )
                        return fok & lok

                return xx
            elif isSlotT:

                def xx(n):
                    slots = Eoslots[n - maxSlotP] if n > maxSlot else (n,)
                    fs = slots[0]
                    ls = slots[-1]
                    fok = set(range(max((1, fs - k)), min((maxSlot, fs + k)) + 1))
                    lok = set(range(max((1, ls - k)), min((maxSlot, ls + k)) + 1))
                    return fok & lok

                return xx
            else:
                if isSlotT is None:

                    def xx(n):
                        fn = Eoslots[n - maxSlotP][0] if n > maxSlot else n
                        ln = Eoslots[n - maxSlotP][-1] if n > maxSlot else n
                        nearf = range(max((1, fn - k)), min((maxSlot, fn + k)) + 1)
                        nearl = range(max((1, ln - k)), min((maxSlot, ln + k)) + 1)
                        fok = set(
                            chain(
                                nearf,
                                chain.from_iterable(
                                    CfirstSlots[lf - 1] for lf in nearf
                                ),
                            )
                        )
                        lok = set(
                            chain(
                                nearl,
                                chain.from_iterable(ClastSlots[lf - 1] for lf in nearl),
                            )
                        )
                        return fok & lok

                else:

                    def xx(n):
                        fn = Eoslots[n - maxSlotP][0] if n > maxSlot else n
                        ln = Eoslots[n - maxSlotP][-1] if n > maxSlot else n
                        nearf = range(max((1, fn - k)), min((maxSlot, fn + k)) + 1)
                        nearl = range(max((1, ln - k)), min((maxSlot, ln + k)) + 1)
                        fok = set(
                            chain.from_iterable(CfirstSlots[lf - 1] for lf in nearf)
                        )
                        lok = set(
                            chain.from_iterable(ClastSlots[lf - 1] for lf in nearl)
                        )
                        return fok & lok

                return xx

        return zz

    # FIRST ENDS WHERE SECOND STARTS

    def adjBeforeR(fTp, tTp):
        isSlotF = isSlotType(fTp)
        isSlotT = isSlotType(tTp)
        if isSlotF and isSlotT:

            def func(n):
                return (n + 1,) if n < maxSlot else ()

            return func
        else:
            if isSlotT:

                def xx(n):
                    if n == maxSlot:
                        return ()
                    myNext = n + 1 if n < maxSlot else Eoslots[n - maxSlotP][-1] + 1
                    if myNext > maxSlot:
                        return ()
                    return (myNext,)

            elif isSlotT is None:

                def xx(n):
                    if n == maxSlot:
                        return ()
                    myNext = n + 1 if n < maxSlot else Eoslots[n - maxSlotP][-1] + 1
                    if myNext > maxSlot:
                        return ()
                    return chain(CfirstSlots[myNext - 1], (myNext,))

            else:

                def xx(n):
                    if n == maxSlot:
                        return ()
                    myNext = n + 1 if n < maxSlot else Eoslots[n - maxSlotP][-1] + 1
                    if myNext > maxSlot:
                        return ()
                    return CfirstSlots[myNext - 1]

            return xx

    # FIRST STARTS WHERE SECOND ENDS

    def adjAfterR(fTp, tTp):
        isSlotF = isSlotType(fTp)
        isSlotT = isSlotType(tTp)
        if isSlotF and isSlotT:

            def func(n):
                return (n - 1,) if n > 1 else ()

            return func
        else:
            if isSlotT:

                def xx(n):
                    if n <= 1:
                        return ()
                    myPrev = n - 1 if n < maxSlot else Eoslots[n - maxSlotP][0] - 1
                    if myPrev <= 1:
                        return ()
                    return (myPrev,)

            elif isSlotT is None:

                def xx(n):
                    if n <= 1:
                        return ()
                    myPrev = n - 1 if n < maxSlot else Eoslots[n - maxSlotP][0] - 1
                    if myPrev <= 1:
                        return ()
                    return chain((myPrev,), ClastSlots[myPrev - 1])

            else:

                def xx(n):
                    if n <= 1:
                        return ()
                    myPrev = n - 1 if n < maxSlot else Eoslots[n - maxSlotP][0] - 1
                    if myPrev <= 1:
                        return ()
                    return ClastSlots[myPrev - 1]

            return xx

    # FIRST ENDS WHERE SECOND STARTS WITHIN k-SLOTS

    def nearBeforeR(k):
        def zz(fTp, tTp):
            isSlotF = isSlotType(fTp)
            isSlotT = isSlotType(tTp)
            if isSlotF and isSlotT:

                def func(n):
                    return range(max((1, n + 1 - k)), min((maxSlot, n + 1 + k)) + 1)

                return func
            else:
                if isSlotT:

                    def xx(n):
                        myNext = n + 1 if n < maxSlot else Eoslots[n - maxSlotP][-1] + 1
                        return range(
                            max((1, myNext - k)), min((maxSlot, myNext + k)) + 1
                        )

                elif isSlotT is None:

                    def xx(n):
                        myNext = n + 1 if n < maxSlot else Eoslots[n - maxSlotP][-1] + 1
                        near = range(
                            max((1, myNext - k)), min((maxSlot, myNext + k)) + 1
                        )
                        return chain(
                            near,
                            chain.from_iterable(CfirstSlots[ls - 1] for ls in near),
                        )

                else:

                    def xx(n):
                        myNext = n + 1 if n < maxSlot else Eoslots[n - maxSlotP][-1] + 1
                        near = range(
                            max((1, myNext - k)), min((maxSlot, myNext + k)) + 1
                        )
                        return chain.from_iterable(CfirstSlots[ls - 1] for ls in near)

                return xx

        return zz

    # FIRST STARTS WHERE SECOND ENDS WITHIN k-SLOTS

    def nearAfterR(k):
        def zz(fTp, tTp):
            isSlotF = isSlotType(fTp)
            isSlotT = isSlotType(tTp)
            if isSlotF and isSlotT:

                def func(n):
                    return range(max((1, n - 1 - k)), min((maxSlot, n - 1 + k)) + 1)

                return func
            else:
                if isSlotT:

                    def xx(n):
                        myPrev = n - 1 if n < maxSlot else Eoslots[n - maxSlotP][0] - 1
                        return tuple(
                            range(max((1, myPrev - k)), min((maxSlot, myPrev + k)) + 1)
                        )

                elif isSlotT is None:

                    def xx(n):
                        myPrev = n - 1 if n < maxSlot else Eoslots[n - maxSlotP][0] - 1
                        near = range(
                            max((1, myPrev - k)), min((maxSlot, myPrev + k)) + 1
                        )
                        return chain(
                            near, chain.from_iterable(ClastSlots[lf - 1] for lf in near)
                        )

                else:

                    def xx(n):
                        myPrev = n - 1 if n < maxSlot else Eoslots[n - maxSlotP][0] - 1
                        near = range(
                            max((1, myPrev - k)), min((maxSlot, myPrev + k)) + 1
                        )
                        return chain.from_iterable(ClastSlots[lf - 1] for lf in near)

                return xx

        return zz

    # SAME FEATURE VALUES

    def spinLeftFisRightG(f, g):
        def zz(fTp, tTp):
            if f not in Sindex:
                Sindex[f] = makeIndex(Fs(f).data)
            if f != g:
                if g not in Sindex:
                    Sindex[g] = makeIndex(Fs(g).data)
            indF = Sindex[f]
            indG = Sindex[g]
            commonValues = set(indF) if f == g else set(indF) & set(indG)

            def doyarns(yF, yT):
                fNodes = {
                    n
                    for n in chain.from_iterable(indF[v] for v in commonValues)
                    if n in yF
                }
                gNodes = {
                    n
                    for n in chain.from_iterable(indG[v] for v in commonValues)
                    if n in yT
                }
                return (fNodes, gNodes)

            return doyarns

        return zz

    def spinLeftGisRightF(f, g):
        return spinLeftFisRightG(g, f)

    def leftFisRightGR_ORIG(f, g):
        def zz(fTp, tTp):
            fData = Fs(f).v
            gData = Fs(g).v

            def uu(n, m):
                nVal = fData(n)
                return False if nVal is None else nVal == gData(m)

            return uu

        return zz

    def leftFisRightGR(f, g):
        def zz(fTp, tTp):
            fData = Fs(f).v if f == OTYPE else Fs(f).data
            gData = Fs(g).v if g == OTYPE else Fs(g).data

            if f == OTYPE and g == OTYPE:

                def uu(n, m):
                    nVal = fData(n)
                    return False if nVal is None else nVal == gData(m)

                return uu

            if f == OTYPE:

                def uu(n, m):
                    nVal = fData(n)
                    return False if nVal is None else nVal == gData.get(m, None)

                return uu

            if g == OTYPE:

                def uu(n, m):
                    nVal = fData.get(n, None)
                    return False if nVal is None else nVal == gData(m)

                return uu

            def uu(n, m):
                nVal = fData.get(n, None)
                return False if nVal is None else nVal == gData.get(m, None)

            return uu

        return zz

    def leftGisRightFR(g, f):
        return leftFisRightGR(f, g)

    # MATCH FEATURE VALUES

    def spinLeftFmatchRightG(f, rPat, rRe, g):
        def zz(fTp, tTp):
            fR = f"{f}~{rPat}"
            gR = f"{g}~{rPat}"

            if fR not in Sindex:
                if f not in Sindex:
                    Sindex[f] = makeIndex(Fs(f).data)
                indFR = {}
                for v, ns in Sindex[f].items():
                    vR = rRe.sub("", v)
                    for n in ns:
                        indFR.setdefault(vR, set()).add(n)
                Sindex[fR] = indFR
            if gR not in Sindex:
                if g not in Sindex:
                    Sindex[g] = makeIndex(Fs(g).data)
                indGR = {}
                for v, ns in Sindex[g].items():
                    vR = rRe.sub("", v)
                    for n in ns:
                        indGR.setdefault(vR, set()).add(n)
                Sindex[gR] = indGR

            indFR = Sindex[fR]
            indGR = Sindex[gR]

            commonValues = set(indFR) & set(indGR)

            def doyarns(yF, yT):
                fNodes = {
                    n
                    for n in chain.from_iterable(indFR[v] for v in commonValues)
                    if n in yF
                }
                gNodes = {
                    n
                    for n in chain.from_iterable(indGR[v] for v in commonValues)
                    if n in yT
                }
                return (fNodes, gNodes)

            return doyarns

        return zz

    def spinLeftGmatchRightF(f, rPat, rRe, g):
        return spinLeftFmatchRightG(g, rPat, rRe, f)

    def leftFmatchRightGR(f, rPat, rRe, g):
        def zz(fTp, tTp):
            fData = Fs(f).v if f == OTYPE else Fs(f).data
            gData = Fs(g).v if g == OTYPE else Fs(g).data

            if f == OTYPE and g == OTYPE:

                def uu(n, m):
                    nVal = fData(n)
                    if nVal is None:
                        return False
                    nVal = rRe.sub("", nVal)
                    mVal = gData(m)
                    if mVal is None:
                        return False
                    mVal = rRe.sub("", mVal)
                    return nVal == mVal

                return uu

            if f == OTYPE:

                def uu(n, m):
                    nVal = fData(n)
                    if nVal is None:
                        return False
                    nVal = rRe.sub("", nVal)
                    mVal = gData.get(m, None)
                    if mVal is None:
                        return False
                    mVal = rRe.sub("", mVal)
                    return nVal == mVal

                return uu

            if g == OTYPE:

                def uu(n, m):
                    nVal = fData.get(n, None)
                    if nVal is None:
                        return False
                    nVal = rRe.sub("", nVal)
                    mVal = gData(m)
                    if mVal is None:
                        return False
                    mVal = rRe.sub("", mVal)
                    return nVal == mVal

                return uu

            def uu(n, m):
                nVal = fData.get(n, None)
                if nVal is None:
                    return False
                nVal = rRe.sub("", nVal)
                mVal = gData.get(m, None)
                if mVal is None:
                    return False
                mVal = rRe.sub("", mVal)
                return nVal == mVal

            return uu

        return zz

    def leftGmatchRightFR(g, rPat, rRe, f):
        return leftFmatchRightGR(f, rPat, rRe, g)

    # UNEQUAL FEATURE VALUES

    def leftFunequalRightGR(f, g):
        def zz(fTp, tTp):
            fData = Fs(f).v if f == OTYPE else Fs(f).data
            gData = Fs(g).v if g == OTYPE else Fs(g).data

            if f == OTYPE and g == OTYPE:

                def uu(n, m):
                    nVal = fData(n)
                    mVal = gData(m)
                    return nVal is None and mVal is None or nVal != mVal

                return uu

            if f == OTYPE:

                def uu(n, m):
                    nVal = fData(n)
                    mVal = gData.get(m, None)
                    return nVal is None and mVal is None or nVal != mVal

                return uu

            if g == OTYPE:

                def uu(n, m):
                    nVal = fData.get(n, None)
                    mVal = gData(m)
                    return nVal is None and mVal is None or nVal != mVal

                return uu

            def uu(n, m):
                nVal = fData.get(n, None)
                mVal = gData.get(m, None)
                return nVal is None and mVal is None or nVal != mVal

            return uu

        return zz

    def leftGunequalRightFR(g, f):
        return leftFunequalRightGR(f, g)

    # GREATER FEATURE VALUES

    def leftFgreaterRightGR(f, g):
        def zz(fTp, tTp):
            fData = Fs(f).v if f == OTYPE else Fs(f).data
            gData = Fs(g).v if g == OTYPE else Fs(g).data

            if f == OTYPE and g == OTYPE:

                def uu(n, m):
                    nVal = fData(n)
                    mVal = gData(m)
                    return nVal is not None and mVal is not None and nVal > mVal

                return uu

            if f == OTYPE:

                def uu(n, m):
                    nVal = fData(n)
                    mVal = gData.get(m, None)
                    return nVal is not None and mVal is not None and nVal > mVal

                return uu

            if g == OTYPE:

                def uu(n, m):
                    nVal = fData.get(n, None)
                    mVal = gData(m)
                    return nVal is not None and mVal is not None and nVal > mVal

                return uu

            def uu(n, m):
                nVal = fData.get(n, None)
                mVal = gData.get(m, None)
                return nVal is not None and mVal is not None and nVal > mVal

            return uu

        return zz

    def leftGlesserRightFR(g, f):
        return leftFgreaterRightGR(f, g)

    # LESSER FEATURE VALUES

    def leftFlesserRightGR(f, g):
        def zz(fTp, tTp):
            fData = Fs(f).v if f == OTYPE else Fs(f).data
            gData = Fs(g).v if g == OTYPE else Fs(g).data

            if f == OTYPE and g == OTYPE:

                def uu(n, m):
                    nVal = fData(n)
                    mVal = gData(m)
                    return nVal is not None and mVal is not None and nVal < mVal

                return uu

            if f == OTYPE:

                def uu(n, m):
                    nVal = fData(n)
                    mVal = gData.get(m, None)
                    return nVal is not None and mVal is not None and nVal < mVal

                return uu

            if g == OTYPE:

                def uu(n, m):
                    nVal = fData.get(n, None)
                    mVal = gData(m)
                    return nVal is not None and mVal is not None and nVal < mVal

                return uu

            def uu(n, m):
                nVal = fData.get(n, None)
                mVal = gData.get(m, None)
                return nVal is not None and mVal is not None and nVal < mVal

            return uu

        return zz

    def leftGgreaterRightFR(g, f):
        return leftFlesserRightGR(f, g)

    # EDGES

    def makeEdgeMaps(efName):
        def edgeAccess(eFunc, doValues, value):
            if doValues:
                if value is None:

                    def func(n):
                        return (m[0] for m in eFunc(n) if m[1] is None)

                    return func
                elif value is True:

                    def func(n):
                        return (m[0] for m in eFunc(n))

                    return func
                elif isinstance(value, types.FunctionType):

                    def func(n):
                        return (m[0] for m in eFunc(n) if value(m[1]))

                    return func
                elif isinstance(value, reTp):

                    def func(n):
                        return (
                            m[0]
                            for m in eFunc(n)
                            if value is not None and value.search(m[1])
                        )

                    return func
                else:
                    (ident, value) = value
                    if ident is None and value is True:

                        def func(n):
                            return (m[0] for m in eFunc(n))

                        return func
                    elif ident:

                        def func(n):
                            return (m[0] for m in eFunc(n) if m[1] in value)

                        return func
                    else:

                        def func(n):
                            return (m[0] for m in eFunc(n) if m[1] not in value)

                        return func
            else:

                def func(n):
                    return eFunc(n)

                return func

        def edgeRV(value):
            def edgeR(fTp, tTp):
                Es = api.Es
                Edata = Es(efName)
                doValues = Edata.doValues
                return edgeAccess(Edata.f, doValues, value)

            return edgeR

        def edgeIRV(value):
            def edgeIR(fTp, tTp):
                Es = api.Es
                Edata = Es(efName)
                doValues = Edata.doValues
                return edgeAccess(Edata.t, doValues, value)

            return edgeIR

        def edgeSRV(value):
            def edgeSR(fTp, tTp):
                Es = api.Es
                Edata = Es(efName)
                doValues = Edata.doValues
                return edgeAccess(Edata.b, doValues, value)

            return edgeSR

        return (edgeRV, edgeIRV, edgeSRV)

    # COLLECT ALL RELATIONS IN A TUPLE

    relations = [
        (
            ("=", spinEqual, equalR, "left equal to right (as node)"),
            ("=", spinEqual, equalR, None),
        ),
        (
            ("#", 0.999, unequalR, "left unequal to right (as node)"),
            ("#", 0.999, unequalR, None),
        ),
        (
            (
                "<",
                0.500,
                canonicalBeforeR,
                "left before right (in canonical node ordering)",
            ),
            (
                ">",
                0.500,
                canonicalAfterR,
                "left after right (in canonical node ordering)",
            ),
        ),
        (
            ("==", spinSameSlots, sameSlotsR, "left occupies same slots as right"),
            ("==", spinSameSlots, sameSlotsR, None),
        ),
        (
            ("&&", spinOverlap, overlapR, "left has overlapping slots with right"),
            ("&&", spinOverlap, overlapR, None),
        ),
        (
            ("##", 0.990, diffSlotsR, "left and right do not have the same slot set"),
            ("##", 0.990, diffSlotsR, None),
        ),
        (
            ("||", 0.900, disjointSlotsR, "left and right do not have common slots"),
            ("||", 0.900, disjointSlotsR, None),
        ),
        (
            ("[[", True, hasR, "left embeds right"),
            ("]]", True, inR, "left embedded in right"),
        ),
        (
            ("<<", 0.490, slotBeforeR, "left completely before right"),
            (">>", 0.490, slotAfterR, "left completely after right"),
        ),
        (
            ("=:", True, sameFirstSlotR, "left and right start at the same slot"),
            ("=:", True, sameFirstSlotR, None),
        ),
        (
            (":=", True, sameLastSlotR, "left and right end at the same slot"),
            (":=", True, sameLastSlotR, None),
        ),
        (
            (
                "::",
                True,
                sameBoundaryR,
                "left and right start and end at the same slot",
            ),
            ("::", True, sameBoundaryR, None),
        ),
        (
            ("<:", True, adjBeforeR, "left immediately before right"),
            (":>", True, adjAfterR, "left immediately after right"),
        ),
        (
            (
                "=k:",
                True,
                nearFirstSlotR,
                "left and right start at k-nearly the same slot",
            ),
            ("=k:", True, nearFirstSlotR, None),
        ),
        (
            (
                ":k=",
                True,
                nearLastSlotR,
                "left and right end at k-nearly the same slot",
            ),
            (":k=", True, nearLastSlotR, None),
        ),
        (
            (
                ":k:",
                True,
                nearBoundaryR,
                "left and right start and end at k-near slots",
            ),
            (":k:", True, nearBoundaryR, None),
        ),
        (
            ("<k:", True, nearBeforeR, "left k-nearly before right"),
            (":k>", True, nearAfterR, "left k-nearly after right"),
        ),
        (
            (".f.", spinLeftFisRightG, leftFisRightGR, "left.f = right.f"),
            (".f.", spinLeftGisRightF, leftGisRightFR, None),
        ),
        (
            (".f=g.", spinLeftFisRightG, leftFisRightGR, "left.f = right.g"),
            (".g=f.", spinLeftGisRightF, leftGisRightFR, None),
        ),
        (
            (
                ".f~r~g.",
                spinLeftFmatchRightG,
                leftFmatchRightGR,
                "left.f matches right.g",
            ),
            (".g~r~f.", spinLeftGmatchRightF, leftGmatchRightFR, None),
        ),
        (
            (".f#g.", 0.8, leftFunequalRightGR, "left.f # right.g"),
            (".g#f.", 0.8, leftGunequalRightFR, None),
        ),
        (
            (".f>g.", 0.4, leftFgreaterRightGR, "left.f > right.g"),
            (".f<g.", 0.4, leftFlesserRightGR, None),
        ),
        (
            (".f<g.", 0.4, leftFlesserRightGR, "left.f < right.g"),
            (".f>g.", 0.4, leftFgreaterRightGR, None),
        ),
    ]

    # BUILD AND INITIALIZE ALL RELATIONAL FUNCTIONS

    api.TF.explore(silent=DEEP)
    edgeMap = {}
    nodeMap = {}

    for efName in sorted(api.TF.featureSets["edges"]):
        if efName == OSLOTS or efName.startswith(OMAP):
            continue
        r = len(relations)

        (edgeRV, edgeIRV, edgeSRV) = makeEdgeMaps(efName)
        doValues = api.TF.features[efName].edgeValues
        extra = " with value specification allowed" if doValues else ""
        relations.append(
            (
                (f"-{efName}>", True, edgeRV, f'edge feature "{efName}"{extra}'),
                (
                    f"<{efName}-",
                    True,
                    edgeIRV,
                    f'edge feature "{efName}"{extra} (opposite direction)',
                ),
            )
        )
        edgeMap[2 * r] = (efName, 1)
        edgeMap[2 * r + 1] = (efName, -1)

        r = len(relations)
        relations.append(
            (
                (
                    f"<{efName}>",
                    True,
                    edgeSRV,
                    f'edge feature "{efName}"{extra} (either direction)',
                ),
                (f"<{efName}>", True, edgeSRV, None),
            )
        )
        edgeMap[2 * r] = (efName, 0)
        edgeMap[2 * r + 1] = (efName, 0)
    lr = len(relations)

    relationsAll = []
    for r, rc in relations:
        relationsAll.extend([r, rc])

    searchExe.relations = [
        dict(
            acro=r[0],
            spin=r[1],
            func=r[2],
            desc=r[3],
        )
        for r in relationsAll
    ]
    searchExe.relationFromName = dict(
        ((r["acro"], i) for (i, r) in enumerate(searchExe.relations))
    )
    searchExe.relationLegend = "\n".join(
        f"{r['acro']:>23} {r['desc']}"
        for r in searchExe.relations
        if r["desc"] is not None
    )
    searchExe.relationLegend += f"""
The warp feature "{OSLOTS}" and {OMAP} features cannot be used in searches.
One of the above relations on nodes and / or slots will suit you better.
"""
    searchExe.converse = dict(
        tuple((2 * i, 2 * i + 1) for i in range(lr))
        + tuple((2 * i + 1, 2 * i) for i in range(lr))
    )
    searchExe.edgeMap = edgeMap
    searchExe.nodeMap = nodeMap


def add_K_Relations(searchExe: SearchExe, varRels: dict[str, set[int]]) -> None:
    relations = searchExe.relations
    tasks: dict[tuple[int, str, int, str], set[int]] = collections.defaultdict(set)
    for acro, ks in varRels.items():
        j = searchExe.relationFromName[acro]
        ji = searchExe.converse[j]
        if ji < j:
            (j, ji) = (ji, j)
        acro = relations[j]["acro"]
        acroi = relations[ji]["acro"]
        tasks[(j, acro, ji, acroi)] |= ks

    for (j, acro, ji, acroi), ks in tasks.items():
        for k in ks:
            newAcro = acro.replace("k", str(k))
            newAcroi = acroi.replace("k", str(k))
            r = relations[j]
            ri = relations[ji]
            lr = len(relations)
            relations.extend(
                [
                    dict(
                        name=acro,
                        acro=newAcro,
                        spin=r["spin"],
                        func=r["func"](k),
                        desc=r["desc"],
                    ),
                    dict(
                        name=acroi,
                        acro=newAcroi,
                        spin=ri["spin"],
                        func=ri["func"](k),
                        desc=ri["desc"],
                    ),
                ]
            )
            searchExe.relationFromName[newAcro] = lr
            searchExe.relationFromName[newAcroi] = lr + 1
            searchExe.converse[lr] = lr + 1
            searchExe.converse[lr + 1] = lr


def add_F_Relations(searchExe: SearchExe, varRels: dict[str, set[Any]]) -> None:
    relations = searchExe.relations
    tasks: dict[tuple[int, str, int, str], set[Any]] = collections.defaultdict(set)
    for acro, feats in varRels.items():
        j = searchExe.relationFromName[acro]
        ji = searchExe.converse[j]
        if ji < j:
            (j, ji) = (ji, j)
        acro = relations[j]["acro"]
        acroi = relations[ji]["acro"]
        tasks[(j, acro, ji, acroi)] |= feats

    for (j, acro, ji, acroi), feats in tasks.items():
        for featInfo in feats:
            if len(featInfo) == 2:
                ((f, fF), (t, gF)) = featInfo
                acroFmt = acro.replace("f", "{f}").replace("g", "{g}")
                acroiFmt = acroi.replace("f", "{f}").replace("g", "{g}")
                newAcro = acroFmt.format(f=fF, g=gF)
                newAcroi = acroiFmt.format(f=fF, g=gF)
                fArgs = (fF, gF)
            else:
                ((f, fF), rPat, (t, gF)) = featInfo
                acroFmt = (
                    acro.replace("f", "{f}").replace("r", "{r}").replace("g", "{g}")
                )
                acroiFmt = (
                    acroi.replace("f", "{f}").replace("r", "{r}").replace("g", "{g}")
                )
                newAcro = acroFmt.format(f=fF, r=rPat, g=gF)
                newAcroi = acroiFmt.format(f=fF, r=rPat, g=gF)
                rRe = re.compile(rPat)
                fArgs = (fF, rPat, rRe, gF)

            r = relations[j]
            ri = relations[ji]
            lr = len(relations)
            spin = r["spin"]
            if isinstance(spin, types.FunctionType):
                spin = spin(*fArgs)
            spini = ri["spin"]
            if isinstance(spini, types.FunctionType):
                spini = spini(*fArgs)
            func = r["func"](*fArgs)
            funci = ri["func"](*fArgs)
            relations.extend(
                [
                    dict(
                        name=acro,
                        acro=newAcro,
                        spin=spin,
                        func=func,
                        desc=r["desc"],
                    ),
                    dict(
                        name=acroi,
                        acro=newAcroi,
                        spin=spini,
                        func=funci,
                        desc=ri["desc"],
                    ),
                ]
            )
            searchExe.relationFromName[newAcro] = lr
            searchExe.relationFromName[newAcroi] = lr + 1
            searchExe.nodeMap.setdefault(f, set()).add(fF)
            searchExe.nodeMap.setdefault(t, set()).add(gF)
            searchExe.converse[lr] = lr + 1
            searchExe.converse[lr + 1] = lr


def add_V_Relations(searchExe: SearchExe, varRels: dict[str, set[Any]]) -> None:
    relations = searchExe.relations
    tasks: dict[tuple[str, int, str, int, str], set[Any]] = collections.defaultdict(set)
    for acro, vals in sorted(varRels.items()):
        for eName, val in vals:
            (b, mid, e) = (acro[0], acro[1:-1], acro[-1])
            norm = b == "-" and e == ">"
            conv = b == "<" and e == "-"
            eRel = f"-{eName}>"
            eReli = f"<{eName}-"
            eRels = f"<{eName}>"
            acroi = f"-{mid}>" if conv else f"<{mid}-" if norm else f"<{mid}>"
            if conv:
                (acro, acroi) = (acroi, acro)
            j = searchExe.relationFromName[eRel]
            ji = searchExe.relationFromName[eReli]
            js = searchExe.relationFromName[eRels]
            if norm or conv:
                tasks[(eName, j, acro, ji, acroi)].add(val)
            else:
                tasks[(eName, js, acro, js, acro)].add(val)

    for (eName, j, acro, ji, acroi), vals in sorted(tasks.items()):
        for val in vals:
            r = relations[j]
            ri = relations[ji]
            lr = len(relations)
            relations.extend(
                [
                    dict(
                        acro=acro,
                        spin=r["spin"],
                        func=r["func"](val),
                        desc=r["desc"],
                    ),
                    dict(
                        acro=acroi,
                        spin=ri["spin"],
                        func=ri["func"](val),
                        desc=ri["desc"],
                    ),
                ]
            )
            searchExe.relationFromName[acro] = lr
            searchExe.relationFromName[acroi] = lr + 1
            if j == ji:
                searchExe.edgeMap[lr] = (eName, 0)
                searchExe.edgeMap[lr + 1] = (eName, 0)
            else:
                searchExe.edgeMap[lr] = (eName, 1)
                searchExe.edgeMap[lr + 1] = (eName, -1)
            searchExe.converse[lr] = lr + 1
            searchExe.converse[lr + 1] = lr
