defmodule Xandra.Page do
  @moduledoc ~S"""
  A struct that represents a page of rows.

  This struct represents a page of rows that have been returned by the
  Cassandra server in response to a query such as `SELECT`, but have not yet
  been parsed into Elixir values.

  This struct implements the `Enumerable` protocol and is therefore a stream. It
  is through this protocol that a `Xandra.Page` struct can be parsed into Elixir
  values. The simplest way of getting a list of single rows out of a
  `Xandra.Page` struct is to use something like `Enum.to_list/1`. Each element
  emitted when streaming out of a `Xandra.Page` struct is a map of string column
  names to their corresponding value.

  See [`%Xandra.Page{}`](`__struct__/0`) for information about which fields are public.

  ## Examples

      statement = "SELECT name, age FROM users"

      %Xandra.Page{} = page = Xandra.execute!(conn, statement, _params = [])

      Enum.each(page, fn %{"name" => name, "age" => age} ->
        IO.puts("Read user with name #{name} (age #{age}) out of the database")
      end)

  """

  @doc """
  The page struct.

  The following fields are public and can be accessed or relied on:

    * `:paging_state` - the current paging state, or `nil` if no paging is occurring.
      Its value can be used to check whether more pages are available to fetch
      after the given page. This is useful when implementing manual paging.
      See also the documentation for `Xandra.execute/4`.

    * `:tracing_id` - the tracing ID (as a UUID binary) if tracing was enabled,
      or `nil` if no tracing was enabled. See the "Tracing" section in `Xandra.execute/4`.

  """
  defstruct [:paging_state, :tracing_id, :custom_payload, :columns, content: []]

  @typedoc """
  The paging state of a page.

  This is intended to be an "opaque" binary value that you can use for further
  pagination. See `Xandra.execute/4`.
  """
  @type paging_state() :: binary()

  @typedoc false
  @typedoc since: "0.18.0"
  @type column() :: {
          keyspace :: String.t(),
          table :: String.t(),
          column_name :: String.t(),
          type :: term()
        }

  @typedoc """
  The type for the page struct.

  The only public fields here are `:paging_state` and `:tracing_id`.
  See [`%Xandra.Page{}`](`__struct__/0`).
  """
  @type t :: %__MODULE__{
          content: [term()],
          columns: [column()] | nil,
          paging_state: paging_state() | nil,
          tracing_id: binary() | nil,
          custom_payload: Xandra.custom_payload() | nil
        }

  defimpl Enumerable do
    def reduce(%@for{content: content, columns: columns}, acc, fun) do
      reduce(content, columns, acc, fun)
    end

    def member?(_page, _value), do: {:error, __MODULE__}

    def count(_page), do: {:error, __MODULE__}

    def slice(_page), do: {:error, __MODULE__}

    defp reduce(_content, _columns, {:halt, acc}, _fun) do
      {:halted, acc}
    end

    defp reduce(content, columns, {:suspend, acc}, fun) do
      {:suspended, acc, &reduce(content, columns, &1, fun)}
    end

    defp reduce([values | rest], columns, {:cont, acc}, fun) do
      row = zip(values, columns, []) |> :maps.from_list()
      reduce(rest, columns, fun.(row, acc), fun)
    end

    defp reduce([], _columns, {:cont, acc}, _fun) do
      {:done, acc}
    end

    defp zip([], [], acc), do: acc

    defp zip([value | values], [{_, _, name, _} | columns], acc) do
      zip(values, columns, [{name, value} | acc])
    end
  end

  defimpl Inspect do
    import Inspect.Algebra

    def inspect(page, options) do
      properties = [
        rows: Enum.to_list(page),
        tracing_id: page.tracing_id,
        more_pages?: page.paging_state != nil
      ]

      concat(["#Xandra.Page<", to_doc(properties, options), ">"])
    end
  end
end
