lib/slots.ex

defmodule Tempus.Slots do
  @moduledoc """
  The interface to deal with the implementation of `Tempus.Slot` ordered collection.

  Comes with several default implementations, backed up by ordered list of slots,
    by `Stream`, by `AVLTree`¹, and by Year-Month² tree.

  ### Examples

      iex> slots = [
      ...>   Tempus.Slot.wrap(~D|2020-08-07|),
      ...>   Tempus.Slot.wrap(~D|2020-08-10|),
      ...>   %Tempus.Slot{
      ...>       from: ~U|2020-08-07 01:00:00Z|, to: ~U|2020-08-08 01:00:00Z|}]
      ...> Enum.into(slots, %Tempus.Slots{})
      %Tempus.Slots{slots: %Tempus.Slots.List{slots: [
        %Tempus.Slot{from: ~U[2020-08-07 00:00:00.000000Z], to: ~U[2020-08-08 01:00:00Z]},
        %Tempus.Slot{from: ~U[2020-08-10 00:00:00.000000Z], to: ~U[2020-08-10 23:59:59.999999Z]}]}}
      iex> Enum.map(slots, & &1.from)
      [~U[2020-08-07 00:00:00.000000Z], ~U[2020-08-10 00:00:00.000000Z], ~U[2020-08-07 01:00:00Z]]
  """

  alias Tempus.{Slot, Slots, Slots.Group}

  import Tempus.Guards

  @type container :: Enumerable.t(Slot.t())
  @type implementation(group, exact_implementation) :: %{
          __struct__: group,
          slots: exact_implementation
        }
  @type implementation(group) :: implementation(group, container())
  @type t(group, exact_implementation) :: %{
          __struct__: Slots,
          slots: implementation(group, exact_implementation)
        }
  @type t(group) :: t(group, implementation(group))
  @type t :: t(Group.t())
  @typedoc "The type to use in navigation and/or rewinding slots enumerables"
  @type locator :: Slot.origin() | (Slot.t() -> :gt | :eq | :lt | true | false)

  @implementation Application.compile_env(:tempus, :implementation, Tempus.Slots.List)

  defstruct slots: struct!(@implementation, [])

  @doc """
  Creates an instance of slots, using a backend given as a parameter.

  ### Examples

      iex> import Tempus.Sigils
      iex> slots = [
      ...>   ~I(2000-01-01T00:00:00.000000Z → 2000-01-01T23:59:59.999999Z),
      ...>   ~I(2010-01-01T00:00:00.000000Z → 2010-01-01T23:59:59.999999Z),
      ...>   ~I(2020-01-01T00:00:00.000000Z → 2020-01-01T23:59:59.999999Z)
      ...> ]
      iex> Tempus.Slots.new([~D|2000-01-01|, ~D|2010-01-01|, ~D|2020-01-01|])
      %Tempus.Slots{slots: Enum.into(slots, Tempus.Slots.List.new())}
  """
  @spec new(implementation :: :list | :data | module(), data :: Enumerable.t()) ::
          Tempus.Slots.t()
  def new(implementation \\ @implementation, data)
  def new(@implementation, %implementation{} = data), do: new(implementation, data)
  def new(:list, data), do: new(Slots.List, data)
  def new(:stream, data), do: new(Slots.Stream, data)
  def new(implementation, data), do: %Slots{slots: Enum.into(data, implementation.new())}

  @spec size(t()) :: non_neg_integer()
  @doc deprecated: "Use `count/1` instead"
  @doc false
  def size(%Slots{} = slots), do: count(slots)

  @spec count(t()) :: non_neg_integer()
  @doc """
  Returns the number of slots

  ### Examples

      iex> import Tempus.Sigils
      iex> slots = [
      ...>   ~I(2000-01-01T00:00:00.000000Z → 2000-01-01T23:59:59.999999Z),
      ...>   ~I(2010-01-01T00:00:00.000000Z → 2010-01-01T23:59:59.999999Z),
      ...>   ~I(2020-01-01T00:00:00.000000Z → 2020-01-01T23:59:59.999999Z)
      ...> ]
      iex> slots |> Tempus.Slots.new() |> Tempus.Slots.count()
      3
  """
  def count(%Slots{} = slots), do: Enum.count(slots)

  @spec identity(t()) :: container()

  @doc false
  def identity(%Slots{slots: slots}), do: %Slots{slots: Group.identity(slots)}

  @spec flatten(t(), keyword()) :: [Slot.t()]
  @doc false
  def flatten(%Slots{slots: slots}, options \\ []), do: Group.flatten(slots, options)

  @spec split(t(), locator(), keyword()) :: {t(), t()}
  @doc false
  def split(%Slots{slots: %implementation{} = slots}, origin, options \\ [])
      when is_locator(origin) do
    case Group.split(slots, origin, options) do
      {:ok, head, tail} ->
        {%Slots{slots: head}, %Slots{slots: tail}}

      {:error, _} ->
        %Slots.List{slots: Group.flatten(slots, options)}
        |> Slots.List.split(origin)
        |> Tuple.to_list()
        |> Enum.map(&wrap(&1, Keyword.put(options, :implementation, implementation)))
        |> List.to_tuple()
    end
  end

  @spec drop_until(t(), locator()) :: t()
  @spec drop_until(t(), locator(), keyword()) :: t()
  def drop_until(%Slots{} = slots, pivot, options \\ []),
    do: slots |> split(pivot, options) |> elem(1)

  @spec take_until(t(), locator()) :: t()
  @spec take_until(t(), locator(), keyword()) :: t()
  def take_until(%Slots{} = slots, pivot, options \\ []),
    do: slots |> split(pivot, options) |> elem(0)

  @spec span(t() | [Slot.t()], non_neg_integer(), unit :: System.time_unit()) :: Enumerable.t()
  def span(slots, duration, unit \\ :second) do
    duration_in_microseconds = System.convert_time_unit(duration, unit, :microsecond)

    slots
    |> Enum.reduce_while({[], duration_in_microseconds}, fn
      %Slot{from: nil} = slot, {[], _ms} ->
        {:halt, [slot]}

      %Slot{from: from, to: nil}, {slots, ms} ->
        {:halt,
         Enum.reverse([%Slot{from: from, to: DateTime.add(from, ms, :microsecond)} | slots])}

      %Slot{from: from} = slot, {slots, ms} ->
        duration = Slot.duration(slot, :microsecond)

        if duration < ms do
          {:cont, {[slot | slots], ms - duration}}
        else
          {:halt,
           Enum.reverse([%Slot{from: from, to: DateTime.add(from, ms, :microsecond)} | slots])}
        end
    end)
    |> case do
      {slots, _duration} -> slots
      slots -> slots
    end
  end

  @spec add(slots :: t(), slot :: Slot.origin()) :: t()
  @doc """
  Adds another slot to the slots collection.

  Joins slots intersecting with the new one, if any.

  ### Example

      iex> Tempus.Slots.add(%Tempus.Slots{}, Tempus.Slot.wrap(~D|2020-08-07|))
      %Tempus.Slots{slots: %Tempus.Slots.List{slots: [
        %Tempus.Slot{from: ~U[2020-08-07 00:00:00.000000Z], to: ~U[2020-08-07 23:59:59.999999Z]}]}}

      iex> %Tempus.Slots{}
      ...> |> Tempus.Slots.add(Tempus.Slot.wrap(~D|2020-08-07|))
      ...> |> Tempus.Slots.add(Tempus.Slot.wrap(~D|2020-08-10|))
      ...> |> Tempus.Slots.add(%Tempus.Slot{
      ...>       from: ~U|2020-08-07 01:00:00Z|, to: ~U|2020-08-08 01:00:00Z|})
      %Tempus.Slots{slots: %Tempus.Slots.List{slots: [
        %Tempus.Slot{from: ~U[2020-08-07 00:00:00.000000Z], to: ~U[2020-08-08 01:00:00Z]},
        %Tempus.Slot{from: ~U[2020-08-10 00:00:00.000000Z], to: ~U[2020-08-10 23:59:59.999999Z]}]}}
  """
  def add(%Slots{slots: slots}, slot, options \\ []) when is_origin(slot),
    do: %Slots{slots: Group.add(slots, Slot.wrap(slot), options)}

  @spec merge(slots :: t() | [t()], Enumerable.t(Slot.t()) | keyword(), keyword()) :: t()
  @doc """
  Merges many slots into the first element in the list given as an argument.

  Other arguments might be enumerables, the first one must be a `Tempus.Slots` instance.

  ### Examples

      iex> slots = [
      ...>   Tempus.Slot.wrap(~D|2020-08-07|),
      ...>   Tempus.Slot.wrap(~D|2020-08-10|)
      ...> ] |> Enum.into(%Tempus.Slots{})
      %Tempus.Slots{slots: %Tempus.Slots.List{slots: [
        %Tempus.Slot{from: ~U[2020-08-07 00:00:00.000000Z], to: ~U[2020-08-07 23:59:59.999999Z]},
        %Tempus.Slot{from: ~U[2020-08-10 00:00:00.000000Z], to: ~U[2020-08-10 23:59:59.999999Z]}]}}
      iex> other = [
      ...>   %Tempus.Slot{from: ~U|2020-08-07 23:00:00Z|, to: ~U|2020-08-08 12:00:00Z|},
      ...>   %Tempus.Slot{from: ~U|2020-08-12 23:00:00Z|, to: ~U|2020-08-12 23:30:00Z|}
      ...> ]
      iex> Tempus.Slots.merge([slots, other])
      %Tempus.Slots{slots: %Tempus.Slots.List{slots: [
        %Tempus.Slot{from: ~U[2020-08-07 00:00:00.000000Z], to: ~U[2020-08-08 12:00:00Z]},
        %Tempus.Slot{from: ~U[2020-08-10 00:00:00.000000Z], to: ~U[2020-08-10 23:59:59.999999Z]},
        %Tempus.Slot{from: ~U[2020-08-12 23:00:00Z], to: ~U[2020-08-12 23:30:00Z]}]}}
      iex> Tempus.Slots.merge([slots, Tempus.Slots.wrap(other, Tempus.Slots.Stream)]) |> Enum.to_list()
      [%Tempus.Slot{from: ~U[2020-08-07 00:00:00.000000Z], to: ~U[2020-08-08 12:00:00Z]},
       %Tempus.Slot{from: ~U[2020-08-10 00:00:00.000000Z], to: ~U[2020-08-10 23:59:59.999999Z]},
       %Tempus.Slot{from: ~U[2020-08-12 23:00:00Z], to: ~U[2020-08-12 23:30:00Z]}]

  """
  def merge(slots, enum \\ [], options \\ [])
  def merge(%Slots{} = slots, enum, options), do: do_merge([slots, enum], options)
  def merge(slots, options, []), do: do_merge(slots, options)

  defp do_merge([], _options), do: %Slots.Void{}
  defp do_merge([%Slots{} = slots], _options), do: slots

  defp do_merge([%Slots{slots: %_{} = head} = slots, %Slots{slots: %_{} = next} | rest], options) do
    case Group.merge(head, next, options) do
      {:ok, merged} -> merge([%Slots{slots: merged} | rest], options)
      {:error, _} -> do_merge([slots, Group.flatten(next, options) | rest], options)
    end
  end

  defp do_merge([%Slots{slots: %_{} = head}, next | rest], options) when is_list(next) do
    next = Enum.reduce(next, head, &Group.add(&2, &1, options))
    merge([%Slots{slots: next} | rest], options)
  end

  @spec inverse(slots :: Slots.t(), keyword()) :: Slots.t()
  @doc """
  Inverses `Slots` returning the new `Slots` instance with slots set where
    there were blanks.

  ### Example

      iex> [
      ...>   Tempus.Slot.wrap(~D|2020-08-07|),
      ...>   Tempus.Slot.wrap(~D|2020-08-08|),
      ...>   Tempus.Slot.wrap(~D|2020-08-10|),
      ...>   Tempus.Slot.wrap(~D|2020-08-12|)
      ...> ] |> Enum.into(%Tempus.Slots{})
      ...> |> Tempus.Slots.inverse()
      %Tempus.Slots{slots: %Tempus.Slots.List{slots: [
        %Tempus.Slot{from: nil, to: ~U[2020-08-06 23:59:59.999999Z]},
        %Tempus.Slot{from: ~U[2020-08-09 00:00:00.000000Z], to: ~U[2020-08-09 23:59:59.999999Z]},
        %Tempus.Slot{from: ~U[2020-08-11 00:00:00.000000Z], to: ~U[2020-08-11 23:59:59.999999Z]},
        %Tempus.Slot{from: ~U[2020-08-13 00:00:00.000000Z], to: nil}]}}

      iex> [
      ...>   %Tempus.Slot{to: ~U[2020-08-08 23:59:59.999999Z]},
      ...>   Tempus.Slot.wrap(~D|2020-08-10|),
      ...>   %Tempus.Slot{from: ~U[2020-08-12 00:00:00.000000Z]}
      ...> ] |> Enum.into(%Tempus.Slots{})
      ...> |> Tempus.Slots.inverse()
      %Tempus.Slots{slots: %Tempus.Slots.List{slots: [
        %Tempus.Slot{from: ~U[2020-08-09 00:00:00.000000Z], to: ~U[2020-08-09 23:59:59.999999Z]},
        %Tempus.Slot{from: ~U[2020-08-11 00:00:00.000000Z], to: ~U[2020-08-11 23:59:59.999999Z]}]}}
  """
  def inverse(slots, options \\ [])

  def inverse(%Slots{slots: slots}, options) do
    case Group.inverse(slots, options) do
      {:ok, inversed} -> %Slots{slots: inversed}
      {:error, _} -> raise "Not yet implemented"
    end
  end

  @spec wrap([Slot.origin()] | Slots.t(), keyword()) :: Slots.t()
  @doc since: "0.3.0"
  @doc """
  Wraps the argument into a slots instance. For `nil` it’d be an empty slots.
  For everything else it’d call `Slot.wrap/1` on an argument and add it to empty slots.

  ## Examples

      iex> Tempus.Slots.wrap(~D|2020-08-06|)
      %Tempus.Slots{slots: %Tempus.Slots.List{slots: [
        %Tempus.Slot{from: ~U[2020-08-06 00:00:00.000000Z], to: ~U[2020-08-06 23:59:59.999999Z]}]}}
  """
  def wrap(any, options \\ [])

  def wrap(any, implementation) when is_atom(implementation),
    do: do_wrap(any, implementation, [])

  def wrap(any, options) when is_list(options) do
    {implementation, options} = Keyword.pop(options, :implementation, @implementation)
    do_wrap(any, implementation, options)
  end

  defp do_wrap(%Slots{slots: %implementation{}} = slots, implementation, _options), do: slots

  defp do_wrap(%Slots{slots: slots}, implementation, options) do
    slots |> Group.flatten(options) |> do_wrap(implementation, options)
  end

  defp do_wrap(slots, implementation, options) do
    merge(%Slots{slots: implementation.new()}, List.wrap(slots), options)
  end

  defimpl Enumerable do
    @moduledoc false
    def reduce(%Slots{slots: %_{} = slots}, acc, fun),
      do: Enumerable.reduce(slots, acc, fun)

    def member?(%Slots{slots: _slots}, _value), do: {:error, __MODULE__}
    def count(%Slots{slots: _slots}), do: {:error, __MODULE__}
    def slice(%Slots{slots: _slots}), do: {:error, __MODULE__}
  end

  defimpl Collectable do
    @moduledoc false
    alias Tempus.Slots

    def into(original) do
      {
        original,
        fn
          slots, {:cont, value} -> Slots.add(slots, value)
          slots, :done -> slots
          _, :halt -> :ok
        end
      }
    end
  end

  defimpl Inspect do
    @moduledoc false
    import Inspect.Algebra

    def inspect(%Tempus.Slots{slots: slots}, opts) do
      concat(["#𝕋<", to_doc(slots, opts), ">"])
    end
  end
end