require Logger

defmodule FileSystem.Backend do
  @moduledoc """
  A behaviour module for implementing different file system backend.
  """

  @callback bootstrap() :: :ok | {:error, atom()}
  @callback supported_systems() :: [{atom(), atom()}]
  @callback known_events() :: [atom()]

  @doc """
  Get and validate backend module.

  Returns `{:ok, backend_module}` upon success and `{:error, reason}` upon
  failure.

  When `nil` is given, will return default backend by OS.

  When a custom module is given, make sure `start_link/1`, `bootstrap/0` and
  `supported_system/0` are defnied.
  """
  @spec backend(atom) :: {:ok, atom()} | {:error, atom()}
  def backend(backend) do
    with {:ok, module} <- backend_module(backend),
         :ok <- validate_os(backend, module),
         :ok <- module.bootstrap() do
      {:ok, module}
    else
      {:error, reason} -> {:error, reason}
    end
  end

  defp backend_module(nil) do
    case :os.type() do
      {:unix, :darwin} -> :fs_mac
      {:unix, :linux} -> :fs_inotify
      {:unix, :freebsd} -> :fs_inotify
      {:unix, :openbsd} -> :fs_inotify
      {:win32, :nt} -> :fs_windows
      system -> {:unsupported_system, system}
    end
    |> backend_module
  end

  defp backend_module(:fs_mac), do: {:ok, FileSystem.Backends.FSMac}
  defp backend_module(:fs_inotify), do: {:ok, FileSystem.Backends.FSInotify}
  defp backend_module(:fs_windows), do: {:ok, FileSystem.Backends.FSWindows}
  defp backend_module(:fs_poll), do: {:ok, FileSystem.Backends.FSPoll}

  defp backend_module({:unsupported_system, system}) do
    Logger.error(
      "I'm so sorry but `file_system` does NOT support your current system #{inspect(system)} for now."
    )

    {:error, :unsupported_system}
  end

  defp backend_module(module) do
    functions = module.__info__(:functions)

    ({:start_link, 1} in functions &&
       {:bootstrap, 0} in functions &&
       {:supported_systems, 0} in functions) ||
      raise "illegal backend"
  rescue
    _ ->
      Logger.error(
        "You are using custom backend `#{inspect(module)}`, make sure it's a legal file_system backend module."
      )

      {:error, :illegal_backend}
  end

  defp validate_os(backend, module) do
    os_type = :os.type()

    if os_type in module.supported_systems() do
      :ok
    else
      Logger.error(
        "The backend `#{backend}` you are using does NOT support your current system #{inspect(os_type)}."
      )

      {:error, :unsupported_system}
    end
  end
end
