defmodule Mix.Tasks.Compile.ElixirMake do
  @moduledoc """
  Runs `make` in the current project.

  This task runs `make` in the current project; any output coming from `make` is
  printed in real-time on stdout.

  ## Configuration

  This compiler can be configured through the return value of the `project/0`
  function in `mix.exs`; for example:

      def project() do
        [app: :myapp,
         make_executable: "make",
         make_makefile: "Othermakefile",
         compilers: [:elixir_make] ++ Mix.compilers,
         deps: deps()]
      end

  The following options are available:

    * `:make_executable` - (binary or `:default`) it's the executable to use as the
      `make` program. If not provided or if `:default`, it defaults to `"nmake"`
      on Windows, `"gmake"` on FreeBSD, OpenBSD and NetBSD, and `"make"` on everything
      else. You can, for example, customize which executable to use on a
      specific OS and use `:default` for every other OS. If the `MAKE`
      environment variable is present, that is used as the value of this option.

    * `:make_makefile` - (binary or `:default`) it's the Makefile to
      use. Defaults to `"Makefile"` for Unix systems and `"Makefile.win"` for
      Windows systems if not provided or if `:default`.

    * `:make_targets` - (list of binaries) it's the list of Make targets that
      should be run. Defaults to `[]`, meaning `make` will run the first target.

    * `:make_clean` - (list of binaries) it's a list of Make targets to be run
      when `mix clean` is run. It's only run if a non-`nil` value for
      `:make_clean` is provided. Defaults to `nil`.

    * `:make_cwd` - (binary) it's the directory where `make` will be run,
      relative to the root of the project.

    * `:make_env` - (map of binary to binary) it's a map of extra environment
      variables to be passed to `make`. You can also pass a function in here in
      case `make_env` needs access to things that are not available during project
      setup; the function should return a map of binary to binary. Many default
      environment variables are set, see section below

    * `:make_error_message` - (binary or `:default`) it's a custom error message
      that can be used to give instructions as of how to fix the error (e.g., it
      can be used to suggest installing `gcc` if you're compiling a C
      dependency).

    * `:make_args` - (list of binaries) it's a list of extra arguments to be passed.

  The following options configure precompilation:

    * `:make_precompiler` - a two-element tuple with the precompiled type
      and module to use. The precompile type is either `:nif` or `:port`
      and then the precompilation module. If the type is a `:nif`, it looks
      for a DDL or a shared object as precompilation target given by
      `:make_precompiler_filename` and the current NIF version is part of
      the precompiled archive. If `:port`, it looks for an executable with
      `:make_precompiler_filename`.

    * `:make_precompiler_url` - the download URL template. Defaults to none.
      Required when `make_precompiler` is set.

    * `:make_precompiler_filename` - the filename of the compiled artefact
      without its extension. Defaults to the app name.

    * `:make_precompiler_downloader` - a module implementing the `ElixirMake.Downloader`
      behaviour. You can use this to customize how the precompiled artefacts
      are downloaded, for example, to add HTTP authentication or to download
      from an SFTP server. The default implementation uses `:httpc`.

    * `:make_force_build` - if build should be forced even if precompiled artefacts
      are available. Defaults to true if the app has a `-dev` version flag.

  See [the Precompilation guide](PRECOMPILATION_GUIDE.md) for more information.

  ## Default environment variables

  There are also several default environment variables set:

    * `MIX_TARGET`
    * `MIX_ENV`
    * `MIX_BUILD_PATH` - same as `Mix.Project.build_path/0`
    * `MIX_APP_PATH` - same as `Mix.Project.app_path/0`
    * `MIX_COMPILE_PATH` - same as `Mix.Project.compile_path/0`
    * `MIX_CONSOLIDATION_PATH` - same as `Mix.Project.consolidation_path/0`
    * `MIX_DEPS_PATH` - same as `Mix.Project.deps_path/0`
    * `MIX_MANIFEST_PATH` - same as `Mix.Project.manifest_path/0`
    * `ERL_EI_LIBDIR`
    * `ERL_EI_INCLUDE_DIR`
    * `ERTS_INCLUDE_DIR`
    * `ERL_INTERFACE_LIB_DIR`
    * `ERL_INTERFACE_INCLUDE_DIR`

  These may also be overwritten with the `make_env` option.

  ## Compilation artifacts and working with priv directories

  Generally speaking, compilation artifacts are written to the `priv`
  directory, as that the only directory, besides `ebin`, which are
  available to Erlang/OTP applications.

  However, note that Mix projects supports the `:build_embedded`
  configuration, which controls if assets in the `_build` directory
  are symlinked (when `false`, the default) or copied (`true`).
  In order to support both options for `:build_embedded`, it is
  important to follow the given guidelines:

    * The "priv" directory must not exist in the source code
    * The Makefile should copy any artifact to `$MIX_APP_PATH/priv`
      or, even better, to `$MIX_APP_PATH/priv/$MIX_TARGET`
    * If there are static assets, the Makefile should copy them over
      from a directory at the project root (not named "priv")

  """

  use Mix.Task
  alias ElixirMake.Artefact

  @recursive true

  @doc false
  def run(args) do
    if function_exported?(Mix, :ensure_application!, 1) do
      Mix.ensure_application!(:inets)
      Mix.ensure_application!(:ssl)
      Mix.ensure_application!(:crypto)
    end

    config = Mix.Project.config()
    app = config[:app]
    version = config[:version]
    force_build = pre_release?(version) or Keyword.get(config, :make_force_build, false)
    {precompiler_type, precompiler} = config[:make_precompiler] || {nil, nil}

    cond do
      precompiler == nil ->
        ElixirMake.Compiler.compile(args)

      force_build == true ->
        precompiler.build_native(args)

      true ->
        rootname = config[:make_precompiler_filename] || "#{app}"

        extname =
          case {precompiler_type, :os.type()} do
            {:nif, {:win32, _}} -> ".dll"
            {:nif, _} -> ".so"
            {:port, {:win32, _}} -> ".exe"
            {:port, _} -> ""
            {_, _} -> raise_unknown_precompiler_type(precompiler_type)
          end

        app_priv = Path.join(Mix.Project.app_path(config), "priv")
        load_path = Path.join(app_priv, rootname <> extname)

        with false <- File.exists?(load_path),
             {:error, message} <- download_or_reuse_nif(config, precompiler, app_priv) do
          recover =
            case message do
              {:unavailable_target, current_target, _description} ->
                if function_exported?(precompiler, :unavailable_target, 1) do
                  precompiler.unavailable_target(current_target)
                else
                  :compile
                end

              _ ->
                Mix.shell().error("""
                Error happened while installing #{app} from precompiled binary: #{inspect(message)}.

                Attempting to compile #{app} from source...\
                """)

                :compile
            end

          case recover do
            :compile -> precompiler.build_native(args)
            :ignore -> {:ok, []}
          end
        else
          _ -> {:ok, []}
        end
    end
  end

  defp raise_unknown_precompiler_type(precompiler_type) do
    Mix.raise("Unknown precompiler type: #{inspect(precompiler_type)} (expected :nif or :port)")
  end

  # This is called by Elixir when `mix clean` runs
  # and `:elixir_make` is in the list of compilers.
  @doc false
  def clean() do
    config = Mix.Project.config()
    {clean_targets, config} = Keyword.pop(config, :make_clean)

    if clean_targets do
      config
      |> Keyword.put(:make_targets, clean_targets)
      |> ElixirMake.Compiler.make([])
    end
  end

  defp pre_release?(version) do
    "dev" in Version.parse!(version).pre
  end

  defp download_or_reuse_nif(config, precompiler, app_priv) do
    nif_version = "#{:erlang.system_info(:nif_version)}"

    case Artefact.current_target_url(config, precompiler, nif_version) do
      {:ok, target, nif_version_to_use, url} ->
        archived_fullpath = Artefact.archive_path(config, target, nif_version_to_use)

        unless File.exists?(archived_fullpath) do
          Mix.shell().info("Downloading precompiled NIF to #{archived_fullpath}")

          with {:ok, archived_data} <- Artefact.download(config, url) do
            File.mkdir_p(Path.dirname(archived_fullpath))
            File.write(archived_fullpath, archived_data)
          end
        end

        Artefact.verify_and_decompress(archived_fullpath, app_priv)

      {:error, msg} ->
        {:error, msg}
    end
  end
end
