#
# converted from the gitrb project
#
# authors: 
#    Matthias Lederhofer <matled@gmx.net>
#    Simon 'corecode' Schubert <corecode@fs.ei.tum.de>
#
# provides native ruby access to git objects and pack files
#

require 'digest/sha1'

module Git

  class RawObject
    attr_accessor :type, :content, :length
    def initialize(type, content)
      @type = type
      @content = content
      @length = content.length
    end

    def sha1
      Digest::SHA1.digest("%s %d\0" % [@type, @content.length] + @content)
    end
  end
  
  # class for author/committer/tagger lines
  class UserInfo
    attr_accessor :name, :email, :date, :offset

    def initialize(str)
      m = /^(.*?) <(.*)> (\d+) ([+-])0*(\d+?)$/.match(str)
      if !m
        raise RuntimeError, "invalid %s header in commit" % str
      end
      @name = m[1]
      @email = m[2]
      @date = Time.at(Integer(m[3]))
      @offset = (m[4] == "-" ? -1 : 1)*Integer(m[5])
    end

    def to_s
      "%s <%s> %s %+05d" % [@name, @email, @date.to_i, @offset]
    end
  end

  # base class for all git objects (blob, tree, commit, tag)
  class Object
    attr_accessor :repository

    def Object.from_raw(rawobject, repository = nil)
      case rawobject.type
      when :blob
        return Blob.from_raw(rawobject, repository)
      when :tree
        return Tree.from_raw(rawobject, repository)
      when :commit
        return Commit.from_raw(rawobject, repository)
      when :tag
        return Tag.from_raw(rawobject, repository)
      else
        raise RuntimeError, "got invalid object-type"
      end
    end

    def initialize
      raise NotImplemented, "abstract class"
    end

    def type
      raise NotImplemented, "abstract class"
    end

    def raw_content
      raise NotImplemented, "abstract class"
    end

    def sha1
      Digest::SHA1.hexdigest("%s %d\0" % \
                             [self.type, self.raw_content.length] + \
                             self.raw_content)
    end
  end

  class Blob < Object
    attr_accessor :content

    def self.from_raw(rawobject, repository)
      new(rawobject.content)
    end

    def initialize(content, repository=nil)
      @content = content
      @repository = repository
    end

    def type
      :blob
    end

    def raw_content
      @content
    end
  end

  class DirectoryEntry
    S_IFMT  = 00170000
    S_IFLNK =  0120000
    S_IFREG =  0100000
    S_IFDIR =  0040000

    attr_accessor :mode, :name, :sha1
    def initialize(buf)
      m = /^(\d+) (.*)\0(.{20})$/m.match(buf)
      if !m
        raise RuntimeError, "invalid directory entry"
      end
      @mode = 0
      m[1].each_byte do |i|
        @mode = (@mode << 3) | (i-'0'[0])
      end
      @name = m[2]
      @sha1 = m[3].unpack("H*")[0]

      if ![S_IFLNK, S_IFDIR, S_IFREG].include?(@mode & S_IFMT)
        raise RuntimeError, "unknown type for directory entry"
      end
    end

    def type
      case @mode & S_IFMT
      when S_IFLNK
        @type = :link
      when S_IFDIR
        @type = :directory
      when S_IFREG
        @type = :file
      else
        raise RuntimeError, "unknown type for directory entry"
      end
    end

    def type=(type)
      case @type
      when :link
        @mode = (@mode & ~S_IFMT) | S_IFLNK
      when :directory
        @mode = (@mode & ~S_IFMT) | S_IFDIR
      when :file
        @mode = (@mode & ~S_IFMT) | S_IFREG
      else
        raise RuntimeError, "invalid type"
      end
    end

    def format_type
      case type
      when :link
        'link'
      when :directory
        'tree'
      when :file
        'blob'
      end
    end

    def format_mode
      "%06o" % @mode
    end
    
    def raw
      "%o %s\0%s" % [@mode, @name, [@sha1].pack("H*")]
    end
  end

  class Tree < Object
    attr_accessor :entry

    def self.from_raw(rawobject, repository=nil)
      entries = []
      rawobject.content.scan(/\d+ .*?\0.{20}/m) do |raw|
        entries << DirectoryEntry.new(raw)
      end
      new(entries, repository)
    end

    def initialize(entries=[], repository = nil)
      @entry = entries
      @repository = repository
    end

    def type
      :tree
    end

    def raw_content
      # TODO: sort correctly
      #@entry.sort { |a,b| a.name <=> b.name }.
      @entry.collect { |e| [[e.format_mode, e.format_type, e.sha1].join(' '), e.name].join("\t") }.join("\n")
    end
    
    def actual_raw
      #@entry.collect { |e| e.raw.join(' '), e.name].join("\t") }.join("\n")
    end
  end

  class Commit < Object
    attr_accessor :author, :committer, :tree, :parent, :message

    def self.from_raw(rawobject, repository=nil)
      parent = []
      tree = author = committer = nil

      headers, message = rawobject.content.split(/\n\n/, 2)
      headers = headers.split(/\n/).map { |header| header.split(/ /, 2) }
      headers.each do |key, value|
        case key
        when "tree"
          tree = value
        when "parent"
          parent.push(value)
        when "author"
          author = UserInfo.new(value)
        when "committer"
          committer = UserInfo.new(value)
        else
          warn "unknown header '%s' in commit %s" % \
            [key, rawobject.sha1.unpack("H*")[0]]
        end
      end
      if not tree && author && committer
        raise RuntimeError, "incomplete raw commit object"
      end
      new(tree, parent, author, committer, message, repository)
    end

    def initialize(tree, parent, author, committer, message, repository=nil)
      @tree = tree
      @author = author
      @parent = parent
      @committer = committer
      @message = message
      @repository = repository
    end

    def type
      :commit
    end

    def raw_content
      "tree %s\n%sauthor %s\ncommitter %s\n\n" % [
        @tree,
        @parent.collect { |i| "parent %s\n" % i }.join,
        @author, @committer] + @message
    end
  end

  class Tag < Object
    attr_accessor :object, :type, :tag, :tagger, :message

    def self.from_raw(rawobject, repository=nil)
      headers, message = rawobject.content.split(/\n\n/, 2)
      headers = headers.split(/\n/).map { |header| header.split(/ /, 2) }
      headers.each do |key, value|
        case key
        when "object"
          object = value
        when "type"
          if !["blob", "tree", "commit", "tag"].include?(value)
            raise RuntimeError, "invalid type in tag"
          end
          type = value.to_sym
        when "tag"
          tag = value
        when "tagger"
          tagger = UserInfo.new(value)
        else
          warn "unknown header '%s' in tag" % \
            [key, rawobject.sha1.unpack("H*")[0]]
        end
        if not object && type && tag && tagger
          raise RuntimeError, "incomplete raw tag object"
        end
      end
      new(object, type, tag, tagger, repository)
    end

    def initialize(object, type, tag, tagger, repository=nil)
      @object = object
      @type = type
      @tag = tag
      @tagger = tagger
      @repository = repository
    end

    def raw_content
      "object %s\ntype %s\ntag %s\ntagger %s\n\n" % \
        [@object, @type, @tag, @tagger] + @message
    end

    def type
      :tag
    end
  end

end
