=begin
	Ruby X11 Client Library
	Copyright 2001 by Mathieu Bouchard

	X11/Display.rb: main file; socket, auth, i/o, global requests.

	$Id: Display.rb,v 1.74 2001/07/09 23:48:12 matju Exp $

	This library is free software; you can redistribute it and/or
	modify it under the terms of the GNU Lesser General Public
	License as published by the Free Software Foundation; either
	version 2 of the License, or (at your option) any later version.

	This library is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
	Lesser General Public License for more details.

	You should have received a copy of the GNU Lesser General Public
	License along with this library; if not, write to the Free Software
	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

=end

require "socket"
require "fcntl"

require "X11/Base"
require "X11/XID"
require "X11/Event"

# big, and not mandatory; load separately.
# require "X11/Keysyms"

module X11 # The whole file goes in it
#----------------------------------------------------------------#
# Log (was: Log.rb)

X11_LOG = ::ENV["X11_LOG"].to_i

LogColors = {
	:caller => "\e[0;33m",
	:in    => "\e[0;1;35m",
	:event => "\e[0;1;35;44m",
	:out   => "\e[0;1;36m",
	:end   => "\e[0m",
	:success =>"\e[0;1;32m",
	:error => "\e[0;1;31m",
}

def self.log(kind,message)
	STDERR.puts "#{LogColors[kind]}#{message}#{LogColors[:end]}"
end

def self.log1(message)
	STDERR.puts "#{message}" if X11_LOG >= 1
end

#----------------------------------------------------------------#
# SocketLog

module SocketLog

	# set this to true to enable logging.
	attr_accessor :log

# category: byte I/O

	# write(data) : writes data
	# read(n)     : reads n bytes exactly (waits for)

	def write(data)
		@count ||= 0
		@count += data.length
		fumble :out, data, caller.join "\n\t" if @log
		super
	end

	def flush
		super
		X11.log :out, "(flush after #{@count} bytes)" if @count>0 && X11_LOG>0
		@count = 0
	end

	def read(n)
		return "" if n==0
		data = super n
		raise "short read?" if data.length<n
		fumble :in, data, caller.join "\n\t" if @log
		data
	end

	def hexify(data,prefix1,prefix2)
		data = data.unpack("H*")[0]
		s="#{prefix1}"
		i=0;j=0
		0.step(data.length-1,8) {|x|
			if i==8 then
				i=0;j+=1
				s << if j>=8 then "...\ntotal: #{data.length} bytes.\n"
					else "\\\n#{prefix2}" end
			end
			next if j>=8
			s << data[x,8] << " "
			i+=1
		}
		s
	end

	def fumble(dir,data,foo="")
		# STDOUT.puts "#{ColorCaller}caller=#{foo}\n"
		s=case dir; when :in; "<"; when :out; ">"; end + " "
		s=hexify(data,s,"  ")
		X11.log(dir,s)
	end
end

#----------------------------------------------------------------#

case [1].pack("L")
when "\0\0\0\1" # like 68k
	ByteOrder = "B"
when "\1\0\0\0" # like i386
	ByteOrder = "l"
else
	raise "Cannot determine byte order"
end

ProtocolMajor = 11
ProtocolMinor = 0
DEFAULT_XFONT = \
	"-adobe-helvetica-medium-r-normal--14-*-*-*-c-*-iso8859-1,"\
	"-misc-fixed-medium-r-normal--14-*-*-*-c-*-jisx0208.1983-0"
DEFAULT_NFONT = 2

module Pad
	def pad(x);  x +  "\0"*padding(x.length); end
	def pad!(x); x << "\0"*padding(x.length); end
	def padding(n); (4 - n%4) % 4; end
end
include Pad
extend Pad

class Host < Tuple; fields_are \
		[:family, HostFamily], 1,
		[:address, Uint16, :length],
		[:address, List.of(Uint8), :data]
end

#----------------------------------------------------------------#
# Tuples for Handshake

class VisualInfo < Tuple; fields_are \
	[:visual_id,Visual],
	[:qlass,VisualClass],
	[:bits_per_rgb_value,Uint8],
	[:colormap_entries,Uint16],
	[:red_mask,  Uint32],
	[:green_mask,Uint32],
	[:blue_mask, Uint32], 4
end

class DepthInfo < Tuple; fields_are \
	[:depth,Uint8], 1,
	[:visuals,Uint16,:length], 4,
	[:visuals,List.of(VisualInfo),:data]
end

class ScreenInfo < Tuple; fields_are \
	[:root,Window],
	[:default_colormap,Colormap],
	[:white_pixel,Colornum],
	[:black_pixel,Colornum],
	[:current_input_masks,EventMask],
	[:size_in_pixels,     Size],
	[:size_in_millimeters,Size],
	[:min_installed_maps,Uint16],
	[:max_installed_maps,Uint16],
	[:root_visual,Visual],
	[:backing_stores,BackingStore],
	[:save_unders,Bool],
	[:root_depth,Uint8],
	[:depths,Uint8,:length],
	[:depths,List.of(DepthInfo),:data]

	# attr_reader :display
end

class FormatInfo < Tuple; fields_are \
	[:depth,Uint8],
	[:bits_per_pixel,Uint8],
	[:scanline_pad,Uint8], 5
end

class DisplayInfo < Tuple; fields_are \
	[:release_number,Uint32],
	[:resource_id_base,Uint32],
	[:resource_id_mask,Uint32],
	[:motion_buffer_size,Uint32],
	[:vendor,Uint16,:length],
	[:maximum_request_length,Uint16],
	[:screens,Uint8,:length],
	[:formats,Uint8,:length],
	[:image_byte_order,Significance],
	[:bitmap_bit_order,Significance],
	[:bitmap_format_scanline_unit,Uint8],
	[:bitmap_format_scanline_pad,Uint8],
	[:min_keycode,KeyCode],
	[:max_keycode,KeyCode], 4,
	[:vendor,String8,:data],
	[:formats,List.of(FormatInfo),:data],
	[:screens,List.of(ScreenInfo),:data]
end

#----------------------------------------------------------------#
# This class is a feature-deprived version of lib/X11/libXau.so
# This is supposed to read stuff from ~/.Xauthority or similar.

class AuthInfo < Tuple; fields_are \
	[:family,     HostFamily], #!@#$ hack
#	[:family,     Uint16BE],
	[:address,    Uint16BE, :length], [:address,    String8, :data],
	[:display_id, Uint16BE, :length], [:display_id, String8, :data],
	[:auth_name,  Uint16BE, :length], [:auth_name,  String8, :data],
	[:auth_data,  Uint16BE, :length], [:auth_data,  String8, :data]
end

#!@#$ i am old crap code. rewrite me
class Auth
	class << self; alias open new; end

	def initialize(fn=nil)
		@filename =
			fn || ::ENV["XAUTHORITY"] || (::ENV["HOME"]+"/.Xauthority")
		@file = File.open(@filename)
	end

	def read(n)
		buf = @file.read(n)
		raise if buf.length != n
		buf
	end

	def get_one
		a=[]
		x=nil
		# if @file.closed? then return nil
		if @file.eof then
			@file.close
			return nil
		end
		x, = read(2).unpack("n") # Family
		address_types = {
			256 => :Local,
			65535 => :Wild,
			254 => :Netname,
			253 => :Krb5Principal,
			252 => :LocalHost,
			0 => :Internet,
			1 => :DECnet,
			2 => :Chaos
		}
		address_type = address_types[x]
		raise "Unknown address type: #{x}" if nil==address_type
		a << address_type
		4.times { n, = read(2).unpack("n"); a << read(n) }

		AuthInfo[*a]
	end

	def get_all
		if not @data
			@data=[]
			while x=get_one; @data<<x; end
		end
		@data
	end

	def get_by_host(host,family,display_id)
		if host == "localhost" or host == "127.0.0.1" then
			host = `hostname`
			host.chomp!
		end
		addr = nil
		addr = TCPSocket.gethostbyname(host) if family == :Internet
		# puts "host #{host}, addr #{addr}, fam #{family}, dpy #{display_id}"
		get_all.each {|a|
			#!@#$ if (family==:Internet && a.address==addr or a.address==host)
			if display_id == a.display_id and family == a.family then
				return [a.auth_name,a.auth_data]
			end
		}
		return nil
	end
end


#----------------------------------------------------------------#
# miscellaneous structures used for Display's requests.

class KeyboardControl < PartialTuple; fields_are \
	[:key_click_volume, Int8], # percent
	[:bell_volume, Int8], # percent
	[:bell_pitch, Int16],
	[:bell_duration, Int16],
	[:led, Uint8],
	[:led_mode, LedMode],
	[:key, KeyCode],
	[:auto_repeat_mode, AutoRepeatMode]
end

class PointerControl < Tuple; fields_are \
	[:numerator, Uint16],
	[:denominator, Uint16],
	[:threshold, Uint16]
end

class ScreenSaverControl < Tuple; fields_are \
	[:timeout, Uint16],
	[:interval, Uint16],
	[:prefer_blanking,ScreenSaver],
	[:allow_exposures,ScreenSaver]
end

#----------------------------------------------------------------#
class Display; extend RemoteClass; include RemoteObject

# category: attributes

	attr_reader :sock, :host
	attr_reader :display_id, :screen_id
	attr_reader :display_info, :format_info
	attr_reader :auth_proto_name, :auth_proto_data
	attr_reader :screens

	attr_reader :event_queue
	attr_reader :reply_set #!@#$ incl errors ???
	attr_reader :waiting_for

	attr_accessor :pause_on_error

	alias connection sock # 0.3 compat
	alias conn sock       # 0.3 compat

	def screens; display_info.screens; end # 0.4 compat

	def initialize(target=nil,auth_info=nil)
		@xdisplay = self # let's look like an XID
		@event_queue = []
		@reply_set = {}
		@atom_cache = {}
		@auth_info = auth_info

		connect_to(target)
	end

# category: misc

	def inspect
		sprintf "\#<#{type} #{host}:#{display_id}.#{screen_id}>"
	end

# category: handshake

	def connect_to(target)
		target ||= ::ENV["DISPLAY"]
		@host,@display_id,@screen_id = nil
		case target
		when Socket
			@sock = target
		when String
			# /^(?:[^:]*?\/)?(.*):(\d+)(?:.(\d+))?$/
			md = /^([\w.-]*):(\d+)(?:.(\d+))?$/.match(target)
			@host,@display_id,@screen_id = md[1],md[2],md[3]
			# @host = "" if @host.length==0
			if @host == "" then
				@sock = UNIXSocket.new("/tmp/.X11-unix/X#{@display_id}")
				# @host = "localhost"
				@family = :Local
			else
				@sock = TCPSocket.new(@host,6000+@display_id.to_i)
				@family = :Internet
			end
			sock.extend X11::SocketLog
		else
			raise "Please specify DISPLAY"
		end

		if X11_LOG >= 2; sock.log = true; end
		if X11_LOG >= 3; self.pause_on_error = true; end
		
		authorization
	end

	def authorization
		@auth_proto_name = ""
		@auth_proto_data = ""
		auth = nil
		case @auth_info
		when Array
			@auth_proto_name,@auth_proto_data = @auth_info
		else #elsif sock then
			auth = X11::Auth.new
			@auth_proto_name,@auth_proto_data =
				auth.get_by_host(@host||"localhost",@family,@display_id)
		end

		#[:proto_major, Uint16],
		#[:proto_minor, Uint16],
		#[:auth_proto_name, Uint16, :length],
		#[:auth_proto_data, Uint16, :length],
		#[:auth_proto_name, String8],
		#[:auth_proto_data, String8],

		sock.write([
			ByteOrder,
			ProtocolMajor,
			ProtocolMinor,
			@auth_proto_name.length,
			@auth_proto_data.length,
		].pack("A2 SS SS xx") +
			  X11.pad(@auth_proto_name) +
			  X11.pad(@auth_proto_data))

		#[:answer,Uint8],
		ret = ServerHandshake.xread(sock)
		case ret
		when :Failed
			#[:reason,Uint8,:length],
			#[:major,Uint16],
			#[:minor,Uint16],
			#[:xlen, Uint16],
			#[:reason,String8,:data]
			reason_length,major,minor,xlen = sock.read(7).unpack("CSSS")
			reason = sock.read(xlen*4)
			reason = reason[0...reason_length]
			raise "Connection to server failed " \
				"(version #{major}.#{minor}) -- #{reason}"
		when :Success
			#[nil,Unused[1]],
			#[:major,Uint16],
			#[:minor,Uint16],
			#[:xlen, Uint16],
			major,minor,xlen = sock.read(7).unpack("xSSS")
			data = XStringReader.new(self,sock.read(xlen*4))
			@display_info = DisplayInfo.xread(data)

			@xid_next  = 0
			@xid_shift = 0
			while (@display_info.resource_id_mask >> @xid_shift) & 1 == 0
				@xid_shift += 1
			end

			@sid = 1

			sock.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK)
			sock.sync = false

		when :Authenticate
			#[nil,Unused[5]],
			#[:xlen,Uint16],
			#[:reason,String8],
			xlen, = sock.read(7).unpack("xxxxxS")
			reason = sock.read(xlen*4)
			reason.sub! /\0+$/, "" # i'm not supposed to do this
			raise "authentication required -- #{reason}"
		else
			raise "unknown response"
		end
	end

# category: Requests, Replies, Events, Errors

	def pick_sid
		@sid += 1
		@sid - 1
	end

	def xreceive
		sock.flush
		chunk = sock.read(32)
		eid, = chunk.unpack("C")
		case eid
		when 0 # error
			error = XError.new(chunk) #!@#$ should be xread
			X11.log :error, error.inspect
			STDIN.readline if pause_on_error

		when 1 # reply
			header = chunk.slice! 0,8
			sid,extra = header.unpack("xxSL")
			chunk << sock.read(4*extra) if extra>0
			X11.log1("Reply 0x%04x\n" % sid)
			reply_handler = @reply_set[sid]
			raise "Unexpected Reply!!!" if not reply_handler
			@reply_set.delete sid
			reply_handler.call XStringReader.new(self,chunk,header)

		else #event
			event_type = EventTypeNumber[eid & 0x7f]
			if not event_type then
				raise "Unknown event type #{event_code}"
			end
			event = event_type.xread(XStringReader.new(self,chunk))
			# X11.log :event, event.inspect
			@event_queue << event
		end
	end

	def xreceive_blocking
		sock.fcntl(Fcntl::F_SETFL, 0)
		begin xreceive ensure
			sock.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK)
		end
	end

	def xreceive_many
		# while IO.select [$display.sock],[],[],0; xreceive; end
		begin
			while true; xreceive; end
		rescue Errno::EWOULDBLOCK
		end
	end

	def next_event
		@event_queue.shift
	end

	# makes sure all send/reply pairs initiated during the callback
	# are completed when it returns. (except for exceptions...)
	def wait_for(&proc)
		old_waiting_for = @waiting_for
		@waiting_for = []
		yield
		while @waiting_for.length > 0
			if @reply_set[@waiting_for[0]]
				xreceive_blocking
			else
				@waiting_for.shift
			end
		end
	ensure
		@waiting_for = old_waiting_for
	end

# category: connection management

	def flush; begin sock.flush; rescue Errno::EWOULDBLOCK; end; end

	def sync
		raise
		# XSync(disp->xdisplay, False);
	end

# category: other

	def new_id
		id = @xid_next
		@xid_next+=1
		
		((id << @xid_shift) &
			@display_info.resource_id_mask) |
				@display_info.resource_id_base
	end

# methods for: atom cache

	def atom(name); atoms(name)[0]; end

	def atoms(*names)
		atoms = {}
		wait_for {
			names.each {|name|
				if @atom_cache[name] then
					atoms[name] = @atom_cache[name]
				else
					self.intern_atom(false,name) {|atom,|
						atoms[name] = atom
						@atom_cache[name] ||= atom
					}
				end
			}
		}
		names.map {|k| atoms[k] }
	end

# methods for: rpc: atoms

	def_remote_with_reply :intern_atom, 16, [
		[:only_if_exists, Bool, :in_header],
		[:name, Uint16, :length], 2,
		[:name, String8, :data],
	], [
		[:atom, Atom]]
	
# methods for: rpc: selection

	def_remote :set_selection_owner, 22, [
		[:owner, Any.of Window,None],
		[:selection, Atom],
		[:time, TimeOrNow]]

	def_remote_with_reply :get_selection_owner, 23, [
		[:selection, Atom],
	], [
		[:owner, Any.of Window,None]]

	def_remote :convert_selection, 24, [
		[:requestor, Window],
		[:selection, Atom],
		[:target, Atom],
		[:property, Any.of Atom,None],
		[:time, TimeOrNow]]

# methods for: rpc: grabs

	def_remote :ungrab_pointer, 27, [
		[:time, TimeOrNow]]

	def_remote :ungrab_button, 29, [
		[:button,Any.of Button,AnyButton],
		[:grab_window, Window],
		[:modifiers, Any.of KeyMask,AnyModifier]]

	def_remote :change_active_pointer_grab, 30, [
		[:cursor, Any.of Cursor,None],
		[:time, TimeOrNow],
		[:event_mask, PointerEventMask]]
		
	def_remote :ungrab_keyboard, 32, [
		[:time, TimeOrNow]]

	def_remote :allow_events, 35, [
		[:mode, AllowEventsMode, :in_header],
		[:time, TimeOrNow]]

	def_remote   :grab_server, 36, []
	def_remote :ungrab_server, 37, []

# methods for: rpc: misc 1

	def_remote :send_event, 25, [
		[:propagate, Bool, :in_header],
		[:destination, Any.of Window,EventDestination],
		[:event_mask, EventMask],
		[:event, Event]]

	def_remote :warp_pointer, 41, [
		[:from_window, Any.of Window,None],
		[:to_window,   Any.of Window,None],
		[:from_point, Point],
		[:size, Size],
		[:to_point,   Point]]

	def_remote :set_input_focus, 42, [
		[:revert_to, InputFocusRevertTo, :in_header],
		[:focus, Any.of Window,SetInputFocus],
		[:time, TimeOrNow]]

	def_remote_with_reply :get_input_focus, 43, [
	], [
		[:revert_to, InputFocusRevertTo, :in_header],
		[:focus, Any.of Window, SetInputFocus]]

# methods for: rpc: fonts

	def_remote_with_reply :list_fonts, 49, [
		[:max_names, Uint16],
		[:pattern, Uint16, :length],
		[:pattern, String8, :data],
	], [
		[:names, Uint16, :length], 22,
		[:names, List.of(Str), :data]]

	def_remote_with_reply :list_fonts_with_info, 50, [
		[:max_names, Uint16],
		[:pattern, Uint16, :length],
		[:pattern, String8, :data],
	], [
		#!@#$ any number of replies like this one:
		[:name, Uint8, :length_in_header],

		#!@#$ the next 12 fields could become FontInfo
		#!@#$ except for :properties.
		[:min_bounds, CharInfo], 4,
		[:max_bounds, CharInfo], 4,
		[:min_char_or_byte2, Uint16],
		[:max_char_or_byte2, Uint16],
		[:default_char, Uint16],
		[:properties, Uint16, :length],
		[:draw_direction, DrawDirection],
		[:min_byte1, Uint8],
		[:max_byte1, Uint8],
		[:all_chars_exist, Bool],
		[:font_ascent,  Uint16],
		[:font_descent, Uint16],

		[:replies_hint, Uint32],
		[:properties, List.of(FontProp), :data],
		[:name, String8, :data]]

	def_remote :set_font_path, 51, [
		[:path, Uint16, :length], 2,
		[:path, List.of(Str), :data]]

	def_remote_with_reply :get_font_path, 52, [
	], [
		[:path, Uint16, :length], 22,
		[:path, List.of(Str), :data]]

# methods for: rpc: extensions

	def_remote_with_reply :query_extension, 98, [
		[:name, Uint16, :length], 2,
		[:name, String8, :data],
	], [
		[:present, Bool],
		[:major_opcode, Uint8],
		[:first_event, Uint8],
		[:first_error, Uint8]]

	def_remote :list_extensions, 99, [
		[:names, Uint8, :length_in_header], 24,
		[:names, List.of(Str), :data]]

# methods for: rpc: keyboard config

	def_remote_with_reply :query_keymap, 44, [
	], [
		[:keys, List.of(Uint8,32)]]
	
	def_remote :change_keyboard_mapping, 100, [
		[:keycode_count, Uint8, :in_header],
		[:first_keycode, KeyCode],
		[:keysyms_per_keycode, Uint8], 2,
		[:keysyms, List.of Keysym]] # count * keysyms_per_keycode

	def_remote_with_reply :get_keyboard_mapping, 101, [
		[:first_keycode, KeyCode],
		[:count, Uint8],
	], [
		[:keysyms_per_keycode, Uint8, :in_header], 24,
		[:keysyms, List.of Keysym]] # count * keysyms_per_keycode

	def_remote :change_keyboard_control, 102, [
		[:control, KeyboardControl]]

	def_remote_with_reply :get_keyboard_control, 103, [
	], [
		[:global_auto_repeat, Bool, :in_header],
		[:led_mask, Uint32],
		[:key_click_volume, Int8], # percent
		[:bell_volume, Int8], # percent
		[:bell_pitch, Int16],
		[:bell_duration, Int16], 2,
		[:auto_repeats, List.of(Uint8,32)]]

# methods for: rpc: mouse config

	def_remote :change_pointer_control, 105, [
		[:control, PointerControl],
		[:do_acceleration, Bool],
		[:do_threshold,    Bool]]

	def_remote_with_reply :get_pointer_control, 106, [
	], [
		[:control, PointerControl]]

	def_remote_with_reply :set_pointer_mapping, 116, [
		[:map, Uint8, :length_in_header],
		[:map, List.of(Uint8), :data],
	], [
		[:status, MappingChangeStatus, :in_header]]

	def_remote_with_reply :get_pointer_mapping, 117, [
	], [
		[:map, Uint8, :length_in_header], 24,
		[:map, List.of(Uint8), :data]]

	def_remote_with_reply :set_modifier_mapping, 118, [
		[:keycodes_per_modifier, Uint8, :in_header],
		[:keycodes, List.of KeyCode],
	], [
		[:status, MappingChangeStatus, :in_header]]

	def_remote_with_reply :get_modifier_mapping, 119, [
	], [
		[:keycodes_per_modifier, Uint8, :in_header], 24,
		[:keycodes, List.of KeyCode]]

# methods for: rpc: screen saver

	def_remote :set_screen_saver, 107, [
		[:control, ScreenSaverControl]]

	def_remote_with_reply :get_screen_saver, 108, [
	], [
		[:control, ScreenSaverControl]]

	def_remote :force_screen_saver, 115, [
		[:mode, ScreenSaverAction, :in_header]]

# methods for: rpc: display admin

	def_remote :change_hosts, 109, [
		[:mode, HostChangeMode, :in_header],
		[:family, HostFamily],
		[:address, Uint16, :length],
		[:address, List.of(Uint8), :data]]

	def_remote_with_reply :list_hosts, 110, [
	], [
		[:mode, AccessMode, :in_header],
		[:hosts, Uint16, :length], 22,
		[:hosts, List.of(Host), :data]]

	def_remote :set_access_control,  111, [[:mode, AccessMode,    :in_header]]
	def_remote :set_close_down_mode, 112, [[:mode, CloseDownMode, :in_header]]
	def_remote :kill_client, 113, [[:resource, Any.of XID,AllTemporary]]

# methods for: rpc: misc 2

	def_remote :bell, 104, [
		[:volume, Uint8, :in_header]] # percent

	def_remote :no_operation, 127, []

end

#----------------------------------------------------------------#
end # of module X11

