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!

  # Protocol

  Current protocol is json based.

  All :wx***.new(args...) calls generate keyword lists like:
    `[id: System.unique_integer([:positive]), type: module, args: args]`

  Most wx***.method(args...) calls are then forwarded via JSON to the native side with
  a 64-bit request id value:
    `<<request_ref :: unsigned-size(64), json :: binary>>`

  The responses correspndingly return the same ref and the json response:
    `<<response_ref :: unsigned-size(64), json :: binary>>`

  For receiving commands from the native side of the bridge there are three special ref
  values:
    * ref = 0 -> This indicates a published system event system, corresponding to `:wx.subscribe_events()`
      needed for publishing files that are shared to the app.
      `<<0 :: unsigned-size(64), event :: binary>>`
    * ref = 1 -> This indicates triggering a callback function call that was previously passed over.
      Internally an `funs` that are passed into `:wx.method()` calls are converted to 64-bit references,
      those can be used here to indicate which function to call.
    `<<1 :: unsigned-size(64), fun :: unsigned-size(64), event :: binary>>`
    * ref = 2 -> This indicates a call from the native side back into the app side. TBD
    `<<2 :: unsigned-size(64), ...>>`

    # JSON Encoding of Elixir Terms



  """
  use GenServer

  defstruct port: nil,
            socket: nil,
            send: nil,
            requests: %{},
            funs: %{},
            events: [],
            subscribers: []

  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
    [id: System.unique_integer([:positive]), type: type, args: args]
  end

  def bridge_call(_type, :getId, args) do
    Keyword.get(args, :id)
  end

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

    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]) + 10
    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(
        {:subscribe_events, pid},
        _from,
        state = %Bridge{events: events, subscribers: subscribers}
      ) do
    for event <- events do
      send(pid, event)
    end

    {:reply, :ok, %Bridge{state | events: [], subscribers: [pid | subscribers]}}
  end

  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, <<0::unsigned-size(64), json::binary>>},
        state = %Bridge{subscribers: subscribers, events: events}
      ) do
    event = decode!(json)

    if [] == subscribers do
      IO.puts("no subscriber for event #{inspect(event)}")
      {:noreply, %Bridge{state | events: events ++ [event]}}
    else
      IO.puts("sending event to subscribers #{inspect(event)}")

      for sub <- subscribers do
        send(sub, event)
      end

      {:noreply, state}
    end
  end

  def handle_info(
        {:tcp, _port, <<1::unsigned-size(64), fun_ref::unsigned-size(64), json::binary>>},
        state = %Bridge{funs: funs}
      ) do
    args = decode!(json)

    case Map.get(funs, fun_ref) do
      nil ->
        IO.puts("no fun defined for fun_ref #{fun_ref} (#{inspect(args)})")

      fun ->
        IO.puts("executing callback fun_ref #{fun_ref} (#{inspect(args)})")
        spawn(fn -> apply(fun, args) end)
    end

    {:noreply, state}
  end

  def handle_info(
        {:tcp, _port, <<2::unsigned-size(64), json::binary>>},
        state = %Bridge{}
      ) do
    json = decode!(json)
    payload = json[:payload]
    pid = json[:pid]
    IO.puts("sending event #{inspect(payload)} to #{inspect(pid)}")
    send(pid, payload)
    {:noreply, state}
  end

  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