;; lib/prolog/query.sx — high-level Prolog query API for SX/Hyperscript callers.
;;
;; Requires tokenizer.sx, parser.sx, runtime.sx to be loaded first.
;;
;; Public API:
;;   (pl-load source-str)                → db
;;   (pl-query-all db query-str)         → list of solution dicts {var-name → term-string}
;;   (pl-query-one db query-str)         → first solution dict or nil
;;   (pl-query source-str query-str)     → list of solution dicts (convenience)

;; Collect variable name strings from a parse-time AST (pre-instantiation).
;; Returns list of unique strings, excluding anonymous "_".
(define
  pl-query-extract-vars
  (fn
    (ast)
    (let
      ((seen {}))
      (let
        ((collect!
          (fn
            (t)
            (cond
              ((not (list? t)) nil)
              ((empty? t) nil)
              ((= (first t) "var")
                (if
                  (not (= (nth t 1) "_"))
                  (dict-set! seen (nth t 1) true)
                  nil))
              ((= (first t) "compound")
                (for-each collect! (nth t 2)))
              (true nil)))))
        (collect! ast)
        (keys seen)))))

;; Build a solution dict from a var-env after a successful solve.
;; Maps each variable name string to its formatted term value.
(define
  pl-query-solution-dict
  (fn
    (var-names var-env)
    (let
      ((d {}))
      (for-each
        (fn (name) (dict-set! d name (pl-format-term (dict-get var-env name))))
        var-names)
      d)))

;; Parse source-str and load clauses into a fresh DB.
;; Returns the DB for reuse across multiple queries.
(define
  pl-load
  (fn
    (source-str)
    (let
      ((db (pl-mk-db)))
      (if
        (and (string? source-str) (not (= source-str "")))
        (pl-db-load! db (pl-parse source-str))
        nil)
      db)))

;; Run query-str against db, returning a list of solution dicts.
;; Each dict maps variable name strings to their formatted term values.
;; Returns an empty list if no solutions.
(define
  pl-query-all
  (fn
    (db query-str)
    (let
      ((parsed (pl-parse (str "q_ :- " query-str "."))))
      (let
        ((body-ast (nth (first parsed) 2)))
        (let
          ((var-names (pl-query-extract-vars body-ast))
           (var-env {}))
          (let
            ((goal (pl-instantiate body-ast var-env))
             (trail (pl-mk-trail))
             (solutions (list)))
            (let
              ((mark (pl-trail-mark trail)))
              (pl-solve!
                db
                goal
                trail
                {:cut false}
                (fn
                  ()
                  (begin
                    (append!
                      solutions
                      (pl-query-solution-dict var-names var-env))
                    false)))
              (pl-trail-undo-to! trail mark)
              solutions)))))))

;; Return the first solution dict, or nil if no solutions.
(define
  pl-query-one
  (fn
    (db query-str)
    (let
      ((all (pl-query-all db query-str)))
      (if (empty? all) nil (first all)))))

;; Convenience: parse source-str, then run query-str against it.
;; Returns a list of solution dicts. Creates a fresh DB each call.
(define
  pl-query
  (fn
    (source-str query-str)
    (pl-query-all (pl-load source-str) query-str)))
