defmodule TypeCheck.Builtin.Lazy do
  defstruct [:module, :function, :arguments]

  use TypeCheck
  @type! t :: %TypeCheck.Builtin.Lazy{module: module(), function: atom(), arguments: list(term())}
  @type! problem_tuple :: TypeCheck.TypeError.Formatter.problem_tuple()

  def lazily_expand_type(s) do
    apply(s.module, s.function, s.arguments)
  end

  defimpl TypeCheck.Protocols.ToCheck do
    def to_check(s, param) do
      quote generated: true, location: :keep do
        type = TypeCheck.Builtin.Lazy.lazily_expand_type(unquote(Macro.escape(s)))
        # Do not inject `param` one step deeper into the check,
        # because that makes dealing with quoting/unquoting difficult.
        lazy_value = unquote(param)
        check_code = TypeCheck.Protocols.ToCheck.to_check(type, Macro.var(:lazy_value, nil))
        {res, _} = Code.eval_quoted(check_code, lazy_value: lazy_value)
        res
      end
    end
  end

  defimpl TypeCheck.Protocols.Inspect do
    def inspect(s, opts) do
      inspected_arguments =
        s.arguments
        |> Enum.map(&TypeCheck.Protocols.Inspect.inspect(&1, opts))
        |> Inspect.Algebra.fold_doc(fn doc, acc ->
          Inspect.Algebra.concat([doc, ",", acc])
        end)

      # |> Enum.map(&to_string/1)
      # |> Enum.join(", ")

      "lazy("
      |> Inspect.Algebra.concat("#{inspect(s.module)}.#{s.function}(")
      |> Inspect.Algebra.concat(inspected_arguments)
      |> Inspect.Algebra.concat(")")
      |> Inspect.Algebra.color(:builtin_type, opts)

      # "lazy( #{s.module}.#{s.function}(#{inspected_arguments}) )"
    end
  end

  if Code.ensure_loaded?(StreamData) do
    defimpl TypeCheck.Protocols.ToStreamData do
      def to_gen(s) do
        StreamData.bind(StreamData.constant(nil), fn _ ->
          s
          |> TypeCheck.Builtin.Lazy.lazily_expand_type()
          |> TypeCheck.Protocols.ToStreamData.to_gen()
          # Since we assume that `lazy` is used for recursive types.
          |> StreamData.scale(fn size -> trunc(:math.log(size + 1)) end)
        end)
      end
    end
  end
end
