lib/grizzly/zwave/smart_start/meta_extension/uuid16.ex

defmodule Grizzly.ZWave.SmartStart.MetaExtension.UUID16 do
  @moduledoc """
  This is used to advertise 16 bytes of manufactured-defined information that
  is unique for a given product.
  Z-Wave UUIDs are not limited to the format outlined in RFC 4122 but can also
  be ASCII characters and a relevant prefix.
  """

  @typedoc """
  The three formats that the Z-Wave UUID can be formatted in are `:ascii`,
  `:hex`, or `:rfc4122`.

  Both `:ascii` and `:hex` can also have the prefix `sn:` or `UUID:`.

  Valid `:hex` formatted UUIDs look like:

  - `0102030405060708090A141516171819`
  - `sn:0102030405060708090A141516171819`
  - `UUID:0102030405060708090A141516171819`

  Valid `:ascii` formatted UUIDs look like:

  - `Hello Elixir!!!`
  - `sn:Hello Elixir!!!`
  - `UUID:Hello Elixir!!!`

  Lastly `rfc4122` format looks like `58D5E212-165B-4CA0-909B-C86B9CEE0111`
  where every two digits make up one hex value.

  More information about RFC 4122 and the specification format can be
  found [here](https://tools.ietf.org/html/rfc4122#section-4.1.2).
  """
  @type format() :: :ascii | :hex | :rfc4122

  @type uuid() :: binary()

  @type t() :: %{uuid: uuid(), format: format()}

  defguardp is_format_hex(value) when value in [0, 2, 4]
  defguardp is_format_ascii(value) when value in [1, 3, 5]
  defguardp is_format_rfc4122(value) when value == 6

  @doc """
  Make a new `UUID16.t()`
  """
  @spec new(String.t(), format()) :: {:ok, t()} | {:error, :invalid_uuid_length | :invalid_format}
  def new(uuid, format) do
    with :ok <- validate_format(format),
         uuid_no_prefix = remove_uuid_prefix(uuid),
         :ok <- validate_uuid_length(uuid_no_prefix, format) do
      {:ok, %{uuid: uuid, format: format}}
    end
  end

  @doc """
  Make a binary string from a `UUID16.t()`
  """
  @spec encode(t()) :: binary()
  def encode(uuid16) do
    [format_prefix, uuid] = get_format_prefix_and_uuid(uuid16.uuid)
    uuid_binary = uuid_to_binary(uuid, uuid16.format)
    <<0x06, 0x11, format_to_byte(uuid16.format, format_prefix)>> <> uuid_binary
  end

  defp get_format_prefix_and_uuid(uuid_string) do
    case String.split(uuid_string, ":") do
      [uuid] -> [:none, uuid]
      [prefix, _uuid] = result when prefix in ["sn", "UUID"] -> result
    end
  end

  @doc """
  Take a binary string and try to make a `UUID16.t()` from it
  If the critical bit is set in teh binary this will return
  `{:error, :critical_bit_set}` and the information should be ignored.
  If the format in the binary is not part of the defined Z-Wave specification
  this will return `{:error, :invalid_format}`
  """
  @spec parse(binary) :: {:ok, t()} | {:error, any()}
  def parse(<<0x03::size(7), 0::size(1), 0x11, presentation_format, uuid::binary>>) do
    with {:ok, uuid_string} <- uuid_from_binary(presentation_format, uuid),
         {:ok, format} <- format_from_byte(presentation_format) do
      new(uuid_string, format)
    end
  end

  def parse(<<0x03::size(7), 0x01::size(1), _rest::binary>>) do
    {:error, :critical_bit_set}
  end

  def parse(bin) when is_binary(bin), do: {:error, :invalid_binary}

  defp format_from_byte(format_byte) when is_format_hex(format_byte), do: {:ok, :hex}
  defp format_from_byte(format_byte) when is_format_ascii(format_byte), do: {:ok, :ascii}
  defp format_from_byte(format_byte) when is_format_rfc4122(format_byte), do: {:ok, :rfc4122}
  defp format_from_byte(format_byte) when format_byte in 7..99, do: {:ok, :hex}
  defp format_from_byte(_), do: {:error, :invalid_format}

  defp format_to_byte(:hex, :none), do: 0
  defp format_to_byte(:hex, "sn"), do: 2
  defp format_to_byte(:hex, "UUID"), do: 4
  defp format_to_byte(:ascii, :none), do: 1
  defp format_to_byte(:ascii, "sn"), do: 3
  defp format_to_byte(:ascii, "UUID"), do: 5
  defp format_to_byte(:rfc4122, :none), do: 6

  defp uuid_to_binary(uuid, :hex) do
    hex_uuid_to_binary(uuid)
  end

  defp uuid_to_binary(uuid, :ascii) do
    ascii_uuid_to_binary(uuid)
  end

  defp uuid_to_binary(uuid, :rfc4122) do
    rfc4122_uuid_to_binary(uuid)
  end

  defp uuid_to_binary(_uuid, _format), do: {:error, :invalid_uuid_length}

  defp rfc4122_uuid_to_binary(uuid) do
    uuid
    |> String.replace("-", "")
    |> Base.decode16!(case: :mixed)
  end

  defp hex_uuid_to_binary(uuid) do
    Base.decode16!(uuid, case: :mixed)
  end

  defp ascii_uuid_to_binary(uuid_string) do
    uuid_string
    |> String.split("", trim: true)
    |> Enum.reduce(<<>>, &(&2 <> &1))
  end

  defp uuid_as_hex_digits(uuid) do
    Base.encode16(uuid)
  end

  defp uuid_as_ascii(uuid) do
    uuid_out_string =
      uuid
      |> to_charlist()
      |> to_string()

    uuid_out_string
  end

  defp uuid_from_binary(format, uuid) when is_format_hex(format) do
    formatted_uuid = uuid_as_hex_digits(uuid)

    case format do
      0 -> {:ok, formatted_uuid}
      2 -> {:ok, "sn:#{formatted_uuid}"}
      4 -> {:ok, "UUID:#{formatted_uuid}"}
    end
  end

  defp uuid_from_binary(format, uuid) when is_format_ascii(format) do
    formatted_uuid = uuid_as_ascii(uuid)

    case format do
      1 -> {:ok, formatted_uuid}
      3 -> {:ok, "sn:#{formatted_uuid}"}
      5 -> {:ok, "UUID:#{formatted_uuid}"}
    end
  end

  defp uuid_from_binary(
         6,
         <<time_low::binary-size(4), time_mid::binary-size(2),
           time_hi_and_version::binary-size(2), clock_seq::binary-size(2), node::binary-size(6)>>
       ) do
    formatted =
      [
        time_low,
        time_mid,
        time_hi_and_version,
        clock_seq,
        node
      ]
      |> Enum.map_join("-", &Base.encode16/1)

    {:ok, formatted}
  end

  defp uuid_from_binary(format, uuid) when format in 7..99 do
    uuid_from_binary(0, uuid)
  end

  defp validate_format(format) when format in [:hex, :ascii, :rfc4122], do: :ok
  defp validate_format(_format), do: {:error, :invalid_format}

  defp remove_uuid_prefix("sn:" <> uuid), do: uuid
  defp remove_uuid_prefix("UUID:" <> uuid), do: uuid
  defp remove_uuid_prefix(uuid), do: uuid

  defp validate_uuid_length(uuid, :hex) when byte_size(uuid) == 32, do: :ok
  defp validate_uuid_length(uuid, :ascii) when byte_size(uuid) == 16, do: :ok
  defp validate_uuid_length(uuid, :rfc4122) when byte_size(uuid) == 36, do: :ok
  defp validate_uuid_length(_uuid, _format), do: {:error, :invalid_uuid_length}
end