#
# Copyright (c) 2002, 2003, 2014 Ward Wouts <ward@wouts.nl>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#

require 'tempfile'

class YencError < RuntimeError; end

class YEnc
class << self

Encoding.default_external="ISO-8859-1"
Encoding.default_internal="ISO-8859-1"
#Encoding.default_external="ASCII-8BIT"
#Encoding.default_internal="ASCII-8BIT"

Debuglevel = 1
@@ymap = {}

def ydecode(data, outfile=nil)
	if @@ymap.empty?
		(-106..255).each do |b|
			@@ymap[b]=((b-42)%256)
		end
	end

	case data.class.to_s
		when "Array"
			puts "Calling _ydecode_array" if Debuglevel>0
			mode, filename, body = _ydecode_array(data)
		when "File", "Tempfile"
			unless outfile
				puts "ydecode: need outfile"
				exit
			end
			puts "Calling _ydecode_file" if Debuglevel>0
			mode, filename, body = _ydecode_file(data, outfile)
		else
			puts "Funny stuff in ydecode. Data of class \"#{data.class.to_s}\""
	end
	return mode, filename, body
end

def _ydecode_line(line)
	i = 0
	ostr = ''
	esc = false
	line.each_byte do |c|
		next if c == 13 or c == 10
		if c == 61 and not esc #escape character hit goto the next one
			esc = true
			next
		else
			if esc
				esc = false
				c = c - 64
			end
			if c.between?(0,41)
				decoded = c + 214
			else
				decoded = c - 42
			end
		end
		ostr << decoded
	end
	return ostr
end

def _ydecode_file(file, outfile)
	mode = 0600 # mode is a bit stupid with yencoding... it don't get it
	filename = "unknown"
	lines = file.pos
	file.pos = 0
	bytes = 0
	total = 0
	oldpartbegin = 0
	oldpartend = 0
	search_begin = false
	skip = false
	closure = true

	while (! file.eof)
		line = file.gets
		print "line: #{line}" if Debuglevel > 1
		if  line.match(/^\=ybegin\s+(.*line\=.*)/)
			m = $1
			puts "	#{Thread.current.inspect} ybegin match; rest: #{m}"  if Debuglevel > 0
			print "full line: #{line}" if Debuglevel > 0
			if matchdata = m.match(/^\s*(part\=(?<part>\d+)\s+)?(total\=(?<total>\d+)\s+)?(line\=(?<linesize>\d+))(\s*size\=(?<totalsize>\d+))(\s*name=(?<filename>.*?\S))\s*$/) or
				 matchdata = m.match(/^\s*(part\=(?<part>\d+)\s+)?(total\=(?<total>\d+)\s+)?(size\=(?<totalsize>\d+))(\s*line\=(?<linesize>\d+))(\s*name=(?<filename>.*?\S))\s*$/)
				part = matchdata[:part].to_i
				total = matchdata[:total].to_i
				linesize = matchdata[:linesize].to_i
				totalsize = matchdata[:totalsize].to_i
				filename = matchdata[:filename]
				if Debuglevel > 0
					print "found beginning"
					if part != nil
						print " of part #{part}"
					end
					if total != nil
						print " of #{total}"
					end
					puts ", linesize = #{linesize}, size = #{totalsize}, filename = #{filename}"
				end
				break
			else
				puts "not a valid yenc begin line"
			end
		end
	end

	if file.eof
		puts "Not yencoded!"
		return false
	end

	while (! file.eof)
		puts "at #{file.pos} need to go to #{lines}" if Debuglevel > 1
		line = file.gets
		line.chop!
#		if ! line.valid_encoding?
#			line = line.encode("UTF-16be", :invalid=>:replace, :replace=>"?").encode('UTF-8')
#		end
		if line.match(/^=yend\s+(.*)\Z/)
			closure = true
			m = $1
			m.match(/(\s*size=(\d+)\s+)(\s*part=(\d+))?(\s+crc32=(\S+))?/)
			size = $2.to_i
			part = $4.to_i
			crc = $6
			if size != bytes
				puts "#{Thread.current.inspect} part size mismatch, is #{bytes}, should be #{size}"
			end
			if part == nil
				return mode, filename
			end
			total += bytes
			if total >= totalsize
				if total != totalsize
					puts "#{Thread.current.inspect} total size mismatch, is #{total}, should be #{totalsize}"
				end
				return mode, filename
			end
			search_begin = true
			bytes = 0
			puts "	#{Thread.current.inspect} yended"  if Debuglevel > 0
			next
		end
		if search_begin && line.match(/^\=ybegin\s+(.*)\Z/)
			closure = false
			m = $1
			search_begin = false
			if matchdata = m.match(/^\s*(part\=(?<part>\d+)\s+)?(total\=(?<total>\d+)\s+)?(line\=(?<linesize>\d+))(\s*size\=(?<totalsize>\d+))(\s*name=(?<filename>.*?\S))\s*$/) or
			   matchdata = m.match(/^\s*(part\=(?<part>\d+)\s+)?(total\=(?<total>\d+)\s+)?(size\=(?<totalsize>\d+))(\s*line\=(?<linesize>\d+))(\s*name=(?<filename>.*?\S))\s*$/)
				part = matchdata[:part].to_i
				total = matchdata[:total].to_i
				linesize = matchdata[:linesize].to_i
				totalsize = matchdata[:totalsize].to_i
				filename = matchdata[:filename]
				puts "found beginning of part #{part}, linesize = #{linesize}, size = #{totalsize}, filename = #{filename}" if Debuglevel > 0
			end
			next
		end
		if search_begin == true
			next
		end
		if line.match(/^=ypart\s+(\s*begin=(\d+))(\s+end=(\d+))/)
			closure = false
			skip = false
			b = $2
			e = $4
			puts "	#{Thread.current.inspect} next part begin #{b}, end #{e}"
			if b.to_i == oldpartbegin && e.to_i == oldpartend
				puts "Skipping duplicate part"
				skip = true
				next
			end
			if b.to_i == oldpartend + 1
				oldpartend = e.to_i
				oldpartbegin = b.to_i
			else
				raise PermError, "#{Thread.current.inspect} Parts not continuous! last end #{oldpartend}, begin #{b}"
			end
			next
		end

# This seems to be a common 'error' - maybe I misunderstand the spec or
# something
#		if line.length != linesize
#			puts "linesize mismatch, was #{line.length}, should be #{linesize}..."
#		end

		if !skip
			puts "line: #{line}" if Debuglevel > 1
			ostr = _ydecode_line(line)
			puts "ostr: #{ostr}" if Debuglevel > 1
			outfile << ostr
			bytes += ostr.length
		end
	end

	puts "No \"=yend\" found!!!" if ! closure
	return mode, filename, outfile
end

def _ydecode_array(data)
	decode = ""
	mode = 0600
	filename = "unknown"
	oldpartend = 0
	oldpartbegin = 0
	c = 0
	lines = data.length
	bytes = 0
	percent = 0
	mark = lines/100

	i = 0
	while (i < data.length)
		if  data[i].match(/^\=ybegin\s+(.*line\=.*)/)
			m = $1
			puts "	#{Thread.current.inspect} ybegin match; rest: #{m}"  if Debuglevel > 0
			if matchdata = m.match(/^\s*(part\=(?<part>\d+)\s+)?(total\=(?<total>\d+)\s+)?(line\=(?<linesize>\d+))(\s*size\=(?<totalsize>\d+))(\s*name=(?<filename>.*?\S))\s*$/) or
			   matchdata = m.match(/^\s*(part\=(?<part>\d+)\s+)?(total\=(?<total>\d+)\s+)?(size\=(?<totalsize>\d+))(\s*line\=(?<linesize>\d+))(\s*name=(?<filename>.*?\S))\s*$/)
				part = matchdata[:part].to_i
				total = matchdata[:total].to_i
				linesize = matchdata[:linesize].to_i
				totalsize = matchdata[:totalsize].to_i
				filename = matchdata[:filename]
				puts "	#{Thread.current.inspect} found beginning, linesize = #{linesize}, size = #{totalsize}, filename = #{filename}" if Debuglevel > 0
				i += 1
				break
			else
				puts "not a valid yenc begin line"
			end
		end
		i += 1
	end

	unless (i < data.length)
		puts "Not yencoded!"
		return false
	end

	while (i < data.length)
		line = data[i]
		line.chomp!("\n")
		line.chomp!("\r")
		puts "at #{i} need to go to #{data.length}" if Debuglevel > 1
		print "line: #{line}" if Debuglevel > 1
		i += 1
		if line.match(/^\=yend(\s+size=(\d+))(\s+crc32=(\S+))?/)
			size = $2.to_i
			crc = $4
			if size != decode.length
				puts "	#{Thread.current.inspect} size mismatch, was #{decode.length}, should be #{size}"
			end
			dec = [ decode ]
			return mode, filename, dec
		end
		if line.match(/^=ypart\s+(\s*begin=(\d+))(\s+end=(\d+))/)
			skip = false
			b = $2
			e = $4
			puts "	#{Thread.current.inspect} next part begin #{b}, end #{e}"
			if b.to_i == oldpartbegin && e.to_i == oldpartend
				puts "Skipping duplicate part"
				skip = true
				next
			end
			if b.to_i == oldpartend + 1
				oldpartend = e.to_i
				oldpartbegin = b.to_i
			else
				raise PermError, "#{Thread.current.inspect} Parts not continuous! last end #{oldpartend}, begin #{b}"
			end
			next
		end

# This seems to be a common 'error' - maybe I misunderstand the spec or
# something
#		if line.length != linesize
#			puts "#{i}: linesize mismatch, was #{line.length}, should be #{linesize}..."
#		end

		if !skip
			print "line: #{line}" if Debuglevel > 1
			ostr = _ydecode_line(line)
			decode << ostr
			bytes += ostr.length
		end
	end

	puts "${i}: no \"=yend\" found!!!"
	dec = [ decode ]
	return mode, filename, dec
end

def is_yencoded(data)
	if data.to_s =~ /=ybegin/m
		return true
	else
		return false
	end
end

def get_filename(data)
	i = 0
	while i < data.length
		line = data[i]
		if matchdata = line.match(/=ybegin\s*(part\=(\d+)\s+)?(total\=(\d+)\s+)?(line\=(\d+))(\s*size\=(\d+))(\s*name=(?<filename>.*?\S))\s*$/m) or
		   matchdata = line.match(/=ybegin\s*(part\=(\d+)\s+)?(total\=(\d+)\s+)?(size\=(\d+))(\s*line\=(\d+))(\s*name=(?<filename>.*?\S))\s*$/m)
			return matchdata[:filename]
		end
		i += 1
	end
	return false
end

end # class
end
