defmodule LivebookWeb.HomeLive do
  use LivebookWeb, :live_view

  import LivebookWeb.SessionHelpers

  alias LivebookWeb.{LearnHelpers, LayoutHelpers}
  alias Livebook.{Sessions, Session, LiveMarkdown, Notebook, FileSystem}

  on_mount LivebookWeb.SidebarHook

  @impl true
  def mount(params, _session, socket) do
    if connected?(socket) do
      Livebook.Sessions.subscribe()
      Livebook.SystemResources.subscribe()
    end

    sessions = Sessions.list_sessions() |> Enum.filter(&(&1.mode == :default))
    notebook_infos = Notebook.Learn.visible_notebook_infos() |> Enum.take(3)

    {:ok,
     assign(socket,
       self_path: ~p"/",
       file: determine_file(params),
       file_info: %{exists: true, access: :read_write},
       sessions: sessions,
       notebook_infos: notebook_infos,
       page_title: "Livebook",
       new_version: Livebook.UpdateCheck.new_version(),
       update_instructions_url: Livebook.Config.update_instructions_url(),
       app_service_url: Livebook.Config.app_service_url(),
       memory: Livebook.SystemResources.memory()
     )}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <LayoutHelpers.layout
      current_page={@self_path}
      current_user={@current_user}
      saved_hubs={@saved_hubs}
    >
      <:topbar_action>
        <div class="flex space-x-2">
          <.link
            patch={~p"/home/import/url"}
            class="button-base button-outlined-gray whitespace-nowrap"
          >
            Import
          </.link>
          <button class="button-base button-blue" phx-click="new">
            New notebook
          </button>
        </div>
      </:topbar_action>
      <.update_notification version={@new_version} instructions_url={@update_instructions_url} />
      <.memory_notification memory={@memory} app_service_url={@app_service_url} />
      <div class="p-4 md:px-12 md:py-6 max-w-screen-lg mx-auto space-y-4">
        <div class="flex flex-row space-y-0 items-center pb-4 justify-between">
          <LayoutHelpers.title text="Home" />
          <div class="hidden md:flex space-x-2" role="navigation" aria-label="new notebook">
            <.link
              patch={~p"/home/import/url"}
              class="button-base button-outlined-gray whitespace-nowrap"
            >
              Import
            </.link>
            <button class="button-base button-blue" phx-click="new">
              New notebook
            </button>
          </div>
        </div>

        <div class="h-80" role="region" aria-label="file system">
          <.live_component
            module={LivebookWeb.FileSelectComponent}
            id="home-file-select"
            file={@file}
            extnames={[LiveMarkdown.extension()]}
            running_files={files(@sessions)}
          >
            <div class="flex justify-end space-x-2">
              <button
                class="button-base button-outlined-gray whitespace-nowrap"
                phx-click="fork"
                disabled={not path_forkable?(@file, @file_info)}
              >
                <.remix_icon icon="git-branch-line" class="align-middle mr-1" />
                <span>Fork</span>
              </button>
              <%= if file_running?(@file, @sessions) do %>
                <.link
                  navigate={~p"/sessions/#{session_id_by_file(@file, @sessions)}"}
                  class="button-base button-blue"
                >
                  Join session
                </.link>
              <% else %>
                <span {open_button_tooltip_attrs(@file, @file_info)}>
                  <button
                    class="button-base button-blue"
                    phx-click="open"
                    disabled={not path_openable?(@file, @file_info, @sessions)}
                  >
                    Open
                  </button>
                </span>
              <% end %>
            </div>
          </.live_component>
        </div>

        <div class="py-12" data-el-learn-section role="region" aria-label="learn section">
          <div class="mb-4 flex justify-between items-center">
            <h2 class="uppercase font-semibold text-gray-500">
              Learn
            </h2>
            <.link navigate={~p"/learn"} class="flex items-center text-blue-600">
              <span class="font-semibold">See all</span>
              <.remix_icon icon="arrow-right-line" class="align-middle ml-1" />
            </.link>
          </div>
          <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
            <% # Note: it's fine to use stateless components in this comprehension,
            # because @notebook_infos never change %>
            <LearnHelpers.notebook_card :for={info <- @notebook_infos} notebook_info={info} />
          </div>
        </div>
        <div id="running-sessions" class="py-12" role="region" aria-label="running sessions">
          <.live_component
            module={LivebookWeb.HomeLive.SessionListComponent}
            id="session-list"
            sessions={@sessions}
            memory={@memory}
          />
        </div>
      </div>
    </LayoutHelpers.layout>

    <.modal
      :if={@live_action == :close_session}
      id="close-session-modal"
      show
      class="w-full max-w-xl"
      patch={@self_path}
    >
      <.live_component
        module={LivebookWeb.HomeLive.CloseSessionComponent}
        id="close-session"
        return_to={@self_path}
        session={@session}
      />
    </.modal>

    <.modal
      :if={@live_action == :import}
      id="import-modal"
      show
      class="w-full max-w-4xl"
      patch={@self_path}
    >
      <.live_component
        module={LivebookWeb.HomeLive.ImportComponent}
        id="import"
        tab={@tab}
        import_opts={@import_opts}
      />
    </.modal>

    <.modal
      :if={@live_action == :edit_sessions}
      id="edit-sessions-modal"
      show
      class="w-full max-w-xl"
      patch={@self_path}
    >
      <.live_component
        module={LivebookWeb.HomeLive.EditSessionsComponent}
        id="edit-sessions"
        action={@bulk_action}
        return_to={@self_path}
        sessions={@sessions}
        selected_sessions={selected_sessions(@sessions, @selected_session_ids)}
      />
    </.modal>
    """
  end

  defp open_button_tooltip_attrs(file, file_info) do
    if regular?(file, file_info) and not writable?(file_info) do
      [class: "tooltip top", data_tooltip: "This file is write-protected, please fork instead"]
    else
      []
    end
  end

  defp update_notification(%{version: nil} = assigns), do: ~H""

  defp update_notification(assigns) do
    ~H"""
    <div class="px-2 py-2 bg-blue-200 text-gray-900 text-sm text-center">
      <span>
        Livebook v<%= @version %> available!
        <%= if @instructions_url do %>
          Check out the news on
          <a
            class="font-medium border-b border-gray-900 hover:border-transparent"
            href="https://livebook.dev/"
            target="_blank"
          >
            livebook.dev
          </a>
          and follow the
          <a
            class="font-medium border-b border-gray-900 hover:border-transparent"
            href={@instructions_url}
            target="_blank"
          >
            update instructions
          </a>
        <% else %>
          Check out the news and installation steps on
          <a
            class="font-medium border-b border-gray-900 hover:border-transparent"
            href="https://livebook.dev/"
            target="_blank"
          >
            livebook.dev
          </a>
        <% end %>
        🚀
      </span>
    </div>
    """
  end

  defp memory_notification(assigns) do
    ~H"""
    <div
      :if={@app_service_url && @memory.free < 30_000_000}
      class="px-2 py-2 bg-red-200 text-gray-900 text-sm text-center"
    >
      <.remix_icon icon="alarm-warning-line" class="align-text-bottom mr-0.5" />
      Less than 30 MB of memory left, consider
      <a
        class="font-medium border-b border-gray-900 hover:border-transparent"
        href={@app_service_url}
        target="_blank"
      >
        adding more resources to the instance
      </a>
      or closing
      <a
        class="font-medium border-b border-gray-900 hover:border-transparent"
        href="#running-sessions"
      >
        running sessions
      </a>
    </div>
    """
  end

  @impl true
  def handle_params(%{"session_id" => session_id}, _url, socket) do
    session = Enum.find(socket.assigns.sessions, &(&1.id == session_id))
    {:noreply, assign(socket, session: session)}
  end

  def handle_params(%{"action" => action}, _url, socket)
      when socket.assigns.live_action == :edit_sessions do
    {:noreply, assign(socket, bulk_action: action)}
  end

  def handle_params(%{"tab" => tab} = params, _url, socket)
      when socket.assigns.live_action == :import do
    import_opts = [url: params["url"]]
    {:noreply, assign(socket, tab: tab, import_opts: import_opts)}
  end

  def handle_params(%{"url" => url}, _url, socket)
      when socket.assigns.live_action == :public_import do
    origin = Notebook.ContentLoader.url_to_location(url)

    origin
    |> Notebook.ContentLoader.fetch_content_from_location()
    |> case do
      {:ok, content} ->
        socket = import_content(socket, content, origin: origin)
        {:noreply, socket}

      {:error, _message} ->
        {:noreply, push_patch(socket, to: ~p"/home/import/url?url=#{url}")}
    end
  end

  def handle_params(%{"path" => path} = _params, _uri, socket)
      when socket.assigns.live_action == :public_open do
    path = Path.expand(path)
    file = FileSystem.File.local(path)

    if file_running?(file, socket.assigns.sessions) do
      session_id = session_id_by_file(file, socket.assigns.sessions)
      {:noreply, push_navigate(socket, to: ~p"/sessions/#{session_id}")}
    else
      {:noreply, open_notebook(socket, FileSystem.File.local(path))}
    end
  end

  def handle_params(_params, _url, socket), do: {:noreply, socket}

  @impl true
  def handle_event("new", %{}, socket) do
    {:noreply, create_session(socket)}
  end

  def handle_event("fork", %{}, socket) do
    file = socket.assigns.file

    socket =
      case import_notebook(file) do
        {:ok, {notebook, messages}} ->
          notebook = Notebook.forked(notebook)
          images_dir = Session.images_dir_for_notebook(file)

          socket
          |> put_import_warnings(messages)
          |> create_session(
            notebook: notebook,
            copy_images_from: images_dir,
            origin: {:file, file}
          )

        {:error, error} ->
          put_flash(socket, :error, Livebook.Utils.upcase_first(error))
      end

    {:noreply, socket}
  end

  def handle_event("open", %{}, socket) do
    file = socket.assigns.file
    {:noreply, open_notebook(socket, file)}
  end

  def handle_event("bulk_action", %{"action" => "disconnect"} = params, socket) do
    socket = assign(socket, selected_session_ids: params["session_ids"])
    {:noreply, push_patch(socket, to: ~p"/home/sessions/edit_sessions/disconnect")}
  end

  def handle_event("bulk_action", %{"action" => "close_all"} = params, socket) do
    socket = assign(socket, selected_session_ids: params["session_ids"])
    {:noreply, push_patch(socket, to: ~p"/home/sessions/edit_sessions/close_all")}
  end

  def handle_event("open_autosave_directory", %{}, socket) do
    file =
      Livebook.Settings.autosave_path()
      |> FileSystem.Utils.ensure_dir_path()
      |> FileSystem.File.local()

    file_info = %{exists: true, access: file_access(file)}
    {:noreply, assign(socket, file: file, file_info: file_info)}
  end

  @impl true
  def handle_info({:set_file, file, info}, socket) do
    file_info = %{exists: info.exists, access: file_access(file)}
    {:noreply, assign(socket, file: file, file_info: file_info)}
  end

  def handle_info({:session_created, session}, socket) do
    if session in socket.assigns.sessions do
      {:noreply, socket}
    else
      {:noreply, assign(socket, sessions: [session | socket.assigns.sessions])}
    end
  end

  def handle_info({:session_updated, session}, socket) do
    sessions =
      Enum.map(socket.assigns.sessions, fn other ->
        if other.id == session.id, do: session, else: other
      end)

    {:noreply, assign(socket, sessions: sessions)}
  end

  def handle_info({:session_closed, session}, socket) do
    sessions = Enum.reject(socket.assigns.sessions, &(&1.id == session.id))
    {:noreply, assign(socket, sessions: sessions)}
  end

  def handle_info({:import_content, content, session_opts}, socket) do
    socket = import_content(socket, content, session_opts)
    {:noreply, socket}
  end

  def handle_info({:memory_update, memory}, socket) do
    {:noreply, assign(socket, memory: memory)}
  end

  def handle_info(_message, socket), do: {:noreply, socket}

  defp files(sessions) do
    Enum.map(sessions, & &1.file)
  end

  defp path_forkable?(file, file_info) do
    regular?(file, file_info)
  end

  defp path_openable?(file, file_info, sessions) do
    regular?(file, file_info) and not file_running?(file, sessions) and
      writable?(file_info)
  end

  defp regular?(file, file_info) do
    file_info.exists and not FileSystem.File.dir?(file)
  end

  defp writable?(file_info) do
    file_info.access in [:read_write, :write]
  end

  defp file_running?(file, sessions) do
    running_files = files(sessions)
    file in running_files
  end

  defp import_notebook(file) do
    with {:ok, content} <- FileSystem.File.read(file) do
      {:ok, LiveMarkdown.notebook_from_livemd(content)}
    end
  end

  defp session_id_by_file(file, sessions) do
    session = Enum.find(sessions, &(&1.file == file))
    session.id
  end

  defp import_content(socket, content, session_opts) do
    {notebook, messages} = Livebook.LiveMarkdown.notebook_from_livemd(content)

    socket =
      socket
      |> put_import_warnings(messages)
      |> put_flash(
        :info,
        "You have imported a notebook, no code has been executed so far. You should read and evaluate code as needed."
      )

    session_opts = Keyword.merge(session_opts, notebook: notebook)
    create_session(socket, session_opts)
  end

  defp file_access(file) do
    case FileSystem.File.access(file) do
      {:ok, access} -> access
      {:error, _} -> :none
    end
  end

  defp selected_sessions(sessions, selected_session_ids) do
    Enum.filter(sessions, &(&1.id in selected_session_ids))
  end

  defp determine_file(%{"path" => path} = _params) do
    path = Path.expand(path)

    cond do
      File.dir?(path) ->
        path
        |> FileSystem.Utils.ensure_dir_path()
        |> FileSystem.File.local()

      File.regular?(path) ->
        FileSystem.File.local(path)

      true ->
        Livebook.Config.local_filesystem_home()
    end
  end

  defp determine_file(_params), do: Livebook.Settings.default_file_system_home()

  defp open_notebook(socket, file) do
    case import_notebook(file) do
      {:ok, {notebook, messages}} ->
        socket
        |> put_import_warnings(messages)
        |> create_session(notebook: notebook, file: file, origin: {:file, file})

      {:error, error} ->
        put_flash(socket, :error, Livebook.Utils.upcase_first(error))
    end
  end
end