(ns schema.experimental.complete
  "(Extremely) experimental support for 'completing' partial datums to match
   a schema. To use it, you must provide your own test.check dependency."
  {:deprecated "1.1.0"}
  (:require
   [clojure.test.check.generators :as check-generators]
   [schema.spec.core :as spec]
   schema.spec.collection
   schema.spec.leaf
   schema.spec.variant
   [schema.coerce :as coerce]
   [schema.core :as s]
   [schema.macros :as macros]
   [schema.utils :as utils]
   [schema.experimental.generators :as generators]))

(def +missing+ ::missing)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Private helpers

(defprotocol Completer
  (completer* [spec s sub-checker generator-opts]
    "A function applied to a datum as part of coercion to complete missing fields."))

(defn sample [g]
  (check-generators/generate g 10))

(extend-protocol Completer
  schema.spec.variant.VariantSpec
  (completer* [spec s sub-checker generator-opts]
    (let [g (apply generators/generator s generator-opts)]
      (if (and (class? s) (isa? s clojure.lang.IRecord) (utils/class-schema s))
        (fn record-completer [x]
          (sub-checker (into (sample g) x)))
        (fn variant-completer [x]
          (if (= +missing+ x)
            (sample g)
            (sub-checker x))))))

  schema.spec.collection.CollectionSpec
  (completer* [spec s sub-checker generator-opts]
    (if (instance? clojure.lang.APersistentMap s) ;; todo: pluggable
      (let [g (apply generators/generator s generator-opts)]
        (fn map-completer [x]
          (if (= +missing+ x)
            (sample g)
            ;; for now, just do required keys when user provides input.
            (let [ks (distinct (concat (keys x)
                                       (->> s
                                            keys
                                            (filter s/required-key?)
                                            (map s/explicit-schema-key))))]
              (sub-checker
               (into {} (for [k ks] [k (get x k +missing+)])))))))
      (let [g (apply generators/generator s generator-opts)]
        (fn coll-completer [x]
          (if (= +missing+ x)
            (sample g)
            (sub-checker x))))))

  schema.spec.leaf.LeafSpec
  (completer* [spec s sub-checker generator-opts]
    (let [g (apply generators/generator s generator-opts)]
      (fn leaf-completer [x]
        (if (= +missing+ x)
          (sample g)
          x)))))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Public

(s/defn completer
  "Produce a function that simultaneously coerces, completes, and validates a datum."
  ([schema] (completer schema {}))
  ([schema coercion-matcher] (completer schema coercion-matcher {}))
  ([schema coercion-matcher leaf-generators]
     (completer schema coercion-matcher leaf-generators {}))
  ([schema
    coercion-matcher :- coerce/CoercionMatcher
    leaf-generators :- generators/LeafGenerators
    wrappers :- generators/GeneratorWrappers]
     (spec/run-checker
      (fn [s params]
        (let [c (spec/checker (s/spec s) params)
              coercer (or (coercion-matcher s) identity)
              completr (completer* (s/spec s) s c [leaf-generators wrappers])]
          (fn [x]
            (macros/try-catchall
             (let [v (coercer x)]
               (if (utils/error? v)
                 v
                 (completr v)))
             (catch t (macros/validation-error s x t))))))
      true
      schema)))

(defn complete
  "Fill in partial-datum to make it validate schema."
  [partial-datum & completer-args]
  ((apply completer completer-args) partial-datum))
