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

# These classes translate the raw binary data kept in the sha encoded files
# into parsed data that can then be used in another fashion
require 'stringio'

module Grit
  module GitRuby

  # class for author/committer/tagger lines
  class UserInfo
    attr_accessor :name, :email, :date, :offset

    def initialize(str)
      @email = ''
      @date = Time.now
      @offset = 0

      m = /^(.*?) <(.*)> (\d+) ([+-])0*(\d+?)$/.match(str)
      if !m
        case str
        when /<.+>/
          m, @name, @email = *str.match(/(.*) <(.+?)>/)
        else
          @name = str
        end
      else
        @name = m[1]
        @email = m[2]
        @date = Time.at(Integer(m[3]))
        @offset = (m[4] == "-" ? -1 : 1)*Integer(m[5])
      end
    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 GitObject
    attr_accessor :repository
    attr_accessor :sha

    def GitObject.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 < GitObject
    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
    S_IFGITLINK = 0160000
    attr_accessor :mode, :name, :sha1
    def initialize(mode, filename, sha1o)
      @mode = 0
      mode.each_byte do |i|
        @mode = (@mode << 3) | (i-'0'.getord(0))
      end
      @name = filename
      @sha1 = sha1o
      if ![S_IFLNK, S_IFDIR, S_IFREG, S_IFGITLINK].include?(@mode & S_IFMT)
        raise RuntimeError, "unknown type for directory entry"
      end
    end

    # Filenames can have weird characters that throw grit's text parsing
    def safe_name
      name.gsub(/[\r\n\0]/, '')
    end

    def type
      case @mode & S_IFMT
      when S_IFGITLINK
        @type = :submodule
      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
      when :submodule
        @mode = (@mode & ~S_IFMT) | S_IFGITLINK
      else
        raise RuntimeError, "invalid type"
      end
    end

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

    def format_mode
      "%06o" % @mode
    end

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


  def self.read_bytes_until(io, char)
    string = ''
    if RUBY_VERSION > '1.9'
      while ((next_char = io.getc) != char) && !io.eof
        string += next_char
      end
    else
      while ((next_char = io.getc.chr) != char) && !io.eof
        string += next_char
      end
    end
    string
  end


  class Tree < GitObject
    attr_accessor :entry

    def self.from_raw(rawobject, repository=nil)
      raw = StringIO.new(rawobject.content)

      entries = []
      while !raw.eof?
        mode      = Grit::GitRuby.read_bytes_until(raw, ' ')
        file_name = Grit::GitRuby.read_bytes_until(raw, "\0")
        raw_sha   = raw.read(20)
        sha = raw_sha.unpack("H*").first

        entries << DirectoryEntry.new(mode, file_name, sha)
      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.safe_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 < GitObject
    attr_accessor :author, :committer, :tree, :parent, :message, :headers

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

      headers, message = rawobject.content.split(/\n\n/, 2)
      all_headers = headers.split(/\n/).map { |header| header.split(/ /, 2) }
      all_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)
        end
      end
      if not tree && author && committer
        raise RuntimeError, "incomplete raw commit object"
      end
      new(tree, parent, author, committer, message, headers, repository)
    end

    def initialize(tree, parent, author, committer, message, headers, repository=nil)
      @tree = tree
      @author = author
      @parent = parent
      @committer = committer
      @message = message
      @headers = headers
      @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

    def raw_log(sha)
      output = "commit #{sha}\n"
      output += @headers + "\n\n"
      output += @message.split("\n").map { |l| '    ' + l }.join("\n") + "\n\n"
    end

  end

  class Tag < GitObject
    attr_accessor :object, :tag, :tagger, :message, :object_type
    attr_writer :type

    def self.from_raw(rawobject, repository=nil)

      headers, message = rawobject.content.split(/\n\n/, 2)
      headers = headers.split(/\n/).map { |header| header.split(' ', 2) }

      object = ''
      type = ''
      tag = ''
      tagger = ''

      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)
        end
      end

      if not object && type && tag && tagger
        raise RuntimeError, "incomplete raw tag object"
      end
      new(object, type, tag, tagger, message, repository)
    end

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

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

    def type
      :tag
    end
  end

  end
end
