# -----------------------------------------------------------------------------
# Clorm ORM FactBase implementation. FactBase provides a set-like container
# specifically for storing facts (Predicate instances).
# ------------------------------------------------------------------------------

import sys
import io
import operator
import collections
import bisect
import inspect
import abc
import functools
import itertools

from .core import *
from .core import get_field_definition, PredicatePath, kwargs_check_keys, \
    validate_root_paths

from .query import *

from .query import Placeholder, OrderBy, desc, asc

from .query import process_where, process_join, process_orderby, \
    make_query_plan, QuerySpec, QueryExecutor

from .factcontainers import FactSet, FactIndex, FactMap, factset_equality

__all__ = [
    'FactBase',
    'Select',
    'Delete',
    ]

#------------------------------------------------------------------------------
# Global
#------------------------------------------------------------------------------

#------------------------------------------------------------------------------
# Support function for printing ASP facts: Note: _trim_docstring() is taken from
# PEP 257 (modified for Python 3): https://www.python.org/dev/peps/pep-0257/
# ------------------------------------------------------------------------------

def _format_asp_facts(iterator,output,width):
    tmp1=""
    for f in iterator:
        fstr="{}.".format(f)
        if tmp1 and len(tmp1) + len(fstr) > width:
            print(tmp1,file=output)
            tmp1 = fstr
        else:
            tmp1 = tmp1 + " " + fstr if tmp1 else fstr
    if tmp1: print(tmp1,file=output)

def _trim_docstring(docstring):
    if not docstring:
        return ''
    # Convert tabs to spaces (following the normal Python rules)
    # and split into a list of lines:
    lines = docstring.expandtabs().splitlines()
    # Determine minimum indentation (first line doesn't count):
    indent = sys.maxsize
    for line in lines[1:]:
        stripped = line.lstrip()
        if stripped:
            indent = min(indent, len(line) - len(stripped))
    # Remove indentation (first line is special):
    trimmed = [lines[0].strip()]
    if indent < sys.maxsize:
        for line in lines[1:]:
            trimmed.append(line[indent:].rstrip())
    # Strip off trailing and leading blank lines:
    while trimmed and not trimmed[-1]:
        trimmed.pop()
    while trimmed and not trimmed[0]:
        trimmed.pop(0)
    # Return a single string:
    return '\n'.join(trimmed)

def _endstrip(string):
    if not string: return
    nl=string[-1]=='\n'
    tmp=string.rstrip()
    return tmp + '\n' if nl else tmp

def _format_docstring(docstring,output):
    if not docstring: return
    tmp=_trim_docstring(docstring)
    tmpstr = "".join(_endstrip("%     " + l) for l in tmp.splitlines(True))
    if tmpstr:
        print("% Description:",file=output)
        print(tmpstr,file=output)

def _maxwidth(lines):
    return max([len(l) for l in lines])

def _format_commented(fm,out):
    pm = fm.predicate.meta
    docstring = _trim_docstring(fm.predicate.__doc__) \
        if fm.predicate.__doc__ else ""
    indent = "    "
    if pm.arity == 0:
        lines = [ "Unary predicate signature:", indent + pm.name ]
    else:
        params=[str(fa.name) for fa in pm]
        lines = [ "Predicate signature:",
                  indent + "{}({})".format(pm.name, ",".join(params)) ]
    if docstring:
        lines.append("Description:")
        for l in docstring.splitlines():lines.append(indent + l)
    bar = "-" * _maxwidth(lines)
    lines.insert(0,bar)
    lines.append(bar)
    for l in lines:
        tmp = l.rstrip()
        if tmp: print("% {}".format(tmp),file=out)
        else: print("%",file=out)
    return

#------------------------------------------------------------------------------
# A FactBase consisting of facts of different types
#------------------------------------------------------------------------------

class FactBase(object):
    """A fact base is a container for facts (i.e., Predicate sub-class instances)

    ``FactBase`` can be behave like a specialised ``set`` object, but can also
    behave like a minimalist database. It stores facts for ``Predicate`` types
    (where a predicate type loosely corresponds to a *table* in a database)
    and allows for certain fields to be indexed in order to perform more
    efficient queries.

    The initaliser can be given a collection of predicates. If it is passed
    another FactBase then it simply makes a copy (including the indexed fields).

    FactBase also has a special mode when it is passed a functor instead of a
    collection. In this case it performs a delayed initialisation. This means
    that the internal data structures are only populated when the FactBase is
    actually used. This mode is particularly useful when extracting facts from
    models. Often a program will only want to keep the data from the final model
    (for example, with optimisation we often want the best model before a
    timeout). Delayed initialisation is useful will save computation as only the
    last model will be properly initialised.

    Args:
      facts([Predicate]|FactBase|callable): a list of facts (predicate
         instances), a fact base, or a functor that generates a list of
         facts. If a functor is passed then the fact base performs a delayed
         initialisation. If a fact base is passed and no index is specified then
         an index will be created matching in input fact base.
      indexes(Field): a list of fields that are to be indexed.

    """

    #--------------------------------------------------------------------------
    # Internal member functions
    #--------------------------------------------------------------------------

    # A special purpose initialiser so that we can delayed initialisation
    def _init(self, facts=None, indexes=None):

        # flag that initialisation has taken place
        self._delayed_init = None

        # If it is delayed initialisation then get the facts
        if facts and callable(facts):
            facts = facts()
        elif facts and isinstance(facts, FactBase) and indexes is None:
            indexes = facts.indexes
        if indexes is None: indexes=[]

        # Create FactMaps for the predicate types with indexed fields
        grouped = {}

        self._indexes = tuple(indexes)
        for path in self._indexes:
            if path.meta.predicate not in grouped: grouped[path.meta.predicate] = []
            grouped[path.meta.predicate].append(path)
        self._factmaps = { pt : FactMap(pt, idxs) for pt, idxs in grouped.items() }

        if facts is None: return
        self._add(facts)

    # Make sure the FactBase has been initialised
    def _check_init(self):
        if self._delayed_init: self._delayed_init()  # Check for delayed init

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

    def _add(self, arg):
        if isinstance(arg, Predicate):
            return self._add_fact(type(arg),arg)
        elif isinstance(arg, str) or not isinstance(arg, collections.abc.Iterable):
            raise TypeError(f"'{arg}' is not a Predicate instance")

        facts = sorted(arg, key=lambda x : type(x).__name__)
        for ptype, g in itertools.groupby(facts, lambda x: type(x)):
            self._add_facts(ptype, g)

    def _add_fact(self, ptype, fact):
        fm = self._factmaps.setdefault(ptype, FactMap(ptype))
        fm.add_fact(fact)

    def _add_facts(self, ptype, facts):
        if not issubclass(ptype,Predicate):
            raise TypeError(f"{list(facts)} are not Predicate instances")
        fm = self._factmaps.setdefault(ptype, FactMap(ptype))
        fm.add_facts(facts)

    def _remove(self, fact, raise_on_missing):
        ptype = type(fact)
        if not isinstance(fact, Predicate) or ptype not in self._factmaps:
            raise KeyError(fact)

        return self._factmaps[ptype].remove(fact,raise_on_missing)

    #--------------------------------------------------------------------------
    # Initiliser
    #--------------------------------------------------------------------------
    def __init__(self, facts=None, indexes=None):
        self._delayed_init=None
        if callable(facts):
            def delayed_init():
                self._init(facts, indexes)
            self._delayed_init=delayed_init
        else:
            self._init(facts, indexes)


    #--------------------------------------------------------------------------
    # An internal API for the query mechanism. Not to be called by users.
    #--------------------------------------------------------------------------
    @property
    def factmaps(self):
        self._check_init()  # Check for delayed init
        return self._factmaps

    #--------------------------------------------------------------------------
    # Set member functions
    #--------------------------------------------------------------------------
    def add(self, arg):
        """Add a single fact or a collection of facts.

        Because a ``FactBase`` can only hold :class:`~Predicate` sub-class
        instances this member function has been overloaded to take either a
        single :class:`~Predicate` sub-class instance or a collection of
        :class:`~Predicate` sub-class instances.

        Args:
          arg: a single fact or a collection of facts.

        """
        self._check_init()  # Check for delayed init
        return self._add(arg)

    def remove(self, arg):
        """Remove a fact from the fact base (raises an exception if no fact). """
        self._check_init()  # Check for delayed init
        return self._remove(arg, raise_on_missing=True)

    def discard(self, arg):
        """Remove a fact from the fact base. """
        self._check_init()  # Check for delayed init
        return self._remove(arg, raise_on_missing=False)

    def pop(self):
        """Pop an element from the FactBase. """
        self._check_init()  # Check for delayed init
        for pt, fm in self._factmaps.items():
            if fm: return fm.pop()
        raise KeyError("pop from an empty FactBase")

    def clear(self):
        """Clear the fact base of all facts."""

        self._check_init()  # Check for delayed init
        for pt, fm in self._factmaps.items(): fm.clear()

    #--------------------------------------------------------------------------
    # Special FactBase member functions
    #--------------------------------------------------------------------------
    def select(self, root):
        """Define a select query using the old Query API.

        .. note::

           This interface will eventually be deprecated when the new
           :class:`Query API<Query>` is finalised. The entry point to this Query
           API is through the :meth:`FactBase.query` method.

        Args:
           predicate: The predicate to query.

        Returns:
           Returns a Select query object for specifying a query.

        """
        self._check_init()  # Check for delayed init

        roots = validate_root_paths([root])
        ptypes = set([ root.meta.predicate for root in roots])

        # Make sure there are factmaps for each referenced predicate type
        for ptype in ptypes: self._factmaps.setdefault(ptype, FactMap(ptype))

        return SelectImpl(self, QuerySpec(roots=roots))

    def delete(self, root):
        self._check_init()  # Check for delayed init

        roots = validate_root_paths([root])
        ptypes = set([ root.meta.predicate for root in roots])

        # Make sure there are factmaps for each referenced predicate type
        for ptype in ptypes: self._factmaps.setdefault(ptype, FactMap(ptype))

        return _Delete(self, QuerySpec(roots=roots))

    def query(self, *roots):
        """Define a query using the new Query API :class:`Query`.

        The parameters consist of a predicates (or aliases) to query (like an
        SQL FROM clause).

        Args:
           *predicates: predicate or predicate aliases

        Returns:
           Returns a Query object for specifying a query.

        """

        self._check_init()  # Check for delayed init

        # Make sure there are factmaps for each referenced predicate type
        ptypes = set([r.meta.predicate for r in validate_root_paths(roots)])
        for ptype in ptypes: self._factmaps.setdefault(ptype, FactMap(ptype))

        qspec = QuerySpec(roots=roots)
        return QueryImpl(self._factmaps, qspec)

    @property
    def predicates(self):
        """Return the list of predicate types that this fact base contains."""

        self._check_init()  # Check for delayed init
        return tuple([pt for pt, fm in self._factmaps.items() if fm])

    @property
    def indexes(self):
        self._check_init()  # Check for delayed init
        return self._indexes

    def facts(self):
        """Return all facts."""

        self._check_init()  # Check for delayed init
        tmp = [ fm.factset for fm in self._factmaps.values() if fm]
        return list(itertools.chain(*tmp))

    def asp_str(self,width=0,commented=False):
        """Return a string representation of the fact base that is suitable for adding
        to an ASP program

        """
        self._check_init()  # Check for delayed init
        out = io.StringIO()

        if not commented:
            _format_asp_facts(self,out,width)
        else:
            first=True
            for fm in self._factmaps.values():
                if first: first=False
                else: print("",file=out)
                _format_commented(fm,out)
                _format_asp_facts(fm.factset,out,width)

        data = out.getvalue()
        out.close()
        return data



    def __str__(self):
        self._check_init()  # Check for delayed init
        tmp = ", ".join([str(f) for f in self])
        return '{' + tmp + '}'

    def __repr__(self):
        return self.__str__()

    #--------------------------------------------------------------------------
    # Special functions to support set and container operations
    #--------------------------------------------------------------------------

    def __contains__(self, fact):
        """Implemement set 'in' operator."""

        self._check_init() # Check for delayed init

        if not isinstance(fact,Predicate): return False
        ptype = type(fact)
        if ptype not in self._factmaps: return False
        return fact in self._factmaps[ptype].factset

    def __bool__(self):
        """Implemement set bool operator."""

        self._check_init() # Check for delayed init

        for fm in self._factmaps.values():
            if fm: return True
        return False

    def __len__(self):
        self._check_init() # Check for delayed init
        return sum([len(fm.factset) for fm in self._factmaps.values()])

    def __iter__(self):
        self._check_init() # Check for delayed init

        for fm in self._factmaps.values():
            for f in fm.factset: yield f

    def __eq__(self, other):
        """Overloaded boolean operator."""

        # If other is not a FactBase then create one
        if not isinstance(other, self.__class__): other=FactBase(other)
        self._check_init(); other._check_init() # Check for delayed init

        self_fms = { p: fm for p,fm in self._factmaps.items() if fm }
        other_fms = { p: fm for p,fm in other._factmaps.items() if fm }
        if self_fms.keys() != other_fms.keys(): return False

        for p, fm1 in self_fms.items():
            fm2 = other_fms[p]
            if not factset_equality(fm1.factset,fm2.factset): return False

        return True

    def __ne__(self, other):
        """Overloaded boolean operator."""
        result = self.__eq__(other)
        if result is NotImplemented: return NotImplemented
        return not result

    def __lt__(self,other):
        """Implemement set < operator."""

        # If other is not a FactBase then create one
        if not isinstance(other, self.__class__): other=FactBase(other)
        self._check_init() ; other._check_init() # Check for delayed init

        self_fms = { p: fm for p,fm in self._factmaps.items() if fm }
        other_fms = { p: fm for p,fm in other._factmaps.items() if fm }
        if len(self_fms) > len(other_fms): return False

        known_ne=False
        for p, spfm in self_fms.items():
            if p not in other_fms: return False
            opfm = other_fms[p]
            if spfm.factset < opfm.factset: known_ne=True
            elif spfm.factset > opfm.factset: return False

        if known_ne: return True
        return False

    def __le__(self,other):
        """Implemement set <= operator."""

        if not isinstance(other, self.__class__): other=FactBase(other)
        self._check_init() ; other._check_init() # Check for delayed init

        self_fms = { p: fm for p,fm in self._factmaps.items() if fm }
        other_fms = { p: fm for p,fm in other._factmaps.items() if fm }
        if len(self_fms) > len(other_fms): return False

        for p, spfm in self_fms.items():
            if p not in other_fms: return False
            opfm = other_fms[p]
            if spfm.factset > opfm.factset: return False
        return True

    def __gt__(self,other):
        """Implemement set > operator."""
        if not isinstance(other, self.__class__): other=FactBase(other)
        return other.__lt__(self)

    def __ge__(self,other):
        """Implemement set >= operator."""
        if not isinstance(other, self.__class__): other=FactBase(other)
        return other.__le__(self)

    def __or__(self,other):
        """Implemement set | operator."""
        return self.union(other)

    def __and__(self,other):
        """Implemement set & operator."""
        return self.intersection(other)

    def __sub__(self,other):
        """Implemement set - operator."""
        return self.difference(other)

    def __xor__(self,other):
        """Implemement set ^ operator."""
        return self.symmetric_difference(other)

    def __ior__(self,other):
        """Implemement set |= operator."""
        self.update(other)
        return self

    def __iand__(self,other):
        """Implemement set &= operator."""
        self.intersection_update(other)
        return self

    def __isub__(self,other):
        """Implemement set -= operator."""
        self.difference_update(other)
        return self

    def __ixor__(self,other):
        """Implemement set ^= operator."""
        self.symmetric_difference_update(other)
        return self

    def __getstate__(self):
        self._check_init()
        return self.__dict__


    #--------------------------------------------------------------------------
    # Set functions
    #--------------------------------------------------------------------------
    def union(self,*others):
        """Implements the set union() function"""
        others=[o if isinstance(o, self.__class__) else FactBase(o) for o in others]
        self._check_init() # Check for delayed init
        for o in others: o._check_init()

        fb=FactBase()
        predicates = set(self._factmaps.keys())
        for o in others: predicates.update(o._factmaps.keys())

        for p in predicates:
            pothers=[ o._factmaps[p] for o in others if p in o._factmaps]
            if p in self._factmaps:
                fb._factmaps[p] = self._factmaps[p].union(*pothers)
            else:
                fb._factmaps[p] = FactMap(p).union(*pothers)
        return fb

    def intersection(self,*others):
        """Implements the set intersection() function"""
        others=[o if isinstance(o, self.__class__) else FactBase(o) for o in others]
        self._check_init() # Check for delayed init
        for o in others: o._check_init()

        fb=FactBase()
        predicates = set(self._factmaps.keys())
        for o in others: predicates.intersection_update(o._factmaps.keys())

        for p in predicates:
            pothers=[ o._factmaps[p] for o in others if p in o._factmaps]
            fb._factmaps[p] = self._factmaps[p].intersection(*pothers)
        return fb

    def difference(self,*others):
        """Implements the set difference() function"""
        others=[o if isinstance(o, self.__class__) else FactBase(o) for o in others]
        self._check_init() # Check for delayed init
        for o in others: o._check_init()

        fb=FactBase()
        predicates = set(self._factmaps.keys())

        for p in predicates:
            pothers=[ o._factmaps[p] for o in others if p in o._factmaps]
            fb._factmaps[p] = self._factmaps[p].difference(*pothers)
        return fb

    def symmetric_difference(self,other):
        """Implements the set symmetric_difference() function"""
        if not isinstance(other, self.__class__): other=FactBase(other)
        self._check_init() # Check for delayed init
        other._check_init()

        fb=FactBase()
        predicates = set(self._factmaps.keys())
        predicates.update(other._factmaps.keys())

        for p in predicates:
            in_self = p in self._factmaps ; in_other = p in other._factmaps
            if in_self and in_other:
                fb._factmaps[p] = self._factmaps[p].symmetric_difference(other._factmaps[p])
            elif in_self:
                fb._factmaps[p] = self._factmaps[p].copy()
            elif in_other:
                fb._factmaps[p] = other._factmaps[p].copy()

        return fb

    def update(self,*others):
        """Implements the set update() function"""
        others=[o if isinstance(o, self.__class__) else FactBase(o) for o in others]
        self._check_init() # Check for delayed init
        for o in others: o._check_init()

        for o in others:
            for p,fm in o._factmaps.items():
                if p in self._factmaps: self._factmaps[p].update(fm)
                else: self._factmaps[p] = fm.copy()

    def intersection_update(self,*others):
        """Implements the set intersection_update() function"""
        others=[o if isinstance(o, self.__class__) else FactBase(o) for o in others]
        self._check_init() # Check for delayed init
        for o in others: o._check_init()
        num = len(others)

        predicates = set(self._factmaps.keys())
        for o in others: predicates.intersection_update(o._factmaps.keys())
        pred_to_delete = set(self._factmaps.keys()) - predicates

        for p in pred_to_delete: self._factmaps[p].clear()
        for p in predicates:
            pothers=[ o._factmaps[p] for o in others if p in o._factmaps]
            self._factmaps[p].intersection_update(*pothers)

    def difference_update(self,*others):
        """Implements the set difference_update() function"""
        others=[o if isinstance(o, self.__class__) else FactBase(o) for o in others]
        self._check_init() # Check for delayed init
        for o in others: o._check_init()

        for p in self._factmaps.keys():
            pothers=[ o._factmaps[p] for o in others if p in o._factmaps ]
            self._factmaps[p].difference_update(*pothers)

    def symmetric_difference_update(self,other):
        """Implements the set symmetric_difference_update() function"""
        if not isinstance(other, self.__class__): other=FactBase(other)
        self._check_init() # Check for delayed init
        other._check_init()

        predicates = set(self._factmaps.keys())
        predicates.update(other._factmaps.keys())

        for p in predicates:
            if p in self._factmaps and p in other._factmaps:
                self._factmaps[p].symmetric_difference_update(other._factmaps[p])
            else:
                if p in other._factmaps: self._factmaps[p] = other._factmaps[p].copy()


    def copy(self):
        """Implements the set copy() function"""
        self._check_init() # Check for delayed init
        fb=FactBase()
        for p,fm in self._factmaps.items():
            fb._factmaps[p] = self._factmaps[p].copy()
        return fb

#------------------------------------------------------------------------------
# Select is an interface query over a FactBase.
# ------------------------------------------------------------------------------

class Select(abc.ABC):
    """An abstract class that defines the interface to original Query API.

    .. note::

       This interface will eventually be deprecated when the new :class:`Query
       API<Query>` is finalised.

    ``Select`` query objects cannot be constructed directly. Instead a
    ``Select`` object is returned by the :meth:`FactBase.select` function. Given
    a ``FactBase`` object ``fb``, a specification is of the form:

          ``query = fb.select(<predicate>).where(<expression>).order_by(<ordering>)``

    where ``<predicate>`` specifies the predicate type to search for,
    ``<expression>`` specifies the search criteria and ``<ordering>`` specifies
    a sort order when returning the results. The ``where()`` and ``order_by()``
    clauses are omitted when not required.

    """

    @abc.abstractmethod
    def where(self, *expressions):
        """Set the select statement's where clause.

        The where clause consists of a set of boolean and comparison
        expressions. This expression specifies a search criteria for matching
        facts within the corresponding ``FactBase``.

        Boolean expression are built from other boolean expression or a
        comparison expression. Comparison expressions are of the form:

               ``<PredicatePath> <compop>  <value>``

       where ``<compop>`` is a comparison operator such as ``==``, ``!=``, or
       ``<=`` and ``<value>`` is either a Python value or another predicate path
       object refering to a field of the same predicate or a placeholder.

        A placeholder is a special value that issubstituted when the query is
        actually executed. These placeholders are named ``ph1_``, ``ph2_``,
        ``ph3_``, and ``ph4_`` and correspond to the 1st to 4th arguments of the
        ``get``, ``get_unique`` or ``count`` function call.

        Args:
          expressions: one or more comparison expressions.

        Returns:
          Returns a reference to itself.

        """

    @abc.abstractmethod
    def order_by(self, *fieldorder):
        """Provide an ordering over the results.

        Args:
          fieldorder: an ordering over fields
        Returns:
          Returns a reference to itself.
        """
        pass

    @abc.abstractmethod
    def get(self, *args, **kwargs):
        """Return all matching entries."""
        pass

    def get_unique(self, *args, **kwargs):
        """Return the unique matching entry (or raise an exception)"""
        pass

    def count(self, *args, **kwargs):
        """Return the number of matches."""
        pass

#------------------------------------------------------------------------------
# Delete is an interface to perform a query delete from a FactBase.
# ------------------------------------------------------------------------------

class Delete(abc.ABC):
    """An abstract class that defines the interface to a original delete query API.

    .. note::

       This interface will eventually be deprecated when the new :class:`Query
       API<Query>` is finalised.

    ``Delete`` query objects cannot be constructed directly. Instead a
    ``Delete`` object is returned by the ``FactBase.delete()`` function. Given a
    ``FactBase`` object ``fb``, a specification is of the form:

          ``query = fb.delete(<predicate>).where(<expression>)``

    where ``<predicate>`` specifies the predicate type to search for,
    ``<expression>`` specifies the search criteria. The ``where()`` clause can
    be omitted in which case all predicates of that type will be deleted.

    """

    @abc.abstractmethod
    def where(self, *expressions):
        """Set the select statement's where clause.

        See the documentation for ``Select.where()`` for further details.
        """
        pass

    @abc.abstractmethod
    def execute(self, *args, **kwargs):
        """Function to execute the delete query"""
        pass

#------------------------------------------------------------------------------
# Query API version 1 with new query engine
#------------------------------------------------------------------------------

class SelectImpl(Select):

    def __init__(self, factbase, qspec):
        self._factbase = factbase
        self._qspec = qspec

    #--------------------------------------------------------------------------
    # Add an order_by expression
    #--------------------------------------------------------------------------
    def where(self, *expressions):
        if self._qspec.where:
            raise TypeError("Cannot specify 'where' multiple times")
        if not expressions:
            raise TypeError("Empty 'where' expression")

        try:
            if len(expressions) == 1:
                where = process_where(expressions[0],self._qspec.roots)
            else:
                where = process_where(and_(*expressions),self._qspec.roots)
            nqspec = self._qspec.newp(where=where)
        except ValueError as e:
            raise TypeError(str(e)) from None
        return SelectImpl(self._factbase,nqspec)

    #--------------------------------------------------------------------------
    # Add an order_by expression
    #--------------------------------------------------------------------------
    def order_by(self, *expressions):
        if self._qspec.order_by:
            raise TypeError("Cannot specify 'order_by' multiple times")
        if not expressions:
            raise TypeError("Empty 'order_by' expression")
        try:
            order_by=process_orderby(expressions,self._qspec.roots)
            nqspec = self._qspec.newp(order_by=order_by)
        except ValueError as e:
            raise TypeError(str(e)) from None
        return SelectImpl(self._factbase,nqspec)

    #--------------------------------------------------------------------------
    #
    #--------------------------------------------------------------------------
    def query_plan(self,*args,**kwargs):
        qspec = self._qspec.fill_defaults()

        (factsets,factindexes) = \
            QueryExecutor.get_factmap_data(self._factbase.factmaps, qspec)
        qplan = make_query_plan(factindexes.keys(), qspec)

        return qplan.ground(*args,**kwargs)

    #--------------------------------------------------------------------------
    # Functions currently mirroring the old interface
    # --------------------------------------------------------------------------

    def get(self, *args, **kwargs):
        qspec = self._qspec
        if args or kwargs:
            if self._qspec.where is None:
                raise ValueError(("No where clause to ground"))
            qspec = self._qspec.bindp(*args, **kwargs)

        qe = QueryExecutor(self._factbase.factmaps, qspec)
        return list(qe.all())

    def get_unique(self, *args, **kwargs):
        qspec = self._qspec
        if args or kwargs:
            if self._qspec.where is None:
                raise ValueError(("No where clause to ground"))
            qspec = self._qspec.bindp(*args, **kwargs)

        qe = QueryExecutor(self._factbase.factmaps, qspec)
        found = None
        for out in qe.all():
            if found: raise ValueError("Query returned more than a single element")
            found = out
        return found

    def count(self, *args, **kwargs):
        qspec = self._qspec
        if args or kwargs:
            if self._qspec.where is None:
                raise ValueError(("No where clause to ground"))
            qspec = self._qspec.bindp(*args, **kwargs)

        qe = QueryExecutor(self._factbase.factmaps, qspec)
        count = 0
        for _ in qe.all(): count += 1
        return count

#------------------------------------------------------------------------------
# The Delete class
#------------------------------------------------------------------------------

class _Delete(Delete):

    def __init__(self, factbase, qspec):
        self._factbase = factbase
        self._root = qspec.roots[0]
        self._select = SelectImpl(factbase,qspec)
        self._has_where = False

    def where(self, *expressions):
        self._has_where = True
        self._select = self._select.where(*expressions)
        return self

    def execute(self, *args, **kwargs):
        factmap = self._factbase.factmaps[self._root.meta.predicate]

        # If there is no where clause then delete everything
        if not self._has_where:
            num_deleted = len(factmap.facts())
            factmap.clear()
            return num_deleted

        # Gather all the facts to delete and remove them
        to_delete = [ f for f in self._select.get(*args, **kwargs) ]
        for fact in to_delete: factmap.remove(fact)
        return len(to_delete)


#------------------------------------------------------------------------------
# New Clorm Query API
#
# QueryImpl
# - factmaps             - dictionary mapping predicate types to FactMap objects
# - qspec                - a dictionary with query parameters
#------------------------------------------------------------------------------
class QueryImpl(Query):

    def __init__(self, factmaps, qspec):
        self._factmaps = factmaps
        self._qspec = qspec

    #--------------------------------------------------------------------------
    # Internal function to test whether a function has been called and add it
    #--------------------------------------------------------------------------
    def _check_join_called_first(self, name,endpoint=False):
        if self._qspec.join is not None or len(self._qspec.roots) == 1: return
        if endpoint:
            raise ValueError(("A query over multiple predicates is incomplete without "
                              "'join' clauses connecting these predicates"))
        raise ValueError("A 'join' clause must be specified before '{}'".format(name))

    #--------------------------------------------------------------------------
    # Add a join expression
    #--------------------------------------------------------------------------
    def join(self, *expressions):
        join=process_join(expressions, self._qspec.roots)
        return QueryImpl(self._factmaps, self._qspec.newp(join=join))

    #--------------------------------------------------------------------------
    # Add an order_by expression
    #--------------------------------------------------------------------------
    def where(self, *expressions):
        self._check_join_called_first("where")

        if not expressions:
            self._qspec.newp(where=None)    # Raise an error

        if len(expressions) == 1:
            where = process_where(expressions[0], self._qspec.roots)
        else:
            where = process_where(and_(*expressions), self._qspec.roots)

        nqspec = self._qspec.newp(where=where)
        return QueryImpl(self._factmaps, nqspec)

    #--------------------------------------------------------------------------
    # Add an orderered() flag
    #--------------------------------------------------------------------------
    def ordered(self, *expressions):
        self._check_join_called_first("ordered")
        if self._qspec.getp("order_by",None) is not None:
            raise ValueError(("Invalid query 'ordered' declaration conflicts "
                              "with previous 'order_by' declaration"))
        nqspec = self._qspec.newp(ordered=True)
        return QueryImpl(self._factmaps, nqspec)

    #--------------------------------------------------------------------------
    # Add an order_by expression
    #--------------------------------------------------------------------------
    def order_by(self, *expressions):
        self._check_join_called_first("order_by")
        if not expressions:
            nqspec = self._qspec.newp(order_by=None)   # raise exception
        elif self._qspec.getp("ordered",False):
            raise ValueError(("Invalid query 'order_by' declaration '{}' "
                              "conflicts with previous 'ordered' "
                              "declaration").format(expressions))
        else:
            nqspec = self._qspec.newp(
                order_by=process_orderby(expressions,self._qspec.roots))
        return QueryImpl(self._factmaps, nqspec)

    #--------------------------------------------------------------------------
    # Add a group_by expression
    #--------------------------------------------------------------------------
    def group_by(self, *expressions):
        if not expressions:
            nqspec = self._qspec.newp(group_by=None)   # raise exception
        else:
            nqspec = self._qspec.newp(
                group_by=process_orderby(expressions,self._qspec.roots))
        return QueryImpl(self._factmaps, nqspec)

    #--------------------------------------------------------------------------
    # Explicitly select the elements to output or delete
    #--------------------------------------------------------------------------
    def select(self,*outsig):
        self._check_join_called_first("select")
        if not outsig:
            raise ValueError("An empty 'select' signature is invalid")
        nqspec = self._qspec.newp(select=outsig)
        return QueryImpl(self._factmaps, nqspec)

    #--------------------------------------------------------------------------
    # The distinct flag
    #--------------------------------------------------------------------------
    def distinct(self):
        self._check_join_called_first("distinct")
        nqspec = self._qspec.newp(distinct=True)
        return QueryImpl(self._factmaps, nqspec)

    #--------------------------------------------------------------------------
    # Ground - bind
    #--------------------------------------------------------------------------
    def bind(self,*args,**kwargs):
        self._check_join_called_first("bind")
        nqspec = self._qspec.bindp(*args, **kwargs)
        return QueryImpl(self._factmaps, nqspec)

    #--------------------------------------------------------------------------
    # The tuple flag
    #--------------------------------------------------------------------------
    def tuple(self):
        self._check_join_called_first("tuple")
        nqspec = self._qspec.newp(tuple=True)
        return QueryImpl(self._factmaps, nqspec)

    #--------------------------------------------------------------------------
    # Overide the default heuristic
    #--------------------------------------------------------------------------
    def heuristic(self, join_order):
        nqspec = self._qspec.newp(heuristic=True, joh=join_order)
        return QueryImpl(self._factmaps, nqspec)

    #--------------------------------------------------------------------------
    # End points that do something useful
    #--------------------------------------------------------------------------

    #--------------------------------------------------------------------------
    # For the user to see what the query plan looks like
    #--------------------------------------------------------------------------
    def query_plan(self,*args,**kwargs):
        self._check_join_called_first("query_plan")
        qspec = self._qspec.fill_defaults()

        (factsets,factindexes) = \
            QueryExecutor.get_factmap_data(self._factmaps, qspec)
        return make_query_plan(factindexes.keys(), qspec)

    #--------------------------------------------------------------------------
    # Return the placeholders
    #--------------------------------------------------------------------------
    @property
    def qspec(self):
        return self._qspec

    #--------------------------------------------------------------------------
    # Select to display all the output of the query
    # --------------------------------------------------------------------------
    def all(self):
        self._check_join_called_first("all",endpoint=True)

        qe = QueryExecutor(self._factmaps, self._qspec)
        return qe.all()

    #--------------------------------------------------------------------------
    # Show the single element and throw an exception if there is more than one
    # --------------------------------------------------------------------------
    def singleton(self):
        self._check_join_called_first("singleton",endpoint=True)

        qe = QueryExecutor(self._factmaps, self._qspec)
        count = 0
        for out in qe.all():
            count += 1
            if count > 1:
                raise ValueError("Query returned more than a single element")
        if count == 0:
            raise ValueError("Query has no matching elements")
        return out

    #--------------------------------------------------------------------------
    # Return the count of elements - Note: the behaviour of what is counted
    # changes if group_by() has been specified.
    # --------------------------------------------------------------------------
    def count(self):
        self._check_join_called_first("count",endpoint=True)

        qe = QueryExecutor(self._factmaps, self._qspec)

        def group_by_generator():
            for k, g in qe.all():
                yield k, sum(1 for _ in g)

        # If group_by is set then we want to count records associated with each
        # key and not just total records.
        if self._qspec.group_by:
            return group_by_generator()
        else:
            return sum(1 for _ in qe.all())


    #--------------------------------------------------------------------------
    # Return the first element of the query
    # --------------------------------------------------------------------------
    def first(self):
        self._check_join_called_first("first",endpoint=True)

        qe = QueryExecutor(self._factmaps, self._qspec)

        for out in qe.all():
            return out
        raise ValueError("Query has no matching elements")

    #--------------------------------------------------------------------------
    # Delete a selection of fact
    #--------------------------------------------------------------------------
    def delete(self):
        self._check_join_called_first("delete",endpoint=True)

        qe = QueryExecutor(self._factmaps, self._qspec)
        return qe.delete()

#------------------------------------------------------------------------------
# main
#------------------------------------------------------------------------------
if __name__ == "__main__":
    raise RuntimeError('Cannot run modules')
