defmodule EXLA.MLIR.Module do
  @moduledoc false
  # Representation of an MLIR module.

  defstruct [:ref]

  alias EXLA.MLIR.ContextPool
  alias EXLA.MLIR.Function

  alias EXLA.Client
  alias EXLA.Executable

  @doc """
  Creates a new MLIR module.
  """
  def new(arg_typespecs, return_typespecs, fun) when is_function(fun, 1) do
    ContextPool.checkout(fn context ->
      ref = EXLA.NIF.mlir_new_module(context)

      %__MODULE__{ref: ref}
      |> create_function("main", arg_typespecs, return_typespecs, true)
      |> fun.()
    end)
  end

  @doc """
  Adds a new function to the MLIR module.
  """
  def add_function(module, name, arg_typespecs, return_typespecs) do
    create_function(module, name, arg_typespecs, return_typespecs, false)
  end

  defp create_function(
         %__MODULE__{ref: module_ref} = module,
         name,
         arg_typespecs,
         return_typespecs,
         is_public
       )
       when is_binary(name) do
    arg_types = EXLA.MLIR.Value.typespecs_to_mlir_types(arg_typespecs)
    return_types = EXLA.MLIR.Value.typespecs_to_mlir_types(return_typespecs)

    ref =
      EXLA.NIF.mlir_create_function(
        module_ref,
        name,
        arg_types,
        return_types,
        is_public
      )

    %Function{module: module, ref: ref, name: name, return_typespecs: return_typespecs}
  end

  @doc """
  Compiles a module into an executable.

  ## Options

    * `:device_id` - the device id to compile to and run the executable on.
      Defaults to the `:default_device_id` on the client. If `:num_replicas`
      or `:num_partitions` are given, this option is ignored and the device
      id is set to `-1`.

    * `:num_replicas` - the number of replicas this computation will run on.
      It defaults to 1 but you can set it if you want to enable single-program
      multiple data.

    * `:use_spmd` - enables Single-Program Multiple-Data partitioning.
      This is set to true if `:num_partitions` is more than one, otherwise is `false`.

    * `:module_compilation` - either `:to_mlir` or `:to_pjrt`. The default is `:to_pjrt`.

      * `:to_pjrt` - the `EXLA.Executable` `:ref` field will hold the reference to a PjRt executable.
      * `:to_mlir` - the `EXLA.Executable` `:ref` field will hold the reference to an MLIR module.

  Currently those options do not have an effect as they related to running the
  same compiled executable on multiple replicas.

  Some options apply to TPU only:

    * `:num_partitions` - the number of partitions this computation will run on.

  """
  def compile(
        module = %__MODULE__{},
        client = %Client{},
        argument_typespecs,
        return_typespecs,
        options \\ []
      ) do
    num_replicas = Keyword.get(options, :num_replicas, 1)
    num_partitions = Keyword.get(options, :num_partitions, 1)

    # JAX comments say SPMD can lead to subtle bugs so they only enable
    # when strictly necessary, which is when num_partitions is greater than 1.
    use_spmd = Keyword.get(options, :use_spmd, true) or num_partitions >= 1

    device_id =
      if num_replicas > 1 or num_partitions > 1,
        do: -1,
        else: Keyword.get(options, :device_id, client.default_device_id)

    # Uncomment to debug the module MLIR source
    # module |> as_string() |> IO.puts()

    ref =
      case Keyword.get(options, :module_compilation, :to_pjrt) do
        :to_mlir ->
          module.ref

        :to_pjrt ->
          EXLA.NIF.mlir_compile(
            client.ref,
            module.ref,
            argument_typespecs,
            num_replicas,
            num_partitions,
            use_spmd,
            device_id
          )
      end

    %Executable{
      client: client,
      ref: ref,
      output_typespecs: return_typespecs,
      num_replicas: num_replicas,
      num_partitions: num_partitions,
      device_id: device_id
    }
  end

  @doc """
  Returns a human-readable representation of the module using MLIR
  syntax.
  """
  def as_string(module = %__MODULE__{}) do
    EXLA.NIF.mlir_module_to_string(module.ref)
  end
end
