lib/bridge.ex

defmodule Bridge do
  @moduledoc """
  Bridge is a drop-in replacement for Erlangs `wx` modules that
  sends commands sent to `wx` modules via tcp/ip to a daemon.
  It's purpose is to make it possible to communicate with native
  parts of iOS/Android applications within the elixir-desktop
  framework to make apps mobile!
  """
  use GenServer
  defstruct port: nil, socket: nil, send: nil, requests: %{}, funs: %{}

  def new([]) do
    # GenServer.start(__MODULE__)
  end

  @impl true
  def init([]) do
    port = String.to_integer(System.get_env("BRIDGE_PORT", "0"))

    {socket, send} =
      if port == 0 do
        {Bridge.Mock, &Bridge.Mock.send/2}
      else
        {:ok, socket} =
          :gen_tcp.connect({127, 0, 0, 1}, port, packet: 4, active: true, mode: :binary)

        {socket, &:gen_tcp.send/2}
      end

    {:ok,
     %Bridge{
       port: port,
       socket: socket,
       send: send
     }}
  end

  def bridge_call(:wx, :batch, [fun]), do: fun.()
  def bridge_call(:wx, :set_env, _args), do: :ok
  def bridge_call(:wx, :get_env, _args), do: :ok
  def bridge_call(:wx, :getObjectType, [obj]), do: Keyword.get(obj, :type)

  def bridge_call(:wx, :new, _args) do
    case Process.whereis(__MODULE__) do
      nil ->
        {:ok, pid} = GenServer.start(__MODULE__, [], name: __MODULE__)
        pid

      pid ->
        pid
    end
  end

  def bridge_call(type, :new, args) do
    [type: type, args: args]
  end

  def bridge_call(module, method = :connect, args) do
    IO.puts("bridge_cast: #{module}.#{method}(#{inspect(args)})")
    ref = System.unique_integer([:positive])
    json = encode!([module, method, args])

    GenServer.cast(__MODULE__, {:bridge_call, ref, json})
  end

  def bridge_call(module, method, args) do
    IO.puts("bridge_call: #{module}.#{method}(#{inspect(args)})")
    ref = System.unique_integer([:positive])
    json = encode!([module, method, args])

    ret =
      GenServer.call(__MODULE__, {:bridge_call, ref, json})
      |> decode!()

    IO.puts("bridge_call: #{module}.#{method}(#{inspect(args)}) => #{inspect(ret)}")

    ret
  end

  def encode!(var) do
    pre_encode!(var)
    |> Jason.encode!()
  end

  def pre_encode!(var) do
    case var do
      tuple when is_tuple(tuple) ->
        pre_encode!(%{_type: :tuple, value: Tuple.to_list(tuple)})

      pid when is_pid(pid) ->
        pre_encode!(%{_type: :pid, value: List.to_string(:erlang.pid_to_list(pid))})

      fun when is_function(fun) ->
        pre_encode!(%{_type: :fun, value: GenServer.call(__MODULE__, {:register_fun, fun})})

      list when is_list(list) ->
        Enum.map(list, &pre_encode!/1)

      map when is_map(map) ->
        Enum.reduce(map, %{}, fn {key, value}, map ->
          Map.put(map, pre_encode!(key), pre_encode!(value))
        end)

      atom when is_atom(atom) ->
        ":" <> Atom.to_string(atom)

      other ->
        other
    end
  end

  def decode!(json) do
    Jason.decode!(json) |> decode()
  end

  defp decode(list) when is_list(list) do
    Enum.map(list, &decode/1)
  end

  defp decode(":" <> name) do
    String.to_atom(name)
  end

  defp decode(map) when is_map(map) do
    Enum.reduce(map, %{}, fn {key, value}, ret ->
      Map.put(ret, decode(key), decode(value))
    end)
    |> decode_map()
  end

  defp decode(other), do: other

  defp decode_map(%{_type: :tuple, value: tuple}) do
    List.to_tuple(tuple)
  end

  defp decode_map(%{_type: :pid, value: pid}) do
    :erlang.list_to_pid(String.to_charlist(pid))
  end

  defp decode_map(other), do: other

  @impl true
  def handle_cast({:bridge_call, ref, json}, state) do
    case handle_call({:bridge_call, ref, json}, nil, state) do
      {:reply, _ret, state} -> {:noreply, state}
      {:noreply, state} -> {:noreply, state}
    end
  end

  @impl true
  def handle_call(
        {:bridge_call, ref, json},
        from,
        state = %Bridge{socket: socket, requests: reqs, send: send}
      ) do
    if socket do
      message = <<ref::unsigned-size(64), json::binary>>
      send.(socket, message)
      {:noreply, %Bridge{state | requests: Map.put(reqs, ref, {from, message})}}
    else
      {:reply, ":ok", state}
    end
  end

  def handle_call({:register_fun, fun}, _from, state = %Bridge{funs: funs}) do
    ref = System.unique_integer([:positive])
    funs = Map.put(funs, ref, fun)
    {:reply, ref, %Bridge{state | funs: funs}}
  end

  @impl true
  def handle_info(
        {:tcp, _port, <<ref::unsigned-size(64), json::binary>>},
        state = %Bridge{requests: reqs}
      ) do
    {from, message} = reqs[ref]

    if json == "use_mock" do
      Bridge.Mock.send(Bridge.Mock, message)
      {:noreply, state}
    else
      if from, do: GenServer.reply(from, json)
      {:noreply, %Bridge{state | requests: Map.delete(reqs, ref)}}
    end
  end

  defmacro generate_bridge_calls(module, names) do
    names = names ++ [:new, :destroy, :connect, :getId]

    methods =
      for name <- names do
        """
          def #{name}(), do: Bridge.bridge_call(:#{module}, :#{name}, [])
          def #{name}(arg), do: Bridge.bridge_call(:#{module}, :#{name}, [arg])
          def #{name}(arg1, arg2), do: Bridge.bridge_call(:#{module}, :#{name}, [arg1, arg2])
          def #{name}(arg1, arg2, arg3), do: Bridge.bridge_call(:#{module}, :#{name}, [arg1, arg2, arg3])
          def #{name}(arg1, arg2, arg3, arg4), do: Bridge.bridge_call(:#{module}, :#{name}, [arg1, arg2, arg3, arg4])
          def #{name}(arg1, arg2, arg3, arg4, arg5), do: Bridge.bridge_call(:#{module}, :#{name}, [arg1, arg2, arg3, arg4, arg5])
          def #{name}(arg1, arg2, arg3, arg4, arg5, arg6), do: Bridge.bridge_call(:#{module}, :#{
          name
        }, [arg1, arg2, arg3, arg4, arg5, arg6])
          def #{name}(arg1, arg2, arg3, arg4, arg5, arg6, arg7), do: Bridge.bridge_call(:#{module}, :#{
          name
        }, [arg1, arg2, arg3, arg4, arg5, arg6, arg7])
        """
      end

    code = """
    defmodule :#{module} do
      #{methods}
    end
    """

    Code.string_to_quoted(code)
  end
end