#!/usr/bin/env ruby
#/ Usage: <progname> [options]...
#/ Get info on pull requests from gazebo's bitbucket repository
# based on http://www.alphadevx.com/a/88-Writing-a-REST-Client-in-Ruby

# to install dependencies on Ubuntu (tested with Precise, Quantal, and Raring):
#sudo apt-get install rubygems ruby-rest-client ruby-json
begin
  require 'rubygems'
  require 'rest_client'
  require 'json'
  require 'optparse'
  require 'date'
rescue LoadError => e
  puts "Error: " + e.message
  gem = e.message.match(/ -- (.*)/)[1]
  puts "Please install missing gem: $ gem install #{gem}"
  exit
end

$stderr.sync = true

class BitbucketPullRequests
  # Pull request summary
  class Summary
    attr_reader :id
    attr_reader :source
    attr_reader :destination
    attr_reader :branch
    attr_reader :createdOn
    attr_reader :title

    def initialize(jsonHash, options)
      @options     = options
      @id          = jsonHash["id"]
      @source      = " "*12
      @destination = " "*12
      @branch      = ""
      @title       = ""
      source       = jsonHash["source"]["commit"]
      destination  = jsonHash["destination"]["commit"]
      branch       = jsonHash["source"]["branch"]
      title        = jsonHash["title"]
      @source      = source["hash"]      if !source.nil?
      @destination = destination["hash"] if !destination.nil?
      @branch      = branch["name"]      if !branch.nil?
      @title       = title               if !title.nil?
      @createdOn   = jsonHash["created_on"]
    end

    def to_s
      title = ""
      title += "\n" + @title + "\n" if @options["title"]

      title +
        @id.to_s.rjust(5, ' ') + "    " +
        DateTime.parse(@createdOn).strftime("%Y-%m-%d") + "    " +
        @source + "    " +
        @destination + "    " +
        @branch + "\n"
    end

    def date_and_string
      [@createdOn, self.to_s]
    end
  end

  # constructor
  def initialize(options)
    @url_pullrequests = 'https://bitbucket.org/api/2.0/repositories/osrf/gazebo/pullrequests'
    @options = options
  end

  # helpers for RestClient.get calls
  def getUrl(url)
    puts url if @options["show-url"]
    RestClient.get(url)
  end
  def getJson(url)
    json = JSON.parse(getUrl(url).body)
    if @options["verbose"]
      puts JSON.pretty_generate(json)
    end
    json
  end

  # summary of open pull requests
  def listPullRequests()
    jsonHash = getJson(@url_pullrequests + "/?state=OPEN")

    output = ""

    # Hash of pull requests.
    pullrequests = {}
    jsonHash["values"].each { |pr|
      date, str = Summary.new(pr, @options).date_and_string
      pullrequests[date] = str
    }

    while jsonHash.has_key? "next"
      jsonHash = getJson(jsonHash["next"])
      jsonHash["values"].each { |pr|
        date, str = Summary.new(pr, @options).date_and_string
        pullrequests[date] = str
      }
    end

    # Generate output sorted by creation time
    pullrequests.keys.sort.each { |k| output += pullrequests[k] }

    return output
  end

  # summary of one pull request
  def getPullRequestSummary(id)
    jsonHash = getJson(@url_pullrequests + "/" + id.to_s)
    return Summary.new(jsonHash, @options)
  end

  ###############################################
  # Output a pull request summary based based on a branch and revision.
  # @param[in] _range A two part array, where the first is a branch name 
  # and the second a revision.
  def getPullRequestSummaryFromRange(range)
    if range.nil?
      puts "Invalid range for --summary-range option."
      return
    elsif range.size < 2
      puts "Error: --summary-range option requires two comma " +
           "separated arguments."
      return
    end

    origin = range[0]
    dest   = range[1]
    # get the list of summaries from the log. Need to be cleaned up
    summaries_hg=`hg log -b #{origin} -P #{dest} | grep "(pull request.*)"`
    summaries_hg.split("\n").each { |sum|
      id = sum.scan(/#[0-9]*\)/).to_s()[/([0-9])+/]
        jsonHash = getJson(@url_pullrequests + "/" + id)
      puts "1. " + jsonHash["title"]
      puts "    * [Pull request #{id}](https://bitbucket.org/osrf/gazebo/pull-request/#{id})"
    }
  end

  # diff of pull request
  def getPullRequestDiff(id)
    response = getUrl(@url_pullrequests + "/" + id.to_s + "/diff")
    puts response if @options["verbose"]
    return response
  end

  # list of files changed by pull request
  def getPullRequestFiles(id)
    getFilesFromDiff(getPullRequestDiff(id))
  end

  # extract list of files added from diff string
  def getFilesFromDiff(diff)
    files = []
    diff.lines.map(&:chomp).each do |line|
      if line.start_with? '+++ b/'
        line["+++ b/"] = ""
        # try to remove anything after a tab character
        # this is needed by --check 0
        begin
          line[/\t.*$/] = ""
        # IndexError is raised if no tab characters are found
        rescue IndexError
        end
        files << line
      end
    end
    return files
  end

  # get ids for open pull requests
  def getOpenPullRequests()
    jsonHash = getJson(@url_pullrequests + "/?state=OPEN")
    ids = []
    jsonHash["values"].each { |pr| ids << pr["id"].to_i }
    while jsonHash.has_key? "next"
      jsonHash = getJson(jsonHash["next"])
      jsonHash["values"].each { |pr| ids << pr["id"].to_i }
    end
    return ids
  end

  # check files changed by the parent commit
  def checkCurrent()
    files = getFilesFromDiff(`hg log -r . --patch`)
    changeset = `hg id`[0..11]
    checkFiles(files, changeset)
  end

  # check changed files in pull request by id
  def checkPullRequest(id, fork=true)
    summary = getPullRequestSummary(id)
    puts "checking pull request #{id}, branch #{summary.branch}"
    files = getPullRequestFiles(id)
    `hg log -r #{summary.destination} 2>&1`
    if $? != 0
      puts "Unknown revision #{summary.destination}, try: hg pull"
      return
    end
    `hg log -r #{summary.source} 2>&1`
    if $? != 0
      puts "Unknown revision #{summary.source}, try: hg pull " +
           "(it could also be a fork)"
      return
    end
    ancestor=`hg log -r "ancestor(#{summary.source},#{summary.destination})" | head -1 | sed -e 's@.*:@@'`.chomp
    if ancestor != summary.destination
      puts "Need to merge branch #{summary.branch} with #{summary.destination}"
    end
    checkFiles(files, summary.source, fork)
  end

  def checkFiles(files, changeset, fork=true)
    files_list = ""
    files.each { |f| files_list += " " + f }
    hg_root = `hg root`.chomp
    if fork
      # this will allow real-time console output
      exec "echo #{files_list} | sh #{hg_root}/tools/code_check.sh --quick #{changeset}"
    else
      puts `echo #{files_list} | sh "#{hg_root}"/tools/code_check.sh --quick #{changeset}`
    end
  end
end

# default options
options  = {}
options["list"]     = false
options["summary"]  = nil
options["check"]    = false
options["check_id"] = nil
options["diff"]     = nil
options["files"]    = nil
options["range"]    = nil
options["show-url"] = false
options["title"]    = false
options["verbose"]  = false

opt_parser = OptionParser.new do |o|
  o.on("-l", "--list",
       "List open pull requests with fields:\n" + " "*37 +
       "[id] [source] [dest] [branch]") { |o| options["list"] = o }
  o.on("-c", "--check [id]", Integer,
       "Run code_check on files changed by pull request [id]\n" + " "*37 +
       "if [id] is not supplied, check all open pull requests\n" + " "*37 +
       "if [id] is 0, check files changed by parent commit")  { |o| options["check_id"] = o; options["check"] = true }
  o.on("-d", "--diff [id]", Integer,
       "Show diff from pull request") { |o| options["diff"] = o }
  o.on("-f", "--files [id]", Integer,
       "Show changed files in a pull request") { |o| options["files"] = o }
  o.on("--summary-range [changeset1],[changeset2]", Array,
       "Display all summaries from pull request\n\t\t\t\t\tmerged between changeset1 and changeset2")  { |o| options["range"] = o }
  o.on("-s", "--summary [id]", Integer,
       "Summarize a pull request with fields:\n" + " "*37 +
       "[id] [source] [dest] [branch]")  { |o| options["summary"] = o }
  o.on("-t", "--title",
       "Show pull request title with --list,\n\t\t\t\t\t--summary") { |o| options["title"] = o }
  o.on("-u", "--show-url",
       "Show urls accessed") { |o| options["show-url"] = o }
  o.on("-v", "--verbose",
       "Verbose output") { |o| options["verbose"] = o }
  o.on("-h", "--help", "Display this help message") do
    puts opt_parser
    exit
  end
end
opt_parser.parse!

client = BitbucketPullRequests.new(options)
if options["list"]
  puts client.listPullRequests()
elsif !options["summary"].nil?
  puts client.getPullRequestSummary(options["summary"])
elsif !options["range"].nil?
  client.getPullRequestSummaryFromRange(options["range"])
elsif !options["diff"].nil?
  puts client.getPullRequestDiff(options["diff"])
elsif !options["files"].nil?
  puts client.getPullRequestFiles(options["files"])
elsif options["check"]
  if options["check_id"].nil?
    # check all open pull requests
    client.getOpenPullRequests().each { |id|
      client.checkPullRequest(id, false)
    }
  elsif options["check_id"] == 0
    client.checkCurrent()
  else
    client.checkPullRequest(options["check_id"])
  end
else
  puts opt_parser
end
