defmodule Unifex.CodeGenerator do
  @moduledoc """
  Behaviour for code generation.
  """

  alias Unifex.Specs

  @type t :: module
  @type code_t :: String.t()
  @type generated_code_t :: {header :: code_t, source :: code_t, generator :: t}

  @callback identification_constant() :: String.t()
  @callback interface_io_name() :: String.t()
  @callback generate_header(specs :: Specs.t()) :: code_t
  @callback generate_source(specs :: Specs.t()) :: code_t

  @doc """
  Generates boilerplate code using generator implementation from `Unifex.CodeGenerators`.
  """
  @spec generate_code(Specs.t()) :: [generated_code_t()]
  def generate_code(specs) do
    for generator <- get_generators(specs) do
      header = generator.generate_header(specs)
      source = generator.generate_source(specs)
      {header, source, generator}
    end
  end

  @spec get_generators(Specs.t()) :: [t]
  defp get_generators(%Specs{name: name, interface: nil}) do
    {:ok, bundlex_project} = Bundlex.Project.get()
    config = bundlex_project.config

    generators =
      [:natives, :libs]
      |> Enum.flat_map(&Keyword.get(config, &1))
      |> Keyword.get_values(name)
      |> Enum.flat_map(&Bunch.listify(Keyword.get(&1, :interface)))
      |> Enum.map(&bundlex_interface/1)
      |> Enum.map(&interface_generator/1)

    case generators do
      [] -> raise "Interface for native #{name} is not specified.
        Please specify it in your *.spec.exs or bundlex.exs file."
      generators -> generators
    end
  end

  defp get_generators(%Specs{interface: interfaces}) do
    interfaces
    |> Bunch.listify()
    |> Enum.map(&interface_generator/1)
  end

  @spec bundlex_interface(Bundlex.Project.native_interface()) :: Specs.interface_t()
  def bundlex_interface(:cnode), do: CNode
  def bundlex_interface(:nif), do: NIF
  def bundlex_interface(:port), do: Port

  @spec interface_generator(Specs.interface_t()) :: t
  def interface_generator(interface) do
    generator = Module.concat(Unifex.CodeGenerators, interface)

    # Ensure CodeGenerator module is present
    true = Code.ensure_loaded?(generator)

    generator
  end
end
