# frozen_string_literal: true
require "concurrent"
require "set"
require "securerandom"

require "speculation/version"
require "speculation/namespaced_symbols"
require "speculation/identifier"
require "speculation/utils"
require "speculation/spec_impl"
require "speculation/error"

module Speculation
  extend NamespacedSymbols

  class << self
    # Enables or disables spec asserts. Defaults to false.
    attr_accessor :check_asserts

    # A soft limit on how many times a branching spec (or/alt/zero_or_more) can
    # be recursed through during generation.  After this a non-recursive branch
    # will be chosen.
    attr_accessor :recursion_limit

    # The number of times an anonymous fn specified by fspec will be
    # (generatively) tested during conform.
    attr_accessor :fspec_iterations

    # The number of elements validated in a collection spec'ed with 'every'.
    attr_accessor :coll_check_limit

    # The number of errors reported by explain in a collection spec'ed with
    # 'every'
    attr_accessor :coll_error_limit
  end

  @check_asserts    = ENV["SPECULATION_CHECK_ASSERTS"] == "true"
  @recursion_limit  = 4
  @fspec_iterations = 21
  @coll_check_limit = 101
  @coll_error_limit = 20

  @registry_ref = Concurrent::Atom.new({})

  # Can be enabled or disabled at runtime:
  # - enabled/disabled by setting `check_asserts`.
  # - enabled by setting environment variable SPECULATION_CHECK_ASSERTS to the
  #   string "true"
  # Defaults to false if not set.
  # @param spec [Spec]
  # @param x value to validate
  # @return x if x is valid? according to spec
  # @raise [Error] with explain_data plus :Speculation/failure of :assertion_failed
  def self.assert(spec, x)
    return x unless check_asserts
    return x if valid?(spec, x)

    ed = _explain_data(spec, [], [], [], x).merge(ns(:failure) => :assertion_failed)
    out = StringIO.new
    explain_out(ed, out)

    raise Speculation::Error.new("Spec assertion failed\n#{out.string}", ed)
  end

  # @param infinite [Boolean] whether +/- infinity allowed (default true)
  # @param nan [Boolean] whether Flaot::NAN allowed (default true)
  # @param min [Boolean] minimum value (inclusive, default none)
  # @param max [Boolean] maximum value (inclusive, default none)
  # @return [Spec] that validates floats
  def self.float_in(min: nil, max: nil, infinite: true, nan: true)
    preds = [Float]
    preds << ->(x) { !x.nan? } unless nan
    preds << ->(x) { !x.infinite? } unless infinite
    preds << ->(x) { x <= max } if max
    preds << ->(x) { x >= min } if min

    min ||= Float::MIN
    max ||= Float::MAX

    gens = [[20, ->(_) { rand(min.to_f..max.to_f) }]]
    gens << [1, ->(r) { r.choose(Float::INFINITY, -Float::INFINITY) }] if infinite
    gens << [1, ->(_) { Float::NAN }] if nan

    spec(self.and(*preds), :gen => ->(rantly) { rantly.freq(*gens) })
  end

  # @param range [Range<Integer>]
  # @return Spec that validates ints in the given range
  def self.int_in(range)
    spec(self.and(Integer, ->(x) { range.include?(x) }),
         :gen => ->(_) { rand(range) })
  end

  # @param time_range [Range<Time>]
  # @return Spec that validates times in the given range
  def self.time_in(time_range)
    spec(self.and(Time, ->(x) { time_range.cover?(x) }),
         :gen => ->(_) { rand(time_range) })
  end

  # @param date_range [Range<Date>]
  # @return Spec that validates dates in the given range
  def self.date_in(date_range)
    spec(self.and(Date, ->(x) { date_range.cover?(x) }),
         :gen => ->(_) { rand(date_range) })
  end

  # @param x [Spec, Object]
  # @return [Spec, false] x if x is a spec, else false
  def self.spec?(x)
    x if x.is_a?(SpecImpl)
  end

  # @param x [Hash, Object]
  # @return [Hash, false] x if x is a (Speculation) regex op, else logical false
  def self.regex?(x)
    Utils.hash?(x) && x[ns(:op)] && x
  end

  # @param value return value of a `conform` call
  # @return [Boolean] true if value is the result of an unsuccessful conform
  def self.invalid?(value)
    value.equal?(ns(:invalid))
  end

  # @param spec [Spec]
  # @param value value to conform
  # @return [Symbol, Object] :Speculation/invalid if value does not match spec, else the (possibly destructured) value
  def self.conform(spec, value)
    spec = Identifier(spec)
    specize(spec).conform(value)
  end

  # Takes a spec and a one-arg generator function and returns a version of the spec that uses that generator
  # @param spec [Spec]
  # @param gen [Proc] generator proc that receives a Rantly instance
  # @return [Spec]
  def self.with_gen(spec, gen)
    if regex?(spec)
      spec.merge(ns(:gfn) => gen)
    else
      specize(spec).tap { |s| s.gen = gen }
    end
  end

  # @private
  def self._explain_data(spec, path, via, inn, value)
    probs = specize(spec).explain(path, via, inn, value)

    if probs && probs.any?
      { ns(:problems) => probs }
    end
  end

  # Given a spec and a value x which ought to conform, returns nil if x
  # conforms, else a hash with at least the key :"Speculation/problems" whose
  # value is a collection of problem-hashes, where problem-hash has at least
  # :path :pred and :val keys describing the predicate and the value that failed
  # at that path.
  # @param spec [Spec]
  # @param x value which ought to conform
  # @return [nil, Hash] nil if x conforms, else a hash with at least the key
  #   :Speculation/problems whose value is a collection of problem-hashes,
  #   where problem-hash has at least :path :pred and :val keys describing the
  #   predicate and the value that failed at that path.
  def self.explain_data(spec, x)
    spec = Identifier(spec)
    name = spec_name(spec)
    _explain_data(spec, [], Array(name), [], x)
  end

  # @param ed [Hash] explain data (per 'explain_data')
  # @param out [IO] destination to write explain human readable message to (default STDOUT)
  def self.explain_out(ed, out = STDOUT)
    return out.puts("Success!") unless ed

    ed.fetch(ns(:problems)).each do |prob|
      path, pred, val, reason, via, inn = prob.values_at(:path, :pred, :val, :reason, :via, :in)

      out.print("In: ", inn.to_a.inspect, " ") unless inn.empty?
      out.print("val: ", val.inspect, " fails")
      out.print(" spec: ", via.last.inspect) unless via.empty?
      out.print(" at: ", path.to_a.inspect) unless path.empty?
      out.print(" predicate: ", pred.inspect)
      out.print(", ", reason.inspect) if reason

      prob.each do |k, v|
        unless [:path, :pred, :val, :reason, :via, :in].include?(k)
          out.print("\n\t ", k.inspect, PP.pp(v, String.new))
        end
      end

      out.puts
    end

    ed.each do |k, v|
      out.puts("#{k} #{PP.pp(v, String.new)}") unless k == ns(:problems)
    end

    nil
  end

  # Given a spec and a value that fails to conform, prints an explaination to STDOUT
  # @param spec [Spec]
  # @param x
  def self.explain(spec, x)
    explain_out(explain_data(spec, x))
  end

  # @param spec [Spec]
  # @param x a value that fails to conform
  # @return [String] a human readable explaination
  def self.explain_str(spec, x)
    out = StringIO.new
    explain_out(explain_data(spec, x), out)
    out.string
  end

  # @private
  def self.gensub(spec, overrides, path, rmap)
    overrides ||= {}

    spec = specize(spec)
    gfn = overrides[spec_name(spec) || spec] || overrides[path]
    g = gfn || spec.gen(overrides, path, rmap)

    if g
      Gen.such_that(->(x) { valid?(spec, x) }, g)
    else
      raise Speculation::Error.new("unable to construct gen at: #{path.inspect} for: #{spec.inspect}",
                                   ns(:failure) => :no_gen, ns(:path) => path)
    end
  end

  # Given a spec, returns the generator for it, or raises if none can be
  # constructed.
  #
  # Optionally an overrides hash can be provided which should map
  # spec names or paths (array of symbols) to no-arg generator Procs.
  # These will be used instead of the generators at those names/paths. Note that
  # parent generator (in the spec or overrides map) will supersede those of any
  # subtrees. A generator for a regex op must always return a sequential
  # collection (i.e. a generator for Speculation.zero_or_more should return
  # either an empty array or an array with one item in it)
  #
  # @param spec [Spec]
  # @param overrides <Hash>
  # @return [Proc]
  def self.gen(spec, overrides = nil)
    spec = Identifier(spec)
    gensub(spec, overrides, [], ns(:recursion_limit) => recursion_limit)
  end

  # @private
  def self.Identifier(x)
    case x
    when Method        then Identifier.new(x.receiver, x.name, false)
    when UnboundMethod then Identifier.new(x.owner, x.name, true)
    else x
    end
  end

  # Given a namespace-qualified symbol key, and a spec, spec name, predicate or
  # regex-op makes an entry in the registry mapping key to the spec
  # @param key [Symbol] namespace-qualified symbol
  # @param spec [Spec, Symbol, Proc, Hash] a spec, spec name, predicate or regex-op
  # @return [Symbol, Method]
  def self.def(key, spec)
    key = Identifier(key)

    unless Utils.ident?(key) && (!key.is_a?(Symbol) || NamespacedSymbols.namespace(key))
      raise ArgumentError, "key must be a namespaced Symbol, e.g. #{ns(:my_spec)}, or a Method"
    end

    spec = if spec?(spec) || regex?(spec) || registry[spec]
             spec
           else
             spec_impl(spec, false)
           end

    @registry_ref.swap do |reg|
      reg.merge(key => with_name(spec, key)).freeze
    end

    key.is_a?(Identifier) ? key.get_method : key
  end

  # @return [Hash] the registry hash
  # @see get_spec
  def self.registry
    @registry_ref.value
  end

  # @param key [Symbol, Method]
  # @return [Spec, nil] spec registered for key, or nil
  def self.get_spec(key)
    registry[Identifier(key)]
  end

  # NOTE: it is not generally necessary to wrap predicates in spec when using
  # `S.def` etc., only to attach a unique generator.
  #
  # Optionally takes :gen generator function, which must be a proc of one arg
  # (Rantly instance) that generates a valid value.
  #
  # @param pred [Proc, Method, Set, Class, Regexp, Hash] Takes a single predicate. A
  #   predicate can be one of:
  #
  #   - Proc, e.g. `-> (x) { x.even? }`, will be called with the given value
  #   - Method, e.g. `Foo.method(:bar?)`, will be called with the given value
  #   - Set, e.g. `Set[1, 2]`, will be tested whether it includes the given value
  #   - Class/Module, e.g. `String`, will be tested for case equality (is_a?)
  #     with the given value
  #   - Regexp, e.g. `/foo/`, will be tested using `===` with given value
  #
  #   Can also be passed the result of one of the regex ops - cat, alt,
  #   zero_or_more, one_or_more, zero_or_one, in which case it will return a
  #   regex-conforming spec, useful when nesting an independent regex.
  #
  # @param gen [Proc] generator function, which must be a proc of one
  #   arg (Rantly instance) that generates a valid value.
  # @return [Spec]
  def self.spec(pred, gen: nil)
    if pred
      spec_impl(pred, false).tap do |spec|
        spec.gen = gen if gen
      end
    end
  end

  # Creates and returns a hash validating spec. :req and :opt are both arrays of
  # namespaced-qualified keywords (e.g. ":MyApp/foo"). The validator will ensure
  # the :req keys are present. The :opt keys serve as documentation and may be
  # used by the generator.
  #
  # The :req key array supports 'and_keys' and 'or_keys' for key groups:
  #
  #     S.keys(req: [ns(:x), ns(:y), S.or_keys(ns(:secret), S.and_keys(ns(:user), ns(:pwd)))],
  #            opt: [ns(:z)])
  #
  # There are also _un versions of :req and :opt. These allow you to connect
  # unqualified keys to specs. In each case, fully qualfied keywords are passed,
  # which name the specs, but unqualified keys (with the same name component)
  # are expected and checked at conform-time, and generated during gen:
  #
  #     S.keys(req_un: [:"MyApp/x", :"MyApp/y"])
  #
  # The above says keys :x and :y are required, and will be validated and
  # generated by specs (if they exist) named :"MyApp/x" :"MyApp/y" respectively.
  #
  # In addition, the values of *all* namespace-qualified keys will be validated
  # (and possibly destructured) by any registered specs. Note: there is
  # no support for inline value specification, by design.
  #
  # @param req [Array<Symbol>]
  # @param opt [Array<Symbol>]
  # @param req_un [Array<Symbol>]
  # @param opt_un [Array<Symbol>]
  # @param gen [Proc] generator function, which must be a proc of one arg
  #   (Rantly instance) that generates a valid value
  def self.keys(req: [], opt: [], req_un: [], opt_un: [], gen: nil)
    HashSpec.new(req, opt, req_un, opt_un).tap do |spec|
      spec.gen = gen
    end
  end

  # @see keys
  def self.or_keys(*ks)
    [ns(:or), *ks]
  end

  # @see keys
  def self.and_keys(*ks)
    [ns(:and), *ks]
  end

  # @param key_preds [Hash] Takes key+pred hash
  # @return [Spec] a destructuring spec that returns a two element array containing the key of the first
  #   matching pred and the corresponding value. Thus the 'key' and 'val' functions can be used to
  #   refer generically to the components of the tagged return.
  # @example
  #   S.or(even: -> (n) { n.even? }, small: -> (n) { n < 42 })
  def self.or(key_preds)
    OrSpec.new(key_preds)
  end

  # @param preds [Array] predicate/specs
  # @return [Spec] a spec that returns the conformed value. Successive
  #   conformed values propagate through rest of predicates.
  # @example
  #   S.and(Numeric, -> (n) { n < 42 })
  def self.and(*preds)
    AndSpec.new(preds)
  end

  # @param preds [Array] hash-validating specs (e.g. 'keys' specs)
  # @return [Spec] a spec that returns a conformed hash satisfying all of the specs.
  # @note Unlike 'and', merge can generate maps satisfying the union of the predicates.
  def self.merge(*preds)
    MergeSpec.new(preds)
  end

  # @note that 'every' does not do exhaustive checking, rather it samples
  #   `coll_check_limit` elements. Nor (as a result) does it do any conforming of
  #   elements. 'explain' will report at most coll_error_limit problems. Thus
  #   'every' should be suitable for potentially large collections.
  # @param pred predicate to validate collections with
  # @param opts [Hash] Takes several kwargs options that further constrain the collection:
  # @option opts :kind (nil) a pred/spec that the collection type must satisfy, e.g. `Array`
  #   Note that if :kind is specified and :into is not, this pred must generate in order for every
  #   to generate.
  # @option opts :count [Integer] (nil) specifies coll has exactly this count
  # @option opts :min_count [Integer] (nil) coll has count >= min_count
  # @option opts :max_count [Integer] (nil) coll has count <= max_count
  # @option opts :distinct [Boolean] (nil) all the elements are distinct
  # @option opts :gen_max [Integer] (20) the maximum coll size to generate
  # @option opts :into [Array, Hash, Set] (Array) one of [], {}, Set[], the
  #   default collection to generate into (default: empty coll as generated by
  #   :kind pred if supplied, else [])
  # @option opts :gen [Proc] generator proc, which must be a proc of one arg
  #   (Rantly instance) that generates a valid value.
  # @see coll_of
  # @see every_kv
  # @return [Spec] spec that validates collection elements against pred
  def self.every(pred, opts = {})
    gen = opts.delete(:gen)

    EverySpec.new(pred, opts).tap do |spec|
      spec.gen = gen
    end
  end

  # Like 'every' but takes separate key and val preds and works on associative collections.
  #
  # Same options as 'every', :into defaults to {}
  #
  # @see every
  # @see hash_of
  # @param kpred key pred
  # @param vpred val pred
  # @param options [Hash]
  # @return [Spec] spec that validates associative collections
  def self.every_kv(kpred, vpred, options)
    every(tuple(kpred, vpred), ns(:kfn) => ->(_i, v) { v.first },
                               :into    => {},
                               **options)
  end

  # Returns a spec for a collection of items satisfying pred. Unlike 'every', coll_of will
  # exhaustively conform every value.
  #
  # Same options as 'every'. conform will produce a collection corresponding to :into if supplied,
  # else will match the input collection, avoiding rebuilding when possible.
  #
  # @see every
  # @see hash_of
  # @param pred
  # @param opts [Hash]
  # @return [Spec]
  def self.coll_of(pred, opts = {})
    every(pred, ns(:conform_all) => true, **opts)
  end

  # Returns a spec for a hash whose keys satisfy kpred and vals satisfy vpred.
  # Unlike 'every_kv', hash_of will exhaustively conform every value.
  #
  # Same options as 'every', :kind defaults to `Speculation::Utils.hash?`, with
  # the addition of:
  #
  # :conform_keys - conform keys as well as values (default false)
  #
  # @see every_kv
  # @param kpred key pred
  # @param vpred val pred
  # @param options [Hash]
  # @return [Spec]
  def self.hash_of(kpred, vpred, options = {})
    every_kv(kpred, vpred, :kind            => Utils.method(:hash?),
                           ns(:conform_all) => true,
                           **options)
  end

  # @param pred
  # @return [Hash] regex op that matches zero or more values matching pred. Produces
  #   an array of matches iff there is at least one match
  def self.zero_or_more(pred)
    rep(pred, pred, [], false)
  end

  # @param pred
  # @return [Hash] regex op that matches one or more values matching pred. Produces
  # an array of matches
  def self.one_or_more(pred)
    pcat(:predicates => [pred, rep(pred, pred, [], true)], :return_value => [])
  end

  # @param pred
  # @return [Hash] regex op that matches zero or one value matching pred. Produces a
  # single value (not a collection) if matched.
  def self.zero_or_one(pred)
    _alt([pred, accept(ns(:nil))], nil)
  end

  # @param kv_specs [Hash] key+pred pairs
  # @example
  #   S.alt(even: :even?.to_proc, small: -> (n) { n < 42 })
  # @return [Hash] regex op that returns a two item array containing the key of the
  #   first matching pred and the corresponding value. Thus can be destructured
  #   to refer generically to the components of the return.
  def self.alt(kv_specs)
    _alt(kv_specs.values, kv_specs.keys).merge(:id => SecureRandom.uuid)
  end

  # @example
  #   S.cat(e: :even?.to_proc, o: :odd?.to_proc)
  # @param named_specs [Hash] key+pred hash
  # @return [Hash] regex op that matches (all) values in sequence, returning a map
  #   containing the keys of each pred and the corresponding value.
  def self.cat(named_specs)
    keys = named_specs.keys
    predicates = named_specs.values

    pcat(:keys => keys, :predicates => predicates, :return_value => {})
  end

  # @param re [Hash] regex op
  # @param preds [Array] predicates
  # @return [Hash] regex-op that consumes input as per re but subjects the
  #   resulting value to the conjunction of the predicates, and any conforming
  #   they might perform.
  def self.constrained(re, *preds)
    { ns(:op) => ns(:amp), :p1 => re, :predicates => preds }
  end

  # @yield [value] predicate function with the semantics of conform i.e. it should
  #   return either a (possibly converted) value or :"Speculation/invalid"
  # @return [Spec] a spec that uses block as a predicate/conformer.
  def self.conformer(&pred)
    spec_impl(pred, true)
  end

  # Takes :args :ret and (optional) :block and :fn kwargs whose values are preds and returns a spec
  # whose conform/explain take a method/proc and validates it using generative testing. The
  # conformed value is always the method itself.
  #
  # fspecs can generate procs that validate the arguments and fabricate a return value compliant
  # with the :ret spec, ignoring the :fn spec if present.
  #
  # @param args predicate
  # @param ret predicate
  # @param fn predicate
  # @param block predicate
  # @param gen [Proc] generator proc, which must be a proc of one arg (Rantly
  #   instance) that generates a valid value.
  # @return [Spec]
  # @see fdef See 'fdef' for a single operation that creates an fspec and registers it, as well as a full description of :args, :block, :ret and :fn
  def self.fspec(args: nil, ret: nil, fn: nil, block: nil, gen: nil)
    FSpec.new(:args => spec(args), :ret => spec(ret), :fn => spec(fn), :block => spec(block)).tap do |spec|
      spec.gen = gen
    end
  end

  # @param preds [Array] one or more preds
  # @return [Spec] a spec for a tuple, an array where each element conforms to
  #   the corresponding pred. Each element will be referred to in paths using its
  #   ordinal.
  def self.tuple(*preds)
    TupleSpec.new(preds)
  end

  # Once registered, specs are checked by instrument and tested by the runner Speculation::Test.check
  #
  # @example to register method specs for the Hash[] method:
  #   S.fdef(Hash.method(:[]),
  #     args: S.alt(
  #       hash: Hash,
  #       array_of_pairs: S.coll_of(S.tuple(ns(S, :any), ns(S, :any)), kind: Array),
  #       kvs: S.constrained(S.one_or_more(ns(S, :any)), -> (kvs) { kvs.count.even? })
  #     ),
  #     ret: Hash
  #   )
  #
  # @param method [Method]
  # @param spec [Hash]
  # @option spec :args [Hash] regex spec for the method arguments as a list
  # @option spec :block an fspec for the method's block
  # @option spec :ret a spec for the method's return value
  # @option spec :fn a spec of the relationship between args and ret - the value passed is
  #   { args: conformed_args, block: given_block, ret: conformed_ret } and is expected to contain
  #   predicates that relate those values
  # @return [Method] the method spec'ed
  # @note Note that :fn specs require the presence of :args and :ret specs to conform values, and so :fn
  #   specs will be ignored if :args or :ret are missing.
  def self.fdef(method, spec)
    self.def(Identifier(method), fspec(spec))
    method
  end

  # @param spec
  # @param x
  # @return [Boolean] true when x is valid for spec.
  def self.valid?(spec, x)
    spec = Identifier(spec)
    spec = specize(spec)

    !invalid?(spec.conform(x))
  end

  # @param pred
  # @return [Spec] a spec that accepts nil and values satisfying pred
  def self.nilable(pred)
    NilableSpec.new(pred)
  end

  # Generates a number (default 10) of values compatible with spec and maps
  # conform over them, returning a sequence of [val conformed-val] tuples.
  # @param spec
  # @param n [Integer]
  # @param overrides [Hash] a generator overrides hash as per `gen`
  # @return [Array] an array of [val, conformed_val] tuples
  # @see gen for generator overrides
  def self.exercise(spec, n: 10, overrides: {})
    Gen.sample(gen(spec, overrides), n).map { |value|
      [value, conform(spec, value)]
    }
  end

  # Exercises the method by applying it to n (default 10) generated samples of
  # its args spec. When fspec is supplied its arg spec is used, and method can
  # be a proc.
  # @param method [Method]
  # @param n [Integer]
  # @param fspec [Spec]
  # @return [Array] an arrray of tuples of [args, ret].
  def self.exercise_fn(method, n: 10, fspec: nil)
    fspec ||= get_spec(method)
    raise ArgumentError, "No fspec found for #{method}" unless fspec

    Gen.sample(gen(fspec.args), n).map { |args| [args, method.call(*args)] }
  end

  ### impl ###

  # @private
  def self.recur_limit?(rmap, id, path, k)
    rmap[id] > rmap[ns(:recursion_limit)] &&
      path.include?(k)
  end

  # @private
  def self.inck(h, k)
    h.merge(k => h.fetch(k, 0).next)
  end

  # @private
  def self.dt(pred, x)
    return x unless pred

    spec = the_spec(pred)

    if spec
      conform(spec, x)
    elsif pred.is_a?(Module) || pred.is_a?(::Regexp)
      pred === x ? x : ns(:invalid)
    elsif pred.is_a?(Set)
      pred.include?(x) ? x : ns(:invalid)
    elsif pred.respond_to?(:call)
      pred.call(x) ? x : ns(:invalid)
    else
      raise "#{pred} is not a class, proc, set or regexp"
    end
  end

  # internal helper function that returns true when x is valid for spec.
  # @private
  def self.pvalid?(pred, x)
    !invalid?(dt(pred, x))
  end

  # @private
  def self.explain1(pred, path, via, inn, value)
    spec = maybe_spec(pred)

    if spec?(spec)
      name = spec_name(spec)
      via = Utils.conj(via, name) if name

      spec.explain(path, via, inn, value)
    else
      [{ :path => path, :val => value, :via => via, :in => inn, :pred => [pred, [value]] }]
    end
  end

  # @private
  def self.spec_impl(pred, should_conform)
    if spec?(pred)
      pred
    elsif regex?(pred)
      RegexSpec.new(pred)
    elsif Utils.ident?(pred)
      the_spec(pred)
    else
      Spec.new(pred, should_conform)
    end
  end

  # @private
  def self.explain_pred_list(preds, path, via, inn, value)
    return_value = value

    preds.each do |pred|
      nret = dt(pred, return_value)

      if invalid?(nret)
        return explain1(pred, path, via, inn, return_value)
      else
        return_value = nret
      end
    end

    nil
  end

  ### regex

  # @private
  def self.re_gen(p, overrides, path, rmap)
    origp = p
    p = reg_resolve!(p)

    id, op, ps, ks, p1, p2, ret, id, gen = p.values_at(
      :id, ns(:op), :predicates, :keys, :p1, :p2, :return_value, :id, ns(:gen)
    ) if regex?(p)

    id = p.id if spec?(p)
    ks ||= []

    rmap = inck(rmap, id) if id

    ggens = ->(preds, keys) do
      preds.zip(keys).map do |pred, k|
        unless rmap && id && k && recur_limit?(rmap, id, path, k)
          if id
            Gen.delay { Speculation.re_gen(pred, overrides, k ? Utils.conj(path, k) : path, rmap) }
          else
            re_gen(pred, overrides, k ? Utils.conj(path, k) : path, rmap)
          end
        end
      end
    end

    ogen = overrides[spec_name(origp)] ||
      overrides[spec_name(p)] ||
      overrides[path]

    if ogen
      if [:accept, nil].include?(op)
        return ->(rantly) { [*ogen.call(rantly)] }
      else
        return ->(rantly) { ogen.call(rantly) }
      end
    end

    return gen if gen

    if p
      case op
      when ns(:accept)
        if ret == ns(:nil)
          ->(_rantly) { [] }
        else
          ->(_rantly) { [ret] }
        end
      when nil
        g = gensub(p, overrides, path, rmap)

        ->(rantly) { [g.call(rantly)] }
      when ns(:amp)
        re_gen(p1, overrides, path, rmap)
      when ns(:pcat)
        gens = ggens.call(ps, ks)

        if gens.all?
          ->(rantly) do
            gens.flat_map { |gg| gg.call(rantly) }
          end
        end
      when ns(:alt)
        gens = ggens.call(ps, ks).compact

        ->(rantly) { rantly.branch(*gens) } unless gens.empty?
      when ns(:rep)
        if recur_limit?(rmap, id, [id], id)
          ->(_rantly) { [] }
        else
          g = re_gen(p2, overrides, path, rmap)

          if g
            ->(rantly) do
              rantly.range(0, 20).times.flat_map { g.call(rantly) }
            end
          end
        end
      end
    end
  end

  # @private
  def self.re_conform(regex, data)
    x, *xs = data

    if data.empty?
      return ns(:invalid) unless accept_nil?(regex)

      return_value = preturn(regex)

      if return_value == ns(:nil)
        nil
      else
        return_value
      end
    else
      dp = deriv(regex, x)

      if dp
        re_conform(dp, xs)
      else
        ns(:invalid)
      end
    end
  end

  # @private
  def self.re_explain(path, via, inn, regex, input)
    p = regex

    input.each_with_index do |value, index|
      dp = deriv(p, value)

      if dp
        p = dp
        next
      end

      if accept?(p)
        if p[ns(:op)] == ns(:pcat)
          return op_explain(p, path, via, Utils.conj(inn, index), input[index..-1])
        else
          return [{ :path   => path,
                    :reason => "Extra input",
                    :val    => input,
                    :via    => via,
                    :in     => Utils.conj(inn, index) }]
        end
      else
        return op_explain(p, path, via, Utils.conj(inn, index), input[index..-1]) ||
            [{ :path   => path,
               :reason => "Extra input",
               :val    => input,
               :via    => via,
               :in     => Utils.conj(inn, index) }]
      end
    end

    if accept_nil?(p)
      nil # success
    else
      op_explain(p, path, via, inn, nil)
    end
  end

  class << self
    private

    # returns the spec/regex at end of alias chain starting with k, throws if not found, k if k not ident
    def reg_resolve!(key)
      return key unless Utils.ident?(key)
      spec = reg_resolve(key)

      if spec
        spec
      else
        raise "Unable to resolve spec: #{key}"
      end
    end

    def deep_resolve(reg, spec)
      spec = reg[spec] while Utils.ident?(spec)
      spec
    end

    # returns the spec/regex at end of alias chain starting with k, nil if not found, k if k not ident
    def reg_resolve(key)
      return key unless Utils.ident?(key)

      spec = @registry_ref.value[key]

      if Utils.ident?(spec)
        deep_resolve(registry, spec)
      else
        spec
      end
    end

    def with_name(spec, name)
      if Utils.ident?(spec)
        spec
      elsif regex?(spec)
        spec.merge(ns(:name) => name)
      else
        spec.tap { |s| s.name = name }
      end
    end

    def spec_name(spec)
      if Utils.ident?(spec)
        spec
      elsif regex?(spec)
        spec[ns(:name)]
      elsif spec.respond_to?(:name)
        spec.name
      end
    end

    # spec_or_key must be a spec, regex or ident, else returns nil. Raises if
    # unresolvable ident (Speculation::Utils.ident?)
    def the_spec(spec_or_key)
      spec = maybe_spec(spec_or_key)
      return spec if spec

      if Utils.ident?(spec_or_key)
        raise "Unable to resolve spec: #{spec_or_key}"
      end
    end

    # spec_or_key must be a spec, regex or resolvable ident, else returns nil
    def maybe_spec(spec_or_key)
      spec = (Utils.ident?(spec_or_key) && reg_resolve(spec_or_key)) ||
        spec?(spec_or_key) ||
        regex?(spec_or_key) ||
        nil

      if regex?(spec)
        with_name(RegexSpec.new(spec), spec_name(spec))
      else
        spec
      end
    end

    def and_preds(x, preds)
      pred, *preds = preds

      x = dt(pred, x)

      if invalid?(x)
        ns(:invalid)
      elsif preds.empty?
        x
      else
        and_preds(x, preds)
      end
    end

    def specize(spec)
      if spec?(spec)
        spec
      else
        case spec
        when Symbol, Identifier
          specize(reg_resolve!(spec))
        else
          spec_impl(spec, false)
        end
      end
    end

    ### regex ###

    def accept(x)
      { ns(:op) => ns(:accept), :return_value => x }
    end

    def accept?(hash)
      if hash.is_a?(Hash)
        hash[ns(:op)] == ns(:accept)
      end
    end

    def pcat(regex)
      predicate, *rest_predicates = regex[:predicates]

      keys = regex[:keys]
      key, *rest_keys = keys

      return unless regex[:predicates].all?

      unless accept?(predicate)
        return { ns(:op)       => ns(:pcat),
                 :predicates   => regex[:predicates],
                 :keys         => keys,
                 :return_value => regex[:return_value] }
      end

      val = keys ? { key => predicate[:return_value] } : predicate[:return_value]
      return_value = Utils.conj(regex[:return_value], val)

      if rest_predicates
        pcat(:predicates   => rest_predicates,
             :keys         => rest_keys,
             :return_value => return_value)
      else
        accept(return_value)
      end
    end

    def rep(p1, p2, return_value, splice)
      return unless p1

      regex = { ns(:op) => ns(:rep), :p2 => p2, :splice => splice, :id => SecureRandom.uuid }

      if accept?(p1)
        regex.merge(:p1 => p2, :return_value => Utils.conj(return_value, p1[:return_value]))
      else
        regex.merge(:p1 => p1, :return_value => return_value)
      end
    end

    def filter_alt(ps, ks, &block)
      if ks
        pks = ps.zip(ks).select { |xs| yield(xs.first) }
        [pks.map(&:first), pks.map(&:last)]
      else
        [ps.select(&block), ks]
      end
    end

    def _alt(predicates, keys)
      predicates, keys = filter_alt(predicates, keys, &Utils.method(:itself))
      return unless predicates

      predicate, *rest_predicates = predicates
      key, *_rest_keys = keys

      return_value = { ns(:op) => ns(:alt), :predicates => predicates, :keys => keys }
      return return_value unless rest_predicates.empty?

      return predicate unless key
      return return_value unless accept?(predicate)

      accept([key, predicate[:return_value]])
    end

    def alt2(p1, p2)
      if p1 && p2
        _alt([p1, p2], nil)
      else
        p1 || p2
      end
    end

    def no_ret?(p1, pret)
      return true if pret == ns(:nil)

      regex = reg_resolve!(p1)
      op = regex[ns(:op)]

      [ns(:rep), ns(:pcat)].include?(op) && pret.empty? || nil
    end

    def accept_nil?(regex)
      regex = reg_resolve!(regex)
      return unless regex?(regex)

      case regex[ns(:op)]
      when ns(:accept) then true
      when ns(:pcat)   then regex[:predicates].all?(&method(:accept_nil?))
      when ns(:alt)    then regex[:predicates].any?(&method(:accept_nil?))
      when ns(:rep)    then (regex[:p1] == regex[:p2]) || accept_nil?(regex[:p1])
      when ns(:amp)
        p1 = regex[:p1]

        return false unless accept_nil?(p1)

        no_ret?(p1, preturn(p1)) ||
          !invalid?(and_preds(preturn(p1), regex[:predicates]))
      else
        raise "Unexpected #{ns(:op)} #{regex[ns(:op)]}"
      end
    end

    def preturn(regex)
      regex = reg_resolve!(regex)
      return unless regex?(regex)

      p0, *_pr = regex[:predicates]
      k, *ks = regex[:keys]

      case regex[ns(:op)]
      when ns(:accept) then regex[:return_value]
      when ns(:pcat)   then add_ret(p0, regex[:return_value], k)
      when ns(:rep)    then add_ret(regex[:p1], regex[:return_value], k)
      when ns(:amp)
        pret = preturn(regex[:p1])

        if no_ret?(regex[:p1], pret)
          ns(:nil)
        else
          and_preds(pret, regex[:predicates])
        end
      when ns(:alt)
        ps, ks = filter_alt(regex[:predicates], regex[:keys], &method(:accept_nil?))

        r = if ps.first.nil?
              ns(:nil)
            else
              preturn(ps.first)
            end

        if ks && ks.first
          [ks.first, r]
        else
          r
        end
      else
        raise "Unexpected #{ns(:op)} #{regex[ns(:op)]}"
      end
    end

    def add_ret(regex, r, key)
      regex = reg_resolve!(regex)
      return r unless regex?(regex)

      prop = -> do
        return_value = preturn(regex)

        if return_value.empty?
          r
        else
          val = key ? { key => return_value } : return_value

          regex[:splice] ? Utils.into(r, val) : Utils.conj(r, val)
        end
      end

      case regex[ns(:op)]
      when ns(:accept), ns(:alt), ns(:amp)
        return_value = preturn(regex)

        if return_value == ns(:nil)
          r
        else
          Utils.conj(r, key ? { key => return_value } : return_value)
        end
      when ns(:pcat), ns(:rep) then prop.call
      else
        raise "Unexpected #{ns(:op)} #{regex[ns(:op)]}"
      end
    end

    def deriv(predicate, value)
      predicate = reg_resolve!(predicate)
      return unless predicate

      unless regex?(predicate)
        return_value = dt(predicate, value)

        return if invalid?(return_value)
        return accept(return_value)
      end

      regex = predicate

      predicates, p1, p2, keys, return_value, splice =
        regex.values_at(:predicates, :p1, :p2, :keys, :return_value, :splice)

      pred, *rest_preds = predicates
      key, *rest_keys = keys

      case regex[ns(:op)]
      when ns(:accept) then nil
      when ns(:pcat)
        regex1 = pcat(:predicates => [deriv(pred, value), *rest_preds], :keys => keys, :return_value => return_value)
        regex2 = nil

        if accept_nil?(pred)
          regex2 = deriv(
            pcat(:predicates => rest_preds, :keys => rest_keys, :return_value => add_ret(pred, return_value, key)),
            value
          )
        end

        alt2(regex1, regex2)
      when ns(:alt)
        _alt(predicates.map { |p| deriv(p, value) }, keys)
      when ns(:rep)
        regex1 = rep(deriv(p1, value), p2, return_value, splice)
        regex2 = nil

        if accept_nil?(p1)
          regex2 = deriv(rep(p2, p2, add_ret(p1, return_value, nil), splice), value)
        end

        alt2(regex1, regex2)
      when ns(:amp)
        p1 = deriv(p1, value)
        return unless p1

        if p1[ns(:op)] == ns(:accept)
          ret = and_preds(preturn(p1), predicates)
          accept(ret) unless invalid?(ret)
        else
          constrained(p1, *predicates)
        end
      else
        raise "Unexpected #{ns(:op)} #{regex[ns(:op)]}"
      end
    end

    def insufficient(pred, path, via, inn)
      [{ :path   => path,
         :reason => "Insufficient input",
         :pred   => [pred, []],
         :val    => [],
         :via    => via,
         :in     => inn }]
    end

    def op_explain(p, path, via, inn, input)
      p = reg_resolve!(p)
      return unless p

      input ||= []
      x = input.first

      unless regex?(p)
        if input.empty?
          return insufficient(p, path, via, inn)
        else
          return explain1(p, path, via, inn, x)
        end
      end

      case p[ns(:op)]
      when ns(:accept) then nil
      when ns(:amp)
        if input.empty?
          if accept_nil?(p[:p1])
            explain_pred_list(p[:predicates], path, via, inn, preturn(p[:p1]))
          else
            insufficient(p, path, via, inn)
          end
        else
          p1 = deriv(p[:p1], x)

          if p1
            explain_pred_list(p[:predicates], path, via, inn, preturn(p1))
          else
            op_explain(p[:p1], path, via, inn, input)
          end
        end
      when ns(:pcat)
        pks = p[:predicates].zip(p[:keys] || [])
        pred, k = if pks.count == 1
                    pks.first
                  else
                    pks.lazy.reject { |(predicate, _)| accept_nil?(predicate) }.first
                  end
        path = Utils.conj(path, k) if k

        if input.empty? && !pred
          insufficient(pred, path, via, inn)
        else
          op_explain(pred, path, via, inn, input)
        end
      when ns(:alt)
        return insufficient(p, path, via, inn) if input.empty?

        probs = p[:predicates].zip(p[:keys]).flat_map { |(predicate, key)|
          op_explain(predicate, key ? Utils.conj(path, key) : path, via, inn, input)
        }

        probs.compact
      when ns(:rep)
        op_explain(p[:p1], path, via, inn, input)
      else
        raise "Unexpected #{ns(:op)} #{p[ns(:op)]}"
      end
    end

    # Resets the spec registry to only builtin specs
    def reset_registry!
      builtins = {
        ns(:any)              => with_gen(Utils.constantly(true), ->(r) { r.branch(*Gen::GEN_BUILTINS.values) }),
        ns(:boolean)          => Set[true, false],
        ns(:positive_integer) => with_gen(self.and(Integer, ->(x) { x > 0 }), ->(r) { r.range(1) }),
        # Rantly#positive_integer is actually a natural integer
        ns(:natural_integer)  => with_gen(self.and(Integer, ->(x) { x >= 0 }), :positive_integer.to_proc),
        ns(:negative_integer) => with_gen(self.and(Integer, ->(x) { x < 0 }), ->(r) { r.range(nil, -1) }),
        ns(:empty)            => with_gen(Utils.method(:empty?), Utils.constantly([]))
      }.freeze

      @registry_ref.reset(builtins)
    end
  end

  reset_registry!
end