require 'rubygems'
require 'redminer'
require 'yaml'

module GitRedmine
  class MessageFormatError < RuntimeError; end
  class CommitLogError < RuntimeError; end
  class DiffError < RuntimeError; end
  class GitVersionError < RuntimeError; end

  module Config
    # host: Issue tracker host
    # port: Issue tracker port # default 80
    # access_key: Issue tracker api access_key
    # verbose: Trace error option # default false
    CONFIG = YAML.load_file(File.dirname(__FILE__) + '/config.yml') rescue {}

    def host; CONFIG["host"] end
    def port; CONFIG["port"] || 80 end
    def access_key; CONFIG["access_key"] end
    def verbose; CONFIG["verbose"] end

    def redmine
      opts = {:port=>port, :verbose=>verbose, :reqtrace=>CONFIG["reqtrace"]}
      @redmine ||= Redminer::Base.new(host, access_key, opts)
    end

    def redmine_url
      ret = "http://#{host}"
      ret << ":#{port}" if port != 80
      ret
    end
  end

  module GitCommands
    def backward_commit_hash(backward_number)
      `git log -#{backward_number} --format='%H'`.split("\n").last
    end

    def last_commit_log
      l = `git log -1 --format="%B"`
      raise GitVersionError.new('git version ~> 1.7.9') if l =~ /%B/
      l
    end

    def diff_from(hash)
      `git diff #{hash}..HEAD --stat`
    end

    def origin_path
      remote = `git remote -v`.split("\n")[0]
      return if remote.nil?

      url = remote.split(" ")[1]
      url = url.gsub(/.*@/, '').gsub(/\.git$/, '').gsub(':', '/')
      url
    end
  end

  class CommitMessage
    include Config
    attr_accessor :issue_id, :message

    def issue_url
      "#{redmine_url}/issues/#{issue_id}"
    end
  end

  class InputCommitMessage < CommitMessage
    def initialize(commit_msg_file)
      @msg = ""
      File.open(commit_msg_file) do |f|
        while true
          begin
            @msg << f.readline
          rescue EOFError
            break
          end
        end
      end

      match = @msg.scan(/^@(\d+) (.*)/m)
      match = @msg.scan(/^(\d+) (.*)/m) if match.empty?
      match = @msg.scan(/^#{redmine_url}\/issues\/(\d+) (.*)/m) if match.empty?
      if match.empty?
        raise MessageFormatError.new("commit message format error\n\n#{@msg}")
      end

      msg = match[0][1] || ""
      msg = msg.strip[1..-1] if msg.strip.start_with?("\n")
      msg = msg.split(/diff --git/)[0] || ""

      @issue_id = match[0][0].to_i
      @message = msg
    end
  end

  class RemakeCommitMessage < CommitMessage
    include GitCommands
    attr_accessor :hash

    def initialize
      @hash = backward_commit_hash(2)
      res = last_commit_log
      raise CommitLogError.new if res.length < 3
      self.parse(res)
    end

    def diff
      df = diff_from(self.hash)
      raise DiffError.new("cannot retrieve diff") if df.empty?
      df
    end

    # commit is a InputCommitMessage instance
    def self.remake(commit, issue)
      remake_msg = "@#{commit.issue_id} #{issue.subject}"
      unless commit.message.empty?
        remake_msg << "\n#{commit.message}"
      end
      remake_msg << "\n\n#{redmine_url}/issues/#{issue.id}"
      remake_msg
    end

    # res is a string of remade commit message
    def parse(res)
      res = res.split("\n")
      @issue_id = res.delete_at(0).scan(/^@(\d+) /)[0][0] rescue nil
      if @issue_id.nil? or @issue_id.empty?
        raise MessageFormatError.new("cannot find issue id\n\n#{res}")
      end
      @issue_id = issue_id.to_i

      issue_link = res.index { |ln| ln =~ /^http:\/\/#{host}/ }
      unless issue_link.nil?
        res.delete_at(issue_link) or 
          (res[its-1].strip.empty? and res.delete_at(issue_link-1))
      end
      @message = res
    end

    def origin_url
      path = origin_path
      return if path.nil?
      "http://#{path}/commit/#{hash}"
    end
  end

  module Hooks
    include GitCommands
    include Config

    def commit_msg(commit_msg_file)
      puts 'Hook: commit-msg'
      commit = InputCommitMessage.new(commit_msg_file)
      issue = redmine.issue(commit.issue_id)
      remake_msg = RemakeCommitMessage.remake(commit, issue)
      File.open(commit_msg_file, 'w+') { |f| f << remake_msg }
      ret = 0
    rescue MessageFormatError => e
      error("Commit message format is not valid")
      puts "Commit message format:"
      puts "  <issue id> <message>"
      puts "Example:"
      puts "  450 fix error"
      puts "  #{redmine_url}/issues/450 fix error"
      ret = 1
    rescue => e
      error(e.message)
      ret = 1
    ensure
      puts e.backtrace.join("\n") if verbose and defined?(e) and not e.nil?
      ret
    end

    def post_commit
      puts 'Hook: post-commit'
      out = "Commit completed, "
      commit = RemakeCommitMessage.new
      issue = redmine.issue(commit.issue_id)
      diff = commit.diff
      diff = "<pre>#{diff}</pre>"
      commit_link = commit.origin_url
      commit_link = if commit_link 
        "commit:\"#{commit.hash[0..6]}\":#{commit.origin_url}"
      else
        "commit:#{commit.hash[0..6]}"
      end
      note = "#{commit.message}\n\n#{commit_link}\n#{diff}"
      issue.update(note)
      ret = 0
    rescue => e
      warn("#{out}, But error occurred... #{e.message}")
      ret = 1
    ensure
      puts e.backtrace.join("\n") if verbose and defined?(e) and not e.nil?
      ret
    end

    def error(msg)
      puts "ERROR!\n\t#{msg}"
      puts "\tWanna skip verification? use --no-verify option\n"
      true
    end

    def warn(msg)
      puts "WARNING!\n\t#{msg}\n"
      true
    end
  end

end