defmodule Spark.Dsl.Fragment do
  @moduledoc """
  Allows splitting up a DSL into multiple modules, potentially organizing large DSLs

  Use the `of` option to expression what your fragment is a fragment of. You can add
  extensions as you would normally to that resource, and they will be added to the
  parent resource.

      defmodule MyApp.Resource.Graphql do
        use Spark.Dsl.Fragment, of: Ash.Resource, extensions: AshGraphql.Resource

        graphql do
          ...
        end
      end

  Then add the fragment to the parent resource.

      defmodule MyApp.Resource do
        use Ash.Resource, fragments: [MyApp.Resource.Graphql], ...
      end
  """

  defmacro __using__(opts) do
    opts = Spark.Dsl.Extension.do_expand(opts, __CALLER__)
    original_opts = opts
    single_extension_kinds = opts[:of].single_extension_kinds()
    many_extension_kinds = opts[:of].many_extension_kinds()

    {opts, extensions} =
      opts[:of].default_extension_kinds()
      |> Enum.reduce(opts, fn {key, defaults}, opts ->
        Keyword.update(opts, key, defaults, fn current_value ->
          cond do
            key in single_extension_kinds ->
              current_value || defaults

            key in many_extension_kinds || key == :extensions ->
              List.wrap(current_value) ++ List.wrap(defaults)

            true ->
              current_value
          end
        end)
      end)
      |> Spark.Dsl.expand_modules(
        [
          single_extension_kinds: single_extension_kinds,
          many_extension_kinds: many_extension_kinds
        ],
        __CALLER__
      )

    extensions =
      extensions
      |> Enum.flat_map(&[&1 | &1.add_extensions()])
      |> Enum.uniq()

    Module.register_attribute(__CALLER__.module, :spark_extension_kinds, persist: true)
    Module.register_attribute(__CALLER__.module, :spark_fragment_of, persist: true)

    Module.put_attribute(__CALLER__.module, :spark_fragment_of, opts[:of])
    Module.put_attribute(__CALLER__.module, :extensions, extensions)
    Module.put_attribute(__CALLER__.module, :original_opts, original_opts)

    Module.put_attribute(
      __CALLER__.module,
      :spark_extension_kinds,
      List.wrap(many_extension_kinds) ++
        List.wrap(single_extension_kinds)
    )

    quote do
      require unquote(opts[:of])
      unquote(Spark.Dsl.Extension.prepare(extensions))
      @before_compile Spark.Dsl.Fragment
    end
  end

  defmacro __before_compile__(_) do
    quote do
      Spark.Dsl.Extension.set_state([], [], false)

      def extensions do
        @extensions
      end

      def opts do
        @original_opts
      end

      def spark_dsl_config do
        @spark_dsl_config
      end

      def validate_sections do
        List.wrap(@validate_sections)
      end

      @persisted @spark_dsl_config[:persist]

      def persisted do
        @persisted
      end
    end
  end
end
