defmodule Kino.JS.Live.Server do
  @moduledoc false

  use GenServer, restart: :temporary

  require Logger

  import Kino.Utils, only: [has_function?: 3]

  alias Kino.JS.Live.Context

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts)
  end

  defdelegate cast(pid, term), to: GenServer

  defdelegate call(pid, term, timeout), to: GenServer

  defdelegate reply(client, term), to: GenServer

  def broadcast_event(ctx, event, payload) do
    ref = ctx.__private__.ref
    Kino.Bridge.broadcast("js_live", ref, {:event, event, payload, %{ref: ref}})
    :ok
  end

  def send_event(ctx, client_id, event, payload) do
    ref = ctx.__private__.ref

    if pid = ctx.__private__.client_ids_with_pid[client_id] do
      Kino.Bridge.send(pid, {:event, event, payload, %{ref: ref}})
    end

    :ok
  end

  def emit_event(ctx, event) do
    send(Kino.SubscriptionManager, {:event, ctx.__private__.ref, event})
    :ok
  end

  @impl true
  def init({module, init_arg, ref, export}) do
    {:ok, ctx, _opts} = call_init(module, init_arg, ref)
    {:ok, %{module: module, ctx: ctx, export: export}}
  end

  @impl true
  def handle_cast(msg, state) do
    {:noreply, ctx} = state.module.handle_cast(msg, state.ctx)
    {:noreply, %{state | ctx: ctx}}
  end

  @impl true
  def handle_call(msg, from, state) do
    case state.module.handle_call(msg, from, state.ctx) do
      {:reply, reply, ctx} -> {:reply, reply, %{state | ctx: ctx}}
      {:noreply, ctx} -> {:noreply, %{state | ctx: ctx}}
    end
  end

  @impl true
  def handle_info({:export, pid, %{}}, state) do
    export_result = state.export.(state.ctx)
    Kino.Bridge.send(pid, {:export_reply, export_result, %{ref: state.ctx.__private__.ref}})
    {:noreply, state}
  end

  def handle_info(msg, state) do
    case call_handle_info(msg, state.module, state.ctx) do
      {:ok, ctx} -> {:noreply, %{state | ctx: ctx}}
      :error -> {:noreply, state}
    end
  end

  @impl true
  def terminate(reason, state) do
    call_terminate(reason, state.module, state.ctx)
  end

  # Handlers shared with Kino.SmartCell.Server

  def call_init(module, init_arg, ref) do
    ctx = Context.new()
    ctx = put_in(ctx.__private__[:ref], ref)
    ctx = put_in(ctx.__private__[:client_ids_with_pid], %{})
    ctx = put_in(ctx.__private__[:monitors_with_client_id], %{})

    if has_function?(module, :init, 2) do
      case module.init(init_arg, ctx) do
        {:ok, ctx} -> {:ok, ctx, []}
        {:ok, ctx, opts} -> {:ok, ctx, opts}
      end
    else
      {:ok, ctx, []}
    end
  end

  def call_handle_info(msg, module, ctx)

  def call_handle_info({:connect, pid, %{origin: origin}}, module, ctx) do
    ctx = add_client(ctx, pid, origin)

    ctx = %{ctx | origin: origin}
    {:ok, data, ctx} = module.handle_connect(ctx)
    ctx = %{ctx | origin: nil}

    Kino.Bridge.send(pid, {:connect_reply, data, %{ref: ctx.__private__.ref}})

    {:ok, ctx}
  end

  def call_handle_info({:event, event, payload, %{origin: origin}}, module, ctx) do
    ctx = %{ctx | origin: origin}
    {:noreply, ctx} = module.handle_event(event, payload, ctx)
    ctx = %{ctx | origin: nil}

    {:ok, ctx}
  end

  def call_handle_info({:ping, pid, term, _info}, _module, ctx) do
    Kino.Bridge.send(pid, {:pong, term, %{ref: ctx.__private__.ref}})

    {:ok, ctx}
  end

  def call_handle_info({:DOWN, ref, :process, _pid, _reason} = msg, module, ctx) do
    with :error <- remove_client(ctx, ref) do
      call_handle_info_fallback(msg, module, ctx)
    end
  end

  def call_handle_info(msg, module, ctx) do
    call_handle_info_fallback(msg, module, ctx)
  end

  def call_handle_info_fallback(msg, module, ctx) do
    if has_function?(module, :handle_info, 2) do
      {:noreply, ctx} = module.handle_info(msg, ctx)
      {:ok, ctx}
    else
      Logger.error(
        "received message in #{inspect(__MODULE__)}, but no handle_info/2 was defined in #{inspect(module)}"
      )

      :error
    end
  end

  def call_terminate(reason, module, ctx) do
    if has_function?(module, :terminate, 2) do
      module.terminate(reason, ctx)
    end

    :ok
  end

  defp add_client(ctx, pid, client_id) do
    if Map.has_key?(ctx.__private__.client_ids_with_pid, client_id) do
      ctx
    else
      monitor_ref = Kino.Bridge.monitor(pid)
      ctx = put_in(ctx.__private__.client_ids_with_pid[client_id], pid)
      put_in(ctx.__private__.monitors_with_client_id[monitor_ref], client_id)
    end
  end

  defp remove_client(ctx, monitor_ref) do
    {client_id, ctx} = pop_in(ctx.__private__.monitors_with_client_id[monitor_ref])

    if client_id do
      {_, ctx} = pop_in(ctx.__private__.client_ids_with_pid[client_id])
      {:ok, ctx}
    else
      :error
    end
  end
end
