#!/usr/bin/ruby
require 'optparse'
require 'readline'

$options = {}
OptionParser.new do |opts|
  opts.banner = <<~END
    Usage: #{File.basename($0)} [Git URI]

      Maintains Git-source PKGBUILDs. Allows selecting a new commit to build.

      If first source is not a git URI, tries to alter PKGBUILD to port from
      autotools distributed source to a git repo. If an absolute Git URI is
      not provided, tries discover it automatically.

      Otherwise, if a Git URI is provided, replaces the first source with the
      provided URI.

      Submodules aren't yet handled. Remember to properly source and inject
      any submodules required.

    Options:
  END

  opts.on("-h", "--help", "Print this help") do
    puts opts
    exit
  end
end.parse!

def header(s)
  puts "\n\e[1m#{s}:\e[0m"
end

def indent(s, c=2)
  s.strip.each_line.map { |l| " " * c + l.strip }.join("\n")
end

class Pkgbuild
  attr_reader :filename, :contents

  def initialize(filename="PKGBUILD")
    @filename = filename
    @contents = File.read filename
  end

  def =~(regex)
    @contents =~ regex
  end

  def has_var?(name)
    @contents =~ /^#{name}=/
  end

  def has_func?(name)
    @contents =~ /^#{name}\(\)/
  end
end

class PkgbuildWriter < Pkgbuild
  def write
    File.write(@filename, @contents)
  end

  def sub(*args, &block)
    @contents.sub!(*args, &block)
  end

  def gsub(*args, &block)
    @contents.gsub!(*args, &block)
  end

  private def replace_thing(re_gen, after, before, separator)
    after.nil? ^ before.nil? or raise ArgumentError, "need either after: or before:"

    return false if @contents.sub!(re_gen[]) { yield($1) }

    if after.nil?
      @contents.sub!(re_gen[before]) { yield("") + separator + $& }
    else
      @contents.sub!(re_gen[after]) { $& + separator + yield("") }
    end

    true
  end

  def replace_func(name, content=nil, after: nil, before: nil)
    content.nil? ^ !block_given? or raise ArgumentError, "need either content or block"
    before.nil? && after.nil? and before=/[A-Za-z0-9_-]+/

    re = lambda { |what=name| /^#{what}\(\)\s*{(.*?)^}/m }

    replace_thing(re, after, before, "\n\n") do |orig|
      "#{name}() {\n#{indent(content || yield(orig))}\n}"
    end
  end

  def replace_var(name, content=nil, after: nil, before: nil)
    content.nil? ^ !block_given? or raise ArgumentError, "need either content or block"

    re = lambda { |what=name| /^#{what}=(\([^)]*\)|.*$)/ }

    replace_thing(re, after, before, "\n") do |orig|
      "#{name}=#{content || yield(orig)}"
    end
  end

  def append_array(name, content, **kwargs)
    replace_var(name, **kwargs) do |orig|
      if orig =~ /^\(([^)]+)\)$/
        "(#{$1} #{content})"
      else
        "(#{content})"
      end
    end
  end
end

class PkgbuildReader < Pkgbuild
  attr_reader :pkgbase, :basevar, :makedepends, :source

  def initialize(*args)
    super

    @bash = IO.popen ["bash"], "r+"
    @bash.puts "{", @contents, "} &>/dev/null"

    @pkgbase = read :pkgbase
    @basevar = :pkgbase
    if @pkgbase.empty?
      @pkgbase = read :pkgname
      @basevar = :pkgname
    end

    @makedepends = readarray :makedepends
    @source = readarray :source
  end

  def exec(*lines)
    lines.each { |l| @bash.puts l }
    @bash.puts "printf '\\x00ENDEXEC\\x00'"
    buf = ""
    buf << @bash.readchar until buf[-1] == ?\x00 && buf =~ /\x00ENDEXEC\x00\Z/
    buf[0..-10]
  end

  def read(varname)
    exec("printf '%s' \"${#{varname}}\"")
  end

  def readarray(varname)
    exec("printf '%s\\x00' \"${#{varname}[@]}\"").split("\x00")
  end
end

class Repo
  SRCDEST = "/var/lib/archbuilddest/srcdest"
  DISCOVER_URLS = [
    "https://anongit.freedesktop.org/git/",
    "https://git.gnome.org/browse/",
    "https://github.com/",
  ]

  attr_reader :name, :url

  def self.is_git_src?(src)
    src =~ /(^|::)git(\+https?)?:\/\//
  end

  def self.split_src(src)
    name, url = src =~ /::/ ? src.split("::", 2) : [nil, src.dup]
    url.sub!(/#.*/, "")
    url.sub!(/^git\+/, "")
    [name, url]
  end

  def self.exists?(src)
    _name, url = split_src src
    !!Kernel.system('git', 'ls-remote', url, [:out, :err] => "/dev/null")
  end

  def self.discover(src)
    name, url = split_src src

    if url !~ /:\/\//
      url = DISCOVER_URLS.map { |u| u + url }.find do |u|
        puts "Trying #{u}"
        exists?(u)
      end
      raise "Discover failure" if url.nil?
    end

    url.prepend "git+" unless Repo.is_git_src?(name)
    url << "#commit=$_commit"

    return "#{name}::#{url}" unless name.nil?
    return url
  end

  def initialize(src)
    @name, @url = self.class.split_src(src)
    @name = url.sub(/(\.git)?\/?$/, "").sub(/^.*\//, "") if @name.nil?

    @dir = File.join(SRCDEST, name)

    if test ?e, @dir
       system('git', 'fetch', '--all', '-p')
    else
      Dir.chdir(SRCDEST) do
        Kernel.system('git', 'clone', '--mirror', url, name) or raise "Clone failure"
      end
    end

    @refs = read('git', 'for-each-ref', '--sort=v:refname', '--format=%(refname)',
                 'refs/heads/*', 'refs/tags/*').lines.
            map { |l| l.strip.sub(/^refs\/(heads|tags)\//, "") }
  end

  def system(*args)
    Dir.chdir(@dir) do
      Kernel.system(*args)
    end
  end

  def read(*args)
    ret = IO.popen(args, chdir: @dir, err: "/dev/null") { |f| f.read }

    if !$?.exited?
      raise "#{args[0] == "git" ? args[1] : args[0]} failed"
    elsif !$?.exitstatus.zero?
      raise "#{args[0] == "git" ? args[1] : args[0]} failed, exit #{$?.exitstatus}"
    end

    ret
  end

  def resolve(ref)
    read('git', 'rev-parse', '--verify', '-q', "#{ref}^{commit}").chomp
  end

  def describe(*args)
    read('git', 'describe', '--tags', *args).chomp
  end

  def read_file(ref, file, default=nil)
    read 'git', 'show', "#{ref}:#{file}"
  rescue
    nil
  end

  def log(*args)
    system('git', 'log', '--first-parent', '-m', '--decorate', *args)
  end

  def complete_ref(s)
    @refs.grep(/^#{Regexp.escape s}/)
  end

  def choose_commit
    header "Branches"
    system 'git', 'branch'

    header "Tags"
    system 'git', 'tag'

    begin
      old_char = Readline.completion_append_character
      old_proc = Readline.completion_proc
      Readline.completion_append_character = ""
      Readline.completion_proc = method(:complete_ref).to_proc
      commit = nil

      loop do
        puts
        if (ref = Readline.readline("Enter ref (or nothing to view all): ", true).strip).empty?
          system('tig', '--branches', '--tags', '--date-order') or log('--graph', '--branches', '--tags', '--oneline')
        else
          begin
            commit = resolve ref
            break
          rescue => e
            puts "Failed to resolve ref (#{e})"
          end
        end
      end
    ensure
      Readline.completion_append_character = old_char
      Readline.completion_proc = old_proc
    end

    log '--stat=80', commit
    system('git', 'show', "#{commit}:NEWS") if read_file(commit, "NEWS")

    commit
  end
end

pbr = PkgbuildReader.new
reposrc = pbr.source.first
gitify = !Repo.is_git_src?(reposrc)

case ARGV.count
when 0
  reposrc = Repo.discover(pbr.pkgbase) if gitify
when 1
  reposrc = Repo.discover(ARGV.first)
else
  raise "Invalid number of arguments"
end

repo = Repo.new reposrc
commit = repo.choose_commit
commit_comment = repo.describe('--contains', '--all', commit)

pbw = PkgbuildWriter.new
pbw.replace_var :_commit, "#{commit}  # #{commit_comment}", before: :source
pbw.sub(/source=\([^) \n]+/, "source=(\"#{reposrc}\"") unless pbr.source.first == reposrc

if gitify
  autogen = repo.read_file(commit, "autogen.sh") || ""
  configure = repo.read_file(commit, "configure.ac") || ""
  meson_build = repo.read_file(commit, "meson.build") || ""

  add_md = lambda do |name, cond=true|
    if !pbr.makedepends.grep(name).empty?
      true
    elsif cond
      pbw.append_array(:makedepends, name, after: :depends)
      true
    else
      false
    end
  end

  add_md["git"]

  meson = !meson_build.empty?
  autotools = !autogen.empty?

  case
  when meson
    add_md["meson"]
    add_md["vala", meson_build =~ /'vala'/]
  when autotools
    unless add_md["gnome-common",
        (autogen =~ /gnome-autogen\.sh/ || configure =~ /^GNOME_[A-Z_]+(\(|$)/)]
      add_md["gtk-doc", configure =~ /^GTK_DOC_CHECK/]
      add_md["yelp-tools", configure =~ /^YELP_HELP_INIT/]
      add_md["autoconf-archive", configure =~ /^AX_/]
      add_md["intltool", configure =~ /^IT_PROG_INTLTOOL/]
    end

    add_md["appstream-glib", configure =~ /^APPSTREAM_/]
    add_md["vala", configure =~ /^AM_PROG_VALAC/]
  end

  if pbr.pkgbase == repo.name
    localname = "$#{pbr.basevar}"
  else
    localname = repo.name
  end

  pbw.gsub(/(\${?#{pbr.basevar}}?|#{repo.name})-\${?pkgver}?/, localname)

  meson &&= pbr !~ /meson/
  autotools &&= pbr !~ /autogen\.sh/

  {
    pkgver: begin
      ver_sed = ""

      begin
        describe = repo.describe(commit)

        ver_sed << "s/^#{$&}//;" if describe =~ /^\D+/
        ver_sed << "s/_/./g;" if describe =~ /_/
      rescue => e
        warn "pkgver() deduction failed: #{e}"
      end

      ver_sed << "s/-/+/g"

      <<~END
        cd #{localname}
        git describe --tags | sed '#{ver_sed}'
      END
    end,

    prepare: case
    when meson
      <<~END
        mkdir build
        cd #{localname}
      END
    when autotools
      <<~END
        cd #{localname}
        NOCONFIGURE=1 ./autogen.sh
      END
    end,

    check: case
    when meson
      <<~END
        cd build
        meson test
      END
    when pbr.has_func?(:check)
    when autotools
      <<~END
        cd #{localname}
        make check
      END
    end,

    build: case
    when meson
      <<~END
        cd build
        meson setup --prefix=/usr --buildtype=release ../#{localname}
        ninja
      END
    end,

    package: case
    when meson
      <<~END
        cd build
        DESTDIR="$pkgdir" ninja install
      END
    end,
  }.reverse_each do |name, content|
    pbw.replace_func(name, content) if content
  end
end

pbw.write
system "updpkgsums"
system "makepkg", "--verifysource"

diff = IO.popen(['diff', '-u', '-', pbw.filename], "r+") do |f|
  f.write pbr.contents
  f.close_write
  f.read
end
unless diff.empty?
  header "Diff"
  IO.popen(["colordiff"], "w") do |f|
    f.write diff
  end rescue puts diff
end

meson_options = repo.read_file(commit, "meson_options.txt")
if meson_options
    header "Meson Options"
    puts meson_options, ""
end

gitmodules = repo.read_file(commit, ".gitmodules")
if gitmodules
  header "Submodules"
  puts gitmodules, ""
end

Dir.unlink "src"
File.unlink(*Dir.glob("*.pkg.tar.xz"))