###RUN_PERL: #!/usr/bin/perl

# /bsta/coin
# chat.pl is generated from chat.1.pl.
#
# The coincidence interface
#
# Copyright (C) 2016, 2017, 2023, 2024  Balthasar Szczepański
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

use strict;
use utf8;
###PERL_LIB: use lib /botm/lib/bsta
# use Encode::Locale ('decode_argv');
use Encode ('encode', 'decode');

###PERL_LIB: use lib /botm/lib/bsta
use botm_common (
	'HTTP_STATUS',
	'read_header_env',
	'url_query_decode', 'url_query_encode',
	'merge_url',
	'html_entity_encode_dec',
	'open_encoded',
	'http_header_status'
);
use bsta_lib (
	'STATE', 'CHAT_STATE', 'CHAT_ACTION',
	'fail_method', 'fail_content_type',
	'get_remote_addr', 'get_id', 'get_password',
	'print_html_start', 'print_html_end',
	'print_html_head_start', 'print_html_head_end',
	'print_html_body_start', 'print_html_body_end',
	'merge_settings',
	'read_chat', 'write_chat',
	'read_coincidence', 'read_settings', 'read_state'
);
use  File::Copy;

###PERL_CGI_PATH:           CGI_PATH           = /bsta/
###PERL_CGI_COIN_PATH:      CGI_COIN_PATH      = /bsta/coin

###PERL_DATA_CHAT_PATH:     DATA_CHAT_PATH     = /botm/data/bsta/chat

###PERL_WEBSITE_NAME:       WEBSITE_NAME       = Bicycles on the Moon

binmode STDIN,  ':encoding(UTF-8)';
binmode STDOUT, ':encoding(UTF-8)';
binmode STDERR, ':encoding(UTF-8)';
# decode_argv();

my %http;
my %cgi;
my %coin;
my %chat;
my %settings;
my %state;

my $time = time();
srand ($time-$$);

my $method;
my $IP;
my $page;
my $words = '';
my $username = '';
my $action = CHAT_ACTION->{'none'};
my $password;
my $fh;
my $state;
my $password_ok;
my @chat_lines;
my $chat_state;
my $status;
my $message;
my $chat_id;
my $last_id;

delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
###PERL_SET_PATH: $ENV{'PATH'} = /usr/local/bin:/usr/bin:/bin;

if ($ENV{'REQUEST_METHOD'} =~ /^(HEAD|GET|POST)$/) {
	$method = $1;
}
else {
	exit fail_method($ENV{'REQUEST_METHOD'}, ['GET', 'POST', 'HEAD']);
}

%http = read_header_env(\%ENV);
%cgi = url_query_decode($ENV{'QUERY_STRING'});

if ($method eq 'POST') {
	if ($http{'content-type'} eq 'application/x-www-form-urlencoded') {
		my %cgi_post = url_query_decode( <STDIN> );
		%cgi = merge_settings(\%cgi, \%cgi_post);
	}
	# multipart not supported
	else{
		exit fail_content_type($method, $http{'content-type'});
	}
}

$IP       = get_remote_addr();
$page     = get_id(\%cgi, -1);
$password = get_password(\%cgi);

%coin      = read_coincidence();
%settings  = read_settings();
%state     = read_state();

$password_ok = ($password eq $settings{'password'});

if ($cgi{'words'} ne '') {
	$words = $cgi{'words'};
}
if ($password_ok && ($cgi{'username'} ne '')) {
	$username = $cgi{'username'};
}
foreach my $action_id ('join', 'leave', 'nopost', 'file') {
	if ($cgi{$action_id} ne '') {
		$action = CHAT_ACTION->{$action_id};
		last;
	}
}

# ongoing chat
if ($page < 0) {
	if (open_encoded($fh, "+<", DATA_CHAT_PATH())) {
		if (flock($fh, 2)) {
			%chat = read_chat($fh);
			
			$chat_state = int($chat{'state'});
			$chat_id    = int($chat{'id'});
			$last_id    = $chat_id;
			
			if ($method ne 'POST') {
				#
			}
			
			elsif (
				($action == CHAT_ACTION->{'none'}) &&
				($words ne '')
			) {
				if (($chat_state < CHAT_STATE->{'ready'}) && !$password_ok) {
					$status = HTTP_STATUS->{'forbidden'};
					$message = 'Not connected.';
				}
				else {
					if ($words !~ /[\r\n]/) {
						if ($username =~ /^[A-Za-z]*$/) {
							$chat{'content'} .= $username.': '.$words."\n";
							if ($chat_state < CHAT_STATE->{'active'}) {
								$chat_state = CHAT_STATE->{'active'};
								$chat{'state'} = $chat_state;
							}
							write_chat($fh, \%chat);
						}
						else {
							$status = HTTP_STATUS->{'bad_request'};
							$message = 'Invalid username.';
						}
					}
					else {
						$status = HTTP_STATUS->{'bad_request'};
						$message = 'Invalid text.';
					}
				}
			}
			
			elsif ($action == CHAT_ACTION->{'join'}) {
				if (($chat_state > CHAT_STATE->{'disconnected'}) && !$password_ok) {
					$message = 'Already connected.';
				}
				else {
					if ($username =~ /^[A-Za-z]*$/) {
						if ($password_ok || $words eq $coin{'server'}) {
							$chat{'content'} .= 'join@'.$username.': '.$words."\n";
							if ($chat_state < CHAT_STATE->{'ready'}) {
								$chat_state = CHAT_STATE->{'ready'};
								$chat{'state'} = $chat_state;
							}
							write_chat($fh, \%chat);
						}
						elsif ($words eq '') {
							$status = HTTP_STATUS->{'bad_request'};
							$message = 'Server ID missing.';
						}
						elsif ($words !~ /^[0-9]+$/) {
							$status = HTTP_STATUS->{'bad_request'};
							$message = 'Invalid server ID.';
						}
						else {
							$status = HTTP_STATUS->{'not_found'};
							$message = 'No active Coincidence server with this ID.';
						}
					}
					else {
						$status = HTTP_STATUS->{'bad_request'};
						$message = 'Invalid username.';
					}
				}
			}
			
			elsif ($action == CHAT_ACTION->{'leave'}) {
				if (($chat_state < CHAT_STATE->{'ready'}) && !$password_ok) {
					$message = 'Already disconnected.';
				}
				else {
					if ($username =~ /^[A-Za-z]*$/) {
						$chat{'content'} .= 'leave@'.$username.': '.$words."\n";
						if ($username ne '') {
							write_chat($fh, \%chat);
						}
						else {
							my %new_chat;
							if ($chat_state > 1) {
								write_chat($chat_id, \%chat);
								$new_chat{'id'} = $chat_id+1;
							}
							else {
								$new_chat{'id'} = $chat_id;
							}
							$new_chat{'state'} = CHAT_STATE->{'disconnected'};
							$new_chat{'content'} = '';
							write_chat($fh, \%new_chat);
						}
					}
					else {
						$status = HTTP_STATUS->{'bad_request'};
						$message = 'Invalid username.';
					}
				}
			}
			
			elsif (
				($action == CHAT_ACTION->{'file'}) &&
				($cgi{'file'} ne '') &&
				($words ne '') && 
				$password_ok
			) {
				if ($words !~ /[\r\n]/) {
					if ($username =~ /^[A-Za-z]*$/) {
						$chat{'content'} .= 'file@'.$username.': '.$words."\n";
						if ($chat_state < CHAT_STATE->{'active'}) {
							$chat_state = CHAT_STATE->{'active'};
							$chat{'state'} = $chat_state;
						}
						write_chat($fh, \%chat);
					}
					else {
						$status = HTTP_STATUS->{'bad_request'};
						$message = 'Invalid username.';
					}
				}
				else {
					$status = HTTP_STATUS->{'bad_request'};
					$message = 'Invalid text.';
				}
			}
			
			@chat_lines = split(/\r?\n/, $chat{'content'});
		}
		else {
			$chat_state = CHAT_STATE->{'disconnected'};
			$status = HTTP_STATUS->{'internal_server_error'};
			$message = 'Can\'t lock data file!';
		}
		
		close($fh);
	}
	else {
		$chat_state = CHAT_STATE->{'disconnected'};
		$status = HTTP_STATUS->{'internal_server_error'};
		$message='Can\'t open data file!';
	}
}
# old chat archive
else {
	$chat_id = $page;
	%chat = read_chat();
	$last_id = int($chat{'id'});
	if ($chat_id < $last_id) {
		%chat = read_chat($page);
		$chat_state = int($chat{'state'});
		@chat_lines = split(/\r?\n/, $chat{'content'});
	}
}

if ($status ne '') {
	print http_header_status($status);
}
print "Content-type: text/html; charset=UTF-8\n\n";
if($method eq 'HEAD') {
	exit;
}

if ($username eq '') {
	$username = $coin{'name'};
}

my $base_url = CGI_PATH();
my $coin_url = CGI_COIN_PATH();
my $form_url = $coin_url;
my $oldest_url = merge_url(
	{'path' => $coin_url},
	{'path' => 0}
);
my $older_url = merge_url(
	{'path' => $coin_url},
	{'path' => $chat_id -1}
);
my $newer_url = ($chat_id < ($last_id -1)) ?
	merge_url(
		{'path' => $coin_url},
		{'path' => $chat_id +1}
	) : $coin_url;

if ($password_ok) {
	my $password_query = url_query_encode({'p', $settings{'password'}});
	$coin_url   = merge_url($coin_url  , {'query' => $password_query, 'append_query' => 1, 'preserve_fragment' => 1});
	$oldest_url = merge_url($oldest_url, {'query' => $password_query, 'append_query' => 1, 'preserve_fragment' => 1});
	$older_url  = merge_url($older_url , {'query' => $password_query, 'append_query' => 1, 'preserve_fragment' => 1});
	$newer_url  = merge_url($newer_url , {'query' => $password_query, 'append_query' => 1, 'preserve_fragment' => 1});
}

my $_password = $password_ok ? html_entity_encode_dec($settings{'password'}, 1): '';
my $abbr = abbr_name($username);
my $_website_name = html_entity_encode_dec(WEBSITE_NAME() , 1);
my $_server       = html_entity_encode_dec($coin    {'server'}  , 1);
my $_key          = html_entity_encode_dec($coin    {'key'}     , 1);
my $_cgi_username = html_entity_encode_dec($cgi     {'username'}, 1);
my $_username     = html_entity_encode_dec($username  , 1);
my $_abbr         = html_entity_encode_dec($abbr      , 1);
my $_message      = html_entity_encode_dec($message   , 1);
my $_base_url     = html_entity_encode_dec($base_url  , 1);
my $_coin_url     = html_entity_encode_dec($coin_url  , 1);
my $_form_url     = html_entity_encode_dec($form_url  , 1);
my $_oldest_url   = html_entity_encode_dec($oldest_url, 1);
my $_older_url    = html_entity_encode_dec($older_url , 1);
my $_newer_url    = html_entity_encode_dec($newer_url , 1);

print_html_start(\*STDOUT);
print_html_head_start(\*STDOUT);

print '  <title>Coincidence &bull; '.$_website_name.'</title>'."\n";

print_html_head_end(\*STDOUT);
print_html_body_start(\*STDOUT);

print '   <div id="inst" class="ins">'."\n";

print '    <div id="title">'."\n";
print '     <H1 id="titletext">Coincidence</H1>'."\n";
print '    </div>'."\n";

print '    <div id="storypuzzle">'."\n";
if ($page >= 0) {
	print '     Before: '.$chat_id."\n";
}
elsif ($chat_state > CHAT_STATE->{'disconnected'}) {
	print '     Connected to server <span class="br">'.$_server.'</span> as user <span class="ni">'.$_username.'</span> (<span class="ni">'.$_abbr.'</span>), public key <span class="br">'.$_key.'</span>.'."\n";
}
else{
	print '     Not connected.'."\n";
}
print '    </div>'."\n";

print '    <div id="command">'."\n";
if ($message ne '') {
	print '     <span class="br">'.$_message.'</span>'."\n";
}
if ($page < 0) {
	print '     <form method="post" action="'.$_form_url.'">'."\n";
	if ($password_ok) {
		print '      <input class="intxc" type="text" name="words">'."\n";
		print '      <input class="inbt" type="submit" value="Send">'."\n";
		print "      |\n";
		print '      <input class="intx" type="text" name="username" value="'.$_cgi_username.'">'."\n";
		print '      <input class="inbt" type="submit" name="nopost" value="Refresh">'."\n";
		print '      <input class="inbt" type="submit" name="join" value="Connect">'."\n";
		print '      <input class="inbt" type="submit" name="leave" value="Disconnect">'."\n";
		print '      <input class="inbt" type="submit" name="file" value="Send file">'."\n";
		print '      <input type="hidden" name="p" value="'.$_password.'">'."\n";
	}
	elsif ($chat_state > CHAT_STATE->{'disconnected'}) {
		print '      <input class="intxc" type="text" name="words">'."\n";
		print '      <input class="inbt" type="submit" value="Send">'."\n";
		print "      |\n";
		print '      <input class="inbt" type="submit" name="nopost" value="Refresh">'."\n";
		print '      <input class="inbt" type="submit" name="leave" value="Disconnect">'."\n";
	}
	else {
		print '      <input class="intx" type="text" name="words">'."\n";
		print '      <input class="inbt" type="submit" name="join" value="Connect">'."\n";
	}
	print '     </form>'."\n";
}
print '    </div>'."\n";

print '   </div>'."\n";
print '   <div id="insb" class="ins">'."\n";

print '    <div id="chat">'."\n";
if ($page < 0) {
	for (my $i = @chat_lines-1; $i>=0; --$i) {
		print '     '.chat_line($chat_lines[$i])."<br>\n";
	}
}
else {
	for (my $i = 0; $i<@chat_lines; ++$i) {
		print '     '.chat_line($chat_lines[$i])."<br>\n";
	}
}
print '    </div>'."\n";

print '    <div id="underlinks">'."\n";
print '     <a href="'.$_base_url.'">BSTA</a> | <a href="'.$_coin_url.'">Once again</a>';
if ($chat_id > 0) {
	print ' | <a href="'.$_older_url.'">Before</a>';
}
if ($chat_id < $last_id) {
	print ' | <a href="'.$_newer_url.'">Unbefore</a>';
}
if ($chat_id > 0) {
	print ' | <a href="'.$_oldest_url.'">Initially</a>';
}
print ' | (This interface is only a demo, a proof of concept. It is very limited. No autorefresh, no private chat, etc. For full functionality use the actual Coincidence client.)'."\n";
print '    </div>'."\n";

print '   </div>'."\n";

print_html_body_end(\*STDOUT, int($state{'state'}) == STATE->{'inactive'});
print_html_end(\*STDOUT);


sub abbr_name {
	(my $name) = @_;
	my $abbr;
	
	if($name !~ /^[A-Za-z]+$/) {
		return '?';
	}
	
	$abbr = uc(substr($name,0,1));
	$name = substr($name,1);
	while($name =~ m/([A-Z])/g) {
		$abbr = $abbr.$1;
	}
	return $abbr;
}

sub chat_line {
	(my $line) = @_;
	
	if ($line =~ /^([a-z]*@)?([A-Za-z]*): (.*)$/) {
		my $action = $1;
		my $name = $2;
		my $text = $3;
		my $color;
		if ($name eq '') {
			$name = $coin{'name'};
			$color = 'ni';
		}
		else {
			$color = 'br';
		}
		$abbr = abbr_name($name);
		
		my $_name   = html_entity_encode_dec($name          , 1);
		my $_abbr   = html_entity_encode_dec($abbr          , 1);
		my $_text   = html_entity_encode_dec($text          , 1);
		my $_server = html_entity_encode_dec($coin{'server'}, 1);
		
		if($action ne '') {
			if ($action eq 'join@') {
				return "$_name ($_abbr) joined the public chat on server $_server.";
			}
			elsif ($action eq 'leave@') {
				return "$_name ($_abbr) left the public chat on server $_server.";
			}
			elsif ($action eq 'file@') {
				return "$_name ($_abbr)  sent the file $_text.";
			}
			else {
				return 'E:E:E';
			}
		}
		else {
			return "<span class=\"$color\">$_abbr: $_text</span>";
		}
	}
	else {
		return 'E:E:E';
	}
}
