#! /usr/bin/ruby

#/ Script to get repository data from github and generate RPM spec file with
#/ genspec. Program works in interactive mode, only one required value is URL.
#/ Then you insert URL, the program will ask you to enter another values, and
#/ prints default value in square brakets, for example [name]. If you accept
#/ it then just press Enter.
#/
#/ Usage: github2spec [options] [-- <genspec-options>]
#/
#/ Available options:
#/   -i, --interactive=level set interactive level:
#/                           0 -- no interaction (only ask url)
#/                           1 -- default value
#/                           2 -- full interaction (ask all values)
#/   -u, --url=url           provide url and set interactive to 0
#/   -v, --version           show version
#/   -p, --print-only        only prints genspec execution string
#/   -s, --spec-only         only creates spec and gear-rules
#/   -e, --executable        use for executable packages instead of libs
#/   -d, --disable-name-translation
#/   --tag=tag               specify tag to reset and pass to genspec
#/   -h, --help              show help

# (c) 2017-2019 Mikhail Gordeev <obirvalger@altlinux.org>
# This program is free software; you can redistribute it and/or modify it
# under the terms of MIT License.

require 'json'
require 'open-uri'
require 'optparse'
require 'pp'
require 'readline'
require 'shellwords'
require 'uri'

NEEDED_PARAMETRS = [
  "name",
  "summary",
  "license",
  "description",
  "type",
  "url",
  "version",
  "changelog",
  "owner",
]

class Hash
  def needed
    self.select { |key, value| NEEDED_PARAMETRS.include? key }
  end

  def needed!
    self.select! { |key, value| NEEDED_PARAMETRS.include? key }
  end

  def to_args
    self.reduce("") do |acc, (key, value)|
      "#{acc} -#{key[0]} #{value}"
    end
  end
end

def tag_to_version(tag)
  version = tag.sub(/^v(?:er(?:sion)?)?/,'')
  return version
end

# TODO specify github api version v3
def get_github_parameters(repo)
  result = {}
  result["owner"] = repo.sub(/^\/(.*?)\/.*$/,'\1')
  github_repo_url = "https://api.github.com/repos#{repo}"
  begin
    jr = JSON.load(open github_repo_url)
    # list of parameters goes as is
    ["name"].each do |v|
      result[v] = jr[v]
    end

    result['type'] = case jr['language']
    when "Ruby"
      "ruby"
    when "Python"
      "python3"
    when "Go"
      "golang-bin"
    else
      "common"
    end

    # Githubs "description" is "summary" in rpms mean
    result["summary"] = jr["description"] unless jr["description"] == ""
    jl = begin
      JSON.load(open (github_repo_url + '/license'))
    rescue OpenURI::HTTPError
      nil
    end
    license = jl["license"]["spdx_id"] if jl
    if license and not ["", "NOASSERTION"].include?(license)
      result["license"] = license
    end
    tag = JSON.load(open (github_repo_url + '/tags')).fetch(0, nil)
    if tag
      result["tag"] = tag["name"]
      result["version"] = tag_to_version(result["tag"])
    end
    return result
  rescue SocketError
    abort "No internet connection"
  rescue OpenURI::HTTPError
    abort "Not a valid github repository"
  end
end

# Function simulating github_parameters for testing
def get_github_test_parameters(repo)
  {
    "name" => "pry",
    "type" => "ruby",
    "summary"=>"An IRB alternative and runtime developer console",
    "license" => "MIT",
    "tag"=>"v0.10.4",
    "version" => "0.10.4",
    "owner" => "pry",
  }
end

def get_gitlab_parameters(repo)
  result = {}
  result["owner"] = repo.sub(/^\/(.*?)\/.*$/,'\1')
  # substitute all but first '/' with '%2F'
  repo = "/" + repo.slice(1..-1).gsub("/", "%2F")
  gitlab_repo_url = "https://gitlab.com/api/v4/projects#{repo}"
  begin
    jr = JSON.load(open gitlab_repo_url)
    # list of parameters goes as is
    ["name"].each do |v|
      result[v] = jr[v]
    end

    language = JSON.load(open (gitlab_repo_url + "/languages")).keys[0]
    result['type'] = case language
    when "Ruby"
      "ruby"
    when "Python"
      "python3"
    when "Go"
      "golang-bin"
    else
      "common"
    end

    # gitlabs "description" is "summary" in rpms mean
    result["summary"] = jr["description"] unless jr["description"] == ""
    # NOTE gitlab does not show license, but according to documentation[1],
    # license short name located in such place.
    # [1] https://docs.gitlab.com/ce/api/projects.html#get-single-project
    result["license"] = jr.fetch("license", {})["nickname"]
    tag = JSON.load(open (gitlab_repo_url + '/repository/tags')).fetch(0, nil)
    if tag
      result["tag"] = tag["name"]
      result["version"] = tag_to_version(result["tag"])
    end
    return result
  rescue SocketError
    abort "No internet connection"
  rescue OpenURI::HTTPError
    abort "Not a valid gitlab repository"
  end
end

def get_url_parameters(url)
  uri = URI(url)
  path = uri.path
  parameters = case uri.scheme
  when "http", "https"
    case uri.host
    when "github.com"
      get_github_parameters(path)
    when "gitlab.com"
      get_gitlab_parameters(path)
    else
      raise NotImplementedError
    end
  when "file", nil
    raise NotImplementedError
  else
    raise NotImplementedError
  end

  return parameters
end

default_parameters = {
  "description" => "%summary",
  "changelog" => "- Initial build for Sisyphus",
}

# Values specified with command line options
cmd_parameters = {}

# Default value for command line options controling this program
options = {
  interactive_level: -1, # To distinguish default value from set in command line
  url: nil,
  print_only: false,
  spec_only: false,
  translation: true,
}

# Boolean options forearded to genspec
GENSPEC_FORWARD_OPTIONS = {}
["--verbose", "--here", "--force", "--[no-]check"].each do |option|
  GENSPEC_FORWARD_OPTIONS[option] = option.sub(/^--(?:\[no-\])?/, "").to_sym
end

OptionParser.new do |opts|
  # Options to control program execution
  opts.on("-i", "--interactive=[level]", Integer) do |level|
    level ||= 1 # default value if no level provided
    abort "Interactive level should be in [0,1,2], get #{level}!" if level<0 or level>2
    options[:interactive_level] = level
  end
  opts.on("-u", "--url=val", String) do |val|
    options[:url] = val
    options[:interactive_level] = 0 if options[:interactive_level] == -1
    cmd_parameters["url"] ||= val
  end
  opts.on("-v", "--version") { puts "1.4.10"; exit }
  opts.on("-p", "--print-only") { options[:print_only] = true }
  opts.on("-s", "--spec-only") { options[:spec_only] = true }
  opts.on("-e", "--executable") { options[:executable] = true }
  opts.on("-d", "--disable-name-translation") { options[:translation] = false }
  opts.on("--tag=val") { |tag| options[:tag] = tag }

  GENSPEC_FORWARD_OPTIONS.each do |option, sym|
    opts.on(option) { |val| options[sym] = val }
  end

  opts.on_tail("-h", "--help") do
    system "grep ^#/<'#{__FILE__}' | cut -c4-"
    puts "\nParameters passed to genspec:"
    NEEDED_PARAMETRS.each do |parameter|
      uc_parameter = parameter.upcase
      puts "  -#{uc_parameter[0]}, --#{uc_parameter}=#{parameter}"
    end
    puts "\nOptions passed to genspec:"
    GENSPEC_FORWARD_OPTIONS.keys.each do |option|
      puts "  #{option}"
    end
    exit 0
  end

  # Parameters get with command line options
  NEEDED_PARAMETRS.each do |parameter|
    # Use upper case values to distinguish with control options
    uc_parameter = parameter.upcase
    opts.on("-#{uc_parameter[0]}", "--#{uc_parameter}=val", String) do |val|
      cmd_parameters[parameter] = val
    end
  end

  opts.parse!
  options[:interactive_level] = 1 if options[:interactive_level] < 0
end.parse!

if ARGV.length >= 1
  unless options[:url]
    options[:url]=ARGV[0]
  end
end

def input_with_check(prompt, default = nil)
  all_prompt = "#{prompt}: "
  if default
    all_prompt = "#{prompt} [#{default}]: "
  end
  res = Readline.readline(all_prompt)
  # skip empty string and string containing only white spaces
  while res =~ /^\s*$/ and default.nil?
      puts "Parameter should not be empty"
      res = Readline.readline(all_prompt)
  end
  if res =~ /^\s*$/
    default
  else
    res
  end
end

def get_available_types(templates_path)
  available_types = []
  begin
    Dir.foreach(templates_path) do |fname|
      available_types << fname.gsub(/.spec$/, '') if fname =~ /.spec$/
    end

  rescue Errno::ENOENT
    puts "Directory #{templates_path} does not exist or does not contain any spec templates"
  end

  available_types.sort
end

available_types = get_available_types(ENV["GENSPEC_TEMPLATES"] ||
                                      "/usr/share/spectemplates/")
def available_types.to_str
  " (#{self.join(',')})"
end

def get_transformed_name(parameters)
  name = parameters["name"]
  type = parameters["type"]

  if %w[ruby python3 python].include?(type)
    sep_re = /[-_]/
    type_re = case type
    when "ruby"
      /ruby/
    when /python3?/
      /python3?(?:-module)?/
    end

    # delete the type from the name if it is separated at the end or in the
    # beggining or in the middle of the name
    name.sub!(/#{sep_re}#{type_re}$|(#{sep_re}?)#{type_re}#{sep_re}/, '\1')
  end

  return name
end

url = (options[:url] || input_with_check('URL')).chomp('/')
cmd_parameters["url"] = url
url_parameters = get_url_parameters(url)

# Priority of parameter sources in ascending order: default, url, cmd
parameters = default_parameters.merge(url_parameters).merge(cmd_parameters)
if options["translation"] and not options[:executable]
  parameters["name"] = get_transformed_name(parameters)
end


NEEDED_PARAMETRS.each do |parameter|
  default = parameters[parameter]
  # TODO available types
  prompt = "Package #{parameter}"
  if parameter == "type"
    prompt += available_types
  end
  ilevel = options[:interactive_level]
  if ilevel == 2 or (ilevel == 1 and default.nil?)
    parameters[parameter] = input_with_check(prompt, default)
  elsif ilevel == 0 and default.nil?
    abort("Can't get value for <#{parameter}>.")
  end
end

if options[:tag]
  parameters["tag"] = options[:tag]
  if not cmd_parameters["version"]
    parameters["version"] = parameters["tag"].gsub(/^v/,'')
  end
end

if options[:executable]
  exec_spec_type = available_types.find { |t| t == parameters["type"] + "-bin" }
  parameters["type"] = exec_spec_type if exec_spec_type
end

exec_options = %W[genspec --tag #{parameters["tag"]}]

unless options[:spec_only]
  exec_options.push("--git")
end

# Always use options
exec_options += ["--disable-name-translation"]

GENSPEC_FORWARD_OPTIONS.values.each do |genspec_flag|
  unless options[genspec_flag].nil?
    if options[genspec_flag]
      exec_options.push("--#{genspec_flag.to_s}")
    else
      exec_options.push("--no-#{genspec_flag.to_s}")
    end
  end
end
parameters.needed.each do |key, value|
  exec_options.push("-#{key[0]}", value)
end

if options[:print_only]
  puts exec_options.shelljoin
else
  # NOTE do not forget to check return code (true is 0)
  exec(*exec_options)
end
