lib/bitcraft.ex

defmodule Bitcraft do
  @moduledoc """
  The following are the main Bitcraft components:

    * `Bitcraft.BitBlock` - This is the main Bitcraft component. It provides
      a DSL that allows to define bit-blocks with their segments (useful for
      building binary protocols) and automatically injects encoding and decoding
      functions for them.
    * `Bitcraft` - This is a helper module that provides utility functions to
      work with bit strings and binaries.

  """

  use Bitwise
  use Bitcraft.Helpers

  # Base data types for binaries
  @type base_type ::
          integer
          | float
          | binary
          | bitstring
          | byte
          | char

  @typedoc "Segment type"
  @type segment_type :: base_type | Bitcraft.BitBlock.Array.t()

  @typedoc "Codable segment type"
  @type codable_segment_type :: base_type | [base_type]

  ## API

  @doc """
  Encodes the given `input` into a bitstring.

  ## Options

    * `:size` - The size in bits for the input to encode. The default
      value depend on the type, for integer is 8, for float is 63, and for
      other data types is `nil`. If the `input` is a list, this option is
      skipped, since it is handled as array and the size will be
      `array_length * element_size`.

    * `:type` - The segment type given by `Bitcraft.segment_type()`.
      Defaults to `:integer`.

    * `:sign` - If the input is an integer, defines if it is `:signed`
      or `:unsigned`. Defaults to `:unsigned`.

    * `:endian` - Applies to `utf32`, `utf16`, `float`, `integer`.
      Defines the endianness, `:big` or `:little`. Defaults to `:big`.

  ## Example

      iex> Bitcraft.encode_segment(15)
      <<15>>

      iex> Bitcraft.encode_segment(255, size: 4)
      <<15::size(4)>>

      iex> Bitcraft.encode_segment(-3.3, size: 64, type: :float)
      <<192, 10, 102, 102, 102, 102, 102, 102>>

      iex> Bitcraft.encode_segment("hello", type: :binary)
      "hello"

      iex> Bitcraft.encode_segment(<<1, 2, 3>>, type: :bits)
      <<1, 2, 3>>

      iex> Bitcraft.encode_segment([1, -2, 3], type: %Bitcraft.BitBlock.Array{
      ...>   type: :integer, element_size: 4},
      ...>   sign: :signed
      ...> )
      <<30, 3::size(4)>>

  """
  @spec encode_segment(codable_segment_type, Keyword.t()) :: bitstring
  def encode_segment(input, opts \\ []) do
    type = Keyword.get(opts, :type, :integer)
    sign = Keyword.get(opts, :sign, :unsigned)
    endian = Keyword.get(opts, :endian, :big)
    size = Keyword.get(opts, :size)

    size =
      cond do
        is_nil(size) and is_integer(input) -> 8
        is_nil(size) and is_float(input) -> 64
        true -> size
      end

    encode_segment(input, size, type, sign, endian)
  end

  @doc """
  Returns a tuple `{decoded value, leftover}` where the first element is the
  decoded value from the given `input` (according to the given `otps` too)
  and the second element is the leftover.

  ## Options

    * `:size` - The size in bits to decode. Defaults to `byte_size(input) * 8`.
      If the type is `Bitcraft.BitBlock.Array.()`, the size should match with
      `array_length * element_size`.

    * `:type` - The segment type given by `Bitcraft.segment_type()`.
      Defaults to `:integer`.

    * `:sign` - If the input is an integer, defines if it is `:signed`
      or `:unsigned`. Defaults to `:unsigned`.

    * `:endian` - Applies to `utf32`, `utf16`, `float`, `integer`.
      Defines the endianness, `:big` or `:little`. Defaults to `:big`.

  ## Example

      iex> 3
      ...> |> Bitcraft.encode_segment(size: 4)
      ...> |> Bitcraft.decode_segment(size: 4)
      {3, ""}

      iex> -3.3
      ...> |> Bitcraft.encode_segment(size: 64, type: :float, sign: :signed)
      ...> |> Bitcraft.decode_segment(size: 64, type: :float, sign: :signed)
      {-3.3, ""}

      iex> "test"
      ...> |> Bitcraft.encode_segment(type: :binary)
      ...> |> Bitcraft.decode_segment(size: 4, type: :binary)
      {"test", ""}

      iex> <<1, 2, 3, 4>>
      ...> |> Bitcraft.encode_segment(type: :bits)
      ...> |> Bitcraft.decode_segment(size: 32, type: :bits)
      {<<1, 2, 3, 4>>, ""}

      iex> alias Bitcraft.BitBlock.Array
      iex> [1, 2]
      ...> |> Bitcraft.encode_segment(type: %Array{})
      ...> |> Bitcraft.decode_segment(size: 16, type: %Array{})
      {[1, 2], ""}
      iex> [3.3, -7.7, 9.9]
      ...> |> Bitcraft.encode_segment(
      ...>   type: %Array{type: :float, element_size: 64},
      ...>   sign: :signed
      ...> )
      ...> |> Bitcraft.decode_segment(
      ...>   size: 192,
      ...>   type: %Array{type: :float, element_size: 64},
      ...>   sign: :signed
      ...> )
      {[3.3, -7.7, 9.9], ""}

  """
  @spec decode_segment(bitstring, Keyword.t()) :: {codable_segment_type, bitstring}
  def decode_segment(input, opts \\ []) do
    type = Keyword.get(opts, :type, :integer)
    sign = Keyword.get(opts, :sign, :unsigned)
    endian = Keyword.get(opts, :endian, :big)
    size = opts[:size] || byte_size(input) * 8

    decode_segment(input, size, type, sign, endian)
  end

  @doc """
  Returns the number of `1`s in binary representation of the given `integer`.

  ## Example

      iex> Bitcraft.count_ones(15)
      4
      iex> Bitcraft.count_ones(255)
      8

  """
  @spec count_ones(integer) :: integer
  def count_ones(integer) when is_integer(integer) do
    count_ones(integer, 0)
  end

  defp count_ones(0, count), do: count

  defp count_ones(integer, count) do
    count_ones(integer &&& integer - 1, count + 1)
  end
end