defmodule Mix.Tasks.Compile.Odu do
  @moduledoc false
  use Mix.Task.Compiler

  @recursive true
  @system_arch List.to_string(:erlang.system_info(:system_architecture))

  @impl Mix.Task.Compiler
  def run(_args) do
    {os, arch} = platform_from_env()

    case {os, arch} do
      {os, arch} when is_binary(os) and is_binary(arch) ->
        build_odu(executable_path({os, arch}), [{"GOOS", os}, {"GOARCH", arch}])

      {nil, nil} ->
        build_for_current_platform()
    end
  end

  def executable_name(platform \\ current_platform()) do
    case platform do
      {"windows", "amd64"} -> "odu_windows_amd64.exe"
      {"windows", "arm64"} -> "odu_windows_arm64.exe"
      {"linux", "amd64"} -> "odu_linux_amd64"
      {"linux", "arm64"} -> "odu_linux_arm64"
      {"darwin", "amd64"} -> "odu_darwin_amd64"
      {"darwin", "arm64"} -> "odu_darwin_arm64"
      # generic name for all other platform
      _ -> "odu"
    end
  end

  defp build_for_current_platform do
    platform = current_platform()
    middleware_mode = System.get_env("EX_CMD_MIDDLEWARE_MODE")

    cond do
      middleware_mode == "compile" && System.find_executable("go") ->
        build_odu(executable_path(platform), [])

      # check if pre-built binary exists for correct platform
      File.exists?(executable_path(platform)) ->
        :ok

      true ->
        compiler_error("""
        ExCmd does not ship with `odu` shim for your operating system and arch.
        Please install `go` to build shim for you.

           System Architecture: #{@system_arch}

        """)
    end
  end

  defp build_odu(executable_path, env) do
    result =
      System.cmd(
        "go",
        ~w/build -ldflags -w -o #{executable_path}/,
        cd: "go_src",
        stderr_to_stdout: true,
        env: env
      )

    case result do
      {_, 0} ->
        {:ok, []}

      {output, _exit_status} ->
        compiler_error(output)
    end
  end

  defp compiler_error(msg) do
    error = %Mix.Task.Compiler.Diagnostic{
      compiler_name: "odu",
      details: nil,
      file: __ENV__.file,
      message: msg,
      position: nil,
      severity: :error
    }

    Mix.shell().error(error.message)
    {:error, [error]}
  end

  defp executable_path(platform) do
    Path.absname("priv")
    |> Path.join(executable_name(platform))
  end

  defp current_platform do
    case current_target(:os.type()) do
      {:ok, {"x86_64", "linux", _}} ->
        {"linux", "amd64"}

      {:ok, {"aarch64", "linux", _}} ->
        {"linux", "arm64"}

      {:ok, {"x86_64", "apple", _}} ->
        {"darwin", "amd64"}

      {:ok, {"aarch64", "apple", _}} ->
        {"darwin", "arm64"}

      {:ok, {"x86_64", "windows", _}} ->
        {"windows", "amd64"}

      {:ok, {"aarch64", "windows", _}} ->
        {"windows", "arm64"}

      {:ok, {_, os_name, _}} ->
        {os_name, nil}
    end
  end

  defp platform_from_env do
    os = System.get_env("ODU_GOOS")
    arch = System.get_env("ODU_GOARCH")

    if os && arch do
      {os, arch}
    else
      {nil, nil}
    end
  end

  defp current_target({:win32, _}) do
    processor_architecture =
      String.downcase(String.trim(System.get_env("PROCESSOR_ARCHITECTURE")))

    compiler =
      case :erlang.system_info(:c_compiler_used) do
        {:msc, _} -> "msvc"
        {:gnuc, _} -> "gnu"
        {other, _} -> Atom.to_string(other)
      end

    # https://docs.microsoft.com/en-gb/windows/win32/winprog64/wow64-implementation-details?redirectedfrom=MSDN
    case processor_architecture do
      "amd64" ->
        {:ok, {"x86_64", "windows", compiler}}

      "ia64" ->
        {:ok, {"ia64", "windows", compiler}}

      "arm64" ->
        {:ok, {"aarch64", "windows", compiler}}

      "x86" ->
        {:ok, {"x86", "windows", compiler}}
    end
  end

  defp current_target({:unix, _}) do
    # get current target triplet from `:erlang.system_info/1`
    system_architecture = to_string(:erlang.system_info(:system_architecture))
    current = String.split(system_architecture, "-", trim: true)

    case length(current) do
      4 ->
        {:ok, {Enum.at(current, 0), Enum.at(current, 2), Enum.at(current, 3)}}

      3 ->
        case :os.type() do
          {:unix, :darwin} ->
            # could be something like aarch64-apple-darwin21.0.0
            # but we don't really need the last 21.0.0 part
            # credo:disable-for-next-line
            if String.match?(Enum.at(current, 2), ~r/^darwin.*/) do
              {:ok, {Enum.at(current, 0), Enum.at(current, 1), "darwin"}}
            else
              {:ok, List.to_tuple(current)}
            end

          _ ->
            {:ok, List.to_tuple(current)}
        end

      _ ->
        {:error, "cannot decide current target"}
    end
  end
end
