defmodule Livebook.FileSystem.Utils do
  alias Livebook.FileSystem

  @doc """
  Asserts that the given path is a directory.
  """
  @spec assert_dir_path!(FileSystem.path()) :: :ok
  def assert_dir_path!(path) do
    unless dir_path?(path) do
      raise ArgumentError, "expected a directory path, got: #{inspect(path)}"
    end

    :ok
  end

  @doc """
  Asserts that the given path is a regular file path.
  """
  @spec assert_regular_path!(FileSystem.path()) :: :ok
  def assert_regular_path!(path) do
    unless regular_path?(path) do
      raise ArgumentError, "expected a regular file path, got: #{inspect(path)}"
    end

    :ok
  end

  @doc """
  Checks if the given path describes a directory.
  """
  @spec dir_path?(FileSystem.path()) :: boolean()
  def dir_path?(path) do
    String.ends_with?(path, "/")
  end

  @doc """
  Checks if the given path describes a regular file.
  """
  @spec regular_path?(FileSystem.path()) :: boolean()
  def regular_path?(path) do
    not String.ends_with?(path, "/")
  end

  @doc """
  Asserts that the given paths are of the same type.
  """
  @spec assert_same_type!(FileSystem.path(), FileSystem.path()) :: :ok
  def assert_same_type!(path1, path2) do
    if dir_path?(path1) != dir_path?(path2) do
      raise ArgumentError,
            "expected paths of the same type, got: #{inspect(path1)} and #{inspect(path2)}"
    end

    :ok
  end

  @doc """
  Converts the given path into dir path by appending a trailing
  slash if necessary.
  """
  @spec ensure_dir_path(String.t()) :: FileSystem.path()
  def ensure_dir_path(path) do
    if String.ends_with?(path, "/") do
      path
    else
      path <> "/"
    end
  end

  @doc """
  Converts the given posix error atom into readable error tuple.
  """
  @spec posix_error(atom()) :: {:error, FileSystem.error()}
  def posix_error(error) do
    message = error |> :file.format_error() |> List.to_string()
    {:error, message}
  end

  @doc """
  Implements `Livebook.FileSystem.resolve_path` assuming Unix-like
  path conventions.

  This function assumes absolute paths to have a leading "/"
  and handles sequences such as "." and "..".
  """
  @spec resolve_unix_like_path(FileSystem.path(), String.t()) :: FileSystem.t()
  def resolve_unix_like_path(relative_to, subject) do
    dir_path = relative_to |> Path.dirname() |> ensure_dir_path()

    subject =
      if Path.basename(subject) in [".", ".."] do
        ensure_dir_path(subject)
      else
        subject
      end

    absolute_path? = String.starts_with?(subject, "/")
    path = if absolute_path?, do: subject, else: dir_path <> subject

    path
    |> String.split("/")
    |> remove_in_middle("")
    |> expand_parts([])
    |> Enum.join("/")
  end

  @doc """
  Returns if given file system is writable.
  """
  @spec writable?(FileSystem.t()) :: boolean()
  def writable?(file_system) do
    case FileSystem.access(file_system, FileSystem.default_path(file_system)) do
      {:ok, access} -> access in [:read_write, :write]
      _otherwise -> false
    end
  end

  @doc """
  Returns the id based on given hub id and data with given prefix.
  """
  @spec id(String.t(), String.t() | nil, String.t() | nil) :: String.t() | nil
  def id(prefix, hub_id, data)

  def id(_, _, nil), do: nil

  def id(prefix, hub_id, data) do
    if hub_id == nil or hub_id == Livebook.Hubs.Personal.id() do
      hashed_id(prefix, data)
    else
      "#{hub_id}-#{hashed_id(prefix, data)}"
    end
  end

  defp hashed_id(prefix, data) do
    hash = :crypto.hash(:sha256, data)
    "#{prefix}-#{Base.url_encode64(hash, padding: false)}"
  end

  defp remove_in_middle([], _elem), do: []
  defp remove_in_middle([head], _elem), do: [head]
  defp remove_in_middle([head | tail], elem), do: remove_in_middle(tail, elem, [head])

  defp remove_in_middle([head], _elem, acc), do: Enum.reverse([head | acc])
  defp remove_in_middle([elem | tail], elem, acc), do: remove_in_middle(tail, elem, acc)
  defp remove_in_middle([head | tail], elem, acc), do: remove_in_middle(tail, elem, [head | acc])

  defp expand_parts([], acc), do: Enum.reverse(acc)
  defp expand_parts(["." | parts], acc), do: expand_parts(parts, acc)
  defp expand_parts([".." | parts], [_parent] = acc), do: expand_parts(parts, acc)
  defp expand_parts([".." | parts], [_parent | acc]), do: expand_parts(parts, acc)
  defp expand_parts([part | parts], acc), do: expand_parts(parts, [part | acc])
end
