
class FileUnpack < File
    ENDIANNESS = [
        [nil, 'n', 'N'], # network order, first for short, second for long
        [nil, 'v', 'V']  # little-endian order, first for short, second for long
    ]

    
	def initialize(name)
		@endianness = 0
		@position = 0
		@blksize = 8192
		@content = ""
		super(name)
		super.binmode
		
		read(@blksize)
	end
	def endianness
		endianness
	end
	def endianness=(endian)
		endianness = endian
	end
	def get16(position)
		get16end(position, @endianness)
	end
	def get16end(position, endianness)
		seek(position)
		read(2).unpack(ENDIANNESS[endianness][1])[0]
	end
	def get32(position)
		get32end(position, @endianness)
	end
	def get32end(position, endianness)
		seek(position)
		read(4).unpack(ENDIANNESS[endianness][2])[0]
	end
		def read(size)
			if size==-1
				size=5000000
			end
			super(size)
			#
			#	while @content.length<@position+size do
			#		read_more
			#	end 
			#	out = @content[@position..@position+size]
			#	@position +=size
			#	return out
		end
	def seek(position)
		super(position, SEEK_SET)
		#while @content.length<position do
		#	read_more
		#end 
		#@position = position
	end
	def size
		super
		#@my.size
	end
		
	def read_more
		@blksize *= 2
		@content += @my.read(@blksize)
	end 
end



module JFIF
	module Jpeg
        JPEG_MARKERS = {
                0xFFc0 => 'M_SOF0',
                0xFFc1 => 'M_SOF1',
                0xFFc2 => 'M_SOF2',
                0xFFc3 => 'M_SOF3',

                0xFFc5 => 'M_SOF5',
                0xFFc6 => 'M_SOF6',
                0xFFc7 => 'M_SOF7',

                0xFFc8 => 'M_JPG',
                0xFFc9 => 'M_SOF9',
                0xFFca => 'M_SOF10',
                0xFFcb => 'M_SOF11',

                0xFFcd => 'M_SOF13',
                0xFFce => 'M_SOF14',
                0xFFcf => 'M_SOF15',

                0xFFc4 => 'M_DHT',

                0xFFcc => 'M_DAC',

                0xFFd0 => 'M_RST0',
                0xFFd1 => 'M_RST1',
                0xFFd2 => 'M_RST2',
                0xFFd3 => 'M_RST3',
                0xFFd4 => 'M_RST4',
                0xFFd5 => 'M_RST5',
                0xFFd6 => 'M_RST6',
                0xFFd7 => 'M_RST7',

                0xFFd8 => 'M_SOI',
                0xFFd9 => 'M_EOI',
                0xFFda => 'M_SOS',
                0xFFdb => 'M_DQT',
                0xFFdc => 'M_DNL',
                0xFFdd => 'M_DRI',
                0xFFde => 'M_DHP',
                0xFFdf => 'M_EXP',

                0xFFe0 => 'M_APP0',
                0xFFe1 => 'M_APP1',
                0xFFe2 => 'M_APP2',
                0xFFe3 => 'M_APP3',
                0xFFe4 => 'M_APP4',
                0xFFe5 => 'M_APP5',
                0xFFe6 => 'M_APP6',
                0xFFe7 => 'M_APP7',
                0xFFe8 => 'M_APP8',
                0xFFe9 => 'M_APP9',
                0xFFea => 'M_APP10',
                0xFFeb => 'M_APP11',
                0xFFec => 'M_APP12',
                0xFFed => 'M_APP13',
                0xFFee => 'M_APP14',
                0xFFef => 'M_APP15',
                0xFFf0 => 'M_JPG0',
                0xFFfd => 'M_JPG13',
                0xFFfe => 'M_COM',
                0xFF01 => 'M_TEM',
                0xFF100 => 'M_ERROR'
        }

        class TooSmallFileSizeException < Exception; end
        class NotAJpegFileException < Exception; end
 

        # base class
        class NodeImage
            attr_accessor :parent, :children, :file
            attr_accessor :ownerImage

            def initialize()
                @children = Array.new()
                @parsed = false
                @header = ''
                @footer = ''
                @ownerImage = nil
                @file = nil
                @indent = 0 
            end
            if $DEBUG
                def _d(p)
                    (0..@indent).each{ |i| print "	"}
                    print p+"\n"
                end
            else
                def _d(p); end
            end
                def load(stream)
                @stream = stream
            end

            # returns next available byte
            def save(stream, offset)
                if !@parsed
                    # print "Saving #{self.type} (unparsed)\n"
                    offset_next = offset+@stream.size
                    stream[offset, @stream.size] = @stream
                    return offset_next
                else
                    pos = offset
                    # print "Saving #{self.type} (parsed)\n"
                    stream[offset, @header.size] = @header
                    pos += @header.size
                    @children.each { |child|
                        pos = child.save(stream, pos)
                    }
                    stream[pos, @footer.size] = @footer
                    pos += @footer.size
                    return pos
                end
            end
            def append_child(node)
                if node != nil
                    @children.push(node)
                    node.parent = self
                    node.ownerImage = self.ownerImage
                    node.file = self.file
                    #if node.class <= TiffTag
                    #    self.ownerImage.listTag.push node
                    #end
                    #if node.class <= TiffIFD
                    #    self.ownerImage.ifd_list.push node
                    #    self.ownerImage.hashIFD[node.class] = node
                    #end
                end
            end
            def getElementByTagName(tag)
                @children.each do |child| 
                    return child if child.class.to_s==tag 
                    o = child.getElementByTagName(tag)
                    return o if o!=nil
                end
                nil
            end
            def get16_at(content,position)
                get16end_at(content, position, 0)
            end
            # get the 32bits value at position depending on the endianness
            def get32_at(content,position)
                get32end_at(content, position, 0)
            end
            def get16end_at(content, position, endianness)
                content[position..position+1].unpack(ENDIANNESS[endianness][1])[0]
            end
            def get32end_at(content, position, endianness)
                content[position..position+3].unpack(ENDIANNESS[endianness][2])[0]
            end
            def set16end_at(content, position, value, endianness)
                content[position..position+1] = [value].pack(ENDIANNESS[endianness][1])
            end
            def set32end_at(content, position, value, endianness)
                content[position..position+3] = [value].pack(ENDIANNESS[endianness][2])
            end
        end
        class Image < NodeImage
            include Jpeg

            attr_accessor :ifd_list, :listTag, :hashIFD
            attr_reader :width, :height

            attr_reader :tag_hash

            attr_reader :comment

            def initialize
                @exif = nil
                
                super
                @width = 0
                @height = 0
                @ownerImage = self
                @ifd_list = Array.new()
                @listTag = Array.new()
                @hashIFD = Hash.new()

                @comment = Array.new()

                @tag_hash = Hash.new()

                @indent = 2
                
                
                @listeners = Hash.new
            end

            def filename
                @sFileName
            end
        
            class InvalidFileException < Exception; end 
            def load_from_file(sFilename)
                @sFileName = sFilename
                begin
                  @size = File.size(sFilename)
                rescue Exception => e
                  throw InvalidFileException.new("#{sFilename}::\n#{e.to_s}")
                end
                if @size < 4
                        throw TooSmallFileSizeException.new("File size is < 4 bytes")
                end
                file = FileUnpack.new(sFilename)
                header = file.get16(0)
                if (header == JPEG_MARKERS.index('M_SOI'))
                        _d "JPEG Image"
                        @file = file
                else
                        throw NotAJpegFileException.new
                end

                # header : 2
                # footer : 2
                pos = 2
                while (pos < @size-2)
                    data = get_block_type(file, pos, @tag_hash)
                    if data.comment?
                        call_listeners(:COM, data)
                        @comment << data
                    end
                    self.append_child(data)
                    if data.can_have_nextsibling == true
                        # Size at offset+2 is data only, needs to add marker size
                        size = file.get16(pos+2)+2
                    else
                        size = t @size-4 - pos
                    end 
                    _d "#{data.class} ("+"%X" % data.marker+",#{size}b)"
                    if data.can_be_parsed
        #				printf "Marker added to children\n"
                        data.parse()
                    end
                    if data.can_have_nextsibling
                        pos += size
                    else
                        break
                    end 
                    if data.exif!=nil
                        call_listeners(:EXIF, data.exif) 
                    end
                end
            end
            def get_block_type(file, pos, hash)
                marker = file.get16(pos)
                m = nil
                case marker
                    when JPEG_MARKERS.index('M_APP1')
                            m = get_marker_in_objectspace("Exif",hash)
                    when JPEG_MARKERS.index('M_SOS')
                            m = get_marker_in_objectspace("Image",hash)
                            # m = ImageData.new(hash)
                    when JPEG_MARKERS.index('M_COM')
                            m = get_marker_in_objectspace("COM",hash)
                            #m = COMMarker.new(hash)
                    when JPEG_MARKERS.index('M_SOF0')
                            #m = SOF0Marker.new(hash)
                            m = get_marker_in_objectspace("SOF0", hash)
                    else
                            m = get_marker_in_objectspace("JPEG", hash)
                            #m = JPEGMarker.new(hash)
                end
                m.load_from_file(file, pos)
                return m
            end
            def get_marker_in_objectspace(prefix, hash)
                if Zoy::Jpeg.constants.include?(prefix+"Marker")
                    return Zoy::Jpeg.const_get((prefix+"Marker").intern).new(hash)
                else
                    return Zoy::Jpeg::JPEGMarker.new(hash)
                end
            end
            def dump_ifds
                @ifd_list.each { |ifd|
                    printf "%-30s\n-------------------------\n", ifd.class
                    ifd.children.each { |tag|
                        if EXIF_TAGS[tag.marker_type] != nil
                                tagname = EXIF_TAGS[tag.marker_type]
                        else
                                tagname = sprintf("0x%04X", tag.marker_type)
                        end
                        if tag.value != nil
                                printf "%-30s #{tag.value.inspect}\n", tagname
                        else
                                printf "%-30s\n", tagname
                        end
                    }
                }
            end

            BLOCK_TYPE = [:COM, :EXIF]
            
            class InvalidBlockType < Exception; end
            
            def add_block_listener( block_type, &listener )
                # validate the block type
                if !BLOCK_TYPE.include?(block_type)
                    throw InvalidBlockType.new("Block type : #{block_type} is unknown.")
                end
                @listeners[block_type] |= Array.new
                @listeners[block_type] << listener
            end
            def call_listeners( block_type, object )
                if !BLOCK_TYPE.include?(block_type)
                    throw InvalidBlockType.new("Block type : #{block_type} is unknown.")
                end
                return if not @listeners.has_key?(block_type)
                @listeners[block_type].each do |block|
                    block.call( object )
                end
            end
 
        end
                
        class JPEGMarker < NodeImage
            # Structure for JPEGMarker is
            #	APP Marker : 2 bytes
            #	Data Size  : 2 bytes
            #	Data			: given by Data Size 
            # (= 2 bytes for datasize info + real data)
            attr_reader :can_have_nextsibling, :can_be_parsed
            attr_reader :marker
            attr_reader :exif, :com, :stream
            def initialize(tag_hash)
                super()
                @exif = nil
                @can_have_nextsibling = true
                @can_be_parsed = false
                    
                @tag_hash = tag_hash
                
                @indent = 2
            end

            def load_from_file(file, pos)
                file.seek(pos)

                @marker = file.get16(pos)
                if @can_have_nextsibling
                    size = file.get16(pos+2)+2 #+2 for marker
                else
                    # Hmmm find a way to know the size of file
                    size = -1
                end
                file.seek(pos)
                stream = file.read(size)
                #@md5sum = MD5.new(stream).digest()
                @stream = stream
            end
            def parse()
                if @can_be_parsed
                    load(@stream)
                end
            end
            # default implementation does not provide access to any comment
            def comment?
                false
            end
        end
    end
end

if __FILE__ == $0 

    require 'test/unit'

    require 'metadata.com-jpeg'

    class JpegImage1_Test < Test::Unit::TestCase
        include Zoy::Jpeg
        
        def test_open_bogus
            image = Image.new
            assert_raise(NameError) do || 
                image.load_from_file(__FILE__)
            end
        end
        def test_open_correct
            image = nil
            assert_nothing_thrown do ||
                image = Image.new
                image.load_from_file("sample.jpeg")
            end
            assert_not_nil(image.comment)
            image.comment.each do | comment |
                puts comment.to_s
            end
        end
    end
end
