require "nomenclature.rb"
#vim:set noet:set ts=3:set sts=3:
#vim:set shiftwidth=3:

module JFIF
	module Helper
		require 'singleton'
		class TagRepository < Hash
            alias :get :[]
			include Singleton
			def clean
                self.clear
			end
            def [](u)
                return f(u)
            end
			def f(u)
				m =self.keys.grep(/#{u}/)
				if m.length > 0
					return self.get(m[0])
				else
					return ''
				end
			end
			def each( id='' )
				self.keys.grep(/^#{id}/).each do
					|k|
					yield k, self.get(k)
				end
			end
		end
	end
end
			


module JFIF
	module Marker
		class M_APP1 < JFIF::Marker::Marker
			attr_reader :thumbnail, :ifd0, :ifd1
			def initialize name, content=nil
				super
				raise Exception.new("Invalid Block #{name} given for M_APP1 Marker parser") if name != "M_APP1" && name !=nil
				check_content if @content!=nil
				@header = @content[0,8]
				parse_content
			end
			def check_content
				raise Exception.new("Invalid EXIF Marker #{@content[2,8]}") if @content[2,6] != "Exif\0\0"
			end
			def parse_content
				# Search for Tags
				#ignore header
				position = @header.length
				data = EXIF::TIFF::Data.new(self.object_id)
                data.load( @content[@header.length, @content. length-@header.length] )
				@ifd0 = data.ifd0
				@ifd1 = data.ifd1
				@thumbnail = @ifd0.thumb_data || @ifd1.thumb_data
			end
		end
	end
end

module EXIF
	module TIFF
		IFD_FORMAT = [ nil, 1,1,2,4,8,1,1,2,4,8,4,8 ]

		ENDIANNESS = [
			[nil, 'n', 'N'], # network order, first for short, second for long
			[nil, 'v', 'V']  # little-endian order, first for short, second for long
		]
		class Generic
			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 <= Tag
					# TODO do something
					# self.ownerImage.listTag.push node
				end
				if node.class <= IFD
					# TODO do something
				end
				end
			end
			def getElementByTagName(tag)
				@children.each{ |child| 
					return child if child.class.to_s==tag 
					o = child.getElementByTagName(tag)
					return o if o!=nil
				}
				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)
				if position+3 > content.length
                    return nil
                end
                return content[position..position+3].unpack(ENDIANNESS[endianness][2])[0]
			end
			def set16end_at(content, position, value, endianness)
				#  print " #{value} #{([value].pack(ENDIANNESS[endianness][1])).inspect}\n"
				content[position..position+1] = [value].pack(ENDIANNESS[endianness][1])
			end
			def set32end_at(content, position, value, endianness)
				#  print " #{value} #{([value].pack(ENDIANNESS[endianness][2])).inspect}\n"
				content[position..position+3] = [value].pack(ENDIANNESS[endianness][2])
			end	
		end
		class Block < Generic
		# used to keep reference to the whole block
			#	(to avoid offset problems)
			attr_accessor :tiffdata
			def initialize()
				super()
				@tiffdata = nil
				
				@indent = 3
			end
			def append_child(node)
				super
				node.tiffdata = self.tiffdata
			end
		end
		class Data < Block
			attr_reader :ifd0, :ifd1 
			def initialize(root_id)
				@root_id = root_id
				super()
				@tiffdata = self
			end
			def get16_at(position)
				get16end_at(@stream, position, @endianness)
			end
			def get32_at(position)
				get32end_at(@stream, position, @endianness)
			end
			def set16_at(position, value)
				set16end_at(@stream, position, value, @endianness)
			end
			def set32_at(position, value)
				set32end_at(@stream, position, value, @endianness)
			end
			def load(stream)
				super
			# Check Tiff Identifier and endianness
				case @stream[0, 2]
					when 'II'
						# little-endian
							@endianness = 1
					when 'MM'
						# network order
							@endianness = 0
					else
							throw "Error !! I'm not a Tiff Data Block !!"
				end
				if self.get16_at(2) != 0x002A
					throw "Error !! I'm not a Tiff Data Block !!"
				end
				@header = @stream[0, 8]
				@footer = ''
				parsecontent()
			end
			def parsecontent()
				pos = self.get32_at(4)
			# TiffData contains IFD linked by themselves, so they need to know their siblings
				hassibling = true
				while pos != 0
					size = (@stream.size-@footer.size)-pos
					case @children.length
							when 0
								data = IFD0.new("",@root_id)
								@ifd0 = data
							when 1
								data = IFD1.new("",@root_id)
								@ifd1 = data
							else
								data = IFD.new("",@root_id)
					end
					self.append_child(data)
                    data.load(@stream, pos)
					pos = data.of_nextifd
				end
				@parsed = true
			end
			def save(stream, offset)
				pos = 0
				@stream = stream[offset, stream.size-offset]
				# print "Saving #{self.class} (parsed)\n"
				@stream[0, @header.size] = @header
				pos += @header.size
				@children.each { |child|
					pos = child.save(@stream, pos)
				}
				@stream[pos, @footer.size] = @footer
				pos += @footer.size
				stream[offset, @stream.size] = @stream
				pos += offset
				return pos
			end
		end
		class IFD < Block
			attr_reader :of_nextifd, :thumb_data
			attr_accessor :thumb_offset, :thumb_length
			def initialize(namespace, root_id)
				super()
				@root_id = root_id
				@thumb_data = nil
				@thumb_length = nil
				@thumb_offset = nil
				
			# this hash contains access to all the tags in the picture
				@namespace = namespace
				@LOCAL_TAGS = {}
				@indent = 3
			end
			def load_thumb
				@thumb_data = @stream[@thumb_offset, @thumb_length]
			end
			def write_thumb(stream, offset)
				stream[offset, @thumb_data.size] = @thumb_data
			end
			def load(stream, of_start)
				super(stream)
                return if self.class==MakerNote
				@of_start = of_start
				@entries_number = @tiffdata.get16_at(@of_start)
				@header = @stream[@of_start, 2]
				@dirsize = @entries_number*12
				pos = @of_start + @header.size + @dirsize
				begin
					@of_nextifd = @tiffdata.get32_at(pos)
				rescue NameError
					@of_nextifd = nil
				end
				@footer = @of_nextifd
				if @of_nextifd != nil
					@size = @of_nextifd - @of_start
				else
					@size = @stream.size - @of_start		
				end
				_d "#{self.class}:#{@entries_number} entries(#{@dirsize}b). Size is #{@size} bytes. Begins at #{@of_start}, next entry is at #{@of_nextifd}"
				parsecontent()
			end
			def save(stream, pos)
				orig_pos = pos
		#			print "Saving #{self.class} (parsed)\n"

				stream[pos, @header.size] = @header
			# fill with blanks until beginning of data
				pos += @header.size
				data_pos = pos+@children.length*12+@footer.size
				stream[pos..data_pos] = " ".rjust(data_pos-pos)
				@children.each { |child|
					data_pos = child.save(stream, pos, data_pos)
					pos += 12
				}
				if @of_nextifd != 0x0
		#					print "Saving next IFD offset #{data_pos}\n"
					@of_nextifd = data_pos
				end
				@tiffdata.set32_at(orig_pos+@header.size+@children.length*12, 
				@of_nextifd)
				return data_pos
			end
			def parsecontent()
				pos = @of_start+2
				0.upto(@entries_number-1){ |i|
					tag = Tag.new(@namespace+"/"+self.class.name.split('::')[-1],@root_id, @LOCAL_TAGS)
					self.append_child(tag)
					tag.load(@stream, pos)
					pos += 12
				}
				@parsed = true
			end
		end
		
		class IFD0 < IFD; end
		class IFD1 < IFD; end
		class ExifOffset < IFD; end
		class InteroperabilityOffset < IFD; end
		class MakerNote < IFD; end
		
		class Tag < Block
			attr_accessor :value
			attr_reader :marker_type
			def initialize(namespace,root_id, nomenclature_hash)
			super()
			@indent = 4
			@root_id = root_id
			@namespace = namespace
			@nomenclature = nomenclature_hash
			end
			def short_name
				return @nomenclature[@marker_type] if @nomenclature[@marker_type] != nil
				return EXIF_TAGS[@marker_type] if EXIF_TAGS[@marker_type] != nil
				return "["+("%04X" % @marker_type)+"]"
			end
			def load(stream, offset)

				super(stream)
                _d("Loading from offset #{offset}")
				@of_start = offset
				@marker_type = @tiffdata.get16_at(@of_start)
				@size_header = 12
				@data_size = 0
				@format = @tiffdata.get16_at(@of_start+2)
				@item_count = @tiffdata.get32_at(@of_start+4)
				# Check that format is 4 (unsigned long, an offset)
				if (["ExifOffset", "MakerNote", "InteroperabilityOffset"].index(short_name()) != nil)
					case EXIF_TAGS[@marker_type]
						when "ExifOffset"
                            _d("Found ExifOffset TAG")
							data = ExifOffset.new(@namespace, @root_id)
						when "InteroperabilityOffset"
							data = InteroperabilityOffset.new(@namespace, @root_id)
						when "MakerNote"
							data = MakerNote.new(@namespace, @root_id)
					end
					self.append_child(data)
					
                    # the next offset for the data is 0
                    # this seems to be pretty bad...
                    if @tiffdata.get16_at(@of_start+8)==0
                        @value = "empty"
                    else
                        data.load(@stream, @tiffdata.get16_at(@of_start+8))
					    @value = "present"
                    end
				else
                    _d "Exif Entry Format #{@format}"
					@data_size = IFD_FORMAT[@format] * @item_count
					if @data_size > 4
						pos = @tiffdata.get32_at(@of_start+8)
						@rawvalue = @stream[pos, @data_size]
					else
						pos = @of_start + 8
						@rawvalue = @stream[pos, 4]
					end
					value = "unknown"
					case @format
					when 1,6
					# unsigned byte
						value = @stream[pos]
					when 2
					# string
						value = @stream[pos, @data_size]
						while value[value.size-1] == 0x0
							value = value[0, value.size-1]
						end
					when 3,8
						value = @tiffdata.get16_at(pos)
					when 7
						value = [@tiffdata.get16_at(pos), @tiffdata.get16_at(pos+2)]
					when 4,9
						value = @tiffdata.get32_at(pos)
					when 5,10
						value = [@tiffdata.get32_at(pos), @tiffdata.get32_at(pos+4)]
					when 11
						#	value = @stream[pos..@stream.length].unpack('g')
					when 12
						#	value = @stream[pos..@stream.length].unpack('G')
					else
						value = "format #{@format}(#{@data_size}) unknown"
					end
					@value = value
					_d(("#{short_name()}(0x"+"%04X"%@marker_type+"):").ljust(40)+" #{@value.inspect}")
					case @marker_type
						when 513 # Thumbnail offset
							@parent.thumb_offset = @value
						when 514 # ThumbnailLength
							@parent.thumb_length = @value
							@parent.load_thumb
					end
				end
				# TODO: fix that
				if @value.class == Array
					JFIF::Helper::TagRepository.instance[@root_id.to_s+@namespace+"/"+short_name()] =@value.join("/")
				else
					
					JFIF::Helper::TagRepository.instance[@root_id.to_s+@namespace+"/"+short_name()] =@value
				end
            #puts @namespace+"/"+short_name
			end
			def save(stream, pos, data_pos)
				orig_pos = pos
				@tiffdata.set16_at(orig_pos, @marker_type)
				@tiffdata.set16_at(orig_pos+2, @format)
				case @format
					when 2 # string
							@value += "\0"
							@data_size = @value.size
							@item_count = @data_size
					else
				end
				@tiffdata.set32_at(orig_pos+4, @item_count)
				if @children.length > 0
					@tiffdata.set32_at(orig_pos+8, data_pos)
					@children.each { |child| data_pos = child.save(stream, data_pos) }
					return data_pos
				else
					if @data_size > 4
							pos = data_pos
							@tiffdata.set32_at(orig_pos+8, data_pos)
							data_pos += @data_size
					else
							pos = pos + 8
					end
					case @format
							when 2 #string
								stream[pos, @value.size] = @value
							else
								stream[pos, @rawvalue.size] = @rawvalue
					end
				# special case : write thumbnail
					case @marker_type
							when 513 #ThumbnailOffset
								if @parent.thumb_length != nil
									@parent.write_thumb(stream, data_pos)
									@tiffdata.set32_at(orig_pos+8, data_pos)
									data_pos += @parent.thumb_length
								end
							when 514 # ThumbnailLength
								if @parent.thumb_length != nil
									@tiffdata.set32_at(orig_pos+8, @parent.thumb_length)
								end
					end
				end
				return data_pos
			end
		end
	end
end

