defmodule Bundlex.Toolchain.Common.Unix do
  @moduledoc false

  use Bunch
  alias Bundlex.{Native, Project, Toolchain}
  alias Bundlex.Toolchain.Common.Compilers

  @spec compiler_commands(
          Native.t(),
          compile :: String.t(),
          link :: String.t(),
          lang :: Project.native_language(),
          options :: Keyword.t()
        ) :: [String.t()]
  def compiler_commands(native, compile, link, lang, options \\ []) do
    includes = native.includes |> paths("-I")

    compiler_flags = resolve_compiler_flags(native.compiler_flags, native.interface, lang)
    output = Toolchain.output_path(native.app, native.name, native.interface)
    output_obj = output <> "_obj"

    objects =
      native.sources
      |> Enum.map(fn source ->
        """
        #{Path.join(output_obj, source |> Path.basename())}_\
        #{:crypto.hash(:sha, source) |> Base.encode16()}.o\
        """
      end)

    compile_commands =
      native.sources
      |> Enum.zip(objects)
      |> Enum.map(fn {source, object} ->
        """
        #{compile} -Wall -Wextra -c -O2 -g #{compiler_flags} \
        -o #{path(object)} #{includes} #{path(source)}
        """
      end)

    ["mkdir -p #{path(output_obj)}"] ++
      compile_commands ++ link_commands(native, link, output, objects, options)
  end

  defp resolve_compiler_flags(compiler_flags, interface, lang) do
    compiler_flags
    |> add_interface_macro_flag(interface)
    |> maybe_add_std_flag(lang)
    |> Enum.join(" ")
  end

  defp add_interface_macro_flag(compiler_flags, nil) do
    compiler_flags
  end

  defp add_interface_macro_flag(compiler_flags, interface) do
    macro_flag = "-DBUNDLEX_#{interface |> Atom.to_string() |> String.upcase()}"
    [macro_flag] ++ compiler_flags
  end

  defp maybe_add_std_flag(compiler_flags, lang) do
    if standard_specyfied?(compiler_flags) do
      compiler_flags
    else
      flag = Compilers.get_default_std_flag(lang)
      [flag | compiler_flags]
    end
  end

  defp standard_specyfied?(compiler_flags) do
    Enum.any?(compiler_flags, &String.match?(&1, ~r/^-std=/))
  end

  defp link_commands(%Native{type: :lib}, _link, output, objects, _options) do
    a_path = path(output <> ".a")
    ["rm -f #{a_path}", "ar rcs #{a_path} #{paths(objects)}"]
  end

  defp link_commands(native, link, output, objects, options) do
    extension =
      case native.interface do
        :nif -> ".so"
        interface when interface in [:cnode, :port] -> ""
      end

    wrap_deps = options |> Keyword.get(:wrap_deps, & &1)

    deps =
      native.deps
      |> Enum.map(&(Toolchain.output_path(&1.app, &1.name, &1.interface) <> ".a"))
      |> paths()
      |> wrap_deps.()

    linker_flags = native.linker_flags |> Enum.join(" ")

    [
      """
      #{link} -o #{path(output <> extension)} \
      #{deps} #{paths(objects)} #{libs(native)} #{linker_flags}
      """
    ]
  end

  defp paths(paths, flag \\ "") do
    Enum.map_join(paths, " ", fn p -> "#{flag}#{path(p)}" end)
  end

  defp path(path) do
    path = path |> String.replace(~S("), ~S(\")) |> Path.expand()
    ~s("#{path}")
  end

  defp libs(native) do
    lib_dirs = native.lib_dirs |> paths("-L")
    libs = native.libs |> Enum.map_join(" ", fn lib -> "-l#{lib}" end)

    "#{lib_dirs} #{libs}"
  end
end
