lib/desktop/menu/adapters/wx.ex

defmodule Desktop.Menu.Adapter.Wx do
  @moduledoc """
  WX Menu Adapter that creates menus and icons using :wx
  """
  alias Desktop.{Wx, OS}
  alias Desktop.Wx.TaskBarIcon

  require Record
  require Logger

  defstruct [
    :menu_pid,
    :env,
    :menubar,
    :menubar_opts,
    :taskbar_icon
  ]

  @type t() :: %__MODULE__{
          menu_pid: pid() | nil,
          env: any(),
          menubar: any(),
          menubar_opts: any(),
          taskbar_icon: TaskBarIcon.t() | nil
        }

  for tag <- [:wx, :wxCommand, :wxMenu] do
    Record.defrecordp(tag, Record.extract(tag, from_lib: "wx/include/wx.hrl"))
  end

  def new(opts) do
    %__MODULE__{
      env: Keyword.get(opts, :env),
      menu_pid: Keyword.get(opts, :menu_pid),
      menubar: nil,
      menubar_opts: Keyword.get(opts, :wx),
      taskbar_icon: nil
    }
  end

  def create(adapter = %__MODULE__{env: env, menubar_opts: menubar_opts}, dom) do
    :wx.set_env(env)

    create_menubar(adapter, menubar_opts, dom)
  end

  def update_dom(adapter = %__MODULE__{}, dom) do
    create_menu(adapter, dom)
  end

  def popup_menu(adapter = %__MODULE__{}) do
    do_popup_menu(adapter, :taskbar_click)
  end

  def recreate_menu(adapter = %__MODULE__{}, dom) do
    create_menu(adapter, dom)
  end

  def menubar(%__MODULE__{menubar: menubar}) do
    menubar
  end

  def get_icon(%__MODULE__{menubar: nil}) do
    nil
  end

  def get_icon(%__MODULE__{menubar: _}) do
    # nil
  end

  def set_icon(%__MODULE__{menubar: nil}, _) do
    {:error, "Cannot set icon on `nil` taskbar"}
  end

  def set_icon(adapter = %__MODULE__{taskbar_icon: nil}, nil) do
    {:ok, adapter}
  end

  def set_icon(adapter = %__MODULE__{taskbar_icon: taskbar_icon}, nil) do
    TaskBarIcon.remove_icon(taskbar_icon)
    {:ok, adapter}
  end

  def set_icon(adapter = %__MODULE__{taskbar_icon: nil}, _icon) do
    {:ok, adapter}
  end

  def set_icon(adapter = %__MODULE__{taskbar_icon: taskbar_icon}, icon) do
    TaskBarIcon.set_icon(taskbar_icon, icon)
    {:ok, adapter}
  end

  def handle_info(
        wx(id: _id, event: wxCommand(type: :command_menu_selected), userData: user_data),
        adapter = %{menu_pid: menu_pid}
      ) do
    spawn_link(Desktop.Menu, :trigger_event, [menu_pid, user_data])

    {:noreply, adapter}
  end

  def handle_info(
        wx(id: _id, event: wxMenu(type: :menu_close), userData: _user_data),
        adapter = %{}
      ) do
    {:noreply, adapter}
  end

  def handle_info(
        wx(event: {:wxTaskBarIcon, :taskbar_left_down}),
        adapter
      ) do
    {:noreply, do_popup_menu(adapter, :taskbar_left_down)}
  end

  def handle_info(
        wx(event: {:wxTaskBarIcon, :taskbar_right_down}),
        adapter
      ) do
    {:noreply, do_popup_menu(adapter, :taskbar_right_down)}
  end

  def handle_info(event = wx(), adapter) do
    Logger.warning("Desktop.Menu received unexpected wx message #{inspect({event, adapter})}")
    {:noreply, adapter}
  end

  # Private functions

  defp do_popup_menu(adapter = %__MODULE__{taskbar_icon: taskbar_icon}, event) do
    TaskBarIcon.popup_menu(taskbar_icon, event)
    adapter
  end

  defp create_menubar(adapter = %__MODULE__{}, {:taskbar, icon}, dom) do
    menubar = :wxMenuBar.new()

    adapter =
      %{adapter | menubar: menubar}
      |> create_menu(dom)

    create_popup = fn ->
      create_popup_menu(adapter)
    end

    taskbar_icon = create_taskbar_icon(create_popup, icon)
    %{adapter | taskbar_icon: taskbar_icon}
  end

  defp create_menubar(adapter = %__MODULE__{}, wx_ref, dom) do
    if :wxMenuBar == :wx.getObjectType(wx_ref) do
      %{adapter | menubar: wx_ref, taskbar_icon: nil}
      |> create_menu(dom)
    else
      %{adapter | menubar: nil, taskbar_icon: nil}
    end
  end

  defp create_taskbar_icon(fn_create_popup, icon) do
    with {:ok, taskbar_icon = %TaskBarIcon{wx_taskbar_icon: wx_taskbar_icon}} <-
           TaskBarIcon.create(fn_create_popup) do
      TaskBarIcon.connect(taskbar_icon)
      TaskBarIcon.set_icon(taskbar_icon, icon)

      if OS.type() == Windows do
        # This links the taskbar icon and the application itself on Windows
        if Code.ensure_loaded?(:wxNotificationMessage) &&
             Kernel.function_exported?(:wxNotificationMessage, :useTaskBarIcon, 1) do
          :wxNotificationMessage.useTaskBarIcon(wx_taskbar_icon)
        end
      end

      taskbar_icon
    else
      error ->
        Logger.warning("Failed to create TaskBar Icon: #{inspect(error)}")
        nil
    end
  end

  defp create_popup_menu(%__MODULE__{menubar: nil}) do
    :wx.null()
  end

  defp create_popup_menu(adapter = %__MODULE__{menubar: menubar}) do
    num_menus = :wxMenuBar.getMenuCount(menubar)

    if num_menus > 0 do
      menu = :wxMenuBar.remove(menubar, 0)

      # Because the menu is removed from the menubar
      # trigger a new menu create for next time the popup opens
      # outside of the Menu -> Adapter flow
      GenServer.cast(adapter.menu_pid, :recreate_menu)

      menu
    else
      :wx.null()
    end
  end

  defp create_menu(adapter, {:menubar, _, children}) do
    create_menu(adapter, children)
  end

  defp create_menu(adapter, {:menu, attrs, children}) do
    attrs =
      if Map.has_key?(attrs, :label) do
        attrs
      else
        Map.put(attrs, :label, "Default")
      end

    create_menu(adapter, [{:menu, attrs, children}])
  end

  defp create_menu(adapter = %__MODULE__{menubar: menubar}, dom) do
    :wx.set_env(Desktop.Env.wx_env())

    :wx.batch(fn ->
      menues = create_menu_items(nil, dom)

      # Enum.each(menues, fn {menu, _label} ->
      #   :wxMenu.connect(
      #     menu,
      #     :menu_close
      #   )
      # end)

      update_menubar(menubar, menues)
    end)

    adapter
  end

  defp update_menubar(adapter = %__MODULE__{menubar: menubar}, menues) do
    :wx.batch(fn ->
      update_menubar(menubar, menues)
    end)

    adapter
  end

  defp update_menubar(menubar, menues) do
    menubar
    |> do_reset_menubar()
    |> do_update_menubar(menues)
  end

  defp do_reset_menubar(menubar) do
    size = :wxMenuBar.getMenuCount(menubar)

    if size > 0 do
      menubar
      |> :wxMenuBar.remove(0)
      |> :wxMenu.destroy()

      do_reset_menubar(menubar)
    else
      menubar
    end
  end

  defp do_update_menubar(menubar, []) do
    menubar
  end

  defp do_update_menubar(menubar, [{menu, label} | menues]) do
    :wxMenuBar.append(menubar, menu, label)
    do_update_menubar(menubar, menues)
  end

  defp create_menu_items(_evt_handler, []) do
    []
  end

  defp create_menu_items(evt_handler, [{:menu, attr, content} | dom_elements]) do
    menu =
      Enum.reduce(content, :wxMenu.new(), fn dom_element, menu ->
        create_menu_item(menu, menu, dom_element)
      end)

    [{menu, attr[:label]} | create_menu_items(evt_handler, dom_elements)]
  end

  defp create_menu_items(evt_handler, [_ | dom_elements]) do
    create_menu_items(evt_handler, dom_elements)
  end

  defp create_menu_item(evt_handler, parent_menu, dom_element) do
    case dom_element do
      {:hr, _attr, _content} ->
        :wxMenu.appendSeparator(parent_menu)

      {:item, attr, content} ->
        kind =
          case attr[:type] do
            "checkbox" -> Wx.wxITEM_CHECK()
            "separator" -> Wx.wxITEM_SEPARATOR()
            "radio" -> Wx.wxITEM_RADIO()
            _other -> Wx.wxITEM_NORMAL()
          end

        item = :wxMenuItem.new(id: Wx.wxID_ANY(), text: List.flatten(content), kind: kind)

        id = :wxMenuItem.getId(item)

        :wxMenu.append(parent_menu, item)

        if attr[:checked] != nil do
          :wxMenuItem.check(item, check: is_true(attr[:checked]))
        end

        if is_true(attr[:disabled]) do
          :wxMenu.enable(parent_menu, id, false)
        end

        if attr[:onclick] != nil do
          :wxMenu.connect(
            evt_handler,
            :command_menu_selected,
            id: id,
            userData: attr[:onclick]
          )
        end

      {:menu, attr, content} ->
        submenu = create_menu_item(evt_handler, :wxMenu.new(), content)

        :wxMenu.append(
          parent_menu,
          Wx.wxID_ANY(),
          String.to_charlist(attr[:label] || ""),
          submenu
        )

      [_ | _] = items ->
        ret =
          Enum.reduce(items, parent_menu, fn item, parent_menu ->
            create_menu_item(evt_handler, parent_menu, item)
            parent_menu
          end)

        ret

      _ ->
        nil
    end

    parent_menu
  end

  defp is_true(value) do
    value != nil and value != "false" and value != "0"
  end
end