#!/usr/bin/env python3

__doc__ = f"""
Datalog (py)
============

An interactive datalog interpreter with commands and persistence

Commands
~~~~~~~~
  .help      (this message)
  .all       display all tuples
  .quit      to exit the REPL

To exit, use control-c or control-d

The interpreter
~~~~~~~~~~~~~~~

The interpreter reads one line at a time from stdin.
Lines are either
 - definitions (ending in .),
 - queries (ending in ?)
 - retractions (ending in !)

A definition may contain arbitrarily many datalog tuples and rules.

   edge(a, b). edge(b, c).  % A pair of definitions
   ⇒ edge(a, b). % The REPL's response that it has been committed
   ⇒ edge(b, c).

A query may contain definitions, but they exist only for the duration of the query.

   edge(X, Y)? % A query which will enumerate all 2-edges
   ⇒ edge(a, b).
   ⇒ edge(b, c).

   edge(c, d). edge(X, Y)? % A query with a local tuple
   ⇒ edge(a, b).
   ⇒ edge(b, c).
   ⇒ edge(c, d).

A retraction may contain only one tuple or clause, which will be expunged.

   edge(a, b)!   % This tuple is in our dataset
   ⇒ edge(a, b)  % So deletion succeeds

   edge(a, b)!   % This tuple is no longer in our dataset
   ⇒ Ø           % So deletion fails

"""

import argparse
import sys

from datalog.evaluator import select
from datalog.reader import read_command, read_dataset
from datalog.types import (CachedDataset, Constant, Dataset, IndexedDataset,
                           LVar, Rule)


def pr_str(e):
  if isinstance(e, Rule):
    return pr_str(rule.pattern) + " :- " + ", ".join([pr_str(c) for c in rule.clauses]) + "."
  elif isinstance(e, Constant):
    return repr(e.value)
  elif isinstance(e, LVar):
    return e.name
  elif isinstance(e, tuple):
    return e[0].value + "(" + ", ".join(pr_str(_e) for _e in e[1:]) + ")"


def print_db(db):
  """Render a database for debugging."""

  for e in db.tuples():
    print(f"⇒ {pr_str(e)}")

  for r in db.rules():
    print(f"⇒ {pr_str(r)}")


def main(args):
  """REPL entry point."""

  if args.db_cls == "simple":
    db_cls = Dataset
  elif args.db_cls == "cached":
    db_cls = CachedDataset
  elif args.db_cls == "indexed":
    db_cls = IndexedDataset

  print(f"Using dataset type {db_cls}")

  db = db_cls([], [])

  if args.dbs:
    for db_file in args.dbs:
      try:
        with open(db_file, "r") as f:
          db = db.merge(read_dataset(f.read()))
          print(f"Loaded {db_file} ...")
      except Exception as e:
        print("Internal error - {e}")
        print(f"Unable to load db {db_file}, skipping")

  while True:
    sys.stdout.write(">>> ")
    sys.stdout.flush()

    # FIXME (arrdem 2019-06-01):
    #   Can't tell the difference between EOF and empty line
    #   Probably need to use a "real" shell building library
    line = sys.stdin.readline().strip()
    if not line:
      print()
      continue

    if line == ".all":
      op = ".all"
    elif line == ".quit":
      break

    elif line in {".help", "help", "?", "??", "???"}:
      print(__doc__)
      continue

    else:
      try:
        op, val = read_command(line)
      except Exception as e:
        print(f"Got an unknown command or syntax error, can't tell which")
        continue

    # Definition merges on the DB
    if op == ".all":
      print_db(db)

    if op == ".":
      db = db.merge(val)
      print_db(val)

    # Queries execute - note that rules as queries have to be temporarily merged.
    elif op == "?":
      # Use a flag to track whether we got any results
      flag = False
      for result, bindings in select(db, val):
        print(f"⇒ {pr_str(result)}")
        flag |= True

      # So we can report empty sets explicitly.
      if not flag:
        print("⇒ Ø")

    # Retractions try to delete, but may fail.
    elif op == "!":
      if val in db.tuples() or val in [r.pattern for r in db.rules()]:
        db = db_cls([u for u in db.tuples() if u != val],
                    [r for r in db.rules() if r.pattern != val])
        print(f"⇒ {pr_str(val)}")
      else:
        print("⇒ Ø")


parser = argparse.ArgumentParser()

# Select which dataset type to use
parser.add_argument("--db-type",
                    choices=["simple", "cached", "indexed"],
                    help="Choose which DB to use (default indexed)",
                    dest="db_cls",
                    default="indexed")

parser.add_argument("--load-db", dest="dbs", action="append",
                    help="Datalog files to load first.")

if __name__ == "__main__":
  args = parser.parse_args(sys.argv[1:])
  main(args)
