module Gitlab
  # Custom parser for Gitlab-flavored Markdown
  #
  # It replaces references in the text with links to the appropriate items in Gitlab.
  #
  # Supported reference formats are:
  #   * @foo for team members
  #   * #123 for issues
  #   * !123 for merge requests
  #   * $123 for snippets
  #   * 123456 for commits
  #
  # Examples
  #
  #   >> m = Markdown.new(...)
  #
  #   >> m.parse("Hey @david, can you fix this?")
  #   => "Hey <a href="/gitlab/team_members/1">@david</a>, can you fix this?"
  #
  #   >> m.parse("Commit 35d5f7c closes #1234")
  #   => "Commit <a href="/gitlab/commits/35d5f7c">35d5f7c</a> closes <a href="/gitlab/issues/1234">#1234</a>"
  class Markdown
    include Rails.application.routes.url_helpers
    include ActionView::Helpers

    REFERENCE_PATTERN = %r{
      ([^\w&;])?      # Prefix (1)
      (               # Reference (2)
        @([\w\._]+)   # User name (3)
        |[#!$](\d+)   # Issue/MR/Snippet ID (4)
        |([\h]{6,40}) # Commit ID (5)
      )
      ([^\w&;])?      # Suffix (6)
    }x.freeze

    attr_reader :html_options

    def initialize(project, html_options = {})
      @project      = project
      @html_options = html_options
    end

    def parse(text)
      text.gsub(REFERENCE_PATTERN) do |match|
        prefix     = $1 || ''
        reference  = $2
        identifier = $3 || $4 || $5
        suffix     = $6 || ''

        if ref_link = reference_link(reference, identifier)
          prefix + ref_link + suffix
        else
          match
        end
      end
    end

    private

    # Private: Dispatches to a dedicated processing method based on reference
    #
    # reference  - Object reference ("@1234", "!567", etc.)
    # identifier - Object identifier (Issue ID, SHA hash, etc.)
    #
    # Returns string rendered by the processing method
    def reference_link(reference, identifier)
      case reference
      when /^@/  then reference_user(identifier)
      when /^#/  then reference_issue(identifier)
      when /^!/  then reference_merge_request(identifier)
      when /^\$/ then reference_snippet(identifier)
      when /^\h/ then reference_commit(identifier)
      end
    end

    def reference_user(identifier)
      if user = @project.users.where(name: identifier).first
        member = @project.users_projects.where(user_id: user).first
        link_to("@#{identifier}", project_team_member_path(@project, member), html_options.merge(class: "gfm gfm-team_member #{html_options[:class]}")) if member
      end
    end

    def reference_issue(identifier)
      if issue = @project.issues.where(id: identifier).first
        link_to("##{identifier}", project_issue_path(@project, issue), html_options.merge(title: "Issue: #{issue.title}", class: "gfm gfm-issue #{html_options[:class]}"))
      end
    end

    def reference_merge_request(identifier)
      if merge_request = @project.merge_requests.where(id: identifier).first
        link_to("!#{identifier}", project_merge_request_path(@project, merge_request), html_options.merge(title: "Merge Request: #{merge_request.title}", class: "gfm gfm-merge_request #{html_options[:class]}"))
      end
    end

    def reference_snippet(identifier)
      if snippet = @project.snippets.where(id: identifier).first
        link_to("$#{identifier}", project_snippet_path(@project, snippet), html_options.merge(title: "Snippet: #{snippet.title}", class: "gfm gfm-snippet #{html_options[:class]}"))
      end
    end

    def reference_commit(identifier)
      if commit = @project.commit(identifier)
        link_to(identifier, project_commit_path(@project, id: commit.id), html_options.merge(title: "Commit: #{commit.author_name} - #{CommitDecorator.new(commit).title}", class: "gfm gfm-commit #{html_options[:class]}"))
      end
    end
  end
end