defmodule SqliteVec.Float32 do
  @moduledoc """
  A vector struct for float32 vectors.
  Vectors are stored as binaries in the endianness of the system.

  > ### Consider endianness {: .warning}
  >
  > `SqliteVec.Float32.Vector` holds data in system endianness.
  > Therefore, the same vector data will be interpreted differently on another system with different endianness.
  > Moreover, you must consider endianness when converting the binary data directly to a list of numbers.

      iex> v = SqliteVec.Float32.new([-1.0, 2.0])
      ...> b = SqliteVec.Float32.to_binary(v)
      ...> <<f1::float-32, f2::float-32>> = b
      ...> [f1, f2]
      case System.endianness() do
        :big -> [-1.0, 2.0]
        :little -> [4.618539608568165e-41, 8.96831017167883e-44]
      end
  """

  @type t :: %__MODULE__{data: binary()}

  defstruct [:data]

  @doc """
  Creates a new vector from a vector, list, or tensor

  The vector must be a `SqliteVec.Float32` vector.
  The list may contain any number but the values will be converted to f32 format.
  The tensor must have a rank of 1 and must be of type :f32.

  ## Examples
      iex> SqliteVec.Float32.new([1.0, 2.0])
      %SqliteVec.Float32{data: <<1.0::float-32-native, 2.0::float-32-native>>}

      iex> v1 = SqliteVec.Float32.new([1, 2])
      ...> SqliteVec.Float32.new(v1)
      %SqliteVec.Float32{data: <<1.0::float-32-native, 2.0::float-32-native>>}

      iex> SqliteVec.Float32.new(Nx.tensor([1, 2], type: :f32))
      %SqliteVec.Float32{data: <<1.0::float-32-native, 2.0::float-32-native>>}
  """
  @spec new(t() | [number()]) :: t()
  def new(vector_or_list_or_tensor)

  def new(%SqliteVec.Float32{} = vector) do
    vector
  end

  def new(list) when is_list(list) do
    if list == [] do
      raise ArgumentError, "list must not be empty"
    end

    bin = for v <- list, into: <<>>, do: <<v::float-32-native>>
    from_binary(<<bin::binary>>)
  end

  if Code.ensure_loaded?(Nx) do
    def new(tensor) when is_struct(tensor, Nx.Tensor) do
      if Nx.rank(tensor) != 1 do
        raise ArgumentError, "expected rank to be 1"
      end

      if Nx.type(tensor) != {:f, 32} do
        raise ArgumentError, "expected type to be :f32"
      end

      bin = tensor |> Nx.to_binary()
      from_binary(<<bin::binary>>)
    end
  end

  @doc """
  Creates a new vector from its binary representation
  """
  @spec from_binary(binary()) :: t()
  def from_binary(binary) when is_binary(binary) do
    %SqliteVec.Float32{data: binary}
  end

  @doc """
  Converts the vector to its binary representation
  """
  @spec to_binary(t()) :: binary()
  def to_binary(vector) when is_struct(vector, SqliteVec.Float32) do
    vector.data
  end

  @doc """
  Converts the vector to a list
  """
  @spec to_list(t()) :: [float()]
  def to_list(vector) when is_struct(vector, SqliteVec.Float32) do
    <<bin::binary>> = vector.data

    for <<v::float-32-native <- bin>>, do: v
  end

  if Code.ensure_loaded?(Nx) do
    @doc """
    Converts the vector to a tensor
    """
    def to_tensor(vector) when is_struct(vector, SqliteVec.Float32) do
      <<bin::binary>> = vector.data
      Nx.from_binary(bin, :f32)
    end
  end
end

defimpl Inspect, for: SqliteVec.Float32 do
  import Inspect.Algebra

  def inspect(vector, opts) do
    concat(["vec_f32('", to_doc(SqliteVec.Float32.to_list(vector), opts), "')"])
  end
end
