lib/puid/decoder/ascii.ex

# MIT License
#
# Copyright (c) 2019-2023 Knoxen
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

defmodule Puid.Decoder.ASCII do
  import Puid.Util

  defmacro __using__(opts) do
    quote do
      charlist = unquote(opts)[:charlist]
      puid_len = unquote(opts)[:puid_len]

      char_count = length(charlist)

      bits_per_char = log_ceil(char_count)
      bits_per_puid = puid_len * bits_per_char

      @puid_len puid_len
      @puid_charlist charlist
      @puid_bits_per_char bits_per_char
      @puid_bits_per_pair 2 * bits_per_char

      @spec decode(puid :: String.t()) :: bitstring() | Puid.Error.t()
      def decode(puid)

      def decode(<<_::binary-size(@puid_len)>> = puid) do
        try do
          puid |> decode_puid_into(<<>>)
        rescue
          _ ->
            {:error, "unable to decode"}
        end
      end

      def decode(_),
        do: {:error, "unable to decode"}

      @spec decode_puid_into(bytes :: binary(), bits :: bitstring()) :: bitstring()
      defp decode_puid_into(bytes, bits)

      defp decode_puid_into(<<>>, bits),
        do: bits

      defp decode_puid_into(<<c::8>>, bits) do
        c_bits = decode_single(c)
        <<bits::bits, c_bits::bits>>
      end

      defp decode_puid_into(<<cc::16, rest::binary>>, bits) do
        cc_bits = decode_pair(cc)
        decode_puid_into(rest, <<bits::bits, cc_bits::bits>>)
      end

      defp chars_values(), do: @puid_charlist |> Enum.with_index()

      defmacrop pair_decoder(cc) do
        quote do
          case unquote(cc) do
            unquote(pair_decoder_clauses())
          end
        end
      end

      defp pair_decoder_clauses() do
        cv = chars_values()

        for {c1, v1} <- cv, {c2, v2} <- cv do
          cc = Bitwise.bsl(c1, 8) + c2
          v = Bitwise.bsl(v1, @puid_bits_per_char) + v2

          [clause] = quote(do: (unquote(cc) -> unquote(v)))
          clause
        end
      end

      defmacrop single_decoder(c) do
        quote do
          case unquote(c) do
            unquote(single_decoder_clauses())
          end
        end
      end

      defp single_decoder_clauses() do
        for {c, v} <- chars_values() do
          [clause] = quote(do: (unquote(c) -> unquote(v)))
          clause
        end
      end

      def decode_pair(cc) do
        vv = pair_decoder(cc)
        <<vv::size(@puid_bits_per_pair)>>
      end

      def decode_single(c) do
        v = single_decoder(c)
        <<v::size(@puid_bits_per_char)>>
      end
    end
  end
end