# SPDX-FileCopyrightText: 2023 Frank Hunleth
#
# SPDX-License-Identifier: Apache-2.0
#
defmodule Toolshed.Core.Common do
  # This file contains definitions that are used by more than one helper.
  # Everything is private.
  require Record

  @doc false
  Record.defrecordp(:hostent, Record.extract(:hostent, from_lib: "kernel/include/inet.hrl"))

  defp get_hosts_by_name(address, family) do
    case :inet.gethostbyname(to_charlist(address), family) do
      {:ok, hostent} ->
        hostent(h_addr_list: ip_list) = hostent
        ip_list

      _ ->
        []
    end
  end

  defp resolve_addr(address) do
    case gethostbyname(address, :inet) || gethostbyname(address, :inet6) do
      nil -> {:error, "Error resolving #{address}"}
      ip -> {:ok, ip}
    end
  end

  defp gethostbyname(address, family) do
    get_hosts_by_name(address, family) |> List.first()
  end

  defp bind_to_device_option(ifname, family_hint \\ :inet) do
    case :os.type() do
      {:unix, :linux} -> [bind_to_device: ifname]
      _ -> [ip: ifname_to_ip(ifname, family_hint)]
    end
  end

  defp ifname_to_ip(nil, _family), do: :any

  defp ifname_to_ip(ifname, family) do
    ifname_cl = to_charlist(ifname)

    with {:ok, ifaddrs} <- :inet.getifaddrs(),
         {_, params} <- Enum.find(ifaddrs, fn {k, _v} -> k == ifname_cl end),
         [addr | _] <- find_addr_by_family(params, family) do
      addr
    else
      _ ->
        # HACK: Give an IP address that will give an address error so
        # that if the interface appears that it will work.
        {192, 0, 2, 1}
    end
  end

  defp find_addr_by_family(params, family) do
    addr_size = family_to_tuple_size(family)
    for {:addr, addr} when tuple_size(addr) == addr_size <- params, do: addr
  end

  defp family_to_tuple_size(:inet), do: 4
  defp family_to_tuple_size(:inet6), do: 6

  defp ip_to_family({_, _, _, _}), do: :inet
  defp ip_to_family({_, _, _, _, _, _}), do: :inet6

  defp run_or_enter(fun) do
    us = self()

    fun_pid =
      spawn_link(fn ->
        fun.()
        send(us, :done)
      end)

    gets_pid =
      spawn_link(fn ->
        if IO.gets("Press enter to stop") != :eof do
          send(us, :done)
        end
      end)

    # Wait for either the pings to stop or enter to be pressed
    receive do
      :done -> :ok
    end

    # Clean up the processes
    Process.unlink(fun_pid)
    Process.exit(fun_pid, :kill)
    Process.unlink(gets_pid)
    Process.exit(gets_pid, :kill)

    # Clean up a second `:done` in case both complete simultaneously
    receive do
      :done -> :ok
    after
      0 -> :ok
    end
  end

  defp check_app(app) do
    case Application.ensure_all_started(app) do
      {:ok, _} ->
        :ok

      {:error, _} ->
        raise RuntimeError, """
        #{inspect(app)} can't be started.
        This probably means that it isn't in the OTP release.
        To fix, edit your mix.exs and add #{inspect(app)} to the :extra_applications list.
        """
    end
  end
end
