# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.MastodonAPI.StatusView do
  use Pleroma.Web, :view

  require Pleroma.Constants

  alias Pleroma.Activity
  alias Pleroma.HTML
  alias Pleroma.Maps
  alias Pleroma.Object
  alias Pleroma.Repo
  alias Pleroma.User
  alias Pleroma.UserRelationship
  alias Pleroma.Web.CommonAPI
  alias Pleroma.Web.CommonAPI.Utils
  alias Pleroma.Web.MastodonAPI.AccountView
  alias Pleroma.Web.MastodonAPI.PollView
  alias Pleroma.Web.MastodonAPI.StatusView
  alias Pleroma.Web.MediaProxy
  alias Pleroma.Web.PleromaAPI.EmojiReactionController
  require Logger

  import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2]

  # This is a naive way to do this, just spawning a process per activity
  # to fetch the preview. However it should be fine considering
  # pagination is restricted to 40 activities at a time
  defp fetch_rich_media_for_activities(activities) do
    Enum.each(activities, fn activity ->
      spawn(fn ->
        Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
      end)
    end)
  end

  # TODO: Add cached version.
  defp get_replied_to_activities([]), do: %{}

  defp get_replied_to_activities(activities) do
    activities
    |> Enum.map(fn
      %{data: %{"type" => "Create"}} = activity ->
        object = Object.normalize(activity, fetch: false)
        object && object.data["inReplyTo"] != "" && object.data["inReplyTo"]

      _ ->
        nil
    end)
    |> Enum.filter(& &1)
    |> Activity.create_by_object_ap_id_with_object()
    |> Repo.all()
    |> Enum.reduce(%{}, fn activity, acc ->
      object = Object.normalize(activity, fetch: false)
      if object, do: Map.put(acc, object.data["id"], activity), else: acc
    end)
  end

  # DEPRECATED This field seems to be a left-over from the StatusNet era.
  # If your application uses `pleroma.conversation_id`: this field is deprecated.
  # It is currently stubbed instead by doing a CRC32 of the context, and
  # clearing the MSB to avoid overflow exceptions with signed integers on the
  # different clients using this field (Java/Kotlin code, mostly; see Husky.)
  # This should be removed in a future version of Pleroma. Pleroma-FE currently
  # depends on this field, as well.
  defp get_context_id(%{data: %{"context" => context}}) when is_binary(context) do
    import Bitwise

    :erlang.crc32(context)
    |> band(bnot(0x8000_0000))
  end

  defp get_context_id(_), do: nil

  # Check if the user reblogged this status
  defp reblogged?(activity, %User{ap_id: ap_id}) do
    with %Object{data: %{"announcements" => announcements}} when is_list(announcements) <-
           Object.normalize(activity, fetch: false) do
      ap_id in announcements
    else
      _ -> false
    end
  end

  # False if the user is logged out
  defp reblogged?(_activity, _user), do: false

  def render("index.json", opts) do
    Logger.debug("Rendering index")
    reading_user = opts[:for]
    # To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
    activities = Enum.filter(opts.activities, & &1)

    # Start fetching rich media before doing anything else, so that later calls to get the cards
    # only block for timeout in the worst case, as opposed to
    # length(activities_with_links) * timeout
    fetch_rich_media_for_activities(activities)
    replied_to_activities = get_replied_to_activities(activities)

    parent_activities =
      activities
      |> Enum.filter(&(&1.data["type"] == "Announce" && &1.data["object"]))
      |> Enum.map(&Object.normalize(&1, fetch: false).data["id"])
      |> Activity.create_by_object_ap_id()
      |> Activity.with_preloaded_object(:left)
      |> Activity.with_preloaded_bookmark(reading_user)
      |> Activity.with_set_thread_muted_field(reading_user)
      |> Repo.all()

    relationships_opt =
      cond do
        Map.has_key?(opts, :relationships) ->
          opts[:relationships]

        is_nil(reading_user) ->
          UserRelationship.view_relationships_option(nil, [])

        true ->
          # Note: unresolved users are filtered out
          actors =
            (activities ++ parent_activities)
            |> Enum.map(&CommonAPI.get_user(&1.data["actor"], false))
            |> Enum.filter(& &1)

          UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes)
      end

    opts =
      opts
      |> Map.put(:replied_to_activities, replied_to_activities)
      |> Map.put(:parent_activities, parent_activities)
      |> Map.put(:relationships, relationships_opt)

    render_many(activities, StatusView, "show.json", opts)
  end

  def render(
        "show.json",
        %{activity: %{id: id, data: %{"type" => "Announce", "object" => _object}} = activity} =
          opts
      ) do
    Logger.debug("Rendering reblog #{id}")
    user = CommonAPI.get_user(activity.data["actor"])
    created_at = Utils.to_masto_date(activity.data["published"])
    object = Object.normalize(activity, fetch: false)

    reblogged_parent_activity =
      if opts[:parent_activities] do
        Activity.Queries.find_by_object_ap_id(
          opts[:parent_activities],
          object.data["id"]
        )
      else
        Activity.create_by_object_ap_id(object.data["id"])
        |> Activity.with_preloaded_bookmark(opts[:for])
        |> Activity.with_set_thread_muted_field(opts[:for])
        |> Repo.one()
      end

    reblog_rendering_opts = Map.put(opts, :activity, reblogged_parent_activity)
    reblogged = render("show.json", reblog_rendering_opts)

    favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])

    bookmarked = Activity.get_bookmark(reblogged_parent_activity, opts[:for]) != nil

    mentions =
      activity.recipients
      |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
      |> Enum.filter(& &1)
      |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)

    {pinned?, pinned_at} = pin_data(object, user)
    lang = language(object)

    %{
      id: to_string(activity.id),
      uri: object.data["id"],
      url: object.data["id"],
      account:
        AccountView.render("show.json", %{
          user: user,
          for: opts[:for]
        }),
      in_reply_to_id: nil,
      in_reply_to_account_id: nil,
      reblog: reblogged,
      content: "",
      created_at: created_at,
      reblogs_count: 0,
      replies_count: 0,
      favourites_count: 0,
      reblogged: reblogged?(reblogged_parent_activity, opts[:for]),
      favourited: present?(favorited),
      bookmarked: present?(bookmarked),
      muted: false,
      pinned: pinned?,
      sensitive: false,
      spoiler_text: "",
      visibility: get_visibility(activity),
      media_attachments: reblogged[:media_attachments] || [],
      mentions: mentions,
      tags: reblogged[:tags] || [],
      application: build_application(object.data["generator"]),
      language: lang,
      emojis: [],
      pleroma: %{
        local: activity.local,
        pinned_at: pinned_at
      }
    }
  end

  def render("show.json", %{activity: %{id: id, data: %{"object" => _object}} = activity} = opts) do
    Logger.debug("Rendering status #{id}")

    with %Object{} = object <- Object.normalize(activity, fetch: false) do
      user = CommonAPI.get_user(activity.data["actor"])
      user_follower_address = user.follower_address

      like_count = object.data["like_count"] || 0
      announcement_count = object.data["announcement_count"] || 0

      hashtags = Object.hashtags(object)
      sensitive = object.data["sensitive"] || Enum.member?(hashtags, "nsfw")

      tags = Object.tags(object)

      tag_mentions =
        tags
        |> Enum.filter(fn tag -> is_map(tag) and tag["type"] == "Mention" end)
        |> Enum.map(fn tag -> tag["href"] end)

      to_data = if is_nil(object.data["to"]), do: [], else: object.data["to"]

      mentions =
        (to_data ++ tag_mentions)
        |> Enum.uniq()
        |> Enum.map(fn
          Pleroma.Constants.as_public() -> nil
          ^user_follower_address -> nil
          ap_id -> User.get_cached_by_ap_id(ap_id)
        end)
        |> Enum.filter(& &1)
        |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)

      favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])

      bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil

      client_posted_this_activity = opts[:for] && user.id == opts[:for].id

      expires_at =
        with true <- client_posted_this_activity,
             %Oban.Job{scheduled_at: scheduled_at} <-
               Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) do
          scheduled_at
        else
          _ -> nil
        end

      thread_muted? =
        cond do
          is_nil(opts[:for]) -> false
          is_boolean(activity.thread_muted?) -> activity.thread_muted?
          true -> CommonAPI.thread_muted?(opts[:for], activity)
        end

      attachment_data = object.data["attachment"] || []
      attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)

      created_at = Utils.to_masto_date(object.data["published"])

      edited_at =
        with %{"updated" => updated} <- object.data,
             date <- Utils.to_masto_date(updated),
             true <- date != "" do
          date
        else
          _ ->
            nil
        end

      reply_to = get_reply_to(activity, opts)

      reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"])

      history_len =
        1 +
          (Object.Updater.history_for(object.data)
           |> Map.get("orderedItems")
           |> length())

      # See render("history.json", ...) for more details
      # Here the implicit index of the current content is 0
      chrono_order = history_len - 1

      content =
        object
        |> render_content()

      content_html =
        content
        |> Activity.HTML.get_cached_scrubbed_html_for_activity(
          User.html_filter_policy(opts[:for]),
          activity,
          "mastoapi:content:#{chrono_order}"
        )

      content_plaintext =
        content
        |> Activity.HTML.get_cached_stripped_html_for_activity(
          activity,
          "mastoapi:content:#{chrono_order}"
        )

      summary = object.data["summary"] || ""

      card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))

      url =
        if user.local do
          Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
        else
          object.data["url"] || object.data["external_url"] || object.data["id"]
        end

      direct_conversation_id =
        with {_, nil} <- {:direct_conversation_id, opts[:direct_conversation_id]},
             {_, true} <- {:include_id, opts[:with_direct_conversation_id]},
             {_, %User{} = for_user} <- {:for_user, opts[:for]} do
          Activity.direct_conversation_id(activity, for_user)
        else
          {:direct_conversation_id, participation_id} when is_integer(participation_id) ->
            participation_id

          _e ->
            nil
        end

      emoji_reactions =
        object.data
        |> Map.get("reactions", [])
        |> EmojiReactionController.filter_allowed_users(
          opts[:for],
          Map.get(opts, :with_muted, false)
        )
        |> Stream.map(fn {emoji, users, url} ->
          build_emoji_map(emoji, users, url, opts[:for])
        end)
        |> Enum.to_list()

      # Status muted state (would do 1 request per status unless user mutes are preloaded)
      muted =
        thread_muted? ||
          UserRelationship.exists?(
            get_in(opts, [:relationships, :user_relationships]),
            :mute,
            opts[:for],
            user,
            fn for_user, user -> User.mutes?(for_user, user) end
          )

      {pinned?, pinned_at} = pin_data(object, user)

      quote = Activity.get_quoted_activity_from_object(object)
      lang = language(object)

      %{
        id: to_string(activity.id),
        uri: object.data["id"],
        url: url,
        account:
          AccountView.render("show.json", %{
            user: user,
            for: opts[:for]
          }),
        in_reply_to_id: reply_to && to_string(reply_to.id),
        in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
        reblog: nil,
        card: card,
        content: content_html,
        text: opts[:with_source] && get_source_text(object.data["source"]),
        created_at: created_at,
        edited_at: edited_at,
        reblogs_count: announcement_count,
        replies_count: object.data["repliesCount"] || 0,
        favourites_count: like_count,
        reblogged: reblogged?(activity, opts[:for]),
        favourited: present?(favorited),
        bookmarked: present?(bookmarked),
        muted: muted,
        pinned: pinned?,
        sensitive: sensitive,
        spoiler_text: summary,
        visibility: get_visibility(object),
        media_attachments: attachments,
        poll: render(PollView, "show.json", object: object, for: opts[:for]),
        mentions: mentions,
        tags: build_tags(tags),
        application: build_application(object.data["generator"]),
        language: lang,
        emojis: build_emojis(object.data["emoji"]),
        quote_id: if(quote, do: quote.id, else: nil),
        quote: maybe_render_quote(quote, opts),
        emoji_reactions: emoji_reactions,
        pleroma: %{
          local: activity.local,
          conversation_id: get_context_id(activity),
          context: object.data["context"],
          in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
          content: %{"text/plain" => content_plaintext},
          spoiler_text: %{"text/plain" => summary},
          expires_at: expires_at,
          direct_conversation_id: direct_conversation_id,
          thread_muted: thread_muted?,
          emoji_reactions: emoji_reactions,
          parent_visible: visible_for_user?(reply_to, opts[:for]),
          pinned_at: pinned_at
        },
        akkoma: %{
          source: object.data["source"]
        }
      }
    else
      nil -> nil
    end
  end

  def render("show.json", _) do
    nil
  end

  def render("history.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
    Logger.debug("Rendering history for #{activity.id}")
    object = Object.normalize(activity, fetch: false)

    hashtags = Object.hashtags(object)

    user = CommonAPI.get_user(activity.data["actor"])

    past_history =
      Object.Updater.history_for(object.data)
      |> Map.get("orderedItems")
      |> Enum.map(&Map.put(&1, "id", object.data["id"]))
      |> Enum.map(&%Object{data: &1, id: object.id})

    history =
      [object | past_history]
      # Mastodon expects the original to be at the first
      |> Enum.reverse()
      |> Enum.with_index()
      |> Enum.map(fn {object, chrono_order} ->
        %{
          # The history is prepended every time there is a new edit.
          # In chrono_order, the oldest item is always at 0, and so on.
          # The chrono_order is an invariant kept between edits.
          chrono_order: chrono_order,
          object: object
        }
      end)

    individual_opts =
      opts
      |> Map.put(:as, :item)
      |> Map.put(:user, user)
      |> Map.put(:hashtags, hashtags)

    render_many(history, StatusView, "history_item.json", individual_opts)
  end

  def render(
        "history_item.json",
        %{
          activity: activity,
          user: user,
          item: %{object: object, chrono_order: chrono_order},
          hashtags: hashtags
        } = opts
      ) do
    sensitive = object.data["sensitive"] || Enum.member?(hashtags, "nsfw")

    attachment_data = object.data["attachment"] || []
    attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)

    created_at = Utils.to_masto_date(object.data["updated"] || object.data["published"])

    content =
      object
      |> render_content()

    content_html =
      content
      |> Activity.HTML.get_cached_scrubbed_html_for_activity(
        User.html_filter_policy(opts[:for]),
        activity,
        "mastoapi:content:#{chrono_order}"
      )

    summary = object.data["summary"] || ""

    %{
      account:
        AccountView.render("show.json", %{
          user: user,
          for: opts[:for]
        }),
      content: content_html,
      sensitive: sensitive,
      spoiler_text: summary,
      created_at: created_at,
      media_attachments: attachments,
      emojis: build_emojis(object.data["emoji"]),
      poll: render(PollView, "show.json", object: object, for: opts[:for])
    }
  end

  def render("source.json", %{activity: %{data: %{"object" => _object}} = activity} = _opts) do
    object = Object.normalize(activity, fetch: false)

    %{
      id: activity.id,
      text: get_source_text(Map.get(object.data, "source", "")),
      spoiler_text: Map.get(object.data, "summary", ""),
      content_type: get_source_content_type(object.data["source"])
    }
  end

  def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
    page_url_data = URI.parse(page_url)

    page_url_data =
      if is_binary(rich_media["url"]) do
        URI.merge(page_url_data, URI.parse(rich_media["url"]))
      else
        page_url_data
      end

    page_url = page_url_data |> to_string

    image_url_data =
      if is_binary(rich_media["image"]) do
        URI.parse(rich_media["image"])
      else
        nil
      end

    image_url = build_image_url(image_url_data, page_url_data)

    %{
      type: "link",
      provider_name: page_url_data.host,
      provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
      url: page_url,
      image: image_url |> MediaProxy.url(),
      title: rich_media["title"] || "",
      description: rich_media["description"] || "",
      pleroma: %{
        opengraph: rich_media
      }
    }
  end

  def render("card.json", _), do: nil

  def render("attachment.json", %{attachment: attachment}) do
    [attachment_url | _] = attachment["url"]
    media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
    href = attachment_url["href"] |> MediaProxy.url()
    href_preview = attachment_url["href"] |> MediaProxy.preview_url()
    meta = render("attachment_meta.json", %{attachment: attachment})

    type =
      cond do
        String.contains?(media_type, "image") -> "image"
        String.contains?(media_type, "video") -> "video"
        String.contains?(media_type, "audio") -> "audio"
        true -> "unknown"
      end

    attachment_id =
      with {_, ap_id} when is_binary(ap_id) <- {:ap_id, attachment["id"]},
           {_, %Object{data: _object_data, id: object_id}} <-
             {:object, Object.get_by_ap_id(ap_id)} do
        to_string(object_id)
      else
        _ ->
          <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
          to_string(attachment["id"] || hash_id)
      end

    %{
      id: attachment_id,
      url: href,
      remote_url: href,
      preview_url: href_preview,
      text_url: href,
      type: type,
      description: attachment["name"],
      pleroma: %{mime_type: media_type},
      blurhash: attachment["blurhash"]
    }
    |> Maps.put_if_present(:meta, meta)
  end

  def render("attachment_meta.json", %{
        attachment: %{"url" => [%{"width" => width, "height" => height} | _]}
      })
      when is_integer(width) and is_integer(height) do
    %{
      original: %{
        width: width,
        height: height,
        aspect: width / height
      }
    }
  end

  def render("attachment_meta.json", _), do: nil

  def render("context.json", %{activity: activity, activities: activities, user: user}) do
    Logger.debug("Rendering context for #{activity.id}")

    %{ancestors: ancestors, descendants: descendants} =
      activities
      |> Enum.reverse()
      |> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
      |> Map.put_new(:ancestors, [])
      |> Map.put_new(:descendants, [])

    %{
      ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
      descendants: render("index.json", for: user, activities: descendants, as: :activity)
    }
  end

  def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
    object = Object.normalize(activity, fetch: false)

    with nil <- replied_to_activities[object.data["inReplyTo"]] do
      # If user didn't participate in the thread
      Activity.get_in_reply_to_activity(activity)
    end
  end

  def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
    object = Object.normalize(activity, fetch: false)

    if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
      Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
    else
      nil
    end
  end

  def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do
    url = object.data["url"] || object.data["id"]

    "<p><a href=\"#{url}\">#{name}</a></p>#{object.data["content"]}"
  end

  def render_content(object), do: object.data["content"] || ""

  @doc """
  Builds a dictionary tags.

  ## Examples

  iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
  [{"name": "fediverse", "url": "/tag/fediverse"},
   {"name": "nextcloud", "url": "/tag/nextcloud"}]

  """
  @spec build_tags(list(any())) :: list(map())
  def build_tags(object_tags) when is_list(object_tags) do
    object_tags
    |> Enum.filter(&is_binary/1)
    |> Enum.map(&%{name: &1, url: "#{Pleroma.Web.Endpoint.url()}/tag/#{URI.encode(&1)}"})
  end

  def build_tags(_), do: []

  @doc """
  Builds list emojis.

  Arguments: `nil` or list tuple of name and url.

  Returns list emojis.

  ## Examples

  iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
  [%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]

  """
  @spec build_emojis(nil | list(tuple())) :: list(map())
  def build_emojis(nil), do: []

  def build_emojis(emojis) do
    emojis
    |> Enum.map(fn {name, url} ->
      name = HTML.strip_tags(name)

      url =
        url
        |> HTML.strip_tags()
        |> MediaProxy.url()

      %{shortcode: name, url: url, static_url: url, visible_in_picker: false}
    end)
  end

  defp present?(nil), do: false
  defp present?(false), do: false
  defp present?(_), do: true

  defp pin_data(%Object{data: %{"id" => object_id}}, %User{pinned_objects: pinned_objects}) do
    if pinned_at = pinned_objects[object_id] do
      {true, Utils.to_masto_date(pinned_at)}
    else
      {false, nil}
    end
  end

  defp build_emoji_map(emoji, users, url, current_user) do
    %{
      name: Pleroma.Web.PleromaAPI.EmojiReactionView.emoji_name(emoji, url),
      count: length(users),
      url: MediaProxy.url(url),
      me: !!(current_user && current_user.ap_id in users),
      account_ids: Enum.map(users, fn user -> User.get_cached_by_ap_id(user).id end)
    }
  end

  @spec build_application(map() | nil) :: map() | nil
  defp build_application(%{"type" => _type, "name" => name, "url" => url}),
    do: %{name: name, website: url}

  defp build_application(_), do: nil

  # Workaround for Elixir issue #10771
  # Avoid applying URI.merge unless necessary
  # TODO: revert to always attempting URI.merge(image_url_data, page_url_data)
  # when Elixir 1.12 is the minimum supported version
  @spec build_image_url(struct() | nil, struct()) :: String.t() | nil
  defp build_image_url(
         %URI{scheme: image_scheme, host: image_host} = image_url_data,
         %URI{} = _page_url_data
       )
       when not is_nil(image_scheme) and not is_nil(image_host) do
    image_url_data |> to_string
  end

  defp build_image_url(%URI{} = image_url_data, %URI{} = page_url_data) do
    URI.merge(page_url_data, image_url_data) |> to_string
  end

  defp build_image_url(_, _), do: nil

  defp maybe_render_quote(nil, _), do: nil

  defp maybe_render_quote(quote, opts) do
    with %User{} = quoted_user <- User.get_cached_by_ap_id(quote.actor),
         false <- Map.get(opts, :do_not_recurse, false),
         true <- visible_for_user?(quote, opts[:for]),
         false <- User.blocks?(opts[:for], quoted_user),
         false <- User.mutes?(opts[:for], quoted_user) do
      opts =
        opts
        |> Map.put(:activity, quote)
        |> Map.put(:do_not_recurse, true)

      render("show.json", opts)
    else
      _ -> nil
    end
  end

  defp get_source_text(%{"content" => content} = _source) do
    content
  end

  defp get_source_text(source) when is_binary(source) do
    source
  end

  defp get_source_text(_) do
    ""
  end

  defp get_source_content_type(%{"mediaType" => type} = _source) do
    type
  end

  defp get_source_content_type(_source) do
    Utils.get_content_type(nil)
  end

  defp language(%Object{data: %{"contentMap" => contentMap}}) when is_map(contentMap) do
    contentMap
    |> Map.keys()
    |> Enum.at(0)
  end

  defp language(_), do: nil
end
