defmodule MemexWeb.CoreComponents do
  @moduledoc """
  Provides core UI components.
  """
  use PhoenixHTMLHelpers
  use Phoenix.Component
  use MemexWeb, :verified_routes
  use Gettext, backend: MemexWeb.Gettext
  import MemexWeb.HTMLHelpers
  alias Memex.{Accounts, Accounts.Invite, Accounts.User}
  alias Memex.Contexts.Context
  alias Memex.Notes.Note
  alias Memex.Pipelines.{Pipeline, Steps.Step}
  alias Phoenix.HTML
  alias Phoenix.LiveView.JS

  embed_templates("core_components/*")

  attr :title_content, :string, default: nil
  attr :current_user, User, default: nil

  def topbar(assigns)

  attr :return_to, :string, required: true
  slot(:inner_block)

  @doc """
  Renders a live component inside a modal.

  The rendered modal receives a `:return_to` option to properly update
  the URL when the modal is closed.

  ## Examples

      <.modal return_to={~p"/\#{<%= schema.plural %>}"}>
        <.live_component
          module={<%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent}
          id={@<%= schema.singular %>.id || :new}
          title={@page_title}
          action={@live_action}
          return_to={~p"/\#{<%= schema.singular %>}"}
          <%= schema.singular %>: @<%= schema.singular %>
        />
      </.modal>
  """
  def modal(assigns)

  defp hide_modal(js \\ %JS{}) do
    js
    |> JS.hide(to: "#modal", transition: "fade-out")
    |> JS.hide(to: "#modal-bg", transition: "fade-out")
    |> JS.hide(to: "#modal-content", transition: "fade-out-scale")
  end

  attr :action, :string, required: true
  attr :value, :boolean, required: true
  attr :id, :string, default: nil
  slot(:inner_block)

  @doc """
  A toggle button element that can be directed to a liveview or a
  live_component's `handle_event/3`.

  ## Examples

  <.toggle_button action="my_liveview_action" value={@some_value}>
    <span>Toggle me!</span>
  </.toggle_button>
  <.toggle_button action="my_live_component_action" target={@myself} value={@some_value}>
    <span>Whatever you want</span>
  </.toggle_button>
  """
  def toggle_button(assigns)

  attr :user, User, required: true
  slot(:inner_block, required: true)

  def user_card(assigns)

  attr :invite, Invite, required: true
  attr :use_count, :integer, default: nil
  attr :current_user, User, required: true
  slot(:inner_block)
  slot(:code_actions)

  def invite_card(assigns)

  attr :id, :string, required: true
  attr :datetime, :any, required: true, doc: "A `DateTime` struct or nil"

  @doc """
  Phoenix.Component for a <time> element that renders the DateTime in the
  user's local timezone
  """
  def datetime(assigns)

  @spec cast_datetime(DateTime.t() | nil) :: String.t()
  defp cast_datetime(%DateTime{} = datetime) do
    datetime |> DateTime.to_iso8601(:extended)
  end

  defp cast_datetime(_datetime), do: ""

  attr :id, :string, required: true
  attr :date, :any, required: true, doc: "A `Date` struct or nil"

  @doc """
  Phoenix.Component for a <date> element that renders the Date in the user's
  local timezone
  """
  def date(assigns)

  attr :content, :string, required: true
  attr :filename, :string, default: "qrcode", doc: "filename without .png extension"
  attr :image_class, :string, default: "w-64 h-max"
  attr :width, :integer, default: 384, doc: "width of png to generate"

  @doc """
  Creates a downloadable QR Code element
  """
  def qr_code(assigns)

  attr :note, Note, required: true

  def note_content(assigns)

  attr :context, Context, required: true

  def context_content(assigns)

  attr :step, Step, required: true

  def step_content(assigns)

  attr :pipeline, Pipeline, required: true

  def pipeline_content(assigns)

  defp display_links(record) do
    record
    |> get_content()
    |> Phoenix.HTML.html_escape()
    |> Phoenix.HTML.safe_to_string()
    |> replace_hyperlinks(record)
    |> replace_triple_links(record)
    |> replace_double_links(record)
    |> replace_single_links(record)
    |> HTML.raw()
  end

  defp get_content(%{content: content}), do: content |> get_text()
  defp get_content(%{description: description}), do: description |> get_text()
  defp get_content(_fallthrough), do: nil |> get_text()

  defp get_text(string) when is_binary(string), do: string
  defp get_text(_fallthrough), do: ""

  # replaces hyperlinks like https://bubbletea.dev
  #
  # link regex from
  # https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url
  # and modified with additional schemes from
  # https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
  defp replace_hyperlinks(content, _record) do
    Regex.replace(
      ~r<((file|git|https?|ipfs|ipns|irc|jabber|magnet|mailto|mumble|tel|udp|xmpp):\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))>,
      content,
      fn _whole_match, link ->
        link =
          link(
            link,
            to: link,
            class: "link inline break-words",
            target: "_blank",
            rel: "noopener noreferrer"
          )
          |> HTML.Safe.to_iodata()
          |> IO.iodata_to_binary()

        "</p>#{link}<p class=\"inline break-words\">"
      end
    )
  end

  # replaces triple links like [[[slug-title]]]
  defp replace_triple_links(content, _record) do
    Regex.replace(
      ~r/(^|[^\[])\[\[\[([\p{L}\p{N}\-]+)\]\]\]($|[^\]])/,
      content,
      fn _whole_match, prefix, slug, suffix ->
        link =
          link(
            "[[[#{slug}]]]",
            to: ~p"/note/#{slug}",
            class: "link inline break-words"
          )
          |> HTML.Safe.to_iodata()
          |> IO.iodata_to_binary()

        "#{prefix}</p>#{link}<p class=\"inline break-words\">#{suffix}"
      end
    )
  end

  # replaces double links like [[slug-title]]
  defp replace_double_links(content, record) do
    Regex.replace(
      ~r/(^|[^\[])\[\[([\p{L}\p{N}\-]+)\]\]($|[^\]])/,
      content,
      fn _whole_match, prefix, slug, suffix ->
        target =
          case record do
            %Pipeline{} -> ~p"/context/#{slug}"
            %Step{} -> ~p"/context/#{slug}"
            _context -> ~p"/note/#{slug}"
          end

        link =
          link(
            "[[#{slug}]]",
            to: target,
            class: "link inline break-words"
          )
          |> HTML.Safe.to_iodata()
          |> IO.iodata_to_binary()

        "#{prefix}</p>#{link}<p class=\"inline break-words\">#{suffix}"
      end
    )
  end

  # replaces single links like [slug-title]
  defp replace_single_links(content, record) do
    Regex.replace(
      ~r/(^|[^\[])\[([\p{L}\p{N}\-]+)\]($|[^\]])/,
      content,
      fn _whole_match, prefix, slug, suffix ->
        target =
          case record do
            %Pipeline{} -> ~p"/pipeline/#{slug}"
            %Step{} -> ~p"/pipeline/#{slug}"
            %Context{} -> ~p"/context/#{slug}"
            _note -> ~p"/note/#{slug}"
          end

        link =
          link(
            "[#{slug}]",
            to: target,
            class: "link inline break-words"
          )
          |> HTML.Safe.to_iodata()
          |> IO.iodata_to_binary()

        "#{prefix}</p>#{link}<p class=\"inline break-words\">#{suffix}"
      end
    )
  end
end