lib/opc_ua/common.ex

defmodule OpcUA.Common do
  @moduledoc """
  This module covers common functions for Client & Server behavior.
  """

  alias OpcUA.QualifiedName
  alias OpcUA.{ExpandedNodeId, NodeId, QualifiedName}

  defmacro __using__(opts) do
    quote location: :keep, bind_quoted: [opts: opts] do
      use GenServer, opts

      require Logger

      @c_timeout 5000

      @mix_env Mix.env()

      defmodule State do
        @moduledoc false

        # port: C port process
        # controlling_process: parent process

        defstruct port: nil,
                  controlling_process: nil
      end

      # Write nodes Attributes functions

      @doc """
      Change the browse name of a node in the server.
      """
      @spec write_node_browse_name(GenServer.server(), %NodeId{}, %QualifiedName{}) ::
              :ok | {:error, binary()} | {:error, :einval}
      def write_node_browse_name(pid, %NodeId{} = node_id, browse_name) do
        GenServer.call(pid, {:write, {:browse_name, node_id, browse_name}})
      end

      @doc """
      Change the display name attribute of a node in the server.
      """
      @spec write_node_display_name(GenServer.server(), %NodeId{}, binary(), binary()) ::
              :ok | {:error, binary()} | {:error, :einval}
      def write_node_display_name(pid, %NodeId{} = node_id, locale, name) do
        GenServer.call(pid, {:write, {:display_name, node_id, {locale, name}}})
      end

      @doc """
      Change description attribute of a node in the server.
      """
      @spec write_node_description(GenServer.server(), %NodeId{}, binary(), binary()) ::
              :ok | {:error, binary()} | {:error, :einval}
      def write_node_description(pid, %NodeId{} = node_id, locale, description) do
        GenServer.call(pid, {:write, {:description, node_id, {locale, description}}})
      end

      # TODO: friendlier write_mask params
      @doc """
      Change 'Write Mask' attribute of a node in the server.
      """
      @spec write_node_write_mask(GenServer.server(), %NodeId{}, integer()) ::
              :ok | {:error, binary()} | {:error, :einval}
      def write_node_write_mask(pid, %NodeId{} = node_id, write_mask) do
        GenServer.call(pid, {:write, {:write_mask, node_id, write_mask}})
      end

      @doc """
      Change 'Is Abstract' attribute of a node in the server.
      """
      @spec write_node_is_abstract(GenServer.server(), %NodeId{}, boolean()) ::
              :ok | {:error, binary()} | {:error, :einval}
      def write_node_is_abstract(pid, %NodeId{} = node_id, is_abstract?) do
        GenServer.call(pid, {:write, {:is_abstract, node_id, is_abstract?}})
      end

      @doc """
      Change 'Inverse name' attribute of a node in the server.
      """
      @spec write_node_inverse_name(GenServer.server(), %NodeId{}, binary(), binary()) ::
              :ok | {:error, binary()} | {:error, :einval}
      def write_node_inverse_name(pid, %NodeId{} = node_id, locale, inverse_name) do
        GenServer.call(pid, {:write, {:inverse_name, node_id, {locale, inverse_name}}})
      end

      @doc """
      Change 'data_type' attribute of a node in the server.
      """
      @spec write_node_data_type(GenServer.server(), %NodeId{}, %NodeId{}) ::
              :ok | {:error, binary()} | {:error, :einval}
      def write_node_data_type(pid, %NodeId{} = node_id, %NodeId{} = data_type_node_id) do
        GenServer.call(pid, {:write, {:data_type, node_id, data_type_node_id}})
      end

      @doc """
      Change 'Value rank' of a node in the server.

      This attribute indicates whether the value attribute of the variable is an array and how many dimensions the array has.
      It may have the following values:

      value_rank >= 1: the value is an array with the specified number of dimensions
      value_rank =  0: the value is an array with one or more dimensions
      value_rank = -1: the value is a scalar
      value_rank = -2: the value can be a scalar or an array with any number of dimensions
      value_rank = -3: the value can be a scalar or a one dimensional array

      """
      @spec write_node_value_rank(GenServer.server(), %NodeId{}, integer()) ::
              :ok | {:error, binary()} | {:error, :einval}
      def write_node_value_rank(pid, %NodeId{} = node_id, value_rank)
          when value_rank >= -3 and is_integer(value_rank) do
        GenServer.call(pid, {:write, {:value_rank, node_id, value_rank}})
      end

      @doc """
      Change 'Array Dimensions' of a node in the server.
      """
      @spec write_node_array_dimensions(GenServer.server(), %NodeId{}, list()) ::
              :ok | {:error, binary()} | {:error, :einval}
      def write_node_array_dimensions(pid, %NodeId{} = node_id, array_dimensions)
          when is_list(array_dimensions) do
        GenServer.call(pid, {:write, {:array_dimensions, node_id, array_dimensions}})
      end

      @doc """
      Change 'Access level' of a node in the server.
      """
      @spec write_node_access_level(GenServer.server(), %NodeId{}, integer()) ::
              :ok | {:error, binary()} | {:error, :einval}
      def write_node_access_level(pid, %NodeId{} = node_id, access_level) do
        GenServer.call(pid, {:write, {:access_level, node_id, access_level}})
      end

      @doc """
      Change 'Minimum Sampling Interval level' of a node in the server.
      """
      @spec write_node_minimum_sampling_interval(GenServer.server(), %NodeId{}, integer()) ::
              :ok | {:error, binary()} | {:error, :einval}
      def write_node_minimum_sampling_interval(
            pid,
            %NodeId{} = node_id,
            minimum_sampling_interval
          ) do
        GenServer.call(
          pid,
          {:write, {:minimum_sampling_interval, node_id, minimum_sampling_interval}}
        )
      end

      @doc """
      Change 'Historizing' attribute of a node in the server.
      """
      @spec write_node_historizing(GenServer.server(), %NodeId{}, boolean()) ::
              :ok | {:error, binary()} | {:error, :einval}
      def write_node_historizing(pid, %NodeId{} = node_id, historizing?) do
        GenServer.call(pid, {:write, {:historizing, node_id, historizing?}})
      end

      @doc """
      Change 'Executable' attribute of a node in the server.
      """
      @spec write_node_executable(GenServer.server(), %NodeId{}, boolean()) ::
              :ok | {:error, binary()} | {:error, :einval}
      def write_node_executable(pid, %NodeId{} = node_id, executable?) do
        GenServer.call(pid, {:write, {:executable, node_id, executable?}})
      end

      @doc """
      Change 'event_notifier' attribute of a node in the server.
      """
      @spec write_node_event_notifier(GenServer.server(), %NodeId{}, integer()) ::
              :ok | {:error, binary()} | {:error, :einval}
      def write_node_event_notifier(pid, %NodeId{} = node_id, event_notifier) when is_integer(event_notifier) do
        GenServer.call(pid, {:write, {:event_notifier, node_id, event_notifier}})
      end

      @doc """
      Change 'Value' attribute of a node in the server.
      """
      @spec write_node_value(GenServer.server(), %NodeId{}, integer(), term()) ::
              :ok | {:error, binary()} | {:error, :einval}
      def write_node_value(pid, %NodeId{} = node_id, data_type, value, index \\ 0) do
        GenServer.call(pid, {:write, {:value, node_id, {data_type, value, index}}})
      end

      @doc """
      Creates a blank 'value array' attribute of a node in the server.
      Note: the array must match with 'value_rank' and 'array_dimensions' attribute.
      """
      @spec write_node_blank_array(GenServer.server(), %NodeId{}, integer(), list()) ::
              :ok | {:error, binary()} | {:error, :einval}
      def write_node_blank_array(pid, %NodeId{} = node_id, data_type, array_dimensions)
          when is_integer(data_type) and is_list(array_dimensions) do
        GenServer.call(pid, {:write, {:array, node_id, {data_type, array_dimensions}}})
      end

      # Read nodes Attributes function

      @doc """
      Reads the node_id attribute of a node in the server.
      """
      @spec read_node_node_id(GenServer.server(), %NodeId{}) ::
              {:ok, %NodeId{}} | {:error, binary()} | {:error, :einval}
      def read_node_node_id(pid, %NodeId{} = node_id) do
        GenServer.call(pid, {:read, {:node_id, node_id}})
      end

      @doc """
      Reads the node_class attribute of a node in the server.
      """
      @spec read_node_node_class(GenServer.server(), %NodeId{}) ::
              {:ok, %NodeId{}} | {:error, binary()} | {:error, :einval}
      def read_node_node_class(pid, %NodeId{} = node_id) do
        GenServer.call(pid, {:read, {:node_class, node_id}})
      end

      @doc """
      Reads the browse name attribute of a node in the server.
      """
      @spec read_node_browse_name(GenServer.server(), %NodeId{}) ::
              {:ok, %QualifiedName{}} | {:error, binary()} | {:error, :einval}
      def read_node_browse_name(pid, %NodeId{} = node_id) do
        GenServer.call(pid, {:read, {:browse_name, node_id}})
      end

      @doc """
      Reads the display name attribute of a node in the server.
      """
      @spec read_node_display_name(GenServer.server(), %NodeId{}) ::
              {:ok, {binary(), binary()}} | {:error, binary()} | {:error, :einval}
      def read_node_display_name(pid, %NodeId{} = node_id) do
        GenServer.call(pid, {:read, {:display_name, node_id}})
      end

      @doc """
      Reads description attribute of a node in the server.
      """
      @spec read_node_description(GenServer.server(), %NodeId{}) ::
              {:ok, {binary(), binary()}} | {:error, binary()} | {:error, :einval}
      def read_node_description(pid, node_id) do
        GenServer.call(pid, {:read, {:description, node_id}})
      end

      @doc """
      Reads 'Is Abstract' attribute of a node in the server.
      """
      @spec read_node_is_abstract(GenServer.server(), %NodeId{}) ::
              {:ok, boolean()} | {:error, binary()} | {:error, :einval}
      def read_node_is_abstract(pid, node_id) do
        GenServer.call(pid, {:read, {:is_abstract, node_id}})
      end

      @doc """
      Reads 'Symmetric' attribute of a node in the server.
      """
      @spec read_node_symmetric(GenServer.server(), %NodeId{}) ::
              {:ok, boolean()} | {:error, binary()} | {:error, :einval}
      def read_node_symmetric(pid, node_id) do
        GenServer.call(pid, {:read, {:symmetric, node_id}})
      end

      # TODO: friendlier write_mask params
      @doc """
      Reads 'Write Mask' attribute of a node in the server.
      """
      @spec read_node_write_mask(GenServer.server(), %NodeId{}) ::
              {:ok, integer()} | {:error, binary()} | {:error, :einval}
      def read_node_write_mask(pid, node_id) do
        GenServer.call(pid, {:read, {:write_mask, node_id}})
      end

      @doc """
      Reads 'data_type' attribute of a node in the server.
      """
      @spec read_node_data_type(GenServer.server(), %NodeId{}) ::
              {:ok, %NodeId{}} | {:error, binary()} | {:error, :einval}
      def read_node_data_type(pid, node_id) do
        GenServer.call(pid, {:read, {:data_type, node_id}})
      end

      @doc """
      Reads 'Inverse name' attribute of a node in the server.
      """
      @spec read_node_inverse_name(GenServer.server(), %NodeId{}) ::
              {:ok, {binary(), binary()}} | {:error, binary()} | {:error, :einval}
      def read_node_inverse_name(pid, node_id) do
        GenServer.call(pid, {:read, {:inverse_name, node_id}})
      end

      @doc """
      Reads 'contains_no_loops' attribute of a node in the server.
      """
      @spec read_node_contains_no_loops(GenServer.server(), %NodeId{}) ::
              {:ok, {binary(), binary()}} | {:error, binary()} | {:error, :einval}
      def read_node_contains_no_loops(pid, node_id) do
        GenServer.call(pid, {:read, {:contains_no_loops, node_id}})
      end

      @doc """
      Reads 'Value Rank' of a node in the server.
      """
      @spec read_node_value_rank(GenServer.server(), %NodeId{}) ::
              {:ok, integer()} | {:error, binary()} | {:error, :einval}
      def read_node_value_rank(pid, node_id) do
        GenServer.call(pid, {:read, {:value_rank, node_id}})
      end

      @doc """
      Reads 'array_dimensions' of a node in the server.
      """
      @spec read_node_array_dimensions(GenServer.server(), %NodeId{}) ::
              {:ok, list()} | {:error, binary()} | {:error, :einval}
      def read_node_array_dimensions(pid, node_id) do
        GenServer.call(pid, {:read, {:array_dimensions, node_id}})
      end

      @doc """
      Reads 'Access level' of a node in the server.
      """
      @spec read_node_access_level(GenServer.server(), %NodeId{}) ::
              {:ok, integer()} | {:error, binary()} | {:error, :einval}
      def read_node_access_level(pid, node_id) do
        GenServer.call(pid, {:read, {:access_level, node_id}})
      end

      @doc """
      Reads 'Minimum Sampling Interval level' of a node in the server.
      """
      @spec read_node_minimum_sampling_interval(GenServer.server(), %NodeId{}) ::
              {:ok, integer()} | {:error, binary()} | {:error, :einval}
      def read_node_minimum_sampling_interval(pid, node_id) do
        GenServer.call(pid, {:read, {:minimum_sampling_interval, node_id}})
      end

      @doc """
      Reads 'Historizing' attribute of a node in the server.
      """
      @spec read_node_historizing(GenServer.server(), %NodeId{}) ::
              {:ok, boolean()} | {:error, binary()} | {:error, :einval}
      def read_node_historizing(pid, node_id) do
        GenServer.call(pid, {:read, {:historizing, node_id}})
      end

      @doc """
      Reads 'Executable' attribute of a node in the server.
      """
      @spec read_node_executable(GenServer.server(), %NodeId{}) ::
              {:ok, boolean()} | {:error, binary()} | {:error, :einval}
      def read_node_executable(pid, node_id) do
        GenServer.call(pid, {:read, {:executable, node_id}})
      end

      @doc """
      Reads 'event_notifier' attribute of a node in the server.
      """
      @spec read_node_event_notifier(GenServer.server(), %NodeId{}) ::
              {:ok, term()} | {:error, binary()} | {:error, :einval}
      def read_node_event_notifier(pid, node_id) do
        GenServer.call(pid, {:read, {:event_notifier, node_id}})
      end

      @doc """
      Reads 'value' attribute of a node in the server.
      Note: If the value is an array you can search a scalar using `index` parameter.
      """
      @spec read_node_value(GenServer.server(), %NodeId{}, integer()) ::
              {:ok, term()} | {:error, binary()} | {:error, :einval}
      def read_node_value(pid, node_id, index \\ 0) do
        if(@mix_env != :test) do
          GenServer.call(pid, {:read, {:value, {node_id, index}}})
        else
          # Valgrind
          GenServer.call(pid, {:read, {:value, {node_id, index}}}, :infinity)
        end
      end

      @doc """
      Reads 'value' attribute of a node in the server.
      Note: If the value is an array you can search a scalar using `index` parameter.
      """
      @spec read_node_value_by_index(GenServer.server(), %NodeId{}, integer()) ::
              {:ok, term()} | {:error, binary()} | {:error, :einval}
      def read_node_value_by_index(pid, node_id, index \\ 0) do
        if(@mix_env != :test) do
          GenServer.call(pid, {:read, {:value_by_index, {node_id, index}}})
        else
          # Valgrind
          GenServer.call(pid, {:read, {:value_by_index, {node_id, index}}}, :infinity)
        end
      end

      @doc """
      Reads 'Value' attribute (matching data type) of a node in the server.
      """
      @spec read_node_value_by_data_type(GenServer.server(), %NodeId{}, integer()) ::
              {:ok, term()} | {:error, binary()} | {:error, :einval}
      def read_node_value_by_data_type(pid, node_id, data_type) when is_integer(data_type) do
        GenServer.call(pid, {:read, {:value_by_data_type, {node_id, data_type}}})
      end

      # Write nodes Attributes handlers
      def handle_call({:write, {:browse_name, node_id, browse_name}}, caller_info, state) do
        c_args = {to_c(node_id), to_c(browse_name)}
        call_port(state, :write_node_browse_name, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:write, {:display_name, node_id, {locale, name}}}, caller_info, state)
          when is_binary(locale) and is_binary(name) do
        c_args = {to_c(node_id), locale, name}
        call_port(state, :write_node_display_name, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call(
            {:write, {:description, node_id, {locale, description}}},
            caller_info,
            state
          )
          when is_binary(locale) and is_binary(description) do
        c_args = {to_c(node_id), locale, description}
        call_port(state, :write_node_description, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:write, {:write_mask, node_id, write_mask}}, caller_info, state)
          when is_integer(write_mask) do
        c_args = {to_c(node_id), write_mask}
        call_port(state, :write_node_write_mask, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:write, {:is_abstract, node_id, is_abstract?}}, caller_info, state)
          when is_boolean(is_abstract?) do
        c_args = {to_c(node_id), is_abstract?}
        call_port(state, :write_node_is_abstract, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call(
            {:write, {:inverse_name, node_id, {locale, inverse_name}}},
            caller_info,
            state
          )
          when is_binary(locale) and is_binary(inverse_name) do
        c_args = {to_c(node_id), locale, inverse_name}
        call_port(state, :write_node_inverse_name, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:write, {:data_type, node_id, data_type_node_id}}, caller_info, state) do
        c_args = {to_c(node_id), to_c(data_type_node_id)}
        call_port(state, :write_node_data_type, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:write, {:value_rank, node_id, value_rank}}, caller_info, state)
          when is_integer(value_rank) do
        c_args = {to_c(node_id), value_rank}
        call_port(state, :write_node_value_rank, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:write, {:array_dimensions, node_id, array_dimensions}}, caller_info, state)
          when is_list(array_dimensions) do
        with true <- all_must_be(:integer, array_dimensions) do
          c_args = {to_c(node_id), length(array_dimensions), List.to_tuple(array_dimensions)}
          call_port(state, :write_node_array_dimensions, caller_info, c_args)
          {:noreply, state}
        else
          _ ->
            {:reply, {:error, :einval}, state}
        end
      end

      def handle_call({:write, {:access_level, node_id, access_level}}, caller_info, state)
          when is_integer(access_level) do
        c_args = {to_c(node_id), access_level}
        call_port(state, :write_node_access_level, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call(
            {:write, {:minimum_sampling_interval, node_id, minimum_sampling_interval}},
            caller_info,
            state
          )
          when is_float(minimum_sampling_interval) do
        c_args = {to_c(node_id), minimum_sampling_interval}
        call_port(state, :write_node_minimum_sampling_interval, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:write, {:historizing, node_id, historizing?}}, caller_info, state)
          when is_boolean(historizing?) do
        c_args = {to_c(node_id), historizing?}
        call_port(state, :write_node_historizing, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:write, {:executable, node_id, executable?}}, caller_info, state)
          when is_boolean(executable?) do
        c_args = {to_c(node_id), executable?}
        call_port(state, :write_node_executable, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:write, {:event_notifier, node_id, event_notifier}}, caller_info, state)
          when is_integer(event_notifier) do
        c_args = {to_c(node_id), event_notifier}
        call_port(state, :write_node_event_notifier, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:write, {:value, node_id, {data_type, raw_value}}}, caller_info, state) do
        c_args = {to_c(node_id), data_type, 0, value_to_c(data_type, raw_value)}
        call_port(state, :write_node_value, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:write, {:value, node_id, {data_type, raw_value, index}}}, caller_info, state) do
        c_args = {to_c(node_id), data_type, index, value_to_c(data_type, raw_value)}
        call_port(state, :write_node_value, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:write, {:array, node_id, {data_type, array_dimensions}}}, caller_info, state)
          when is_integer(data_type) and is_list(array_dimensions) do
        with  true <- all_must_be(:integer, array_dimensions),
              array_raw_size <- get_array_raw_size(array_dimensions) do
          #{node_id, data_type, }
          c_args = {to_c(node_id), data_type, length(array_dimensions), array_raw_size, List.to_tuple(array_dimensions)}
          call_port(state, :write_node_blank_array, caller_info, c_args)
          {:noreply, state}
        else
          _ ->
            {:reply, {:error, :einval}, state}
        end
      end

      # Read nodes Attributes handlers

      def handle_call({:read, {:node_id, node_id}}, caller_info, state) do
        c_args = to_c(node_id)
        call_port(state, :read_node_node_id, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:read, {:browse_name, node_id}}, caller_info, state) do
        c_args = to_c(node_id)
        call_port(state, :read_node_browse_name, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:read, {:node_class, node_id}}, caller_info, state) do
        c_args = to_c(node_id)
        call_port(state, :read_node_node_class, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:read, {:display_name, node_id}}, caller_info, state) do
        c_args = to_c(node_id)
        call_port(state, :read_node_display_name, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:read, {:description, node_id}}, caller_info, state) do
        c_args = to_c(node_id)
        call_port(state, :read_node_description, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:read, {:is_abstract, node_id}}, caller_info, state) do
        c_args = to_c(node_id)
        call_port(state, :read_node_is_abstract, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:read, {:symmetric, node_id}}, caller_info, state) do
        c_args = to_c(node_id)
        call_port(state, :read_node_symmetric, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:read, {:write_mask, node_id}}, caller_info, state) do
        c_args = to_c(node_id)
        call_port(state, :read_node_write_mask, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:read, {:data_type, node_id}}, caller_info, state) do
        c_args = to_c(node_id)
        call_port(state, :read_node_data_type, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:read, {:inverse_name, node_id}}, caller_info, state) do
        c_args = to_c(node_id)
        call_port(state, :read_node_inverse_name, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:read, {:contains_no_loops, node_id}}, caller_info, state) do
        c_args = to_c(node_id)
        call_port(state, :read_node_contains_no_loops, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:read, {:value_rank, node_id}}, caller_info, state) do
        c_args = to_c(node_id)
        call_port(state, :read_node_value_rank, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:read, {:array_dimensions, node_id}}, caller_info, state) do
        c_args = to_c(node_id)
        call_port(state, :read_node_array_dimensions, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:read, {:access_level, node_id}}, caller_info, state) do
        c_args = to_c(node_id)
        call_port(state, :read_node_access_level, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:read, {:minimum_sampling_interval, node_id}}, caller_info, state) do
        c_args = to_c(node_id)
        call_port(state, :read_node_minimum_sampling_interval, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:read, {:historizing, node_id}}, caller_info, state) do
        c_args = to_c(node_id)
        call_port(state, :read_node_historizing, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:read, {:executable, node_id}}, caller_info, state) do
        c_args = to_c(node_id)
        call_port(state, :read_node_executable, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:read, {:event_notifier, node_id}}, caller_info, state) do
        c_args = to_c(node_id)
        call_port(state, :read_node_event_notifier, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:read, {:value, {node_id, index}}}, caller_info, state) do
        c_args = {to_c(node_id), index}
        call_port(state, :read_node_value, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:read, {:value_by_index, {node_id, index}}}, caller_info, state) do
        c_args = {to_c(node_id), index}
        call_port(state, :read_node_value_by_index, caller_info, c_args)
        {:noreply, state}
      end

      def handle_call({:read, {:value_by_data_type, {node_id, data_type}}}, caller_info, state) do
        c_args = {to_c(node_id), data_type}
        call_port(state, :read_node_value_by_data_type, caller_info, c_args)
        {:noreply, state}
      end

      # Catch all handlers

      def handle_info({_port, {:data, <<?r, c_response::binary>>}}, state) do
        state =
          c_response
          |> :erlang.binary_to_term()
          |> handle_c_response(state)

        {:noreply, state}
      end

      # Write nodes Attributes C handlers

      defp handle_c_response({:write_node_browse_name, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:write_node_display_name, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:write_node_description, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:write_node_write_mask, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:write_node_is_abstract, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:write_node_inverse_name, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:write_node_data_type, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:write_node_value_rank, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:write_node_array_dimensions, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:write_node_access_level, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response(
             {:write_node_minimum_sampling_interval, caller_metadata, data},
             state
           ) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:write_node_historizing, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:write_node_executable, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:write_node_event_notifier, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:write_node_value, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:write_node_blank_array, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      # Read nodes Attributes C handlers

      defp handle_c_response({:read_node_node_id, caller_metadata, node_id_response}, state) do
        response = parse_node_id(node_id_response)
        GenServer.reply(caller_metadata, response)
        state
      end

      defp handle_c_response({:read_node_node_class, caller_metadata, data}, state) do
        response = charlist_to_string(data)
        GenServer.reply(caller_metadata, response)
        state
      end

      defp handle_c_response(
             {:read_node_browse_name, caller_metadata, browse_name_response},
             state
           ) do
        response = parse_browse_name(browse_name_response)
        GenServer.reply(caller_metadata, response)
        state
      end

      defp handle_c_response({:read_node_display_name, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:read_node_description, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:read_node_write_mask, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:read_node_is_abstract, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:read_node_symmetric, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:read_node_inverse_name, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:read_node_data_type, caller_metadata, data_type_response}, state) do
        response = parse_data_type(data_type_response)
        GenServer.reply(caller_metadata, response)
        state
      end

      defp handle_c_response({:read_node_value_rank, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:read_node_array_dimensions, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:read_node_access_level, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:read_node_minimum_sampling_interval, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:read_node_contains_no_loops, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:read_node_historizing, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:read_node_executable, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:read_node_event_notifier, caller_metadata, data}, state) do
        GenServer.reply(caller_metadata, data)
        state
      end

      defp handle_c_response({:read_node_value, caller_metadata, value_response}, state) do
        response = parse_value(value_response)
        GenServer.reply(caller_metadata, response)
        state
      end

      defp handle_c_response({:read_node_value_by_index, caller_metadata, value_response}, state) do
        response = parse_value(value_response)
        GenServer.reply(caller_metadata, response)
        state
      end

      defp handle_c_response(
             {:read_node_value_by_data_type, caller_metadata, value_response},
             state
           ) do
        response = parse_value(value_response)
        GenServer.reply(caller_metadata, response)
        state
      end

      defp use_valgrind?(), do: System.get_env("USE_VALGRIND", "false")

      defp open_port(executable, "false") do
        Port.open({:spawn_executable, to_charlist(executable)}, [
          {:args, []},
          {:packet, 2},
          :use_stdio,
          :binary,
          :exit_status
        ])
      end

      defp open_port(executable, valgrind_env) do
        Logger.warn("(#{__MODULE__}) Valgrind Activated: #{inspect valgrind_env}")
        Port.open({:spawn_executable, to_charlist("/usr/bin/valgrind.bin")}, [
          {:args,
           [
             "-q",
             "--undef-value-errors=no",
             "--leak-check=full",
             "--show-leak-kinds=all",
             #"--verbose",
             #"--track-origins=yes",
             executable
           ]},
          {:packet, 2},
          :use_stdio,
          :binary,
          :exit_status
        ])
      end

      defp call_port(state, command, caller, arguments) do
        msg = {command, caller, arguments}
        send(state.port, {self(), {:command, :erlang.term_to_binary(msg)}})
      end

      defp charlist_to_string({:ok, charlist}), do: {:ok, to_string(charlist)}
      defp charlist_to_string(error_response), do: error_response

      defp to_c(%NodeId{ns_index: ns_index, identifier_type: id_type, identifier: identifier}),
        do: {id_type, ns_index, identifier}

      defp to_c(%QualifiedName{ns_index: ns_index, name: name}),
        do: {ns_index, name}

      defp to_c(_invalid_struct), do: raise("Invalid Data type")

      # For NodeId, QualifiedName.
      defp value_to_c(data_type, value) when data_type in [16, 17, 19], do: to_c(value)
      # SEMANTICCHANGESTRUCTUREDATATYPE
      defp value_to_c(data_type, {arg1, arg2}) when data_type == 25, do: {to_c(arg1), to_c(arg2)}
      defp value_to_c(_data_type, value), do: value

      defp parse_browse_name({:ok, {ns_index, name}}),
        do: {:ok, QualifiedName.new(ns_index: ns_index, name: name)}

      defp parse_browse_name(response), do: response

      defp parse_node_id({:ok, {ns_index, type, name}}),
        do: {:ok, NodeId.new(ns_index: ns_index, identifier_type: type, identifier: name)}

      defp parse_node_id(response), do: response

      defp parse_data_type({:ok, {ns_index, type, name}}),
        do: {:ok, NodeId.new(ns_index: ns_index, identifier_type: type, identifier: name)}

      defp parse_data_type(response), do: response

      defp parse_value({:ok, {ns_index, type, name, name_space_uri, server_index}}),
        do:
          {:ok,
           ExpandedNodeId.new(
             node_id: NodeId.new(ns_index: ns_index, identifier_type: type, identifier: name),
             name_space_uri: name_space_uri,
             server_index: server_index
           )}

      defp parse_value({:ok, {ns_index, type, name}}),
        do: {:ok, NodeId.new(ns_index: ns_index, identifier_type: type, identifier: name)}

      defp parse_value({:ok, {{ns_index1, type1, name1}, {ns_index2, type2, name2}}}),
        do: {
          :ok,
          {
            NodeId.new(ns_index: ns_index1, identifier_type: type1, identifier: name1),
            NodeId.new(ns_index: ns_index2, identifier_type: type2, identifier: name2)
          }
        }

      defp parse_value({:ok, {ns_index, name}}) when is_integer(ns_index),
        do: {:ok, QualifiedName.new(ns_index: ns_index, name: name)}

      defp parse_value({:ok, array}) when is_list(array),
        do: {:ok, Enum.map(array, fn(data) -> parse_c_value(data) end)}

      defp parse_value(response), do: response

      defp parse_c_value({ns_index, type, name, name_space_uri, server_index}),
        do:
          ExpandedNodeId.new(
            node_id: NodeId.new(ns_index: ns_index, identifier_type: type, identifier: name),
            name_space_uri: name_space_uri,
            server_index: server_index
          )

      defp parse_c_value({ns_index, type, name}),
        do: NodeId.new(ns_index: ns_index, identifier_type: type, identifier: name)

      defp parse_c_value({{ns_index1, type1, name1}, {ns_index2, type2, name2}}),
        do: {
          NodeId.new(ns_index: ns_index1, identifier_type: type1, identifier: name1),
          NodeId.new(ns_index: ns_index2, identifier_type: type2, identifier: name2)
        }

      defp parse_c_value({ns_index, name}) when is_integer(ns_index),
        do: QualifiedName.new(ns_index: ns_index, name: name)

      defp parse_c_value(array) when is_list(array),
        do: Enum.map(array, fn(data) -> parse_c_value(data) end)

      defp parse_c_value(response), do: response

      @doc false
      def set_ld_library_path(priv_dir) do
        System.get_env("LD_LIBRARY_PATH", "")
        |> String.contains?(priv_dir)
        |> write_ld_library_path(priv_dir)
      end

      defp write_ld_library_path(false, priv_dir) do
        ld_dirs =
          System.get_env("LD_LIBRARY_PATH", "")
          |> Path.join(":")
          |> Path.join(priv_dir)
          |> Path.join("/lib")

        System.put_env("LD_LIBRARY_PATH", ld_dirs)
        priv_dir
      end

      defp all_must_be(:integer, list),
        do: Enum.all?(list, fn x -> is_integer(x) end)

      defp get_array_raw_size(array_dimensions),
        do: Enum.reduce(array_dimensions, 1, fn(x, acc) -> x * acc end)

      defp write_ld_library_path(true, priv_dir), do: priv_dir

      defp get_binary_data(binary) when is_binary(binary), do: binary
      defp get_binary_data(function) when is_function(function), do: function.()
      defp get_binary_data(_non_binary_or_function), do: nil
    end
  end
end