#!/usr/bin/env perl

use Time::HiRes qw/gettimeofday/;
use sigtrap qw/handler finish normal-signals/;
use Cwd qw(cwd);
use File::Copy qw(copy);
use File::Path qw(remove_tree);
# use strict; # sigh FIXME
use Getopt::Long;

$outfile = "Untitled Recording.html";
$verbose = 0;
$editimages = 0;
$imgext="jpg";
$screeni = 0; # counter for screenshot number

sub usage {
	return <<endusage;
Usage: $0 [options] [outfile]

Options:

  -o|--out outfile		Output file name (also can be first argument)
  -e|--edit-images-before-save	Edit images before saving file
  -c|--image-extension ext	Extension of image output (png or jpg)
  -h|--help			Print this message

endusage
}

GetOptions ("out|o=s" => \$outfile, "edit-images-before-save|e" => \$editimages, "image-extension|c=s" => \$imgext, "verbose|v" => \$verbose) or die "$!\n" . usage();

if (@ARGV == 1) {
	$outfile = $ARGV[0];
}
elsif (@ARGV == 0) {
	#fine.
}
else {
	print usage();
	die "Too many arguments.\n";
}

if ($imgext == "png") {$mimetype = "image/png"}
elsif ($imgext == "jpg") {$mimetype = "image/jpeg"}
else {print usage(); die "Invalid file type $imagext.\n"}

# change to a temporary directory
$originaldir = cwd;
$tmpdir = `mktemp -d`;
chomp	$tmpdir; # mktemp returns a newline
chdir $tmpdir or die "$! Is /tmp mounted?\n";

# check for features, warn and do not use if false 6, 3
$xdotool = `which xdotool`;
chomp($xdotool);
$xdotool or warn "xdotool unavailable: mouse cursor will not appear in screenshots\n";
$composite = `which composite`;
chomp($composite);
$composite or warn "composite unavailable: mouse cursor will not appear in screenshots\n";
$scrot = `which scrot`;
chomp($scrot);
$scrot or die "scrot unavailable: cannot take screenshots\n";


$outfile_saveext = $outfile;
$outfile_saveext =~ s/\.[^\.]*$//;

sub convertToB64 {
	if (! -e $_[0]) {
		warn "Cannot convert $_[0]: no such file or directory\n";
		return $_[0];
	}
	return `base64 -w 0 $_[0]`; # -w 0 : do not wrap
}

sub finish {
	print STDOUT "\n";
	if (FOUT) {
		handletypingstate();
		print <<"/html";
		<div class="footer">
			<i>Made using <a href="https://github.com/nonnymoose/xsr">X Steps Recorder</a>.</i>
		</div>
	</body>
</html>
/html

		FOUT->flush();
		close FOUT;
		close XIN;

		if ($xdotool && $composite) {
			open "MOUSEIN", "<", "mousegrabs" or warn "Couldn't open mousegrabs: mouse will not appear in screenshots\n";
			for ($i = 0; $i < $screeni; $i++) {
				$curmouse = <MOUSEIN>;
				chomp $curmouse;
				if ($curmouse =~ /x:(\d+).*y:(\d+)/) {
					$curmousex = $1 - 6; # offset of cursor in image is 6, 3
					$curmousey = $2 - 3;
				} else {last;}
				system("composite -geometry +$curmousex+$curmousey /usr/share/xsr/Cursor.png $i.$imgext $i.$imgext") and warn "Had a problem adding the cursor to image $i\n"; # shell has reversed error codes
			}
			close "MOUSEIN";
		}

		# Wait here if user wants to edit the images
		if ($editimages) {
			$xdgopen = `which xdg-open`;
			chomp $xdgopen;
			if ($xdgopen) {system("xdg-open \"$tmpdir\"")}
			print STDOUT "Initial image processing complete. If you would like to edit the images before the file is saved, they are located in $tmpdir. Press return when finished.\n";
			<STDIN>
		}

		open "ASSOCFILE", "<", "tmpassoconly.html"; # this file contains only image associations, not base64
		open "FINALFILE", ">", $outfile; # this file will contain base64
		select FINALFILE;
		while (<ASSOCFILE>) {
			$_ =~ s/<img src=\"([^\"]+)\" \/>/"<img src=\"data:$mimetype;base64," . convertToB64($1) . "\" \/>"/gie; # replace <img> tags' src attr with a base64 uri
			print $_;
		}
		close ASSOCFILE;
		close FINALFILE;
		chdir $originaldir;
		copy "$tmpdir/$outfile", $outfile; # put the output in the original directory
		remove_tree $tmpdir;
	}
	print STDOUT "\n";
}

print "Starting in ";
for ($i = 5; $i > 0; $i--) {
	print $i . "\b";
	STDOUT->flush();
	sleep 1;
}
print "0\nStarted. (Why are you still here?)\n";


# From here on out the code is inspired by this answer: https://unix.stackexchange.com/a/129171, posted by Stéphane Chazelas
open "X", "-|", "xmodmap -pke"; # open keymap
while (<X>) {
	if (/^keycode\s+(\d+) = (\w+)(?:\s+(\w+))?/) { # keycode <number> = <normal> <shift> ...
		$k{$1}=$2; # store normal in k, a hash
		$K{$1}=$3 if $3; # store shift in K, a hash
	}
}
open "X", "-|", "xmodmap -pm"; <X>;<X>; # open modmap
while (<X>) {if (/^(\w+)\s+(\w*)/){($k=$2)=~s/_[LR]$//;$m[$i++]=$k||$1}} # get a list of modifiers and stick it in an array
close X;
$mregex = "(shift|meta|alt|super|mod|lock|control)"; # a regex to tell whether a KeyPress is a modifier or not

%realchars = ("exclam","!","at","@","numbersign","#","dollar","\$","percent","%","asciicircum","^","ampersand","&","asterisk","*","parenleft","(","parenright",")","minus","-","underscore","_","equal","=","plus","+","bracketleft","[","braceleft","{","bracketright","]","braceright","}","semicolon",";","colon",":","apostrophe","'","quotedbl","\"","grave","`","asciitilde","~","backslash","\\","bar","|","comma",",","less","<","period",".","greater",">","slash","/","question","?","Multiply","*","space"," ","Subtract","-","Left"," ← ","Right"," → ","Add","+","Down"," ↓ ","less","<","greater",">","Divide","/","Print","PrintScreen","Up"," ↑ ","Prior","PageUp","Next","PageDown","Equal","=","plusminus","±","Decimal",".");
# ↑: a hash with human-readable names associated with the machine-readable key names returned by xinput
@realbuttons = (undef, "Left-click", "Middle-click", "Right-click", "Scroll Up", "Scroll Down"); # an array of mouse button names (note that button IDs are 1-indexed)

sub getrealchar { # function that makes using the above hash easier
	$keycode = $_[0];
	$keycode =~ s/KP_//i; # remove any number pad designation
	if ($realchars{$keycode}) {
		return $realchars{$keycode}; # make sure to only return the value if it's in the array (many aren't because the machine-readable name is also human-readable)
	}
	else {
		return $keycode;
	}
}

sub getrealbutton { # function that makes using the above array easier
	if ($realbuttons[$_[0]]) {
		return $realbuttons[$_[0]];
	}
	else {
		return "Click mouse button $_[0]"; # fail-safe
	}
}

sub handletypingstate { # called by non-typing events
	if ($typing) {
		print "</div>\n</div>\n"; # finish the output line beginning with "Type:"
		$typing = 0; # reprint "Type:" and the like next time
	}
}

sub takescreenshot {
	system("xdotool getmouselocation >> mousegrabs &") if $xdotool && $composite;
	system("scrot $screeni.$imgext &");
	return $screeni++;
}

open "FOUT", ">", "tmpassoconly.html"; # open output file (NOTE: no safety here! Watch out!)
select FOUT;

# header
print <<"/html";
<!DOCTYPE html>
<html>
	<head>
		<title>Steps Recording</title>
		<link href="https://fonts.googleapis.com/css?family=Ubuntu" rel="stylesheet">
		<style>@font-face{font-family:'Ubuntu';font-style:normal;font-weight:400;src:local('Ubuntu Regular'),local('Ubuntu-Regular'),url(https://fonts.gstatic.com/s/ubuntu/v10/sDGTilo5QRsfWu6Yc11AXg.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2212,U+2215;}body>div.title{font-family:"Ubuntu",sans-serif;font-size:2em;text-align:center;border-bottom-style:solid;border-width:thin;margin-bottom:10px;}div.instruction{margin:10px auto;padding:10px;max-width:80%;border:thin solid lightgray;border-radius:10px;display:table;}div.instruction div.title{font-family:monospace;font-size:1.2em;text-align:center;}div.footer{padding:5px;text-align:center;background-color:#f7f7f7;}img{margin-top:15px;display:block;width:100%}kbd{display:inline-block;margin:0 .1em;padding:.3em .4em;font-family:Ubuntu,Arial,"libra sans",sans-serif;font-size:75%;line-height:inherit;color:#242729;text-shadow:0 1px 0 #FFF;background-color:#e1e3e5;border:1px solid #adb3b9;border-radius:3px;box-shadow:0 1px 0 rgba(12,13,14,0.2),0 0 0 2px #FFF inset;white-space:nowrap;}</style>
	</head>
	<body>
		<div class="title">$outfile_saveext</div>
/html

open "XIN", "-|", "xinput --test-xi2 --root"; # execute xinput with the monitoring options

while (<XIN>) {
	if (/^EVENT type.*\((.*)\)/) {$e = $1} # store event type in $e
	elsif (/detail: (\d+)/) {$d=$1} # store event detail in $d
	elsif (/flags: /) { # process Raw mouse events (for some reason, maximized windows don't return standard motion events)
		if ($e =~ /^RawButtonPress/){
			if ($d == 0) {
				# WHAT THE HECK!?!?!?!?
				# This should not happen and will cause problems. Skip it.
				next;
			}
			my $shotnumber = takescreenshot();
			handletypingstate();
			$movedsince = 0; # haven't moved yet since this button press
			unless ($d == $lastscroll) { # only do printing and stuff if this is not a second or later scroll event
				$buttondown = gettimeofday(); # store time of buttonpress
				print "<div class=\"instruction\">\n<div class=\"title\">";
				my @mods;
				for (0..$#m) {
					if (hex($m) & (1<<$_)) {
						if ($m[$_] =~ /num.*lock/i) {
							# do nothing, does not affect clicking
						}
						else {
							push @mods, $m[$_]; # get modifiers
						}
					}
				}
				if (@mods > 0) {
					for (0..$#mods) {print "<kbd>$mods[$_]</kbd>+"} # print modifiers if necessary
				}
				print getrealbutton($d);
				if ($d == 4 || $d == 5) {$lastscroll = $d} # if scrolling, don't recognize any more events in the same direction until we stop scrolling
				else {$lastscroll = 0} # we stopped scrolling because we clicked or something
				print "</div>\n<img src=\"$shotnumber.$imgext\" />\n</div>\n";
			}
		}
		elsif ($e =~ /^RawMotion/) {
			$movedsince = 1; # moved since clicked
		}
		elsif ($e =~ /^RawButtonRelease/){
			handletypingstate();
			if (gettimeofday() - $buttondown >= .075 && $movedsince) { # if wwe have moved the mouse since clicking and it's been more than 0.075 seconds, recognize it as a click and drag
																																 # This is very similar to the system default
				my $shotnumber = takescreenshot();
				print "<div class=\"instruction\">\n<div class=\"title\">... and drag</div>\n<img src=\"$shotnumber.$imgext\" />\n</div>\n";
			}
		}
	}
	elsif (/modifiers:.*effective: (.*)/) { # do the real work now that we have all of the information
	  $m=$1; # store modifier counter in $m
		if ($e =~ /^KeyPress/){ # handle typing
			$lastscroll = 0; # definitely not scrolling anymore
			$key = getrealchar($k{$d}); # get machine-readable name from detail, then get human-readable name from that
			if ($key =~ /$mregex/i) { # skip this iteration if the key is a modifier
				next;
			}
			if (! $typing) {print "<div class=\"instruction\">\n<div class=\"title\">Type: "; $typing = 1} # print a type instruction if not already typing, then note that already typing from now on
			my @mods;
			for (0..$#m) {
				if (hex($m) & (1<<$_)) { # this is the tricky part
					# xinput returns a hexadecimal string that converts to a byte
					#                   so, 00000000 if no modifier keys are pressed
					# the modifier order is 87654321
					# this loop goes through all modifiers and checks if their bit is set
					if ($k{$d} =~ /^KP_/i && $m[$_] =~ /num.*lock/i) { # if it's a keypad key and num lock is on
						$key = getrealchar($K{$d}) unless getrealchar($K{$d}) eq "NoSymbol"; # get its second level (if there is one)
					}
					elsif ($m[$_] =~ /num.*lock/i) {
						# do nothing, num lock should not appear in the modifier array
					}
					elsif ($m[$_] =~ /shift/i) { # if shift is pressed
						if (getrealchar($K{$d}) ne "NoSymbol") {
							$key = getrealchar($K{$d}); # get its second level, if there is one
						}
						else {
							push @mods, $m[$_]; # otherwise DO add shift to modifier list
						}
					}
					else {
						push @mods, $m[$_]; # add any other modifier to the list
					}
				}
			}
			if (@mods > 0) {
				for (0..$#mods) {print "<kbd>$mods[$_]</kbd>+"} # if there are modifiers in effect, print as a sequence of keys
				print "<kbd>$key</kbd>";
			}
			elsif (length($key) == 1) {
				print $key; # if the realchar of a key is a single character, don't style it as a key
			}
			else {
				print "<kbd>$key</kbd>"; # else style it as a key
			}
			if ($k{$d} =~ /Return|Enter/i) {
				my $shotnumber = takescreenshot();
				print "</div>\n<img src=\"$shotnumber.$imgext\" />\n</div>\n"; # finish the output line beginning with "Type:"
				$typing = 0; # reprint "Type:" and the like next time
			} # if the user presses return, then end the type instruction and add a screenshot
	  }
	}
}
