#!/usr/bin/perl
################################################################################
# FULL-NON-BLOCKING !!
# FULL BLOCKING & NON-BLOCKING !!

package gterm::key;

################################################################################

use strict; no strict 'refs';  no strict 'subs';
use warnings;
use Exporter; use vars qw($VERSION @ISA @EXPORT @EXPORT_OK);

$VERSION     = '1.0.1';
@ISA         = qw(Exporter);
@EXPORT      = qw();
@EXPORT_OK   = qw();

################################################################################

use utf8 qw(decode_utf8); no utf8; use bytes;
use Term::TermKey qw(FLAG_UTF8 RES_KEY RES_AGAIN RES_EOF RES_NONE FORMAT_VIM);
use Term::Size::Any;
use Term::ReadKey;
use gerr;
use gfio; 
use glib qw(dechex octhex clonecopy);
use gterm::cntrl qw(tc);


################################################################################

$::GTKEY 		= undef;
my $ESC 		= tc("escape");
my $CTRLESC 	= tc("escape_esc");
my $ESCMOUSE 	= tc('altesc_mouse');
my $ENTER 		= tc("carriage_return");
my $CTRLENTER 	= tc("carriage_return_esc");

################################################################################

sub new {
	my ($class)=@_;
	if (defined $::GTKEY){ return $::GTKEY }
	$::GTKEY={ eventlist	=> [], buttons 	=> []};
	bless $::GTKEY;
	# IO HANDLING
	gio->new('gtkey-std',\*STDIN,undef,undef,0);
	# ensure perl and libtermkey agree on Unicode handling
	if (!defined $::GTKEY->{tk}) { $::GTKEY->{tk}=Term::TermKey->new(\*STDIN) }
	#binmode( STDOUT, $::GTKEY->{tk}->get_flags() && FLAG_UTF8 ? ":encoding(UTF-8)" : undef );
	$::GTKEY->flush();
	return $::GTKEY
}

################################################################################
# IO:Select
################################################################################
sub set_io 		{  # read write exception timeout
	my $self = shift();
	gio->init('gtkey-std',@_);
	return $self
}
################################################################################
sub takeloop { 
	my ($self)=@_;
	select(STDIN);
	#binmode( STDOUT, $::GTKEY->{tk}->get_flags() && FLAG_UTF8 ? ":encoding(UTF-8)" : undef );
	# STDIN ReadBuffer
	$self->{buff}		= [];
	# SCREEN Size
	my $size 			= [ defined $::DESK_COLS && defined $::DESK_ROWS ? ($::DESK_COLS,$::DESK_ROWS) : size() ];
	# on Resize Event
	if (defined $self->{size} && ($self->{size}[0] != $size->[0] || $self->{size}[1] != $size->[1])) {
		$self->{size}=$size;
		push @{$self->{eventlist}}, { type => 'size', columns => $size->[0], rows => $size->[1] };
	}
	# Init Size
	else {
		$self->{size}=$size;
	}
	return $self->keys_rd()->key_ctrl()
}

################################################################################
# BLOCKING LINE INPUT WITH NON-BLOCKING LOOPBACK CALLER & INPUT CALLER
# ENTER for line input & ESC for canceling
sub getline {
	my ($self,$callback,$loopcall)=@_;
	if (!$self->{getline}) { # protect against recursive looping
		$self->takeloop(); # flush to normal input loop
		$self->{getline} = 1;
		my $inp = ""; my $lastinp;
		while ( 1 ) {
		    $self->takeloop();
		    while (defined (my $event=shift(@{$self->{eventlist}}))) {
		    	$lastinp = substr($event->{inp},-1);
			    if ($lastinp eq $ENTER || $lastinp eq $CTRLENTER) {
			    	$inp .= substr($event->{inp},0,-1);
			   		$self->{event} = {
			   			type	=> "line",
				    	event 	=> "input",
				    	inp  	=> $inp,
				    	key 	=> "ENTER",
				    };
					if (defined $callback && ref($callback) eq 'CODE') { &{$callback}($self->{event}) }
				    return $self->{event}
				}
			    elsif ($lastinp eq $ESC || $lastinp eq $CTRLESC) {
			    	$inp .= substr($event->{inp},0,-1);
			   		$self->{event} = {
			   			type	=> "line",
				    	event 	=> "input",
				    	inp  	=> $inp,
				    	key 	=> "ESC",
				    };
					if (defined $callback && ref($callback) eq 'CODE') { &{$callback}($self->{event}) }
				    return $self->{event}
				}
				else {
					$inp .= $self->{event}{inp};
				}
			}
			if (defined $loopcall && ref($loopcall) eq 'CODE') { &{$loopcall}() }
		}
	}
	return {error=>"Nothing Happend Correctly??"}
}

################################################################################

sub reset_mouse {
	my ($self)=@_;
	if (defined $self->{down}) { delete $self->{down} }
	if (defined $self->{drag}) { delete $self->{drag} }
	return $self
}

################################################################################

sub size { return Term::Size::Any::chars() }    # : Columns, Rows

sub flush { my $self=shift(); $self->cbreak_md(); while (defined $self->nonblocked_rd()) { } return $self->restore_md() }

################################################################################

sub key_ctrl {
	my ($self)=@_;
	while ($#{$self->{buff}} > -1) {
		$self->read_ctrl()
	}
	select(STDOUT);
	return $self
}

################################################################################

sub read_buff { my ($self)=@_; return shift(@{$self->{buff}}) }

sub read_ctrl {
	my ($self)=@_;
	while ($#{$self->{buff}} > -1) {
		$self->{event}={inp => $self->read_buff()};
	 	$self->{event}{chr} = join(",",@{[map { defined $_ ? ord($_) : -1 } split(//,$self->{event}{inp})]});
	  	$self->{event}{hex} = join(":",@{[map { defined $_ ? octhex($_) : '--' } split(//,$self->{event}{inp})]});
		my ($key,$col,$row)=tc('mouse_event',$self->{event}{inp});
		if (defined $key) {
			$self->{event}{type} = "mouse";
			$self->{event}{event} = $key;
			$self->{event}{key} = $key;
			$self->{event}{col} = $col-1;
			$self->{event}{row} = $row-1;
			$self->{event}{moved} = 1;
		}
		elsif (substr($self->{event}{inp},0,3) eq $ESCMOUSE) {
			$self->{event}{type} = "mouse"; my @m=split(//,$self->{event}{inp});
			my $baseinp=join("",$m[0],$m[1],$m[2],$m[3]);
			if (defined $::GTCNTRL_ID->{$self->{event}{inp}}) {
				$self->{event}{key} = $::GTCNTRL_ID->{$self->{event}{inp}};
			}
			elsif (defined $::GTCNTRL_ID->{$baseinp}) {
				$self->{event}{key} = $::GTCNTRL_ID->{$baseinp};
				$self->{event}{crd} = substr($self->{event}{inp},length($baseinp));
				$self->{event}{x} = (defined $m[4] ? ord($m[4]) : -1);
				$self->{event}{y} = (defined $m[5] ? ord($m[5]) : -1);
				$self->{event}{col} = ($self->{event}{x} < 33 ?  $self->{event}{x} < 3 ? $self->{size}[0] + $self->{event}{x} - 15 : 0 : $self->{event}{x} - 33);
				$self->{event}{row} = ($self->{event}{y} < 33 ?  $self->{event}{y} < 3 ? $self->{size}[1] + $self->{event}{y} - 15 : 0 : $self->{event}{y} - 33);
				$self->{event}{moved} = 1;
				#print STDOUT "POS[$self->{event}{key}: $self->{event}{x},$self->{event}{y}]\r";
			}
			# Figure out what we don't have
			else {
				$self->{event}{type} = "key";
			 	$self->{event}{key} = "keypress";
			}
			$self->{event}{event} = $self->{event}{key};
		}
			#########################################################
			# TERMINAL CALL & RESPONSE
			#########################################################
			#
			# ECMA-48 Status Report Commands
			# ESC [ 5 n
			# Device status report (DSR): Answer is ESC [ 0 n (Terminal OK).
			# ESC [ 6 n
			# Cursor position report (CPR): Answer is ESC [ y ; x R, where x,y is the cursor location.
			#
			#########################################################
			#    identify                                =>  "\e[c",   
			#    terminal_type                           =>  "\e[0c",  
			#########################################################
			#    terminal_type_response                  =>  sub {     
			#        if ($_[0] =~ /^\e\[\?1\;(.+)0c$/){ return $1 }
			#        return undef
			#    },                                                  
			#########################################################
		elsif (defined $::GTCNTRL_ID->{$self->{event}{inp}}) {
			$self->{event}{key} = $::GTCNTRL_ID->{$self->{event}{inp}};
			if ($self->{event}{key} eq 'terminal_ok') {
			}
		}
		elsif ($self->{event}{inp} =~ /^.\[(.+)0c$/gs ) {
			$self->{event}{type} = "term";
			$self->{event}{event} = "response";
			my $rc=$1;
			#########################################################
			# TERMINAL RESPONSE CODES
			#########################################################
			if (defined $::GTCNTRL_ID->{$self->{event}{inp}}) {
				$self->{event}{key} = $::GTCNTRL_ID->{$self->{event}{inp}};
			}
			#    terminal_ok                             =>  "\e0n", 
			#    terminal_notok                          =>  "\e3n", 
			#########################################################
			#    terminal_identify                       =>  "\eZ",  
			#    terminal_vt52                           =>  "\e/Z", 
			#########################################################
			#    terminal_device_status_report           =>  "\e5n", 
			#########################################################
		} else {
			$self->{event}{type} = "key";
			$self->{event}{event} = "keypress";
			$self->{event}{key} = (defined $::GTCNTRL_ID->{$self->{event}{inp}} ? $::GTCNTRL_ID->{$self->{event}{inp}} : "<[$self->{event}{inp}]>");
		}
		return $self->event_ctrl()
	}
	return $self
}

################################################################################

sub event_ctrl {
	my ($self)=@_;
	$self->{event}{controls} =  $self->{event}{key};
	$self->{event}{controls} =~ s/\_down//gsi;
	$self->{event}{controls} =~ s/\_up//gsi;
	# is Mouse Event ?
	if (defined $self->{event}){
		if (defined $self->{event}{type}){
			if ($self->{event}{type} eq 'mouse') {
	  			if (defined($self->{event}{button} = (
		  			$self->{event}{key} =~ /mouse\_up/ ? $self->{buttons}[$#{$self->{buttons}}] :
	  				$self->{event}{key} =~ /left\_/ ? 'left' :
	  				$self->{event}{key} =~ /middle\_/ ? 'middle' :
		  			$self->{event}{key} =~ /right\_/ ? 'right' :
		  			undef
		  		))) {
					eval('$self->{event}{controls} =~ s/\\_'.$self->{event}{button}.'//gsi;');
				}
			  	if ($self->{event}{key} =~ /\_up/) { $self->on_mouse_up() }
			  	elsif ($self->{event}{key} =~ /\_down/) { $self->on_mouse_down() }
		  		else  { $self->on_mouse_move() }
		  	}
  		} else {
			$self->{event}{type} = "key";
			$self->{event}{event} = "keypress";
  		}
	}
	return $self->listener_ctrl()
}

################################################################################

sub listener_ctrl {
	my ($self)=@_;
	if (1
		#( $self->{event}{type} eq 'mouse' && ($self->{event}{key} =~ /move/ || $self->{event}{key} =~ /drag/)) ?
		#( !defined $self->{lastpos} || $self->{lastpos}{col}!=$self->{event}{col} || $self->{lastpos}{row}!=$self->{event}{row} ) && 
		#( ref($self->{lastpos}={col=>$self->{event}{col},row=>$self->{event}{row}}) eq 'HASH' ) 
		#? 
		#1 : 0 : 1
	) {
		push @{$self->{eventlist}}, clonecopy($self->{event})
	}
	return $self
}

################################################################################

sub on_mouse_down {
	my ($self)=@_;
	if (defined $self->{drag}) { delete $self->{drag} }
	my $button=$self->{event}{button};
	if ($self->{event}{key} =~ /wheel\_/) {
		$self->{event}{scroll} = -1
	} else {
		$self->{down} = {
			col => $self->{event}{col},
			row => $self->{event}{row}
		};
		$self->set_area( $self->{down}{col}, $self->{down}{row}, $self->{event}{col}, $self->{event}{row} );
		$self->{event}{area}=$self->{area};
		push @{$self->{buttons}}, $button;
	}
}

sub on_mouse_move {
	my ($self)=@_;
	if (defined $self->{down}) {
		$self->{drag} = {
			col => $self->{event}{col},
			row => $self->{event}{row}
		};
		$self->set_area( $self->{down}{col}, $self->{down}{row}, $self->{event}{col}, $self->{event}{row} );
		$self->{event}{area}=$self->{area};
	} else {
		$self->{move} = {
			col => $self->{event}{col},
			row => $self->{event}{row}
		};
	}
}


sub on_mouse_up {
	my ($self)=@_;
	my $button=$self->{event}{button};
	if ($self->{event}{key} =~ /wheel\_/) {
		$self->{event}{scroll} = 1
	} else {
		$button = shift(@{$self->{buttons}});
		$self->{event}{up} = { 	col => $self->{event}{col},	row => $self->{event}{row} };
		$self->set_area( $self->{down}{col}, $self->{down}{row}, $self->{event}{col}, $self->{event}{row} );
		$self->{event}{area}=$self->{area};
		if ($self->{drag}) {
			$self->{event}{down} = { 	col => $self->{down}{col},	row => $self->{down}{row}	};
			$self->{event}{drop} = { 	col => $self->{drag}{col},	row => $self->{drag}{row}	};
			$self->{event}{event} =~ s/mouse_up/$button\_drop/gsi;
			$self->{event}{event} =~ s/_up$/\_drop/gsi;
		} else {
			$self->{event}{event} =~ s/mouse_up/$button\_click/gsi;
			$self->{event}{event} =~ s/_up$/\_click/gsi;
		}
	}
}

sub set_area {
	my ($self,$sx,$sy,$ex,$ey)=@_;
	my $l=$sx;
	my $r=$ex;
	my $t=$sy;
	my $b=$ey;
	my $w=abs($r-$l);
	my $h=abs($b-$t);
    if ($l > $r) { my $x=$r; $r=$l; $l=$x }
    if ($t > $b) { my $y=$b; $b=$t; $t=$y }
	$self->{area}={
		l => $l,
		t => $t,
		r => $r,
		b => $b,
		w => $w,
		h => $h,
		size => [$w,$h],
		hc => ($l+($w>>1)),
		vc => ($t+($h>>1)),
		sx => $sx, sy => $sy, ex => $ex, ey => $ey,
		box => [$l, $t, $r, $b],
		inbox => [$l+1, $t+1, $r-1, $b-1],
		outbox => [$l-1, $t-1, $r+1, $b+1],
		from => [$sx, $sy],
		to => [$ex, $ey]
	};
	return $self
}


################################################################################
# ReadMode MODE [, Filehandle]
#
# 	Takes an integer argument or a string synonym (case insensitive),
# 	which can currently be one of the following values :
#
# 	INT   SYNONYM    DESCRIPTION
# 	0    'restore'   Restore original settings.
# 	1    'normal'    Change to what is commonly the default mode,
#	                 echo on, buffered, signals enabled, Xon/Xoff
#	                 possibly enabled, and 8-bit mode possibly disabled.
# 	2    'noecho'    Same as 1, just with echo off. Nice for
#	                 reading passwords.
# 	3    'cbreak'    Echo off, unbuffered, signals enabled, Xon/Xoff
#	                 possibly enabled, and 8-bit mode possibly enabled.
# 	4    'raw'       Echo off, unbuffered, signals disabled, Xon/Xoff
#	                 disabled, and 8-bit mode possibly disabled.
# 	5    'ultra-raw' Echo off, unbuffered, signals disabled, Xon/Xoff 
#	                 disabled, 8-bit mode enabled if parity permits,
#	                 and CR to CR/LF translation turned off.
################################################################################
sub restore_md		{ my $self = shift; ReadMode(0); return $self }
sub normal_md 		{ my $self = shift; ReadMode(1); return $self }
sub noecho_md 		{ my $self = shift; ReadMode(2); return $self }
sub cbreak_md 		{ my $self = shift; ReadMode(3); return $self }
sub raw_md 			{ my $self = shift; ReadMode(4); return $self }
sub uraw_md 		{ my $self = shift; ReadMode(5); return $self }
################################################################################

################################################################################
# ReadKey MODE [, Filehandle]
#
# 	Takes an integer argument, which can currently be one of the following values:
#
#	0    Perform a normal read using getc
#	-1   Perform a non-blocked read
#	>0   Perform a timed read
#
#	If the filehandle is not supplied, it will default to STDIN. If there is nothing 
#	waiting in the buffer during a non-blocked read, then undef will be returned. 
#	In most situations, you will probably want to use ReadKey -1.
#
#	NOTE that if the OS does not provide any known mechanism for non-blocking reads, 
#	then a ReadKey -1 can die with a fatal error. This will hopefully not be common.
#
#	If MODE is greater then zero, then ReadKey will use it as a timeout value in 
#	seconds (fractional seconds are allowed), and won't return undef until that time expires.
#
#	NOTE, again, that some OS's may not support this timeout behaviour.
#
#	If MODE is less then zero, then this is treated as a timeout of zero, and thus 
#	will return immediately if no character is waiting. A MODE of zero, however, 
#	will act like a normal getc.
#
#	NOTE, there are currently some limitations with this call under Windows. It 
#	may be possible that non-blocking reads will fail when reading repeating keys
#	from more then one console.
#
################################################################################
sub nonblocked_rd	{ my $self = shift; return ReadKey(-1) }
sub normal_rd 		{ my $self = shift; return ReadKey(0) }
sub timed_rd 		{ my $self = shift; return ReadKey(@_) }
################################################################################
sub takeloop_rd		{ my ($self) = @_; $self->{key_buff}=""; while ( defined($self->{key_in} = $self->nonblocked_rd()) ) { $self->{key_buff}.=$self->{key_in} }; return $self }
################################################################################
sub key_rd 			{ my ($self) = @_; return $self->uraw_md()->takeloop_rd()->restore_md()->{key_buff} }
sub keys_rd 		{ my ($self) = @_;
#	if ($::GIO->can_rd('gtkey-std')) {
#		$::GIO->save_select('gtkey-std');
		while ($self->key_rd() ne "") { 
			my $buf="";
			for my $c (split(//,$self->{key_buff})) {
				if (ord($c) == 27 && length($buf)) { push @{$self->{buff}}, $buf; $buf = $c } 
				else { $buf .= $c }
			}
			if (length($buf)) { push @{$self->{buff}}, $buf }
		}
#		$::GIO->restore_select()
#	}
	return $self
}
################################################################################

################################################################################
1 # EOF gterm::key.pm (C) 2020 OnEhIppY, Groningen, Domero
################################################################################


################################################################################
# ReadLine MODE [, Filehandle]
# Takes an integer argument, which can currently be one of the following values:
#0    Perform a normal read using scalar(<FileHandle>)
#-1   Perform a non-blocked read
#>0   Perform a timed read
#If there is nothing waiting in the buffer during a non-blocked read, then undef will be returned.
#
#NOTE, that if the OS does not provide any known mechanism for non-blocking reads, then a ReadLine 1 can die with a fatal error. This will hopefully not be common.
#
#NOTE that a non-blocking test is only performed for the first character in the line, not the entire line. This call will probably not do what you assume, especially with ReadMode MODE values higher then 1. For example, pressing Space and then Backspace would appear to leave you where you started, but any timeouts would now be suspended.
#
#This call is currently not available under Windows.
#
################################################################################

################################################################################
#GetTerminalSize [Filehandle]
# Returns either an empty array if this operation is unsupported, or a four
# element array containing: the width of the terminal in characters, the height
# of the terminal in character, the width in pixels, and the height in pixels.
# (The pixel size will only be valid in some environments.)
#
# NOTE, under Windows, this function must be called with an output filehandle,
# such as STDOUT, or a handle opened to CONOUT$.
#
################################################################################

################################################################################
#SetTerminalSize WIDTH,HEIGHT,XPIX,YPIX [, Filehandle]
# Return -1 on failure, 0 otherwise.
#
# NOTE that this terminal size is only for informative value, and changing the
# size via this mechanism will not change the size of the screen. For example,
# XTerm uses a call like this when it resizes the screen. If any of the new
# measurements vary from the old, the OS will probably send a SIGWINCH signal
# to anything reading that tty or pty.
#
# This call does not work under Windows.
#
################################################################################

################################################################################
#GetSpeed [, Filehandle]
# Returns either an empty array if the operation is unsupported, or a two value
# array containing the terminal in and out speeds, in decimal. E.g, an in speed
# of 9600 baud and an out speed of 4800 baud would be returned as (9600,4800).
# Note that currently the in and out speeds will always be identical in some OS's.
#
# No speeds are reported under Windows.
#
################################################################################

################################################################################
#GetControlChars [, Filehandle]
# Returns an array containing key/value pairs suitable for a hash. The pairs
# consist of a key, the name of the control character/signal, and the value of
# that character, as a single character.
#
# This call does nothing under Windows.
#
# Each key will be an entry from the following list:
#
# DISCARD
# DSUSPEND
# EOF
# EOL
# EOL2
# ERASE
# ERASEWORD
# INTERRUPT
# KILL
# MIN
# QUIT
# QUOTENEXT
# REPRINT
# START
# STATUS
# STOP
# SUSPEND
# SWITCH
# TIME
#
# Thus, the following will always return the current interrupt character, regardless of platform.
#
# %keys = GetControlChars;
# $int = $keys{INTERRUPT};
################################################################################

################################################################################
#SetControlChars [, Filehandle]
# Takes an array containing key/value pairs, as a hash will produce. The pairs
# should consist of a key that is the name of a legal control character/signal,
# and the value should be either a single character, or a number in the range
# 0-255. SetControlChars will die with a runtime error if an invalid character
# name is passed or there is an error changing the settings. The list of valid
# names is easily available via
#
#%cchars = GetControlChars();
#@cnames = keys %cchars;
#This call does nothing under Windows.
################################################################################


#########################################################################################################################################
# ╔═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ #
# ║ EOF (C) 2020 OnEhIppY, Groningen, Domero                                                                                          ║ #
# ╚═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝ #
#########################################################################################################################################
