lib/desktop/fallback.ex

defmodule Desktop.Fallback do
  require Logger
  alias Desktop.{Wx, OS}

  @moduledoc """
    Fallback handles version differences in the :wx modules needed for showing the
    WebView and Desktop notifications and it uses the highest available
    feature level while trying to stays backwards compatible to older :wx versions.
  """

  def webview_new(frame) do
    with :ok <- check_has_webview(),
         sizer <- clear_windows(frame),
         {:ok, webview} <- do_webview_new(frame) do
      call(:wxWebView, :connect, [webview, :webview_newwindow])
      call(:wxWebView, :connect, [webview, :webview_error])
      call(:wxWebView, :enableContextMenu, [webview, [enable: false]])

      :wxBoxSizer.add(sizer, webview, proportion: 1, flag: Wx.wxEXPAND())
      :wxSizer.layout(sizer)
      :wxSizer.show(sizer, true)
      :wxFrame.refresh(frame)
      webview
    else
      {:error, reason} ->
        Logger.warning(reason)
        nil
    end
  end

  defp check_has_webview() do
    if is_module?(:wxWebView) do
      :ok
    else
      {:error, "Missing support for wxWebView - upgrade to OTP/24. Will show OS browser instead"}
    end
  end

  defp clear_windows(frame) do
    sizer = :wxFrame.getSizer(frame)
    :wxSizer.clear(sizer, delete_windows: true)
    sizer
  end

  defp backend_available?(backend) do
    try do
      call(:wxWebView, :isBackendAvailable, [String.to_charlist(backend)])
    rescue
      _ in ErlangError -> false
    end
  end

  defp do_webview_new(frame) do
    cond do
      backend_available?(webview_backend_env()) ->
        do_webview_new(frame, backend: String.to_charlist(webview_backend_env()))

      backend_available?("wxWebViewChromium") ->
        do_webview_new(frame, backend: ~c"wxWebViewChromium")

      backend_available?("wxWebViewEdge") ->
        do_webview_new(frame, backend: ~c"wxWebViewEdge")

      OS.type() == Windows ->
        error_missing_edge(frame)

      true ->
        do_webview_new(frame, [])
    end
  end

  defp do_webview_new(frame, opts) do
    Desktop.Env.put(:webview_backend, Keyword.get(opts, :backend, "default"))

    try do
      {:ok, call(:wxWebView, :new, [frame, -1, [{:style, Desktop.Wx.wxNO_BORDER()} | opts]])}
    rescue
      _ -> {:error, "Your erlang-wx is missing wxWebView support. Will show OS browser instead"}
    end
  end

  def webview_backend_env() do
    System.get_env("WX_WEBVIEW_BACKEND", "none")
  end

  defp error_missing_edge(frame) do
    win = :wxHtmlWindow.new(frame, [])

    :wxHtmlWindow.setPage(win, """
      <html>
        <body>
          <h1>Missing Edge Runtime</h1>
          <p>This demo requires the edge runtime to be installed</p>
          <p>Please download it <a href="https://go.microsoft.com/fwlink/p/?LinkId=2124703">here</a> and try again</p>
          <p>
            <a href="https://go.microsoft.com/fwlink/p/?LinkId=2124703">https://go.microsoft.com/fwlink/p/?LinkId=2124703</a>
          </p>
        </body>
      </html>
    """)

    :wxHtmlWindow.connect(win, :command_html_link_clicked, skip: true)

    {:error,
     """
     Missing support for wxWebViewEdge.
     Check your OTP install for edge support and download it here:
     https://go.microsoft.com/fwlink/p/?LinkId=2124703
     """}
  end

  def webview_can_fix(nil), do: false

  def webview_can_fix(webview) do
    is_module?(:wxWebView) and OS.type() == Windows and
      backend_available?("wxWebViewEdge") and
      call(:wxWebView, :isShownOnScreen, [webview])
  end

  def webview_url(%Desktop.Window{webview: nil, last_url: last_url}), do: last_url

  def webview_url(%Desktop.Window{webview: webview}) do
    call(:wxWebView, :getCurrentURL, [webview])
  end

  def webview_show(%Desktop.Window{webview: nil}, url, _) do
    OS.launch_default_browser(url)
  end

  def webview_show(
        %Desktop.Window{webview: webview, frame: frame, last_url: last},
        url,
        only_open
      ) do
    if last == nil or not only_open do
      call(:wxWebView, :loadURL, [webview, url])
    end

    if :wxTopLevelWindow.isIconized(frame) do
      :wxTopLevelWindow.iconize(frame, iconize: false)
    end

    if not :wxWindow.isShown(frame) do
      :wxWindow.show(frame, show: true)
      :wxTopLevelWindow.centerOnScreen(frame)
    end

    OS.raise_frame(frame)
  end

  def webview_rebuild(%Desktop.Window{webview: nil}), do: nil

  def webview_rebuild(%Desktop.Window{frame: frame, last_url: url}) do
    # url = call(:wxWebView, :getCurrentURL, [webview])
    webview = webview_new(frame)
    Logger.info("Rebuilding WebView on Windows with url: #{inspect(url)}")

    if url != nil do
      call(:wxWebView, :loadURL, [webview, url])
    end

    webview
  end

  def notification_new(title, type) do
    if is_module?(:wxNotificationMessage) do
      flag =
        case type do
          :info -> Wx.wxICON_INFORMATION()
          :warning -> Wx.wxICON_WARNING()
          :error -> Wx.wxICON_ERROR()
        end

      notification = call(:wxNotificationMessage, :new, [title, [flags: flag]])

      if notification_events_available?() do
        for event <- [
              :notification_message_click,
              :notification_message_dismissed,
              :notification_message_action
            ] do
          call(:wxNotificationMessage, :connect, [notification, event])
        end
      else
        Logger.warning(
          "Missing support for wxNotificationMessage Events - upgrade to wxWidgets 3.1 - messages won't be clickable"
        )
      end

      notification
    else
      Logger.warning(
        "Missing support for wxNotificationMessage - upgrade to OTP/24. Messages will be logged only"
      )
    end
  end

  def notification_show(notification, message, timeout, title \\ nil) do
    if is_module?(:wxNotificationMessage) do
      if title != nil do
        call(:wxNotificationMessage, :setTitle, [notification, to_charlist(title)])
      end

      call(:wxNotificationMessage, :setMessage, [notification, to_charlist(message)])
      call(:wxNotificationMessage, :show, [notification, [timeout: timeout]])
    else
      Logger.notice("NOTIFICATION: #{title}: #{message}")
    end
  end

  def wx_subscribe() do
    call(:wx, :subscribe_events)
  end

  def wx_new(opts) do
    call(:wx, :new, [opts])
  end

  def wx_get_env() do
    call(:wx, :get_env)
  end

  defp is_module?(module) do
    Code.ensure_compiled(module) == {:module, module}
  end

  defp notification_events_available?() do
    {Wx.wxMAJOR_VERSION(), Wx.wxMINOR_VERSION(), Wx.wxRELEASE_NUMBER()}
    |> case do
      {major, minor, _} when major >= 3 and minor >= 1 -> true
      _ -> false
    end
  end

  defp call(module, method, args \\ []) do
    if System.get_env("NO_WX") == nil and Code.ensure_loaded?(module) and
         Kernel.function_exported?(module, method, length(args)) do
      apply(module, method, args)
    end
  end
end