# frozen_string_literal: true

require "set"

class HTMLPipeline
  class NodeFilter
    # HTML filter that replaces @user mentions with links. Mentions within <pre>,
    # <code>, and <a> elements are ignored. Mentions that reference users that do
    # not exist are ignored.
    #
    # Context options:
    #   :base_url - Used to construct links to user profile pages for each
    #               mention.
    #   :info_url - Used to link to "more info" when someone mentions @mention
    #               or @mentioned.
    #   :username_pattern - Used to provide a custom regular expression to
    #                       identify usernames
    #
    class MentionFilter < NodeFilter
      class << self
        # Public: Find user @mentions in text.  See
        # MentionFilter#mention_link_filter.
        #
        #   MentionFilter.mentioned_logins_in(text) do |match, login, is_mentioned|
        #     "<a href=...>#{login}</a>"
        #   end
        #
        # text - String text to search.
        #
        # Yields the String match, the String login name, and a Boolean determining
        # if the match = "@mention[ed]".  The yield's return replaces the match in
        # the original text.
        #
        # Returns a String replaced with the return of the block.
        def mentioned_logins_in(text, username_pattern = USERNAME_PATTERN)
          text.gsub(MENTION_PATTERNS[username_pattern]) do |match|
            login = Regexp.last_match(1)
            yield match, login
          end
        end
      end
      # Hash that contains all of the mention patterns used by the pipeline
      MENTION_PATTERNS = Hash.new do |hash, key|
        hash[key] = %r{
          (?:^|\W)                    # beginning of string or non-word char
          @((?>#{key}))  # @username
          (?!/)                      # without a trailing slash
          (?=
            \.+[ \t\W]|               # dots followed by space or non-word character
            \.+$|                     # dots at end of line
            [^0-9a-zA-Z_.]|           # non-word character except dot
            $                         # end of line
          )
        }ix
      end

      # Default pattern used to extract usernames from text. The value can be
      # overriden by providing the username_pattern variable in the context.
      USERNAME_PATTERN = /[a-z0-9][a-z0-9-]*/

      # Don't look for mentions in text nodes that are children of these elements
      IGNORE_PARENTS = ["pre", "code", "a", "style", "script"]

      SELECTOR = Selma::Selector.new(match_text_within: "*", ignore_text_within: IGNORE_PARENTS)

      def after_initialize
        result[:mentioned_usernames] ||= []
      end

      def selector
        SELECTOR
      end

      def handle_text_chunk(text)
        content = text.to_s
        return unless content.include?("@")

        html = mention_link_filter(content, base_url: base_url, username_pattern: username_pattern)
        return if html == content

        text.replace(html, as: :html)
      end

      # The URL to provide when someone @mentions a "mention" name, such
      # as @mention or @mentioned, that will give them more info on mentions.
      def info_url
        context[:info_url] || nil
      end

      def username_pattern
        context[:username_pattern] || USERNAME_PATTERN
      end

      # Replace user @mentions in text with links to the mentioned user's
      # profile page.
      #
      # text      - String text to replace @mention usernames in.
      # base_url  - The base URL used to construct user profile URLs.
      # info_url  - The "more info" URL used to link to more info on @mentions.
      #             If nil we don't link @mention or @mentioned.
      # username_pattern  - Regular expression used to identify usernames in
      #                     text
      #
      # Returns a string with @mentions replaced with links. All links have a
      # 'user-mention' class name attached for styling.
      def mention_link_filter(text, base_url: "/", username_pattern: USERNAME_PATTERN)
        self.class.mentioned_logins_in(text, username_pattern) do |match, login|
          link = link_to_mentioned_user(base_url, login)

          link ? match.sub("@#{login}", link) : match
        end
      end

      def link_to_mentioned_user(base_url, login)
        result[:mentioned_usernames] |= [login]

        url = base_url.dup
        excluded_prefixes = %r{[/(?:~|@]\z}
        url << "/" unless excluded_prefixes.match?(url)

        "<a href=\"#{url << login}\" class=\"user-mention\">" \
          "@#{login}" \
          "</a>"
      end
    end
  end
end
