lib/grizzly/zwave/commands/central_scene_supported_report.ex

defmodule Grizzly.ZWave.Commands.CentralSceneSupportedReport do
  @moduledoc """
  This command is used to report the maximum number of supported scenes and the Key Attributes
  supported for each scene.

  Versions 1 and 2 are obsolete. Version 3+ fields are required.

  Params:

    * `:supported_scenes` - This field indicates the maximum number of scenes supported by the requested device. (required)

    * `:slow_refresh_support` - This field indicates whether the node supports the Slow Refresh capability. (required)

    * `:identical` - This field indicates if all scenes are supporting the same Key Attributes (required)

    * `:bit_mask_bytes` - This field advertises the size of each “Supported Key Attributes” field measured in bytes. Must be 1..3. (required)

    * `:supported_key_attributes` - This field advertises the attributes supported by the corresponding scene (required)
                                    A list of lists of key attributes where a key attribute
                                    is one of :key_pressed_1_time | :key_released | :key_held_down | :key_pressed_2_times
                                    | :key_pressed_3_times | :key_pressed_4_times | :key_pressed_5_times.
                                    If not identical, the first list of key attributes corresponds to scene 1, the second to scene 2 etc. for each of supported_scenes
                                    If identical, only the key attributes of scene 1 are to be listed

  """

  @behaviour Grizzly.ZWave.Command

  alias Grizzly.ZWave.{Command, DecodeError}
  alias Grizzly.ZWave.CommandClasses.CentralScene

  # give me some type specs for your params
  @type param ::
          {:supported_scenes, non_neg_integer}
          | {:slow_refresh_support, boolean}
          | {:identical, boolean}
          | {:bit_mask_bytes, 1..3}
          | {:supported_key_attributes, [CentralScene.key_attributes()]}
  @impl true
  @spec new([param()]) :: {:ok, Command.t()}
  def new(params) do
    command = %Command{
      name: :central_scene_supported_report,
      command_byte: 0x02,
      command_class: CentralScene,
      params: params,
      impl: __MODULE__
    }

    {:ok, command}
  end

  @impl true
  @spec encode_params(Command.t()) :: binary()
  def encode_params(command) do
    supported_scenes = Command.param!(command, :supported_scenes)
    identical? = Command.param!(command, :identical)
    identical_bit = identical? |> CentralScene.boolean_to_bit()

    slow_refresh_support_bit =
      Command.param!(command, :slow_refresh_support) |> CentralScene.boolean_to_bit()

    bit_mask_bytes = Command.param!(command, :bit_mask_bytes)

    supported_key_attributes =
      Command.param!(command, :supported_key_attributes)
      |> CentralScene.validate_supported_key_attributes(supported_scenes, identical?)

    supported_key_attributes_binary =
      supported_key_attributes_to_binary(bit_mask_bytes, supported_key_attributes)

    <<supported_scenes, slow_refresh_support_bit::size(1), 0x00::size(4), bit_mask_bytes::size(2),
      identical_bit::size(1)>> <> supported_key_attributes_binary
  end

  @impl true
  @spec decode_params(binary()) :: {:ok, [param()]} | {:error, DecodeError.t()}
  def decode_params(
        <<supported_scenes, slow_refresh_support_bit::size(1), 0x00::size(4),
          bit_mask_bytes::size(2), identical_bit::size(1),
          supported_key_attributes_binary::binary>>
      ) do
    identical? = identical_bit == 1

    with {:ok, supported_key_attributes} <-
           supported_key_attributes_from_binary(
             supported_key_attributes_binary,
             supported_scenes,
             bit_mask_bytes,
             identical?
           ) do
      {:ok,
       [
         supported_scenes: supported_scenes,
         slow_refresh_support: slow_refresh_support_bit == 1,
         identical: identical?,
         bit_mask_bytes: bit_mask_bytes,
         supported_key_attributes: supported_key_attributes
       ]}
    else
      {:error, %DecodeError{}} = error ->
        error
    end
  end

  defp supported_key_attributes_to_binary(bit_mask_bytes, supported_key_attributes) do
    Enum.reduce(supported_key_attributes, <<>>, fn scene_key_attributes, acc ->
      acc <> key_attributes_bit_masks_binary(bit_mask_bytes, scene_key_attributes)
    end)
  end

  defp key_attributes_bit_masks_binary(bit_mask_bytes, scene_key_attributes) do
    # [{byte_index, bit_index}, ...]
    bit_indices =
      for key_attribute <- scene_key_attributes,
          do: CentralScene.key_attribute_to_bit_index(key_attribute)

    # [[1,4,5], [], []]
    byte_bit_indices =
      for i <- 1..bit_mask_bytes do
        Enum.reduce(bit_indices, [], fn {byte_index, bit_index}, acc ->
          if byte_index == i, do: [bit_index | acc], else: acc
        end)
      end

    # [<<128>>, <<>>, <<>>]
    bit_masks =
      for per_byte_bit_indices <- byte_bit_indices do
        for bit_index <- 7..0, into: <<>> do
          if bit_index in per_byte_bit_indices, do: <<1::size(1)>>, else: <<0::size(1)>>
        end
      end

    # <<...>>
    for bit_mask <- bit_masks, into: <<>>, do: bit_mask
  end

  defp supported_key_attributes_from_binary(
         supported_key_attributes_binary,
         supported_scenes,
         bit_mask_bytes,
         identical?
       ) do
    # [ [0,2,3], [], [1,4], ...]
    # [[4, 3, 2, 1, 0], [], [], []]
    all_bit_masks_as_lists = bit_masks_from_binary(supported_key_attributes_binary)
    # [ [ [0,2,3], [] ], ...]
    per_scene_bit_indices = Enum.chunk_every(all_bit_masks_as_lists, bit_mask_bytes)
    scene_count = Enum.count(per_scene_bit_indices)

    # Some devices may return more than one set of scene bit indices though they are meant to identical (the superfluous will be ignored)
    valid? = if identical?, do: scene_count >= 1, else: scene_count == supported_scenes

    if valid? do
      per_scene_bit_indices =
        if identical? do
          Enum.take(per_scene_bit_indices, 1)
        else
          per_scene_bit_indices
        end

      supported_key_attributes =
        for scene_bit_indices <- per_scene_bit_indices do
          # [{[0,2,3], 1}, {[], 2}]
          byte_indexed_scene_bit_indices = Enum.with_index(scene_bit_indices, 1)

          Enum.reduce(
            byte_indexed_scene_bit_indices,
            [],
            fn {bit_indices, byte_index}, acc ->
              attribute_keys = attribute_keys_from_bit_indices(bit_indices, byte_index)
              acc ++ attribute_keys
            end
          )
          |> List.flatten()
        end

      {:ok, supported_key_attributes}
    else
      {:error,
       %DecodeError{
         param: :supported_key_attributes,
         value: supported_key_attributes_binary,
         command: :central_scene_supported_report
       }}
    end
  end

  defp bit_masks_from_binary(supported_key_attributes_binary) do
    for byte <- :erlang.binary_to_list(supported_key_attributes_binary) do
      indexed_bit_list =
        for(<<(bit::size(1) <- <<byte>>)>>, do: bit) |> Enum.reverse() |> Enum.with_index()

      Enum.reduce(
        indexed_bit_list,
        [],
        fn {bit, bit_index}, acc ->
          case bit do
            0 -> acc
            1 -> [bit_index | acc]
          end
        end
      )
    end
  end

  defp attribute_keys_from_bit_indices(bit_indices, byte_index) do
    for(
      bit_index <- bit_indices,
      do: CentralScene.key_attribute_from_bit_index(byte_index, bit_index)
    )
    |> Enum.reject(&(&1 == :ignore))
  end
end