defmodule BDS.Desktop.MainWindow do
  @moduledoc false

  use GenServer

  alias Desktop.Window
  require Logger

  @window_id __MODULE__
  @server_name BDS.Desktop.MainWindow.Watcher
  @persist_interval_ms 1_000
  @default_size {1280, 780}
  @default_min_size {800, 600}
  @state_file "window-state.json"

  def start_link(_opts) do
    GenServer.start_link(__MODULE__, :ok, name: @server_name)
  end

  def window_id, do: @window_id
  def server_name, do: @server_name

  def persist_now(timeout \\ 100) do
    GenServer.call(@server_name, :persist_bounds_now, timeout)
  catch
    :exit, reason ->
      Logger.debug("swallowed main_window persist_now exit: #{inspect(reason)}")
      :ok
  end

  def window_options(extra_opts \\ []) do
    desktop_config = Application.get_env(:bds, :desktop, [])
    restored = restore_bounds()
    {default_width, default_height} = Keyword.get(desktop_config, :window_size, @default_size)
    {min_width, min_height} = Keyword.get(desktop_config, :window_min_size, @default_min_size)

    startup_bounds =
      clamp_startup_bounds(restored || %{width: default_width, height: default_height})

    base_opts = [
      app: :bds,
      id: window_id(),
      title: Keyword.get(desktop_config, :title, "Blogging Desktop Server"),
      size: {startup_bounds.width, startup_bounds.height},
      min_size: {min_width, min_height}
    ]

    Keyword.merge(base_opts, extra_opts)
  end

  def restore_bounds do
    with path when is_binary(path) <- window_state_path(),
         true <- File.exists?(path),
         {:ok, body} <- File.read(path),
         {:ok, decoded} <- Jason.decode(body),
         {:ok, bounds} <- normalize_bounds(decoded) do
      clamp_startup_bounds(bounds)
    else
      _ -> nil
    end
  end

  def persist_bounds(%{x: _x, y: _y, width: _width, height: _height} = bounds) do
    path = window_state_path()
    File.mkdir_p!(Path.dirname(path))
    File.write(path, Jason.encode!(bounds))
  end

  @impl true
  def init(:ok) do
    Process.flag(:trap_exit, true)
    send(self(), :attach_window)
    {:ok, %{frame: nil, last_bounds: restore_bounds()}}
  end

  @impl true
  def handle_info(:attach_window, state) do
    case lookup_frame() do
      nil ->
        Process.send_after(self(), :attach_window, 200)
        {:noreply, state}

      frame ->
        apply_restored_bounds(frame)
        BDS.Desktop.Shutdown.install_handlers(frame)
        schedule_persist()

        {:noreply,
         %{state | frame: frame, last_bounds: current_bounds(frame) || state.last_bounds}}
    end
  end

  def handle_info(:persist_bounds, %{frame: frame} = state) do
    next_bounds = current_bounds(frame) || state.last_bounds

    if next_bounds && next_bounds != state.last_bounds do
      :ok = persist_bounds(next_bounds)
    end

    schedule_persist()
    {:noreply, %{state | last_bounds: next_bounds}}
  end

  @impl true
  def handle_call(:persist_bounds_now, _from, state) do
    {:reply, :ok, persist_current_bounds(state)}
  end

  @impl true
  def terminate(_reason, %{last_bounds: last_bounds}) do
    if bounds = last_bounds do
      _ = persist_bounds(bounds)
    end

    :ok
  end

  defp schedule_persist do
    Process.send_after(self(), :persist_bounds, @persist_interval_ms)
  end

  defp persist_current_bounds(%{frame: frame} = state) do
    next_bounds = current_bounds(frame) || state.last_bounds

    if next_bounds do
      _ = persist_bounds(next_bounds)
    end

    %{state | last_bounds: next_bounds}
  end

  defp apply_restored_bounds(frame) do
    case restore_bounds() do
      %{x: x, y: y, width: width, height: height} ->
        with_wx_env(fn ->
          :wxWindow.setSize(frame, {width, height})
          :wxWindow.move(frame, {x, y})
        end)

      _ ->
        :ok
    end
  end

  defp lookup_frame do
    try do
      Window.frame(window_id())
    catch
      :exit, _ -> nil
    end
  end

  defp current_bounds(nil), do: nil

  defp current_bounds(frame) do
    try do
      with_wx_env(fn ->
        cond do
          not :wxWindow.isShown(frame) ->
            nil

          :wxTopLevelWindow.isFullScreen(frame) ->
            nil

          :wxTopLevelWindow.isMaximized(frame) ->
            nil

          true ->
            {x, y} = :wxWindow.getPosition(frame)
            {width, height} = :wxWindow.getSize(frame)
            %{x: x, y: y, width: width, height: height}
        end
      end)
    rescue
      error in ErlangError ->
        Logger.debug("swallowed main_window restore_bounds error: #{inspect(error)}")
        nil

      error in FunctionClauseError ->
        Logger.debug("swallowed main_window restore_bounds error: #{inspect(error)}")
        nil
    catch
      :exit, reason ->
        Logger.debug("swallowed main_window restore_bounds exit: #{inspect(reason)}")
        nil
    end
  end

  defp with_wx_env(fun) do
    :wx.set_env(Desktop.Env.wx_env())
    fun.()
  end

  defp window_state_path do
    desktop_config = Application.get_env(:bds, :desktop, [])

    Keyword.get_lazy(desktop_config, :window_state_path, fn ->
      Path.join(config_dir(), @state_file)
    end)
  end

  defp config_dir do
    case :filename.basedir(:user_config, "BDS2") do
      path when is_list(path) -> List.to_string(path)
      path -> path
    end
  end

  defp normalize_bounds(%{"x" => x, "y" => y, "width" => width, "height" => height}) do
    normalize_bounds(%{x: x, y: y, width: width, height: height})
  end

  defp normalize_bounds(%{x: x, y: y, width: width, height: height})
       when is_integer(x) and is_integer(y) and is_integer(width) and is_integer(height) and
              width > 0 and height > 0 do
    {:ok, %{x: x, y: y, width: width, height: height}}
  end

  defp normalize_bounds(_value), do: :error

  defp clamp_startup_bounds(bounds) do
    case client_area() do
      %{width: area_width, height: area_height} ->
        %{bounds | width: min(bounds.width, area_width), height: min(bounds.height, area_height)}

      nil ->
        bounds
    end
  end

  defp client_area do
    desktop_config = Application.get_env(:bds, :desktop, [])

    case Keyword.get(desktop_config, :window_client_area_override) do
      {x, y, width, height}
      when is_integer(x) and is_integer(y) and is_integer(width) and is_integer(height) ->
        %{x: x, y: y, width: width, height: height}

      _ ->
        read_client_area_from_wx()
    end
  end

  defp read_client_area_from_wx do
    created_wx? = wx_env_undefined?()

    try do
      if created_wx?, do: :wx.new()

      display = :wxDisplay.new()

      if :wxDisplay.isOk(display) do
        {x, y, width, height} = :wxDisplay.getClientArea(display)
        :wxDisplay.destroy(display)
        %{x: x, y: y, width: width, height: height}
      else
        :wxDisplay.destroy(display)
        nil
      end
    rescue
      _ -> nil
    after
      if created_wx?, do: :wx.destroy()
    end
  end

  defp wx_env_undefined? do
    try do
      _ = :wx.get_env()
      false
    rescue
      ErlangError -> true
    end
  end
end
