module Zip
  class ZipExtraField < Hash
    ID_MAP = {}

    # Meta class for extra fields
    class Generic
      def self.register_map
        if self.const_defined?(:HEADER_ID)
          ID_MAP[self.const_get(:HEADER_ID)] = self
        end
      end

      def self.name
        self.to_s.split("::")[-1]
      end

      # return field [size, content] or false
      def initial_parse(binstr)
        if ! binstr
          # If nil, start with empty.
          return false
        elsif binstr[0,2] != self.class.const_get(:HEADER_ID)
          $stderr.puts "Warning: weired extra feild header ID. skip parsing"
          return false
        end
        [binstr[2,2].unpack("v")[0], binstr[4..-1]]
      end

      def ==(other)
        return false if self.class != other.class
        each do |k, v|
          v != other[k] and return false
        end
        true
      end

      def to_local_bin
        s = pack_for_local
        self.class.const_get(:HEADER_ID) + [s.bytesize].pack("v") + s
      end

      def to_c_dir_bin
        s = pack_for_c_dir
        self.class.const_get(:HEADER_ID) + [s.bytesize].pack("v") + s
      end
    end

    # Info-ZIP Additional timestamp field
    class UniversalTime < Generic
      HEADER_ID = "UT"
      register_map

      def initialize(binstr = nil)
        @ctime = nil
        @mtime = nil
        @atime = nil
        @flag  = nil
        binstr and merge(binstr)
      end
      attr_accessor :atime, :ctime, :mtime, :flag

      def merge(binstr)
        return if binstr.empty?
        size, content = initial_parse(binstr)
        size or return
        @flag, mtime, atime, ctime = content.unpack("CVVV")
        mtime and @mtime ||= DOSTime.at(mtime)
        atime and @atime ||= DOSTime.at(atime)
        ctime and @ctime ||= DOSTime.at(ctime)
      end

      def ==(other)
        @mtime == other.mtime &&
        @atime == other.atime &&
        @ctime == other.ctime
      end

      def pack_for_local
        s = [@flag].pack("C")
        @flag & 1 != 0 and s << [@mtime.to_i].pack("V")
        @flag & 2 != 0 and s << [@atime.to_i].pack("V")
        @flag & 4 != 0 and s << [@ctime.to_i].pack("V")
        s
      end

      def pack_for_c_dir
        s = [@flag].pack("C")
        @flag & 1 == 1 and s << [@mtime.to_i].pack("V")
        s
      end
    end

    # Info-ZIP Extra for UNIX uid/gid
    class IUnix < Generic
      HEADER_ID = "Ux"
      register_map

      def initialize(binstr = nil)
        @uid = 0
        @gid = 0
        binstr and merge(binstr)
      end
      attr_accessor :uid, :gid

      def merge(binstr)
        return if binstr.empty?
        size, content = initial_parse(binstr)
        # size: 0 for central directory. 4 for local header
        return if(!size || size == 0)
        uid, gid = content.unpack("vv")
        @uid ||= uid
        @gid ||= gid
      end

      def ==(other)
        @uid == other.uid &&
        @gid == other.gid
      end

      def pack_for_local
        [@uid, @gid].pack("vv")
      end

      def pack_for_c_dir
        ""
      end
    end

    ## start main of ZipExtraField < Hash
    def initialize(binstr = nil)
      binstr and merge(binstr)
    end

    def merge(binstr)
      return if binstr.empty?
      i = 0 
      while i < binstr.bytesize
        id = binstr[i,2]
        len = binstr[i + 2,2].to_s.unpack("v")[0]
        if id && ID_MAP.member?(id)
          field_name = ID_MAP[id].name
          if self.member?(field_name)
            self[field_name].mergea(binstr[i, len + 4])
          else
            field_obj = ID_MAP[id].new(binstr[i, len + 4])
            self[field_name] = field_obj
          end
        elsif id
          unless self["Unknown"]
            s = ""
            class << s
              alias_method :to_c_dir_bin, :to_s
              alias_method :to_local_bin, :to_s
            end
            self["Unknown"] = s
          end
          if !len || len + 4 > binstr[i..-1].bytesize
            self["Unknown"] << binstr[i..-1]
            break
          end
          self["Unknown"] << binstr[i, len + 4]
        end
        i += len + 4
      end
    end

    def create(name)
      field_class = nil
      ID_MAP.each { |id, klass|
        if klass.name == name
          field_class = klass
          break
        end
      }
      if ! field_class
        raise ZipError, "Unknown extra field '#{name}'"
      end
      self[name] = field_class.new()
    end

    def to_local_bin
      s = ""
      each do |k, v|
        s << v.to_local_bin
      end
      s
    end
    alias :to_s :to_local_bin

    def to_c_dir_bin
      s = ""
      each do |k, v|
        s << v.to_c_dir_bin
      end
      s
    end

    def c_dir_length
      to_c_dir_bin.bytesize
    end
    def local_length
      to_local_bin.bytesize
    end
    alias :c_dir_size :c_dir_length
    alias :local_size :local_length
    alias :length     :local_length
    alias :size       :local_length
  end
end

# Copyright (C) 2002, 2003 Thomas Sondergaard
# rubyzip is free software; you can redistribute it and/or
# modify it under the terms of the ruby license.
