# = Net::NNTP class
# Author:: Anton Bangratz
require 'socket'
require 'thread'
require 'timeout' # :nodoc:
require 'net/nntp_group'
require 'net/nntp_article'


module Net      
	class NNTP
		VERSION = "0.0.2"
		include Timeout # :nodoc:

		# Error to indicate that NNTP Command failed gracefully
		class CommandFailedError < StandardError
		end
		# Error to indicate that a Protocol Error occured during NNTP command execution
		class ProtocolError < StandardError
		end
		attr_reader :socket, :grouplist, :overview_format

		# initializes NNTP class with host and port
		def initialize(host, port = 119)
			@host = host
			@port = port
			@socket_class = TCPSocket
			@group = nil
		end

		# Actually connects to NNTP host and port given in new()
		def connect
			@socket = @socket_class.new(@host, @port)
			@welcome = @socket.recv 1024
		end

		# Uses authinfo commands to authenticate. Timeout for first command is set to 10 seconds. 
		#
		# Returns true on success, false on failure.
		def authenticate(user, pass)
			cmd = "authinfo user #{user}"
			send_cmd cmd
			response = nil
			timeout(10) do
				response = @socket.recv 1024
			end
			if response[0..2] == '381' then
				cmd = "authinfo pass #{pass}"
				send_cmd cmd
				response = @socket.recv 1024
			end
			return response && response[0..2] == '281'
		end

		# Issues 'GROUP' command to NNTP Server and creates new active group from returning data.
		#
		# Throws CommandFailedError
		#
		def group(group)
			send_cmd "group #{group}"
			response = @socket.readline
			responsecode, cnt, first, last, name = response.split
			if responsecode == '211'
				@group = Group.new(name)
				@group.article_info = [cnt, first, last]
				@group
			else
				raise CommandFailedError, response
			end
		end

		# Issues 'HELP' command to NNTP Server, and returns raw response.
		def help
			send_cmd("help")
			read_response()
		end

		# Issues 'XHDR' command to NNTP Server, and returns raw response. Checks if group has been selected, selects group
		# otherwise.
		#
		# TODO:: Implement XHDR header <message-id>
		def xhdr(groupname, header, rest)
			group(groupname) unless (@group && @group.name == groupname)
			cmd = "xhdr #{header}"
			suffix = numbers_or_id(rest)
			send_cmd([cmd, suffix].join(' '))
			read_response()
		end

		# Issues 'XOVER' command to NNTP Server, and returns raw response. Checks if group has been selected, selects group
		# otherwise.
		#
		def xover(groupname, rest)
			group(groupname) unless (@group && @group.name == groupname)
			prefix = "xover"
			suffix = numbers_or_id(rest)
			cmd = [prefix, suffix].join ' ' 
			send_cmd(cmd)
			read_response()
		end

		# Issues 'LISTGROUP' command to NNTP Server, and returns raw response. Checks if group has been selected, selects group
		# otherwise.
		#
		def listgroup(groupname)
			group(groupname) unless (@group && @group.name == groupname)
			send_cmd('listgroup')
			read_response()
		end

		# Issues 'LIST' command to NNTP Server. Depending on server capabilities and given keyword, can either set overview
		# format (if called with 'overview.fmt') or create a grouplist (see Attributes)
		#
		# Throws CommandFailedError

		def list(keyword=nil, pattern=nil)
			cmd = ['list', keyword, pattern].join ' '
			send_cmd(cmd)
			list = read_response()
			responsecode = list[0][0..2]
			case responsecode 
			when '215'
				case keyword
				when /overview.fmt/
					@overview_format_raw = list
					@overview_format = Net::NNTP.parse_overview_format list.join
				else
					create_grouplist(list)
				end

			when '501', '503', '500'
				raise CommandFailedError, list
			end
		end

		# prepares overview format as read from server, used by Net::NNTP::Article and list()
		def self.parse_overview_format(format)
			overview_format = %w{id}
			format.split(/\r?\n/).each do |line|
				next if line[0] == ?2 || line[0] == ?.
				ident = line.scan(/\w/).join.downcase
				unless ident[0..3] == 'xref'
					overview_format << ident 
				else
					overview_format << 'xref'
				end
			end
			overview_format
		end

		# TODO: complete implementation
		def stat
		end

		# Issues 'HEAD' command to NNTP server, returning raw response
		#
		# Throws CommandFailedError
		def head(args=nil)
			suffix = numbers_or_id(args)
			cmd = 'head'
			cmd = ['head', suffix].join " " if suffix
			send_cmd(cmd)
			response = read_response()
			case response[0][0..2]
			when '221'
				return response
			else
				raise CommandFailedError, response
			end
		end

		# Issues 'BODY' command to NNTP server, returning raw response.
		# options:: messageid|id
		#
		# Throws CommandFailedError
		def body(args=nil)
			suffix = args
			cmd = 'body'
			cmd = ['body', suffix].join " " if suffix
			send_cmd(cmd)
			response = read_response()
			case response[0][0..2]
			when '222'
				return response
			else
				raise CommandFailedError, response
			end
		end

		# Issues 'ARTICLE' command to NNTP server, returning raw response.
		# options:: messageid|id
		#
		# Throws CommandFailedError
		def article(args=nil)
			suffix = args
			cmd = 'article'
			cmd = ['article', suffix].join " " if suffix
			send_cmd(cmd)
			response = read_response()
			case response[0][0..2]
			when '220'
				return response
			else
				raise CommandFailedError, response
			end
		end

		def last_or_next(cmd)
			raise ProtocolError, "No group selected" unless @group
			send_cmd(cmd)
			response = read_response()[0]
			code = response[0..2]
			article = @group.articles.create
			case code
			when '223'
				code, id, msgid, what = response.split
				article.id = id
				article.messageid = msgid
			else
				raise CommandFailedError, response
			end
			response
		end

		# Issues the LAST command to the NNTP server, returning the raw response
		def last
			last_or_next("last")
		end

		def next
			last_or_next("next")
		end

		def create_grouplist(response)
			@grouplist = {}
			response.each do |line|
				next if line[0..2] == '215'
				break if line =~ /^\.\r\n$/
				groupinfo = line.split 
				group = Group.new groupinfo.shift
				group.article_info = groupinfo
				@grouplist[group.name] = group
			end
			@grouplist
		end

		def send_cmd(cmd)
			@socket.write(cmd+"\r\n")
		end

		def read_response
			response = ''
			queue = Queue.new
			str = ''
			ra = []
			loop do
				str = @socket.readline
				queue << str
				ra << str
				break if str == ".\r\n" || !str
			end
			ra
		end

		def numbers_or_id(hash)
			return nil unless hash
			suffix = ''
			from = hash[:from]
			to = hash[:to]
			msgid = hash[:message_id]
			if from
				suffix = "#{from}-"
				suffix += "#{to}" if to
			elsif msgid
				suffix = "#{msgid}"
			end
			suffix
		end


		private :read_response, :numbers_or_id, :send_cmd, :last_or_next, :create_grouplist

	end
end

# vim:sts=2:ts=2:sw=2:sta:noet