#!/usr/bin/env ruby
#
# Generate a changelog entry file in the correct location.
#
# Automatically stages the file and amends the previous commit if the `--amend`
# argument is used.

require 'optparse'
require 'yaml'

Options = Struct.new(
  :amend,
  :author,
  :dry_run,
  :force,
  :merge_request,
  :title,
  :type
)
INVALID_TYPE = -1

module ChangelogHelpers
  Abort = Class.new(StandardError)
  Done = Class.new(StandardError)

  MAX_FILENAME_LENGTH = 99 # GNU tar has a 99 character limit

  def capture_stdout(cmd)
    output = IO.popen(cmd, &:read)
    fail_with "command failed: #{cmd.join(' ')}" unless $?.success?
    output
  end

  def fail_with(message)
    raise Abort, "\e[31merror\e[0m #{message}"
  end
end

class ChangelogOptionParser
  extend ChangelogHelpers

  Type = Struct.new(:name, :description)
  TYPES = [
    Type.new('added', 'New feature'),
    Type.new('fixed', 'Bug fix'),
    Type.new('changed', 'Feature change'),
    Type.new('deprecated', 'New deprecation'),
    Type.new('removed', 'Feature removal'),
    Type.new('security', 'Security fix'),
    Type.new('performance', 'Performance improvement'),
    Type.new('other', 'Other')
  ].freeze
  TYPES_OFFSET = 1

  class << self
    def parse(argv)
      options = Options.new

      parser = OptionParser.new do |opts|
        opts.banner = "Usage: #{__FILE__} [options] [title]\n\n"

        # Note: We do not provide a shorthand for this in order to match the `git
        # commit` interface
        opts.on('--amend', 'Amend the previous commit') do |value|
          options.amend = value
        end

        opts.on('-f', '--force', 'Overwrite an existing entry') do |value|
          options.force = value
        end

        opts.on('-m', '--merge-request [integer]', Integer, 'Merge Request ID') do |value|
          options.merge_request = value
        end

        opts.on('-n', '--dry-run', "Don't actually write anything, just print") do |value|
          options.dry_run = value
        end

        opts.on('-u', '--git-username', 'Use Git user.name configuration as the author') do |value|
          options.author = git_user_name if value
        end

        opts.on('-t', '--type [string]', String, "The category of the change, valid options are: #{TYPES.map(&:name).join(', ')}") do |value|
          options.type = parse_type(value)
        end

        opts.on('-h', '--help', 'Print help message') do
          $stdout.puts opts
          raise Done.new
        end
      end

      parser.parse!(argv)

      # Title is everything that remains, but let's clean it up a bit
      options.title = argv.join(' ').strip.squeeze(' ').tr("\r\n", '')

      options
    end

    def read_type
      read_type_message

      type = TYPES[$stdin.getc.to_i - TYPES_OFFSET]
      assert_valid_type!(type)

      type.name
    end

    private

    def parse_type(name)
      type_found = TYPES.find do |type|
        type.name == name
      end
      type_found ? type_found.name : INVALID_TYPE
    end

    def read_type_message
      $stdout.puts "\n>> Please specify the index for the category of your change:"
      TYPES.each_with_index do |type, index|
        $stdout.puts "#{index + TYPES_OFFSET}. #{type.description}"
      end
      $stdout.print "\n?> "
    end

    def assert_valid_type!(type)
      unless type
        raise Abort, "Invalid category index, please select an index between 1 and #{TYPES.length}"
      end
    end

    def git_user_name
      capture_stdout(%w[git config user.name]).strip
    end
  end
end

class ChangelogEntry
  include ChangelogHelpers

  attr_reader :options

  def initialize(options)
    @options = options
  end

  def execute
    assert_feature_branch!
    assert_title! unless editor
    assert_new_file!

    # Read type from $stdin unless is already set
    options.type ||= ChangelogOptionParser.read_type
    assert_valid_type!

    $stdout.puts "\e[32mcreate\e[0m #{file_path}"
    $stdout.puts contents

    unless options.dry_run
      write
      amend_commit if options.amend
    end

    if editor
      system("#{editor} '#{file_path}'")
    end
  end

  private

  def contents
    yaml_content = YAML.dump(
      'title'         => title,
      'merge_request' => options.merge_request,
      'author'        => options.author,
      'type'          => options.type
    )
    remove_trailing_whitespace(yaml_content)
  end

  def write
    File.write(file_path, contents)
  end

  def editor
    ENV['EDITOR']
  end

  def amend_commit
    fail_with "git add failed" unless system(*%W[git add #{file_path}])

    Kernel.exec(*%w[git commit --amend])
  end

  def assert_feature_branch!
    return unless branch_name == 'master'

    fail_with "Create a branch first!"
  end

  def assert_new_file!
    return unless File.exist?(file_path)
    return if options.force

    fail_with "#{file_path} already exists! Use `--force` to overwrite."
  end

  def assert_title!
    return if options.title.length > 0 || options.amend

    fail_with "Provide a title for the changelog entry or use `--amend`" \
      " to use the title from the previous commit."
  end

  def assert_valid_type!
    return unless options.type && options.type == INVALID_TYPE

    fail_with 'Invalid category given!'
  end

  def title
    if options.title.empty?
      last_commit_subject
    else
      options.title
    end
  end

  def last_commit_subject
    capture_stdout(%w[git log --format=%s -1]).strip
  end

  def file_path
    base_path = File.join(
      unreleased_path,
      branch_name.gsub(/[^\w-]/, '-'))

    # Add padding for .yml extension
    base_path[0..MAX_FILENAME_LENGTH - 5] + '.yml'
  end

  def unreleased_path
    path = File.join('changelogs', 'unreleased')
    path = File.join('ee', path) if ee?

    path
  end

  def ee?
    @ee ||= File.exist?(File.expand_path('../CHANGELOG-EE.md', __dir__))
  end

  def branch_name
    @branch_name ||= capture_stdout(%w[git symbolic-ref --short HEAD]).strip
  end

  def remove_trailing_whitespace(yaml_content)
    yaml_content.gsub(/ +$/, '')
  end
end

if $0 == __FILE__
  begin
    options = ChangelogOptionParser.parse(ARGV)
    ChangelogEntry.new(options).execute
  rescue ChangelogHelpers::Abort => ex
    $stderr.puts ex.message
    exit 1
  rescue ChangelogHelpers::Done
    exit
  end
end

# vim: ft=ruby
