lib/basex.ex

defmodule BaseX do
  @moduledoc """
  coding for arbitrary alphabets and block sizes
  """

  @doc """
  prepare a coding module

  Returns the name of the module.

  The resulting module will appear in the BaseX namespace and have `encode`
  and `decode` functions available.

  Examples:

  ```
  iex> BaseX.prepare_module("Base2", "01", 4)
  BaseX.Base2

  iex> BaseX.Base2.encode("Hi!")
  "010010000110100100100001"

  iex> BaseX.Base2.decode("010010000110100100100001")
  "Hi!"
  ```

  These functions are only suitable for complete messages.
  Streaming applications should manage their own incomplete message state.

  The supplied module name should be both valid (by Elixir rules) and unique.
  Care should be taken when regenerating modules with the same name.

  Alphabets may be defined by `{"t","u","p","l","e"}`, `"string"`, `'charlist'` or
  `["l","i","s","t"]` as desired.
  """
  @spec prepare_module(String.t(), binary | list | tuple, pos_integer) :: term
  def prepare_module(name, alphabet, block_size) when is_tuple(alphabet) do
    full_name = Module.concat("BaseX", name)
    generate_module(full_name, alphabet, block_size)

    case Code.ensure_compiled(full_name) do
      {:module, module} -> module
      {:error, why} -> raise(why)
    end
  end

  def prepare_module(name, [a | bc], bs) when is_bitstring(a),
    do: prepare_module(name, [a | bc] |> List.to_tuple(), bs)

  def prepare_module(name, [a | bc], bs) when is_integer(a),
    do: prepare_module(name, [a | bc] |> Enum.map(&:binary.encode_unsigned/1), bs)

  def prepare_module(name, abc, bs) when is_binary(abc),
    do:
      prepare_module(
        name,
        abc
        |> String.split("")
        |> Enum.filter(fn c -> c != "" end),
        bs
      )

  defp generate_module(name, abc, s) do
    require BaseX.ModuleMaker
    a = tuple_size(abc)
    b = s * 8
    c = chars_for_bits(b, a)
    {vb, vc, vn} = valid_sizes(b, a, {%{}, %{}, %{}})

    BaseX.ModuleMaker.gen_module(
      name,
      Macro.escape(abc),
      a,
      b,
      c,
      abc |> index_map_from_tuple |> Macro.escape(),
      Macro.escape(vb),
      Macro.escape(vc),
      Macro.escape(vn),
      Macro.escape(single_octets?(abc))
    )
  end

  defp single_octets?(chars), do: single_octets(Tuple.to_list(chars), true)
  defp single_octets([], acc), do: acc
  defp single_octets([h | t], acc), do: single_octets(t, acc and bit_size(h) <= 8)

  defp chars_for_bits(b, a), do: trunc(Float.ceil(b / :math.log2(a)))
  defp max_num_for_bits(b), do: (:math.pow(2, b) - 1) |> trunc
  defp valid_sizes(0, _a, acc), do: acc

  defp valid_sizes(b, a, acc) do
    c = chars_for_bits(b, a)

    valid_sizes(
      b - 8,
      a,
      {Map.put(elem(acc, 0), b, c), Map.put(elem(acc, 1), c, b),
       Map.put(elem(acc, 2), b, b |> max_num_for_bits)}
    )
  end

  defp index_map_from_tuple(tuple), do: map_elements(tuple |> Tuple.to_list(), 0, %{})
  defp map_elements([], _index, map), do: map

  defp map_elements([e | rest], index, map),
    do: map_elements(rest, index + 1, Map.put(map, e, index))
end