defmodule Aja.Enum do
  @moduledoc """
  Drop-in replacement for the `Enum` module, optimized to work with Aja's data structures such as `Aja.Vector`.

  It currently only covers a subset of `Enum`, but `Aja.Enum` aims to completely mirror the API of `Enum`,
  and should behave exactly the same for any type of `Enumerable`.
  The only expected difference should be a significant increase in performance for Aja structures.

  ## Rationale

  Structures such as `Aja.Vector` or `Aja.OrdMap` are implementing the `Enumerable` protocol, which means they can be
  used directly with the `Enum` module. The `Enumerable` protocol however comes with its overhead and is strongly
  limited in terms of performance.

  On the other hand, `Aja.Enum` provides hand-crafted highly-optimized functions that fully take advantage of
  immutable vectors. The speedup can easily reach more than a factor 10 compared to `Enum` used on non-list
  structures, and sometimes even be noticeably faster than `Enum` used over lists.

  One of the main reasons to adopt a specific data structure is the performance.
  Using vectors with `Enum` would defeat the purpose, hence the introduction of `Aja.Enum`.

      iex> vector = Aja.Vector.new(1..10000)
      iex> Enum.sum(vector)      # slow
      50005000
      iex> Aja.Enum.sum(vector)  # same result, much faster
      50005000

  """

  require Aja.Vector.Raw, as: RawVector
  alias Aja.EnumHelper, as: H
  alias Aja.RandomHelper

  @compile :inline_list_funcs

  @type index :: integer
  @type value :: any
  @type t(value) :: Aja.Vector.t(value) | [value] | Enumerable.t()

  @empty_vector RawVector.empty()

  # TODO optimize ranges (sum, random...)

  @doc """
  Converts `enumerable` to a list.

  Mirrors `Enum.to_list/1` with higher performance for Aja structures.
  """
  @spec to_list(t(val)) :: [val] when val: value
  defdelegate to_list(enumerable), to: H

  @doc """
  Returns the size of the `enumerable`.

  Mirrors `Enum.count/1` with higher performance for Aja structures.
  """
  @spec count(t(any)) :: non_neg_integer
  def count(enumerable) do
    case enumerable do
      list when is_list(list) -> length(list)
      %Aja.Vector{__vector__: vector} -> RawVector.size(vector)
      %Aja.OrdMap{__ord_map__: map} -> map_size(map)
      %MapSet{} -> MapSet.size(enumerable)
      _.._//_ -> Range.size(enumerable)
      _ -> Enum.count(enumerable)
    end
  end

  @doc """
  Returns the count of elements in the `enumerable` for which `fun` returns
  a truthy value.

  Mirrors `Enum.count/2` with higher performance for Aja structures.
  """
  @spec count(t(val), (val -> as_boolean(term))) :: non_neg_integer when val: value
  def count(enumerable, fun) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil ->
        Enum.count(enumerable, fun)

      list when is_list(list) ->
        count_list(list, fun, 0)

      vector ->
        RawVector.count(vector, fun)
    end
  end

  defp count_list([], _fun, acc), do: acc

  defp count_list([head | tail], fun, acc) do
    new_acc =
      if fun.(head) do
        acc + 1
      else
        acc
      end

    count_list(tail, fun, new_acc)
  end

  @doc """
  Returns `true` if `enumerable` is empty, otherwise `false`.

  Mirrors `Enum.empty?/1` with higher performance for Aja structures.
  """
  @spec empty?(t(any)) :: boolean
  def empty?(enumerable) do
    case enumerable do
      list when is_list(list) -> list == []
      %Aja.Vector{__vector__: vector} -> vector === @empty_vector
      %Aja.OrdMap{__ord_map__: map} -> map == %{}
      %MapSet{} -> MapSet.size(enumerable) == 0
      first..last//step -> empty_range?(first, last, step)
      _ -> Enum.empty?(enumerable)
    end
  end

  defp empty_range?(first, last, step) when step > 0, do: first > last
  defp empty_range?(first, last, step) when step < 0, do: first < last

  # Note: Could not optimize it noticeably for vectors
  @doc """
  Checks if `element` exists within the `enumerable`.

  Just an alias for `Enum.member?/2`, does not improve performance.
  """
  @spec member?(t(val), val) :: boolean when val: value
  defdelegate member?(enumerable, value), to: Enum

  # TODO optimize for vector
  @doc """
  Returns a subset list of the given `enumerable` by `index_range`.

  Mirrors `Enum.slice/2` with higher performance for Aja structures.
  """
  @spec slice(t(val), Range.t()) :: [val] when val: value
  defdelegate slice(enumerable, index_range), to: Enum

  @doc """
  Returns a subset list of the given `enumerable`, from `start_index` (zero-based)
  with `amount` number of elements if available.

  Mirrors `Enum.slice/3`.
  """
  @spec slice(t(val), index, non_neg_integer) :: [val] when val: value
  defdelegate slice(enumerable, start_index, amount), to: Enum

  @doc """
  Inserts the given `enumerable` into a `collectable`.

  Mirrors `Enum.into/2` with higher performance for Aja structures.
  """
  @spec into(t(val), Collectable.t()) :: Collectable.t() when val: value
  def into(enumerable, collectable)

  def into(enumerable, %Aja.Vector{} = vector) do
    # TODO improve when this is the empty vector/ord_map
    Aja.Vector.concat(vector, enumerable)
  end

  def into(enumerable, %Aja.OrdMap{} = ord_map) do
    Aja.OrdMap.merge_list(ord_map, H.to_list(enumerable))
  end

  def into(enumerable, collectable) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> enumerable
      list when is_list(list) -> list
      vector -> RawVector.to_list(vector)
    end
    |> Enum.into(collectable)
  end

  @doc """
  Inserts the given `enumerable` into a `collectable` according to the `transform` function.

  Mirrors `Enum.into/3` with higher performance for Aja structures.
  """
  def into(enumerable, collectable, transform)

  def into(enumerable, %Aja.Vector{} = vector, transform) do
    # TODO we can probably improve this with the builder
    Aja.Vector.concat(vector, H.map(enumerable, transform))
  end

  def into(enumerable, %Aja.OrdMap{} = ord_map, transform) do
    Aja.OrdMap.merge_list(ord_map, H.map(enumerable, transform))
  end

  def into(enumerable, collectable, transform) when is_function(transform, 1) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> enumerable
      list when is_list(list) -> list
      vector -> RawVector.to_list(vector)
    end
    |> Enum.into(collectable, transform)
  end

  @doc """
  Given an enumerable of enumerables, concatenates the `enumerables` into
  a single list.

  Mirrors `Enum.concat/1` with higher performance for Aja structures.
  """
  @spec concat(t(t(val))) :: t(val) when val: value
  def concat(enumerables) do
    case H.try_get_raw_vec_or_list(enumerables) do
      nil -> Enum.reverse(enumerables) |> concat_wrap([])
      list when is_list(list) -> :lists.reverse(list) |> concat_wrap([])
      vector -> RawVector.foldr(vector, [], &concat/2)
    end
  end

  defp concat_wrap(_reversed = [], acc), do: acc

  defp concat_wrap([head | tail], acc) do
    concat_wrap(tail, concat(head, acc))
  end

  @doc """
  Concatenates the enumerable on the `right` with the enumerable on the `left`.

  Mirrors `Enum.concat/2` with higher performance for Aja structures.
  """
  @spec concat(t(val), t(val)) :: t(val) when val: value
  def concat(left, right)

  def concat(left, right) when is_list(left) and is_list(right) do
    left ++ right
  end

  def concat(left, right) do
    case H.try_get_raw_vec_or_list(left) do
      nil -> Enum.concat(left, right)
      list when is_list(list) -> list ++ to_list(right)
      vector -> RawVector.to_list(vector, to_list(right))
    end
  end

  @doc """
  Finds the element at the given `index` (zero-based).

  Mirrors `Enum.at/3` with higher performance for Aja structures.
  """
  @spec at(t(val), integer, default) :: val | default when val: value, default: any
  def at(enumerable, index, default \\ nil) when is_integer(index) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil ->
        Enum.at(enumerable, index, default)

      list when is_list(list) ->
        Enum.at(list, index, default)

      vector ->
        size = RawVector.size(vector)

        case RawVector.actual_index(index, size) do
          nil -> default
          actual_index -> RawVector.fetch_positive!(vector, actual_index)
        end
    end
  end

  @doc """
  Finds the element at the given `index` (zero-based).

  Mirrors `Enum.fetch/2` with higher performance for Aja structures.
  """
  @spec fetch(t(val), integer) :: {:ok, val} | :error when val: value
  def fetch(enumerable, index) when is_integer(index) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil ->
        Enum.fetch(enumerable, index)

      list when is_list(list) ->
        Enum.fetch(list, index)

      vector ->
        size = RawVector.size(vector)

        case RawVector.actual_index(index, size) do
          nil -> :error
          actual_index -> {:ok, RawVector.fetch_positive!(vector, actual_index)}
        end
    end
  end

  @doc """
  Finds the element at the given `index` (zero-based).

  Mirrors `Enum.fetch!/2` with higher performance for Aja structures.
  """
  @spec fetch!(t(val), integer) :: val when val: value
  def fetch!(enumerable, index) when is_integer(index) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil ->
        Enum.fetch!(enumerable, index)

      list when is_list(list) ->
        Enum.fetch!(list, index)

      vector ->
        size = RawVector.size(vector)

        case RawVector.actual_index(index, size) do
          nil -> raise Enum.OutOfBoundsError
          actual_index -> RawVector.fetch_positive!(vector, actual_index)
        end
    end
  end

  @doc """
  Returns a list of elements in `enumerable` in reverse order.

  Mirrors `Enum.reverse/1` with higher performance for Aja structures.
  """
  @spec reverse(t(val)) :: [val] when val: value
  def reverse(enumerable) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.reverse(enumerable)
      list when is_list(list) -> :lists.reverse(list)
      vector -> RawVector.reverse_to_list(vector, [])
    end
  end

  @doc """
  Reverses the elements in `enumerable`, concatenates the `tail`,
  and returns it as a list.

  Mirrors `Enum.reverse/2` with higher performance for Aja structures.
  """
  @spec reverse(t(val), t(val)) :: [val] when val: value
  def reverse(enumerable, tail) do
    tail = H.to_list(tail)

    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.reverse(enumerable, tail)
      list when is_list(list) -> :lists.reverse(list, tail)
      vector -> RawVector.reverse_to_list(vector, tail)
    end
  end

  @doc """
  Returns a list where each element is the result of invoking
  `fun` on each corresponding element of `enumerable`.

  Mirrors `Enum.map/2` with higher performance for Aja structures.
  """
  @spec map(t(v1), (v1 -> v2)) :: [v2] when v1: value, v2: value
  defdelegate map(enumerable, fun), to: H

  @doc """
  Filters the `enumerable`, i.e. returns only those elements
  for which `fun` returns a truthy value.

  Mirrors `Enum.filter/2` with higher performance for Aja structures.
  """
  @spec filter(t(val), (val -> as_boolean(term))) :: [val] when val: value
  def filter(enumerable, fun) when is_function(fun, 1) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.filter(enumerable, fun)
      list when is_list(list) -> filter_list(list, fun, [])
      vector -> RawVector.filter_to_list(vector, fun)
    end
  end

  defp filter_list([], _fun, acc), do: :lists.reverse(acc)

  defp filter_list([head | tail], fun, acc) do
    acc =
      if fun.(head) do
        [head | acc]
      else
        acc
      end

    filter_list(tail, fun, acc)
  end

  @doc """
  Returns a list of elements in `enumerable` excluding those for which the function `fun` returns
  a truthy value.

  Mirrors `Enum.reject/2` with higher performance for Aja structures.
  """
  @spec reject(t(val), (val -> as_boolean(term))) :: [val] when val: value
  def reject(enumerable, fun) when is_function(fun, 1) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.reject(enumerable, fun)
      list when is_list(list) -> Enum.reject(list, fun)
      vector -> RawVector.reject_to_list(vector, fun)
    end
  end

  @doc """
  Splits the `enumerable` in two lists according to the given function `fun`.

  Mirrors `Enum.split_with/2` with higher performance for Aja structures.
  """
  @spec split_with(t(val), (val -> as_boolean(term))) :: {[val], [val]} when val: value
  def split_with(enumerable, fun) when is_function(fun, 1) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.split_with(enumerable, fun)
      list when is_list(list) -> Enum.split_with(list, fun)
      vector -> vector |> RawVector.to_list() |> Enum.split_with(fun)
    end
  end

  @doc """
  Invokes `fun` for each element in the `enumerable` with the
  accumulator.

  Mirrors `Enum.reduce/2` with higher performance for Aja structures.
  """
  @spec reduce(t(val), (val, val -> val)) :: val when val: value
  def reduce(enumerable, fun) when is_function(fun, 2) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.reduce(enumerable, fun)
      list when is_list(list) -> Enum.reduce(list, fun)
      vector -> RawVector.reduce(vector, fun)
    end
  end

  @doc """
  Invokes `fun` for each element in the `enumerable` with the accumulator.

  Mirrors `Enum.reduce/3` with higher performance for Aja structures.
  """
  @spec reduce(t(val), acc, (val, acc -> acc)) :: acc when val: value, acc: term
  def reduce(enumerable, acc, fun) when is_function(fun, 2) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.reduce(enumerable, acc, fun)
      list when is_list(list) -> :lists.foldl(fun, acc, list)
      vector -> RawVector.foldl(vector, acc, fun)
    end
  end

  # FINDS

  @doc """
  Returns `true` if at least one element in `enumerable` is truthy.

  When an element has a truthy value (neither `false` nor `nil`) iteration stops
  immediately and `true` is returned. In all other cases `false` is returned.

  ## Examples

      iex> Aja.Enum.any?([false, false, false])
      false

      iex> Aja.Enum.any?([false, true, false])
      true

      iex> Aja.Enum.any?([])
      false

  """
  @spec any?(t(as_boolean(val))) :: boolean when val: value
  def any?(enumerable) do
    case enumerable do
      %Aja.Vector{__vector__: vector} -> RawVector.any?(vector)
      _ -> Enum.any?(enumerable)
    end
  end

  @doc """
  Returns `true` if `fun.(element)` is truthy for at least one element in `enumerable`.

  Iterates over the `enumerable` and invokes `fun` on each element. When an invocation
  of `fun` returns a truthy value (neither `false` nor `nil`) iteration stops
  immediately and `true` is returned. In all other cases `false` is returned.

  ## Examples

      iex> Aja.Enum.any?([2, 4, 6], fn x -> rem(x, 2) == 1 end)
      false

      iex> Aja.Enum.any?([2, 3, 4], fn x -> rem(x, 2) == 1 end)
      true

      iex> Aja.Enum.any?([], fn x -> x > 0 end)
      false

  """
  # TODO When only support Elixir 1.12
  # @doc copy_doc_for.(:any?, 2)
  @spec any?(t(val), (val -> as_boolean(term))) :: boolean when val: value
  def any?(enumerable, fun) when is_function(fun, 1) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.any?(enumerable, fun)
      list when is_list(list) -> Enum.any?(list, fun)
      vector -> RawVector.any?(vector, fun)
    end
  end

  @doc """
  Returns `true` if  all elements in `enumerable` are truthy.

  When an element has a falsy value (`false` or `nil`) iteration stops immediately
  and `false` is returned. In all other cases `true` is returned.

  ## Examples

      iex> Aja.Enum.all?([1, 2, 3])
      true

      iex> Aja.Enum.all?([1, nil, 3])
      false

      iex> Aja.Enum.all?([])
      true

  """
  @spec all?(t(as_boolean(val))) :: boolean when val: value
  def all?(enumerable) do
    case enumerable do
      %Aja.Vector{__vector__: vector} -> RawVector.all?(vector)
      _ -> Enum.all?(enumerable)
    end
  end

  @doc """
  Returns `true` if `fun.(element)` is truthy for all elements in `enumerable`.

  Iterates over `enumerable` and invokes `fun` on each element. If `fun` ever
  returns a falsy value (`false` or `nil`), iteration stops immediately and
  `false` is returned. Otherwise, `true` is returned.

  ## Examples

      iex> Aja.Enum.all?([2, 4, 6], fn x -> rem(x, 2) == 0 end)
      true

      iex> Aja.Enum.all?([2, 3, 4], fn x -> rem(x, 2) == 0 end)
      false

      iex> Aja.Enum.all?([], fn _ -> nil end)
      true

  """
  # TODO When only support Elixir 1.12
  # @doc copy_doc_for.(:all?, 2)
  @spec all?(t(val), (val -> as_boolean(term))) :: boolean when val: value
  def all?(enumerable, fun) when is_function(fun, 1) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.all?(enumerable, fun)
      list when is_list(list) -> Enum.all?(list, fun)
      vector -> RawVector.all?(vector, fun)
    end
  end

  @doc """
  Returns the first element for which `fun` returns a truthy value.
  If no such element is found, returns `default`.

  Mirrors `Enum.find/3` with higher performance for Aja structures.
  """
  @spec find(t(val), default, (val -> as_boolean(term))) :: val | default
        when val: value, default: value
  def find(enumerable, default \\ nil, fun) when is_function(fun, 1) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.find(enumerable, default, fun)
      list when is_list(list) -> Enum.find(list, default, fun)
      vector -> RawVector.find(vector, default, fun)
    end
  end

  @doc """
  Similar to `find/3`, but returns the value of the function
  invocation instead of the element itself.

  Mirrors `Enum.find_value/3` with higher performance for Aja structures.
  """
  @spec find_value(t(val), default, (val -> new_val)) :: new_val | default
        when val: value, new_val: value, default: value
  def find_value(enumerable, default \\ nil, fun) when is_function(fun, 1) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.find_value(enumerable, default, fun)
      list when is_list(list) -> Enum.find_value(list, default, fun)
      vector -> RawVector.find_value(vector, fun) || default
    end
  end

  @doc """
  Similar to `find/3`, but returns the index (zero-based)
  of the element instead of the element itself.

  Mirrors `Enum.find_index/2` with higher performance for Aja structures.
  """
  @spec find_index(t(val), (val -> as_boolean(term))) :: non_neg_integer | nil when val: value
  def find_index(enumerable, fun) when is_function(fun, 1) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.find_index(enumerable, fun)
      list when is_list(list) -> Enum.find_index(list, fun)
      vector -> RawVector.find_index(vector, fun)
    end
  end

  ## FOLDS

  @doc """
  Returns the sum of all elements.

  Mirrors `Enum.sum/1` with higher performance for Aja structures.

  Raises `ArithmeticError` if `enumerable` contains a non-numeric value.
  """
  @spec sum(t(num)) :: num when num: number
  def sum(enumerable) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.sum(enumerable)
      list when is_list(list) -> :lists.sum(list)
      vector -> RawVector.sum(vector)
    end
  end

  @doc """
  Maps and sums the given `enumerable` in one pass.

  Mirrors `Enum.sum_by/2` with higher performance for Aja structures.

  Raises `ArithmeticError` if `mapper` returns a non-numeric value.
  """
  @spec sum_by(t(val), (val -> num)) :: num when val: value, num: number
  def sum_by(enumerable, fun) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil ->
        # TODO use Enum.sum_by/1 for Elixir 1.18+
        reduce(enumerable, 0, fn el, acc -> fun.(el) + acc end)

      list when is_list(list) ->
        sum_by_list(list, fun, 0)

      vector ->
        RawVector.sum_by(vector, fun)
    end
  end

  defp sum_by_list([], _fun, acc), do: acc

  defp sum_by_list([head | rest], fun, acc) do
    sum_by_list(rest, fun, fun.(head) + acc)
  end

  @doc """
  Returns the product of all elements in the `enumerable`.

  Mirrors `Enum.product/1`.

  Raises `ArithmeticError` if `enumerable` contains a non-numeric value.

  ## Examples

      iex> Aja.Enum.product(1..5)
      120
      iex> Aja.Enum.product([])
      1

  """
  @spec product(t(num)) :: num when num: number
  def product(enumerable) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.product(enumerable)
      list when is_list(list) -> Enum.product(list)
      vector -> RawVector.product(vector)
    end
  end

  @doc """
  Maps and computes the product of the given `enumerable` in one pass.

  Mirrors `Enum.product_by/2`.

  Raises `ArithmeticError` if `mapper` returns a non-numeric value.

  ## Examples

      iex> Aja.Enum.product_by(1..3, & &1 + 1)
      24
      iex> Aja.Enum.product_by([], & &1 + 1)
      1

  """
  @spec product_by(t(val), (val -> num)) :: num when val: value, num: number
  def product_by(enumerable, fun) do
    case H.try_get_raw_vec_or_list(enumerable) do
      # TODO use Enum.product_by/1 for Elixir 1.18+
      nil -> Enum.reduce(enumerable, 1, fn el, acc -> fun.(el) * acc end)
      list when is_list(list) -> product_by_list(list, fun, 1)
      vector -> RawVector.product_by(vector, fun)
    end
  end

  defp product_by_list([], _fun, acc), do: acc

  defp product_by_list([head | rest], fun, acc) do
    product_by_list(rest, fun, fun.(head) * acc)
  end

  @doc """
  Joins the given `enumerable` into a string using `joiner` as a
  separator.

  Mirrors `Enum.join/2` with higher performance for Aja structures.
  """
  @spec join(t(val), String.t()) :: String.t() when val: String.Chars.t()
  def join(enumerable, joiner \\ "") when is_binary(joiner) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil ->
        Enum.join(enumerable, joiner)

      list when is_list(list) ->
        Enum.join(list, joiner)

      vector ->
        # TODO add join_as_iodata
        RawVector.join_as_iodata(vector, joiner) |> IO.iodata_to_binary()
    end
  end

  @doc """
  Maps and joins the given `enumerable` in one pass.

  Mirrors `Enum.map_join/3` with higher performance for Aja structures.
  """
  @spec map_join(t(val), String.t(), (val -> String.Chars.t())) :: String.t()
        when val: value
  def map_join(enumerable, joiner \\ "", mapper)
      when is_binary(joiner) and is_function(mapper, 1) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil ->
        Enum.map_join(enumerable, joiner, mapper)

      list when is_list(list) ->
        Enum.map_join(list, joiner, mapper)

      # TODO do this in one pass
      vector ->
        vector
        |> RawVector.map(mapper)
        |> RawVector.join_as_iodata(joiner)
        |> IO.iodata_to_binary()
    end
  end

  @doc """
  Intersperses `separator` between each element of the given `enumerable`.

  Mirrors `Enum.intersperse/2` with higher performance for Aja structures.
  """
  @spec intersperse(t(val), separator) :: [val | separator] when val: value, separator: value
  def intersperse(enumerable, separator) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.intersperse(enumerable, separator)
      list when is_list(list) -> Enum.intersperse(list, separator)
      vector -> RawVector.intersperse_to_list(vector, separator)
    end
  end

  @doc """
  Maps and intersperses the given `enumerable` in one pass.

  Mirrors `Enum.map_intersperse/3` with higher performance for Aja structures.
  """
  @spec map_intersperse(t(val), separator, (val -> mapped_val)) :: [mapped_val | separator]
        when val: value, separator: value, mapped_val: value
  def map_intersperse(enumerable, separator, mapper)
      when is_function(mapper, 1) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.map_intersperse(enumerable, separator, mapper)
      list when is_list(list) -> Enum.map_intersperse(list, separator, mapper)
      vector -> RawVector.map_intersperse_to_list(vector, separator, mapper)
    end
  end

  @doc """
  Maps the given `fun` over `enumerable` and flattens the result.

  Mirrors `Enum.flat_map/2` with higher performance for Aja structures.
  """
  @spec flat_map(t(val), (val -> t(mapped_val))) :: [mapped_val]
        when val: value, mapped_val: value
  defdelegate flat_map(enumerable, fun), to: H

  @doc """
  Returns a map with keys as unique elements of `enumerable` and values
  as the count of every element.

  Mirrors `Enum.frequencies/1` with higher performance for Aja structures.
  """
  @spec frequencies(t(val)) :: %{optional(val) => non_neg_integer} when val: value
  def frequencies(enumerable) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.frequencies(enumerable)
      list when is_list(list) -> Enum.frequencies(list)
      vector -> RawVector.frequencies(vector)
    end
  end

  @doc """
  Returns a map with keys as unique elements given by `key_fun` and values
  as the count of every element.

  Mirrors `Enum.frequencies_by/2` with higher performance for Aja structures.
  """
  @spec frequencies_by(t(val), (val -> key)) :: %{optional(key) => non_neg_integer}
        when val: value, key: any
  def frequencies_by(enumerable, key_fun) when is_function(key_fun, 1) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.frequencies_by(enumerable, key_fun)
      list when is_list(list) -> Enum.frequencies_by(list, key_fun)
      vector -> RawVector.frequencies_by(vector, key_fun)
    end
  end

  @doc """
  Splits the `enumerable` into groups based on `key_fun`.

  Mirrors `Enum.group_by/3` with higher performance for Aja structures.
  """
  @spec group_by(t(val), (val -> key), (val -> mapped_val)) :: %{optional(key) => [mapped_val]}
        when val: value, key: any, mapped_val: any
  def group_by(enumerable, key_fun, value_fun \\ fn x -> x end)
      when is_function(key_fun, 1) and is_function(value_fun, 1) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.group_by(enumerable, key_fun, value_fun)
      list when is_list(list) -> Enum.group_by(list, key_fun, value_fun)
      vector -> RawVector.group_by(vector, key_fun, value_fun)
    end
  end

  @doc """
  Invokes the given `fun` for each element in the `enumerable`.

  Mirrors `Enum.each/2` with higher performance for Aja structures.
  """
  @spec each(t(val), (val -> term)) :: :ok when val: value
  def each(enumerable, fun) when is_function(fun, 1) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.each(enumerable, fun)
      list when is_list(list) -> :lists.foreach(fun, list)
      vector -> RawVector.each(vector, fun)
    end
  end

  ## RANDOM

  @doc """
  Returns a random element of an `enumerable`.

  Mirrors `Enum.random/1` with higher performance for Aja structures.
  """
  @spec random(t(val)) :: val when val: value
  def random(enumerable) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.random(enumerable)
      list when is_list(list) -> Enum.random(list)
      vector -> RawVector.random(vector)
    end
  end

  @doc """
  Takes `count` random elements from `enumerable`.

  Mirrors `Enum.take_random/2` with higher performance for Aja structures.
  """
  @spec take_random(t(val), non_neg_integer) :: [val] when val: value
  def take_random(enumerable, count)
  def take_random(_enumerable, 0), do: []

  # TODO: optimize 1 for non-empty vectors

  def take_random(enumerable, count) do
    enumerable
    |> H.to_list()
    |> RandomHelper.take_random(count)
  end

  @doc """
  Returns a list with the elements of `enumerable` shuffled.

  Mirrors `Enum.shuffle/1` with higher performance for Aja structures.
  """
  @spec shuffle(t(val)) :: [val] when val: value
  def shuffle(enumerable) do
    enumerable
    |> H.to_list()
    |> RandomHelper.shuffle()
  end

  # UNIQ

  @doc """
  Enumerates the `enumerable`, returning a list where all consecutive
  duplicated elements are collapsed to a single element.

  Mirrors `Enum.dedup/1` with higher performance for Aja structures.
  """
  @spec dedup(t(val)) :: [val] when val: value
  def dedup(enumerable)

  def dedup(%MapSet{} = set) do
    MapSet.to_list(set)
  end

  def dedup(enumerable) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.dedup(enumerable)
      list when is_list(list) -> dedup_list(list)
      vector -> RawVector.dedup_list(vector)
    end
  end

  @doc """
  Enumerates the `enumerable`, returning a list where all consecutive
  duplicated elements are collapsed to a single element.

  Mirrors `Enum.dedup_by/2` with higher performance for Aja structures.
  """
  @spec dedup_by(t(val), (val -> term)) :: [val] when val: value
  def dedup_by(enumerable, fun) when is_function(fun, 1) do
    enumerable
    |> H.to_list()
    |> Enum.dedup_by(fun)
  end

  @doc """
  Enumerates the `enumerable`, removing all duplicated elements.

  Mirrors `Enum.uniq/1` with higher performance for Aja structures.
  """
  @spec uniq(t(val)) :: [val] when val: value
  def uniq(enumerable)

  def uniq(%MapSet{} = set) do
    MapSet.to_list(set)
  end

  def uniq(enumerable) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.uniq(enumerable)
      list when is_list(list) -> Enum.uniq(list)
      vector -> RawVector.uniq_list(vector)
    end
  end

  @doc """
  Enumerates the `enumerable`, by removing the elements for which
  function `fun` returned duplicate elements.

  Mirrors `Enum.uniq_by/2` with higher performance for Aja structures.
  """
  @spec uniq_by(t(val), (val -> term)) :: [val] when val: value
  def uniq_by(enumerable, fun) when is_function(fun, 1) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.uniq_by(enumerable, fun)
      list when is_list(list) -> Enum.uniq_by(list, fun)
      vector -> RawVector.uniq_by_list(vector, fun)
    end
  end

  # ## MIN-MAX

  defguardp is_list_or_struct(enumerable)
            when is_list(enumerable) or :erlang.map_get(:__struct__, enumerable) |> is_atom()

  defguardp is_empty_list_or_vec(list_or_vec)
            when list_or_vec === [] or list_or_vec === @empty_vector

  @doc false
  def min(enumerable) when is_list_or_struct(enumerable) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.min(enumerable)
      empty when is_empty_list_or_vec(empty) -> raise Enum.EmptyError
      list when is_list(list) -> :lists.min(list)
      vector -> RawVector.min(vector)
    end
  end

  @doc false
  @spec min(t(val), (-> empty_result)) :: val | empty_result when val: value, empty_result: any
  def min(enumerable, empty_fallback) when is_function(empty_fallback, 0) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.min(enumerable, empty_fallback)
      empty when is_empty_list_or_vec(empty) -> empty_fallback.()
      list when is_list(list) -> :lists.min(list)
      vector -> RawVector.min(vector)
    end
  end

  @doc """
  Returns the minimal element in the `enumerable` according
  to Erlang's term ordering.

  Mirrors `Enum.min/3` with higher performance for Aja structures.
  """
  @spec min(t(val), (val, val -> boolean) | module, (-> empty_result)) :: val | empty_result
        when val: value, empty_result: any
  def min(enumerable, sorter \\ &<=/2, empty_fallback \\ fn -> raise Enum.EmptyError end)
      when is_function(empty_fallback, 0) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.min(enumerable, sorter, empty_fallback)
      @empty_vector -> empty_fallback.()
      list when is_list(list) -> Enum.min(list, sorter, empty_fallback)
      vector -> RawVector.custom_min_max(vector, min_sort_fun(sorter))
    end
  end

  @doc false
  def max(enumerable) when is_list_or_struct(enumerable) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.max(enumerable)
      empty when is_empty_list_or_vec(empty) -> raise Enum.EmptyError
      list when is_list(list) -> :lists.max(list)
      vector -> RawVector.max(vector)
    end
  end

  @doc false
  @spec max(t(val), (-> empty_result)) :: val | empty_result when val: value, empty_result: any
  def max(enumerable, empty_fallback) when is_function(empty_fallback, 0) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.max(enumerable, empty_fallback)
      empty when is_empty_list_or_vec(empty) -> empty_fallback.()
      list when is_list(list) -> :lists.max(list)
      vector -> RawVector.max(vector)
    end
  end

  @doc """
  Returns the maximal element in the `enumerable` according
  to Erlang's term ordering.

  Mirrors `Enum.max/3` with higher performance for Aja structures.
  """
  @spec max(t(val), (val, val -> boolean) | module, (-> empty_result)) :: val | empty_result
        when val: value, empty_result: any
  def max(enumerable, sorter \\ &>=/2, empty_fallback \\ fn -> raise Enum.EmptyError end)
      when is_function(empty_fallback, 0) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.max(enumerable, sorter, empty_fallback)
      @empty_vector -> empty_fallback.()
      list when is_list(list) -> Enum.max(list, sorter, empty_fallback)
      vector -> RawVector.custom_min_max(vector, max_sort_fun(sorter))
    end
  end

  @doc false
  def min_by(enumerable, fun, empty_fallback)
      when is_function(fun, 1) and is_function(empty_fallback, 0) do
    min_by(enumerable, fun, &<=/2, empty_fallback)
  end

  @doc """
  Returns the minimal element in the `enumerable` as calculated
  by the given `fun`.

  Mirrors `Enum.min_by/4` with higher performance for Aja structures.
  """
  @spec min_by(t(val), (val -> key), (key, key -> boolean) | module, (-> empty_result)) ::
          val | empty_result
        when val: value, key: term, empty_result: any
  def min_by(enumerable, fun, sorter \\ &<=/2, empty_fallback \\ fn -> raise Enum.EmptyError end)
      when is_function(fun, 1) and is_function(empty_fallback, 0) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.min_by(enumerable, fun, sorter, empty_fallback)
      list when is_list(list) -> Enum.min_by(list, fun, sorter, empty_fallback)
      @empty_vector -> empty_fallback.()
      vector -> RawVector.custom_min_max_by(vector, fun, min_sort_fun(sorter))
    end
  end

  @doc false
  def max_by(enumerable, fun, empty_fallback)
      when is_function(fun, 1) and is_function(empty_fallback, 0) do
    max_by(enumerable, fun, &>=/2, empty_fallback)
  end

  @doc """
  Returns the maximal element in the `enumerable` as calculated
  by the given `fun`.

  Mirrors `Enum.max_by/4` with higher performance for Aja structures.
  """
  @spec max_by(t(val), (val -> key), (key, key -> boolean) | module, (-> empty_result)) ::
          val | empty_result
        when val: value, key: term, empty_result: any
  def max_by(enumerable, fun, sorter \\ &>=/2, empty_fallback \\ fn -> raise Enum.EmptyError end)
      when is_function(fun, 1) and is_function(empty_fallback, 0) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.max_by(enumerable, fun, sorter, empty_fallback)
      list when is_list(list) -> Enum.max_by(list, fun, sorter, empty_fallback)
      @empty_vector -> empty_fallback.()
      vector -> RawVector.custom_min_max_by(vector, fun, max_sort_fun(sorter))
    end
  end

  defp max_sort_fun(sorter) when is_function(sorter, 2), do: sorter
  defp max_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) != :lt)

  defp min_sort_fun(sorter) when is_function(sorter, 2), do: sorter
  defp min_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) != :gt)

  ## MAP-REDUCE

  @doc ~S"""
  Returns a list with with each element of `enumerable` wrapped in a tuple alongside its index.

  Mirrors `Enum.with_index/2`: may receive a function or an integer offset.

  If an integer `offset` is given, it will index from the given `offset` instead of from zero.

  If a `function` is given, it will index by invoking the function for each
  element and index (zero-based) of the `enumerable`.

  ## Examples

      iex> Aja.Enum.with_index([:a, :b, :c])
      [a: 0, b: 1, c: 2]

      iex> Aja.Enum.with_index([:a, :b, :c], 3)
      [a: 3, b: 4, c: 5]

      iex> Aja.Enum.with_index([:a, :b, :c], fn element, index -> {index, element} end)
      [{0, :a}, {1, :b}, {2, :c}]

  """
  @spec with_index(t(val), index) :: [{val, index}] when val: value
  @spec with_index(t(val), (val, index -> mapped_val)) :: [mapped_val]
        when val: value, mapped_val: value
  def with_index(enumerable, offset_or_fun \\ 0)

  def with_index(enumerable, offset) when is_integer(offset) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.with_index(enumerable, offset)
      list when is_list(list) -> with_index_list_offset(list, offset, [])
      vector -> RawVector.with_index(vector, offset) |> RawVector.to_list()
    end
  end

  def with_index(enumerable, fun) when is_function(fun, 2) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil ->
        enumerable
        |> Enum.map_reduce(0, fn x, i -> {fun.(x, i), i + 1} end)
        |> elem(0)

      list when is_list(list) ->
        with_index_list_fun(list, 0, fun, [])

      vector ->
        RawVector.with_index(vector, 0, fun) |> RawVector.to_list()
    end
  end

  defp with_index_list_offset([], _offset, acc), do: :lists.reverse(acc)

  defp with_index_list_offset([head | tail], offset, acc) do
    with_index_list_offset(tail, offset + 1, [{head, offset} | acc])
  end

  defp with_index_list_fun([], _offset, _fun, acc), do: :lists.reverse(acc)

  defp with_index_list_fun([head | tail], offset, fun, acc) do
    with_index_list_fun(tail, offset + 1, fun, [fun.(head, offset) | acc])
  end

  @doc """
  Invokes the given function to each element in the `enumerable` to reduce
  it to a single element, while keeping an accumulator.

  Mirrors `Enum.map_reduce/3` with higher performance for Aja structures.
  """
  @spec map_reduce(t(val), acc, (val, acc -> {mapped_val, acc})) :: {t(mapped_val), acc}
        when val: value, mapped_val: value, acc: any
  def map_reduce(enumerable, acc, fun) when is_function(fun, 2) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil ->
        Enum.map_reduce(enumerable, acc, fun)

      list when is_list(list) ->
        :lists.mapfoldl(fun, acc, list)

      vector ->
        {new_vector, new_acc} = RawVector.map_reduce(vector, acc, fun)
        {RawVector.to_list(new_vector), new_acc}
    end
  end

  @doc """
  Applies the given function to each element in the `enumerable`,
  storing the result in a list and passing it as the accumulator
  for the next computation. Uses the first element in the `enumerable`
  as the starting value.

  Mirrors `Enum.scan/2` with higher performance for Aja structures.
  """
  @spec scan(t(val), (val, val -> val)) :: val when val: value
  def scan(enumerable, fun) when is_function(fun, 2) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.scan(enumerable, fun)
      list when is_list(list) -> Enum.scan(list, fun)
      vector -> RawVector.scan(vector, fun) |> RawVector.to_list()
    end
  end

  @doc """
  Applies the given function to each element in the `enumerable`,
  storing the result in a list and passing it as the accumulator
  for the next computation. Uses the given `acc` as the starting value.

  Mirrors `Enum.scan/3` with higher performance for Aja structures.
  """
  @spec scan(t(val), acc, (val, acc -> acc)) :: acc when val: value, acc: term
  def scan(enumerable, acc, fun) when is_function(fun, 2) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.scan(enumerable, acc, fun)
      list when is_list(list) -> Enum.scan(list, acc, fun)
      vector -> RawVector.scan(vector, acc, fun) |> RawVector.to_list()
    end
  end

  ## SLICING

  @doc """
  Takes an `amount` of elements from the beginning or the end of the `enumerable`.

  Mirrors `Enum.take/2` with higher performance for Aja structures.
  """
  @spec take(t(val), integer) :: [val] when val: value
  def take(enumerable, amount) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.take(enumerable, amount)
      list when is_list(list) -> Enum.take(list, amount)
      vector -> do_take_vector(vector, amount)
    end
  end

  defp do_take_vector(_vector, 0), do: []

  defp do_take_vector(vector, amount) when amount > 0 do
    size = RawVector.size(vector)

    if amount < size do
      RawVector.slice(vector, 0, amount - 1)
    else
      RawVector.to_list(vector)
    end
  end

  defp do_take_vector(vector, amount) do
    size = RawVector.size(vector)
    start = amount + size

    if start > 0 do
      RawVector.slice(vector, start, size - 1)
    else
      RawVector.to_list(vector)
    end
  end

  @doc """
  Drops the `amount` of elements from the `enumerable`.

  Mirrors `Enum.drop/2` with higher performance for Aja structures.
  """
  @spec drop(t(val), integer) :: [val] when val: value
  def drop(enumerable, amount) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil -> Enum.drop(enumerable, amount)
      list when is_list(list) -> Enum.drop(list, amount)
      vector -> do_drop_vector(vector, amount)
    end
  end

  defp do_drop_vector(vector, 0), do: RawVector.to_list(vector)

  defp do_drop_vector(vector, amount) when amount > 0 do
    size = RawVector.size(vector)

    if amount < size do
      RawVector.slice(vector, amount, size - 1)
    else
      []
    end
  end

  defp do_drop_vector(vector, amount) do
    size = RawVector.size(vector)
    last = amount + size

    if last > 0 do
      RawVector.slice(vector, 0, last - 1)
    else
      []
    end
  end

  @doc """
  Splits the `enumerable` into two enumerables, leaving `count` elements in the first one.

  Mirrors `Enum.split/2` with higher performance for Aja structures.
  """
  @spec split(t(val), integer) :: {[val], [val]} when val: value
  def split(enumerable, amount) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil ->
        Enum.split(enumerable, amount)

      list when is_list(list) ->
        Enum.split(list, amount)

      vector ->
        if amount >= 0 do
          {do_take_vector(vector, amount), do_drop_vector(vector, amount)}
        else
          {do_drop_vector(vector, amount), do_take_vector(vector, amount)}
        end
    end
  end

  @doc """
  Takes the elements from the beginning of the `enumerable` while `fun` returns a truthy value.

  Mirrors `Enum.take_while/2` with higher performance for Aja structures.
  """
  @spec take_while(t(val), (val -> as_boolean(term()))) :: [val] when val: value
  def take_while(enumerable, fun) when is_function(fun, 1) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil ->
        Enum.take_while(enumerable, fun)

      list when is_list(list) ->
        Enum.take_while(list, fun)

      vector ->
        case RawVector.find_falsy_index(vector, fun) do
          nil -> RawVector.to_list(vector)
          index -> do_take_vector(vector, index)
        end
    end
  end

  @doc """
  Drops elements at the beginning of the `enumerable` while `fun` returns a truthy value.

  Mirrors `Enum.drop_while/2` with higher performance for Aja structures.
  """
  @spec drop_while(t(val), (val -> as_boolean(term()))) :: [val] when val: value
  def drop_while(enumerable, fun) when is_function(fun, 1) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil ->
        Enum.drop_while(enumerable, fun)

      list when is_list(list) ->
        Enum.drop_while(list, fun)

      vector ->
        case RawVector.find_falsy_index(vector, fun) do
          nil -> []
          index -> do_drop_vector(vector, index)
        end
    end
  end

  @doc """
  Splits `enumerable` in two at the position of the element for which `fun` returns a falsy value
  (`false` or `nil`) for the first time.

  Mirrors `Enum.split_while/2` with higher performance for Aja structures.
  """
  @spec split_while(t(val), (val -> as_boolean(term()))) :: {[val], [val]} when val: value
  def split_while(enumerable, fun) when is_function(fun, 1) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil ->
        Enum.split_while(enumerable, fun)

      list when is_list(list) ->
        Enum.split_while(list, fun)

      vector ->
        case RawVector.find_falsy_index(vector, fun) do
          nil -> {RawVector.to_list(vector), []}
          index -> {do_take_vector(vector, index), do_drop_vector(vector, index)}
        end
    end
  end

  ## SORT

  @doc """
  Sorts the `enumerable` according to Erlang's term ordering.

  Mirrors `Enum.sort/1` with higher performance for Aja structures.
  """
  @spec sort(t(val)) :: [val] when val: value
  def sort(enumerable) do
    enumerable
    |> H.to_list()
    |> Enum.sort()
  end

  @doc """
  Sorts the `enumerable` by the given function.

  Mirrors `Enum.sort/2` with higher performance for Aja structures.
  """
  @spec sort(
          t(val),
          (val, val -> boolean)
          | :asc
          | :desc
          | module
          | {:asc | :desc, module}
        ) :: [val]
        when val: value
  def sort(enumerable, fun) do
    enumerable
    |> H.to_list()
    |> Enum.sort(fun)
  end

  @doc """
  Sorts the mapped results of the `enumerable` according to the provided `sorter`
  function.

  Mirrors `Enum.sort_by/3` with higher performance for Aja structures.
  """
  @spec sort_by(
          t(val),
          (val -> mapped_val),
          (val, val -> boolean)
          | :asc
          | :desc
          | module
          | {:asc | :desc, module}
        ) :: [val]
        when val: value, mapped_val: value
  def sort_by(enumerable, mapper, sorter \\ &<=/2) do
    enumerable
    |> H.to_list()
    |> Enum.sort_by(mapper, sorter)
  end

  @doc """
  Zips corresponding elements from two enumerables into one list of tuples.

  Mirrors `Enum.zip/2` with higher performance for Aja structures.
  """
  @spec zip(t(val1), t(val2)) :: list({val1, val2}) when val1: value, val2: value
  def zip(enumerable1, enumerable2) do
    case {H.try_get_raw_vec_or_list(enumerable1), H.try_get_raw_vec_or_list(enumerable2)} do
      {vector1, vector2} when is_tuple(vector1) and is_tuple(vector2) ->
        RawVector.zip(vector1, vector2) |> RawVector.to_list()

      {list1, list2} when is_list(list1) and is_list(list2) ->
        zip_lists(list1, list2, [])

      {result1, result2} ->
        list_or_enum1 = zip_try_get_list(result1, enumerable1)
        list_or_enum2 = zip_try_get_list(result2, enumerable2)
        Enum.zip(list_or_enum1, list_or_enum2)
    end
  end

  defp zip_try_get_list(list, _enumerable) when is_list(list), do: list
  defp zip_try_get_list(nil, enumerable), do: enumerable
  defp zip_try_get_list(vector, _enumerable), do: RawVector.to_list(vector)

  defp zip_lists(list1, list2, acc) when list1 == [] or list2 == [] do
    :lists.reverse(acc)
  end

  defp zip_lists([head1 | tail1], [head2 | tail2], acc) do
    zip_lists(tail1, tail2, [{head1, head2} | acc])
  end

  @doc """
  Opposite of `zip/2`. Extracts two-element tuples from the given `enumerable`
  and groups them together.

  Mirrors `Enum.unzip/1` with higher performance for Aja structures.
  """
  @spec unzip(t({val1, val2})) :: {list(val1), list(val2)} when val1: value, val2: value
  def unzip(enumerable) do
    case H.try_get_raw_vec_or_list(enumerable) do
      nil ->
        Enum.unzip(enumerable)

      list when is_list(list) ->
        Enum.unzip(list)

      vector ->
        {vector1, vector2} = RawVector.unzip(vector)
        {RawVector.to_list(vector1), RawVector.to_list(vector2)}
    end
  end

  # Private functions

  defp dedup_list([]), do: []
  defp dedup_list([elem, elem | rest]), do: dedup_list([elem | rest])
  defp dedup_list([elem | rest]), do: [elem | dedup_list(rest)]
end
