# -*- coding: utf-8 -*-
module GitObjectBrowser

  module Models

    class GitObject < Bindata

      attr_reader :sha1, :type, :size, :entries, :contents
      attr_reader :properties, :message

      def initialize(input)
        super(input)
      end

      def parse
        content = Zlib::Inflate.inflate(@in.read(nil))
        parse_inflated(content)
        self
      end

      def parse_inflated(content)
        @sha1 = Digest::SHA1.hexdigest(content)
        @in   = StringIO.new(content)

        @type = find_char ' '
        @size = find_char "\0"

        @type = type
        @size = size

        if @type == 'tree'
          @entries = parse_tree_entries
        else
          @content = @in.read(nil)
          if @type == 'commit' or @type == 'tag'
            (@properties, @message) = parse_contents
          end

          @content = @content.force_encoding('UTF-8')
          @content = '(not UTF-8)' unless @content.valid_encoding?
          @content = @content[0, 3000] + "\n..." if @content.length > 3000
        end

        self
      end


      def to_hash
        return {
          :type       => @type,
          :sha1       => @sha1,
          :size       => @size,
          :entries    => @entries,
          :content    => @content,
          :properties => @properties,
          :message    => @message
        }
      end

      def self.path?(relpath)
        relpath =~ %r{\Aobjects/[0-9a-f]{2}/[0-9a-f]{38}\z}
      end


      private

      def parse_tree_entries
        @content = ''
        entries = []
        loop do
          entry = {}
          entry[:mode]     = find_char ' '
          break if entry[:mode].empty?
          entry[:filename] = find_char "\0"
          entry[:sha1]     = hex(20)
          @content += "#{entry[:mode]} #{entry[:filename]}\\0\\#{entry[:sha1]}\n"
          entries << entry
        end
        return entries
      end

      def parse_contents
        lines = @content.split /\n/
        line = ''
        properties = []
        message = ''
        while ! lines.empty?
          line = lines.shift
          break if line.empty?
          prop = {}
          (prop[:key], prop[:value]) = line.split(/ /, 2)
          if prop[:value] =~ /\A([0-9a-f]{2})([0-9a-f]{38})\z/
            prop[:type] = 'sha1'
            prop[:path] = "objects/#{ $1 }/#{ $2 }"
          elsif %w{author committer tagger}.include?(prop[:key]) &&
              # couldn't find the spec...
              prop[:value].to_s =~ /\A(.*) <(.*)> (\d+)(?: ((?:(?:\+|-)(?:\d{4}|\d{2}:\d{2}))|Z))?\z/
            prop[:type]  = 'user'
            prop[:name]  = $1.force_encoding("UTF-8")
            prop[:name]  = '(not UTF-8)' unless prop[:name].valid_encoding?
            prop[:email] = $2.force_encoding("UTF-8")
            prop[:email] = '(not UTF-8)' unless prop[:email].valid_encoding?
            prop[:unixtime] = $3
            prop[:timezone] = $4
            prop[:date] = epoch($3.to_i, $4).iso8601
          else
            prop[:type] = 'text'
          end
          properties << prop
        end
        message = lines.join("\n").force_encoding("UTF-8")
        message = '(not UTF-8)' unless message.valid_encoding?

        [properties, message]
      end

      def epoch(sec, timezone)
        DateTime.strptime(sec.to_s, '%s').new_offset(parse_timezone(timezone))
      end

      def parse_timezone(timezone)
        timezone = '+00:00' if timezone == 'Z'
        return Rational(0, 24) unless timezone =~ /(\+|-)?(\d\d):?(\d\d)/
        Rational($2.to_i, 24) + Rational($3, 60) * (($1 == '-') ? -1 : 1)
      end

    end
  end
end