module Grit

  class Commit
    extend Lazy

    attr_reader :id
    attr_reader :repo
    lazy_reader :parents
    lazy_reader :tree
    lazy_reader :author
    lazy_reader :authored_date
    lazy_reader :committer
    lazy_reader :committed_date
    lazy_reader :message
    lazy_reader :short_message

    # Parses output from the `git-cat-file --batch'.
    #
    # repo   - Grit::Repo instance.
    # sha    - String SHA of the Commit.
    # size   - Fixnum size of the object.
    # object - Parsed String output from `git cat-file --batch`.
    #
    # Returns an Array of Grit::Commit objects.
    def self.parse_batch(repo, sha, size, object)
      info, message = object.split("\n\n", 2)

      lines = info.split("\n")
      tree = lines.shift.split(' ', 2).last
      parents = []
      parents << lines.shift[7..-1] while lines.first[0, 6] == 'parent'
      author,    authored_date  = Grit::Commit.actor(lines.shift)
      committer, committed_date = Grit::Commit.actor(lines.shift)

      Grit::Commit.new(
        repo, sha, parents, tree,
        author, authored_date,
        committer, committed_date,
        message.to_s.split("\n"))
    end

    # Instantiate a new Commit
    #   +id+ is the id of the commit
    #   +parents+ is an array of commit ids (will be converted into Commit instances)
    #   +tree+ is the correspdonding tree id (will be converted into a Tree object)
    #   +author+ is the author string
    #   +authored_date+ is the authored Time
    #   +committer+ is the committer string
    #   +committed_date+ is the committed Time
    #   +message+ is an array of commit message lines
    #
    # Returns Grit::Commit (baked)
    def initialize(repo, id, parents, tree, author, authored_date, committer, committed_date, message)
      @repo = repo
      @id = id
      @parents = parents.map { |p| Commit.create(repo, :id => p) }
      @tree = Tree.create(repo, :id => tree)
      @author = author
      @authored_date = authored_date
      @committer = committer
      @committed_date = committed_date
      @message = message.join("\n")
      @short_message = message.find { |x| !x.strip.empty? } || ''
    end

    def id_abbrev
      @id_abbrev ||= @repo.git.rev_parse({}, self.id).chomp[0, 7]
    end

    # Create an unbaked Commit containing just the specified attributes
    #   +repo+ is the Repo
    #   +atts+ is a Hash of instance variable data
    #
    # Returns Grit::Commit (unbaked)
    def self.create(repo, atts)
      self.allocate.create_initialize(repo, atts)
    end

    # Initializer for Commit.create
    #   +repo+ is the Repo
    #   +atts+ is a Hash of instance variable data
    #
    # Returns Grit::Commit (unbaked)
    def create_initialize(repo, atts)
      @repo = repo
      atts.each do |k, v|
        instance_variable_set("@#{k}", v)
      end
      self
    end

    def lazy_source
      self.class.find_all(@repo, @id, {:max_count => 1}).first
    end

    # Count the number of commits reachable from this ref
    #   +repo+ is the Repo
    #   +ref+ is the ref from which to begin (SHA1 or name)
    #
    # Returns Integer
    def self.count(repo, ref)
      repo.git.rev_list({}, ref).size / 41
    end

    # Find all commits matching the given criteria.
    #   +repo+ is the Repo
    #   +ref+ is the ref from which to begin (SHA1 or name) or nil for --all
    #   +options+ is a Hash of optional arguments to git
    #     :max_count is the maximum number of commits to fetch
    #     :skip is the number of commits to skip
    #
    # Returns Grit::Commit[] (baked)
    def self.find_all(repo, ref, options = {})
      allowed_options = [:max_count, :skip, :since]

      default_options = {:pretty => "raw"}
      actual_options = default_options.merge(options)

      if ref
        output = repo.git.rev_list(actual_options, ref)
      else
        output = repo.git.rev_list(actual_options.merge(:all => true))
      end

      self.list_from_string(repo, output)
    rescue Grit::GitRuby::Repository::NoSuchShaFound
      []
    end

    # Parse out commit information into an array of baked Commit objects
    #   +repo+ is the Repo
    #   +text+ is the text output from the git command (raw format)
    #
    # Returns Grit::Commit[] (baked)
    #
    # really should re-write this to be more accepting of non-standard commit messages
    # - it broke when 'encoding' was introduced - not sure what else might show up
    #
    def self.list_from_string(repo, text)
      text_gpgless = text.gsub(/gpgsig -----BEGIN PGP SIGNATURE-----[\n\r](.*[\n\r])*? -----END PGP SIGNATURE-----[\n\r]/, "")
      lines = text_gpgless.split("\n")

      commits = []

      while !lines.empty?
        # GITLAB patch
        # Skip all garbage unless we get real commit
        while !lines.empty? && lines.first !~ /^commit [a-zA-Z0-9]*$/
          lines.shift
        end

        id = lines.shift.split.last
        tree = lines.shift.split.last

        parents = []
        parents << lines.shift.split.last while lines.first =~ /^parent/

        author_line = lines.shift
        author_line << lines.shift if lines[0] !~ /^committer /
        author, authored_date = self.actor(author_line)

        committer_line = lines.shift
        committer_line << lines.shift if lines[0] && lines[0] != '' && lines[0] !~ /^encoding/
        committer, committed_date = self.actor(committer_line)

        # not doing anything with this yet, but it's sometimes there
        encoding = lines.shift.split.last if lines.first =~ /^encoding/

        # GITLAB patch
        # Skip Signature and other raw data
        lines.shift while lines.first =~ /^ /

        lines.shift

        message_lines = []
        message_lines << lines.shift[4..-1] while lines.first =~ /^ {4}/

        lines.shift while lines.first && lines.first.empty?

        commits << Commit.new(repo, id, parents, tree, author, authored_date, committer, committed_date, message_lines)
      end

      commits
    end

    # Show diffs between two trees.
    #
    # repo    - The current Grit::Repo instance.
    # a       - A String named commit.
    # b       - An optional String named commit.  Passing an array assumes you
    #           wish to omit the second named commit and limit the diff to the
    #           given paths.
    # paths   - An optional Array of paths to limit the diff.
    # options - An optional Hash of options.  Merged into {:full_index => true}.
    #
    # Returns Grit::Diff[] (baked)
    def self.diff(repo, a, b = nil, paths = [], options = {})
      if b.is_a?(Array)
        paths = b
        b     = nil
      end
      paths.unshift("--") unless paths.empty?
      paths.unshift(b)    unless b.nil?
      paths.unshift(a)
      options = {:full_index => true}.update(options)
      text    = repo.git.diff(options, *paths)
      Diff.list_from_string(repo, text)
    end

    def show
      if parents.size > 1
        diff = @repo.git.native(:diff, {:full_index => true}, "#{parents[0].id}...#{parents[1].id}")
      else
        diff = @repo.git.show({:full_index => true, :pretty => 'raw'}, @id)
      end

      if diff =~ /diff --git a/
        diff = diff.sub(/.*?(diff --git a)/m, '\1')
      else
        diff = ''
      end
      Diff.list_from_string(@repo, diff)
    end

    # Shows diffs between the commit's parent and the commit.
    #
    # options - An optional Hash of options, passed to Grit::Commit.diff.
    #
    # Returns Grit::Diff[] (baked)
    def diffs(options = {})
      show
    end

    def stats
      stats = @repo.commit_stats(self.sha, 1)[0][-1]
    end

    # Convert this Commit to a String which is just the SHA1 id
    def to_s
      @id
    end

    def sha
      @id
    end

    def date
      @committed_date
    end

    def to_patch
      @repo.git.format_patch({'1' => true, :stdout => true}, to_s)
    end

    def notes
      ret = {}
      notes = Note.find_all(@repo)
      notes.each do |note|
        if n = note.commit.tree/(self.id)
          ret[note.name] = n.data
        end
      end
      ret
    end

    # Calculates the commit's Patch ID. The Patch ID is essentially the SHA1
    # of the diff that the commit is introducing.
    #
    # Returns the 40 character hex String if a patch-id could be calculated
    #   or nil otherwise.
    def patch_id
      show = @repo.git.show({}, @id)
      patch_line = @repo.git.native(:patch_id, :input => show)
      if patch_line =~ /^([0-9a-f]{40}) [0-9a-f]{40}\n$/
        $1
      else
        nil
      end
    end

    # Pretty object inspection
    def inspect
      %Q{#<Grit::Commit "#{@id}">}
    end

    # private

    # Parse out the actor (author or committer) info
    #
    # Returns [String (actor name and email), Time (acted at time)]
    def self.actor(line)
      m, actor, epoch = *line.match(/^.+? (.*) (\d+) .*$/)
      [Actor.from_string(actor), Time.at(epoch.to_i)]
    end

    def author_string
      "%s <%s> %s %+05d" % [author.name, author.email, authored_date.to_i, 800]
    end

    def to_hash
      {
        'id'       => id,
        'parents'  => parents.map { |p| { 'id' => p.id } },
        'tree'     => tree.id,
        'message'  => message,
        'author'   => {
          'name'  => author.name,
          'email' => author.email
        },
        'committer' => {
          'name'  => committer.name,
          'email' => committer.email
        },
        'authored_date'  => authored_date.xmlschema,
        'committed_date' => committed_date.xmlschema,
      }
    end
  end # Commit

end # Grit
