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


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 FileUnpack < File 
	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

# 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{ |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)
		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 JPEGImage < NodeImage
		attr_accessor :ifd_list, :listTag, :hashIFD
		attr_reader :width, :height
		# this is the COM Marker comment.
		attr_reader :comment, :exif

		attr_reader :tag_hash

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

		@tag_hash = Hash.new()

		@indent = 2
		end

		def filename
			@sFileName
		end
	

		def load_from_file(sFilename)
		_d("************** #{sFilename} ************")
		@sFileName = sFilename
		@size = File.size(sFilename)
		if @size < 4
				throw "empty file !";
		end
		file = FileUnpack.new(sFilename)
		header = file.get16(0)
		if (header == JPEG_MARKERS.index('M_SOI'))
				_d "JPEG Image"
				@file = file
		else
				_d "Unknown image type"
				return
		end

		# header : 2
		# footer : 2
		pos = 2
		while (pos < @size-2)
			data = JPEGMarker.get_block_type(file, pos, @tag_hash)
			print data.class.to_s+"\n"
			if data.comment?
				@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 = @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
				_d "Found DATA EXIF segment\n"
				@exif = data.exif			
			end
		end
		print "----\n"
	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
			}
			puts
		}
	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 comment?
			false
		end

		def parse()
			if @can_be_parsed
				load(@stream)
			end
		end
		def load(stream)
			super
		end
		def JPEGMarker.get_block_type(file, pos, hash)
			marker = file.get16(pos)
			m = nil
			case marker
				when JPEG_MARKERS.index('M_APP1')
						m = ExifMarker.new(hash)
						@exif = m
				when JPEG_MARKERS.index('M_SOS')
						m = ImageData.new(hash)
				when JPEG_MARKERS.index('M_COM')
						m = COMMarker.new(hash)
						com = m
					when JPEG_MARKERS.index('M_SOF0')
						m = SOF0Marker.new(hash)
				else
						m = JPEGMarker.new(hash)
			end
			m.load_from_file(file, pos)
			return m
		end
end

class SOF0Marker < JPEGMarker

		def initialize(hash)
			super(hash)
			@can_be_parsed = true
		end

		# set the height and width of current picture
		def load(stream)
		h = get16_at(stream, 5)
		w = get16_at(stream, 7)
		@ownerImage.instance_eval{ @height = h; @width=w}
		end
end

# this is a COMment marker, used to add comment in jpeg Files...
class COMMarker < JPEGMarker
		def initialize(blah)
		super
		@can_be_parsed = true
		@comment = nil
		end
		# get the COM Marker back from the stream
		def load(stream)
		@comment = stream[4, stream.size-4]
		end
		def comment?
			true
		end
		def to_s
			@comment
		end
end

class ImageData < JPEGMarker
		def initialize(hash)
			super
			@can_have_nextsibling = false
		end
end

class ExifMarker < JPEGMarker
		attr_reader :ifd0, :ifd1
		def initialize(tag_hash)
			super
			@can_be_parsed = true
			@tag_hash = tag_hash
		end

		def load(stream)
			super
			# Check Exif Identifier Code
			if (stream[4, 6] != "Exif\0\0")
				print "Error !! I'm not an Exif Marker !\n"
			else
				@header = @stream[0, 10]
				@footer = ''
				parsecontent()
			end
		end

		def save(stream, offset)
			pos = super
			# put real size
			# remove 2 because the APP marker is not counted
			stream[offset+2, 2] = [pos-offset-2].pack('n')
			return pos
		end

		def parsecontent()
			# Search for Tags
			# ignore header
			position = @header.size
			data = TiffData.new(@tag_hash)
			self.append_child(data)
			data.load(@stream[@header.size, @stream.size-@header.size])
			# get the ifd back in control...
			@ifd0 = data.ifd0
			@ifd1 = data.ifd1
			@parsed = true
		end
end

class TiffBlock < NodeImage
		# 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 TiffData < TiffBlock
		attr_reader :ifd0, :ifd1
		def initialize(tag_hash)
			super()
			@tag_hash = tag_hash
			@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("",@tag_hash)
							@ifd0 = data
						when 1
							data = IFD1.new("",@tag_hash)
							@ifd1 = data
						else
							data = TiffIFD.new("",@tag_hash)
				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 TiffIFD < TiffBlock
		attr_reader :of_nextifd, :thumb_data
		attr_accessor :thumb_offset, :thumb_length
		def initialize(namespace, tag_hash)
			super()
			@thumb_data = nil
			@thumb_length = nil
			@thumb_offset = nil
			
			# this hash contains access to all the tags in the picture
			@tag_hash = tag_hash
			@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)
			@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 = 0x0
			end
			@footer = @of_nextifd
			if @of_nextifd != 0x0
				@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}."

			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 = TiffTag.new(@namespace+"/"+self.class.to_s,@tag_hash, @LOCAL_TAGS)
				self.append_child(tag)
				tag.load(@stream, pos)
				pos += 12
			}
			@parsed = true
		end
end

class IFD0 < TiffIFD; end
class IFD1 < TiffIFD; end
class ExifIFD < TiffIFD; end
class InteroperabilityIFD < TiffIFD; end
class MakerNoteIFD < TiffIFD
	def initialize(namespace, tag_hash)
		super
		case tag_hash["/IFD0/Make"]
		when /nikon/i
		@LOCAL_TAGS = NIKON_TAGS	
		end
	end
end

# these three classes are just to match their description in the EXIF norm
class ExifOffset < ExifIFD; end
class InteroperabilityOffset < InteroperabilityIFD; end
class MakerNote < MakerNoteIFD; end



class TiffTag < TiffBlock
		attr_accessor :value
		attr_reader :marker_type

		def initialize(namespace,tag_hash, nomenclature_hash)
		super()
		@indent = 4

		@tag_hash = tag_hash
		@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)
		@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"
						data = ExifOffset.new(@namespace, @tag_hash)
					when "InteroperabilityOffset"
						data = InteroperabilityOffset.new(@namespace, @tag_hash)
					when "MakerNote"
						data = MakerNote.new(@namespace, @tag_hash)
				end
				self.append_child(data)
				data.load(@stream, @tiffdata.get16_at(@of_start+8))
				@value = "present"
		else
		
				@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
		@tag_hash[@namespace+"/"+short_name()] =@value
		
		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

