lib/scrollable/utility/direction.ex

defmodule FloUI.Scrollable.Direction do
  @moduledoc """
  Utility module for limiting certain operations along a certain direction only. A value can be connected to either horizontal or vertical directions.
  """

  @typedoc """
  The directions a value can be associated with.
  """
  @type direction :: :horizontal | :vertical

  @typedoc """
  The Direction type. A value can be either associated with the horizontal or the vertical direction, by pairing the :horizontal or :vertical atoms with the value in a tuple.
  """
  @type t :: {:horizontal, term} | {:vertical, term}

  @typedoc """
  Data structure representing a vector 2, in the form of an {x, y} tuple.
  """
  @type v2 :: Scenic.Scrollable.v2()

  @doc """
  Associate a value with a direction.

  ## Examples

      iex> Scenic.Scrollable.Direction.return(5, :horizontal)
      {:horizontal, 5}

      iex> Scenic.Scrollable.Direction.return(6, :vertical)
      {:vertical, 6}
  """
  @spec return(term, direction) :: t
  def return(x, :horizontal), do: {:horizontal, x}
  def return(x, :vertical), do: {:vertical, x}

  @doc """
  Associate a value with the horizontal direction.

  ## Examples

      iex> Scenic.Scrollable.Direction.as_horizontal(5)
      {:horizontal, 5}
  """
  @spec as_horizontal(term) :: t
  def as_horizontal(x), do: return(x, :horizontal)

  @doc """
  Associate a value with the vertical direction.

  ## Examples

      iex> Scenic.Scrollable.Direction.as_vertical(6)
      {:vertical, 6}
  """
  @spec as_vertical(term) :: t
  def as_vertical(x), do: return(x, :vertical)

  @doc """
  Convert a `t:Scenic.Scrollable.Direction.t/0` to a `t:Scenic.Math.vector_2/0`.
  If the value is non numeric, the vector {0, 0} will be returned.

  ## Examples

      iex> Scenic.Scrollable.Direction.as_horizontal(5)
      ...> |> Scenic.Scrollable.Direction.to_vector_2
      {5, 0}

      iex> Scenic.Scrollable.Direction.as_vertical(5)
      ...> |> Scenic.Scrollable.Direction.to_vector_2
      {0, 5}

      iex> Scenic.Scrollable.Direction.as_horizontal(:non_numeric_value)
      ...> |> Scenic.Scrollable.Direction.to_vector_2
      {0, 0}
  """
  @spec to_vector_2(t) :: v2
  def to_vector_2({:horizontal, x}) when is_number(x), do: {x, 0}
  def to_vector_2({:vertical, y}) when is_number(y), do: {0, y}
  def to_vector_2(_), do: {0, 0}

  @doc """
  Create a `t:Scenic.Scrollable.Direction.t/0` from a `t:Scenic.Math.vector_2/0`.

  ## Examples

      iex> Scenic.Scrollable.Direction.from_vector_2({3, 5}, :horizontal)
      {:horizontal, 3}

      iex> Scenic.Scrollable.Direction.from_vector_2({3, 5}, :vertical)
      {:vertical, 5}
  """
  @spec from_vector_2(v2, direction) :: t
  def from_vector_2({x, _}, :horizontal), do: {:horizontal, x}
  def from_vector_2({_, y}, :vertical), do: {:vertical, y}

  @doc """
  Obtain the inner value from a `t:Scenic.Scrollable.Direction.t/0`.

  ## Examples

      iex> Scenic.Scrollable.Direction.as_horizontal(5)
      ...> |> Scenic.Scrollable.Direction.unwrap
      5
  """
  @spec unwrap(t) :: term
  def unwrap({_, x}), do: x

  @doc """
  Convert a horizontal `t:Scenic.Scrollable.Direction.t/0` to a vertical one, and vice versa.

  ## Examples

      iex> Scenic.Scrollable.Direction.as_horizontal(5)
      ...> |> Scenic.Scrollable.Direction.invert
      {:vertical, 5}
  """
  @spec invert(t) :: t
  def invert({:horizontal, x}), do: {:vertical, x}
  def invert({:vertical, x}), do: {:horizontal, x}

  @doc """
  Add two `t:Scenic.Scrollable.Direction.t/0` values. Only values associated with the same direction will be added. Non numeric values are ignored.

  ## Examples

      iex> five = Scenic.Scrollable.Direction.as_horizontal(5)
      ...> six = Scenic.Scrollable.Direction.as_horizontal(6)
      ...> Scenic.Scrollable.Direction.add(five, six)
      {:horizontal, 11}

      iex> three = Scenic.Scrollable.Direction.as_vertical(3)
      ...> seven = Scenic.Scrollable.Direction.as_vertical(7)
      ...> Scenic.Scrollable.Direction.add(three, seven)
      {:vertical, 10}

      iex> five = Scenic.Scrollable.Direction.as_horizontal(5)
      ...> six = Scenic.Scrollable.Direction.as_vertical(6)
      ...> Scenic.Scrollable.Direction.add(five, six)
      {:horizontal, 5}

      iex> non_numeric_value = Scenic.Scrollable.Direction.as_horizontal(:non_numeric_value)
      ...> six = Scenic.Scrollable.Direction.as_vertical(6)
      ...> Scenic.Scrollable.Direction.add(non_numeric_value, six)
      {:horizontal, :non_numeric_value}
  """
  @spec add(t, t) :: t
  def add({:horizontal, x}, {:horizontal, y}) when is_number(x) and is_number(y),
    do: {:horizontal, x + y}

  def add({:vertical, x}, {:vertical, y}) when is_number(x) and is_number(y),
    do: {:vertical, x + y}

  def add({:horizontal, x}, _), do: {:horizontal, x}
  def add({:vertical, x}, _), do: {:vertical, x}

  @doc """
  Subtract two `t:Scenic.Scrollable.Direction.t/0` values. Only values associated with the same direction will be subtracted. Non numeric values are ignored.

  ## Examples

      iex> five = Scenic.Scrollable.Direction.as_horizontal(5)
      ...> six = Scenic.Scrollable.Direction.as_horizontal(6)
      ...> Scenic.Scrollable.Direction.subtract(five, six)
      {:horizontal, -1}

      iex> three = Scenic.Scrollable.Direction.as_vertical(3)
      ...> seven = Scenic.Scrollable.Direction.as_vertical(7)
      ...> Scenic.Scrollable.Direction.subtract(three, seven)
      {:vertical, -4}

      iex> five = Scenic.Scrollable.Direction.as_horizontal(5)
      ...> six = Scenic.Scrollable.Direction.as_vertical(6)
      ...> Scenic.Scrollable.Direction.subtract(five, six)
      {:horizontal, 5}

      iex> non_numeric_value = Scenic.Scrollable.Direction.as_horizontal(:non_numeric_value)
      ...> six = Scenic.Scrollable.Direction.as_vertical(6)
      ...> Scenic.Scrollable.Direction.subtract(non_numeric_value, six)
      {:horizontal, :non_numeric_value}
  """
  @spec subtract(t, t) :: t
  def subtract({:horizontal, x}, {:horizontal, y}) when is_number(x) and is_number(y),
    do: {:horizontal, x - y}

  def subtract({:vertical, x}, {:vertical, y}) when is_number(x) and is_number(y),
    do: {:vertical, x - y}

  def subtract({:horizontal, x}, _), do: {:horizontal, x}
  def subtract({:vertical, x}, _), do: {:vertical, x}

  @doc """
  Multiply two `t:Scenic.Scrollable.Direction.t/0` values. Only values associated with the same direction will be multiplied. Non numeric values are ignored.

  ## Examples

      iex> five = Scenic.Scrollable.Direction.as_horizontal(5)
      ...> six = Scenic.Scrollable.Direction.as_horizontal(6)
      ...> Scenic.Scrollable.Direction.multiply(five, six)
      {:horizontal, 30}

      iex> three = Scenic.Scrollable.Direction.as_vertical(3)
      ...> seven = Scenic.Scrollable.Direction.as_vertical(7)
      ...> Scenic.Scrollable.Direction.multiply(three, seven)
      {:vertical, 21}

      iex> five = Scenic.Scrollable.Direction.as_horizontal(5)
      ...> six = Scenic.Scrollable.Direction.as_vertical(6)
      ...> Scenic.Scrollable.Direction.multiply(five, six)
      {:horizontal, 5}

      iex> non_numeric_value = Scenic.Scrollable.Direction.as_horizontal(:non_numeric_value)
      ...> six = Scenic.Scrollable.Direction.as_vertical(6)
      ...> Scenic.Scrollable.Direction.multiply(non_numeric_value, six)
      {:horizontal, :non_numeric_value}
  """
  @spec multiply(t, t) :: t
  def multiply({:horizontal, x}, {:horizontal, y}) when is_number(x) and is_number(y),
    do: {:horizontal, x * y}

  def multiply({:vertical, x}, {:vertical, y}) when is_number(x) and is_number(y),
    do: {:vertical, x * y}

  def multiply({:horizontal, x}, _), do: {:horizontal, x}
  def multiply({:vertical, x}, _), do: {:vertical, x}

  def multiply(x, y, z) do
    multiply(x, y)
    |> multiply(z)
  end

  @doc """
  Divide two `t:Scenic.Scrollable.Direction.t/0` values. Only values associated with the same direction will be divided. Non numeric values are ignored.

  ## Examples

      iex> fifty = Scenic.Scrollable.Direction.as_horizontal(50)
      ...> ten = Scenic.Scrollable.Direction.as_horizontal(10)
      ...> Scenic.Scrollable.Direction.divide(fifty, ten)
      {:horizontal, 5.0}

      iex> nine = Scenic.Scrollable.Direction.as_vertical(9)
      ...> three = Scenic.Scrollable.Direction.as_vertical(3)
      ...> Scenic.Scrollable.Direction.divide(nine, three)
      {:vertical, 3.0}

      iex> six = Scenic.Scrollable.Direction.as_horizontal(6)
      ...> two = Scenic.Scrollable.Direction.as_vertical(2)
      ...> Scenic.Scrollable.Direction.divide(six, two)
      {:horizontal, 6}

      iex> non_numeric_value = Scenic.Scrollable.Direction.as_horizontal(:non_numeric_value)
      ...> six = Scenic.Scrollable.Direction.as_vertical(6)
      ...> Scenic.Scrollable.Direction.divide(non_numeric_value, six)
      {:horizontal, :non_numeric_value}
  """
  @spec divide(t, t) :: t
  def divide({:horizontal, x}, {:horizontal, y}) when is_number(x) and is_number(y),
    do: {:horizontal, x / y}

  def divide({:vertical, x}, {:vertical, y}) when is_number(x) and is_number(y),
    do: {:vertical, x / y}

  def divide({:horizontal, x}, _), do: {:horizontal, x}
  def divide({:vertical, x}, _), do: {:vertical, x}

  @doc """
  Apply a function only if the `t:Scenic.Scrollable.Direction.t\0` is associated with the horizontal direction.
  Returns a new `t:Scenic.Scrollable.Direction.t\0`.

  ## Examples

      iex> Scenic.Scrollable.Direction.map_horizontal({:horizontal, 5}, & &1 * 2)
      {:horizontal, 10}

      iex> Scenic.Scrollable.Direction.map_horizontal({:vertical, 5}, & &1 * 2)
      {:vertical, 5}
  """
  @spec map_horizontal(t, (term -> term)) :: t
  def map_horizontal({:horizontal, x}, fun), do: {:horizontal, fun.(x)}
  def map_horizontal(x, _), do: x

  @doc """
  Apply a function only if the `t:Scenic.Scrollable.Direction.t\0` is associated with the vertical direction.
  Returns a new `t:Scenic.Scrollable.Direction.t\0`.

  ## Examples

      iex> Scenic.Scrollable.Direction.map_vertical({:vertical, 5}, & &1 * 2)
      {:vertical, 10}

      iex> Scenic.Scrollable.Direction.map_vertical({:horizontal, 5}, & &1 * 2)
      {:horizontal, 5}
  """
  @spec map_vertical(t, (term -> term)) :: t
  def map_vertical({:vertical, x}, fun), do: {:vertical, fun.(x)}
  def map_vertical(x, _), do: x

  @doc """
  Apply a function to the `t:Scenic.Scrollable.Direction.t\0` inner value.
  Returns a new `t:Scenic.Scrollable.Direction.t\0`.

  ## Examples

      iex> Scenic.Scrollable.Direction.map({:horizontal, 5}, & &1 * 2)
      {:horizontal, 10}

      iex> Scenic.Scrollable.Direction.map({:vertical, 5}, & &1 * 2)
      {:vertical, 10}
  """
  @spec map(t, (term -> term)) :: t
  def map({direction, value}, fun), do: {direction, fun.(value)}
end