#!/usr/bin/perl

use warnings;
use strict;
use DBI;
use LWP::UserAgent;
use Image::Magick;
use GPSD::Parse;
use File::Copy qw(move);
use Algorithm::GooglePolylineEncoding;

$| = 1; # auto-flush socket
my $sessionID = shift;
my ($QUAD, $ZERO, $ZERO2, $PI3, $PI4, $PI5) = (0, 1, 2, 3, 4, 5); # deviceType
my ($NORMAL, $TEST, $SYNC) = (0, 1, 2); # mode
my ($deviceType, $mapID, $pinID, $isOnline, $mode, $rotation) = ($QUAD, 0, 0, 0, 0, 180);
my ($SUNNY, $NIGHT, $DARK) = (30000, 800, 400);
my ($checkDark, $mpv, $camName, $onward) = (12, ($DARK - 1), 'CENTER', 1);
my ($locFreq, $picFreq, $clock, $asleep, $sleepFreq, $sleepLimit) = (10, 30, 0, 0, 2, 300);
my ($path, $lat, $lon, $timestamp, $ele, $speed, $tmpt) = ('', '', '', '', '', '', '');
my ($mhdQuality, $mhdWidth, $mhdHeight, $mldQuality, $mldWidth, $mldHeight) = (0, 0, 0, 0, 0, 0);
my $url = '--URL for server--'; # removed for code sample
my %picsToSend = ();

my ($trailDir, $sessionDir) = ('/home/pi/trail', '');
my $configFile = "$trailDir/raw/camconfig.txt";
unlink $configFile;

my $gps = GPSD::Parse->new;
my $ua = LWP::UserAgent->new;
$ua->timeout(10);

my ($dsn, $dbUser, $dbPass) = ('DBI:SQLite:dbname=mapsDB', 'user', 'pw');
my $dbh = DBI->connect($dsn, $dbUser, $dbPass, { RaiseError => 1 }) 
    or die $DBI::errstr;

&loadConfiguration();
# wait for the camera cover to be removed
while (($checkDark > 0) && ($mpv < $DARK)) { &takePicture(0, 500); sleep(5); }

do
{
    print "[clock: $clock] [locFreq: $locFreq] [picFreq: $picFreq]\n";

    &pollGPS();
    while ($onward && (($lat eq '') || ($speed < 0.5)))  # we're not moving
    {
	print "We're not moving.\n";
	$asleep += 5; sleep(5);
	if ($asleep >= $sleepLimit) { $onward = 0; }
	elsif ($checkDark && ($asleep > $checkDark))
	{
	    &takePicture(0, 500);   # is the camera shuttered?
	    if ($mpv < $DARK) { print "UH-OH DARK! ...exiting.\n"; $onward = 0; }
	}
	&pollGPS(); 
    }

    if ($onward)
    {
	$timestamp = time();
	&saveSpot();

	my $picID = 0;
	if ($clock % $picFreq == 0) # round up available pictures
	{
	    if ($deviceType != $QUAD) { &writeCamConfig(); sleep(4); }
	    &takePicture(0, 2000);
	    &pollGPS();
	    if ($deviceType == $PI5) { &takePicture(1, 2000); }
	    if ($deviceType != $QUAD) { sleep(6); } # wait for pics from other cameras
	    opendir(DIR, "${trailDir}/raw");
	    my @picFiles = grep(/\.jpg$/, readdir(DIR));
	    closedir(DIR);
	    
	    foreach my $cFile (@picFiles)
	    {
		my ($cam, $ending) = split('\.', $cFile);
		my $dFile = $sessionDir . '/' . $cam . '_lr_' . $timestamp . '.jpg';
		$cFile = $trailDir . '/raw/' . $cam . '.jpg';
		move($cFile, $dFile);
		$picsToSend{$cam} = $dFile;
	    }
	    if (scalar(@picFiles) > 0) { $picID = &savePhotos(); }
	}
	
	# at the path interval, send coords and available pics
	if ($isOnline && ($clock % $locFreq == 0))
	{
	    my $markerID = &sendPackage();
	    if ($markerID < 0) { $onward = 0; }
	    elsif (($picID > 0) && ($markerID > 0))
	    { $dbh->do("UPDATE pics SET markerID = $markerID WHERE ID = $picID"); }
	    %picsToSend = ();
	}
	
	$clock += $sleepFreq; sleep($sleepFreq);
    }
} while ($onward);

$dbh->disconnect;

###################################################################################
sub pollGPS
{
    $gps->poll; # Sample the GPS
    ($lat, $lon, $ele, $speed) = ($mode == $TEST) ? (rand(2) + 43, rand(2) - 101, 10, 1.0)
	: ($gps->lat, $gps->lon, $gps->alt, $gps->speed);
    $tmpt = `cat /sys/class/thermal/thermal_zone0/temp` / 1000;  # in degrees C
    print "($lat, $lon)   [speed $speed] [temperature $tmpt]\n";
}

sub sendPackage
{
    my ($markerID, $newPinID) = (0, 0);
    my $coords = &buildCoords();
    my $fileCount = keys %picsToSend;

    print "Sending package [$lat, $lon] with $fileCount pictures... ";
    my $resp = $ua->post($url,
			 ['mapID' => $mapID, 'ts' => $timestamp, 'lat' => $lat, 'lon' => $lon,
			  (map { "file_".($_) => [ $picsToSend{$_} ] } keys %picsToSend),
			  'count' => $fileCount, 'coords' => "$coords"],
			 Content_Type => 'form-data');    
    if ($resp->is_success)
    {
	my @values = split("\n", $resp->decoded_content);
	if ($#values < 2) { $markerID = $values[0]; }
	else
	{
	    ($markerID, $newPinID, $locFreq, $picFreq) = @values;
	    if ($newPinID != $pinID) { &changeSession($newPinID); $pinID = $newPinID; }
	    &saveSession();
	}
	$dbh->do("UPDATE spots SET delivered = 1 WHERE sessionID = $sessionID");
    }
    print "done [markerID: $markerID]\n";
    return $markerID;
}

# --info-text arg (=#%frame (%fps fps) exp %exp ag %ag dg %dg)
# -awb mode can be auto, incandescent, tungsten, fluorescent, indoor, daylight, cloudy
sub takePicture
{
    my ($camNum, $expTime) = (shift, shift);
    my $lightString = ($mpv >= $SUNNY) ? '--awb daylight' : '--awb cloudy';
    my $picFile = ($expTime < 1000) ? $trailDir . '/' . 'discard.jpg'
	: $sessionDir . '/' . $camName . $camNum . '_hr_' . $timestamp . '.jpg';

    print "Taking picture [${expTime}ms] [$picFile]... ";
    my $exitCode = system("/usr/bin/libcamera-still --verbose=0 --autofocus-mode auto --quality ${mhdQuality} --width ${mhdWidth} --height ${mhdHeight} --rotation $rotation ${lightString} --nopreview -t $expTime --camera $camNum -e jpg -o $picFile");

    if ($exitCode <= 0)  # picture taken successfully
    {
	my $image = Image::Magick->new;
	$image->Read("$picFile");
	$mpv = $image->Get("%[mean]");
	undef $image;
	print "[MPV: $mpv]... done.\n";

	if (($expTime > 1000) && ($mpv > $DARK))
	{
	    my $compressedFile = $trailDir . '/raw/' . $camName . $camNum . '.jpg';
	    if ($deviceType == $QUAD) { &splitPicture($picFile); }
	    else { &shrinkPicture($picFile, $compressedFile); }
	}
    }
    else { print "PHOTO FAILED.\n"; }
}

sub shrinkPicture
{
    my ($origFile, $compressedFile) = (shift, shift);
    print "Shrinking picture [$origFile].\n";
    
    my $image = Image::Magick->new;
    $image->Read("$origFile");
    $image->Resize(geometry=>"${mldWidth}x${mldHeight}");
    $image->Write(filename=> "$compressedFile", quality=> $mldQuality);
    undef $image;
}

sub splitPicture
{
    my $origFile = shift;
    my @camNames = ('REAR', 'CENTER', 'LEFT', 'RIGHT');
    my @x = (0, ($mhdWidth / 2), 0, ($mhdWidth / 2));
    my @y = (0, 0, ($mhdHeight / 2), ($mhdHeight / 2));
    print "Splitting picture [$origFile].\n";

    for (my $i = 1; $i <= $#camNames; $i++)   # choose cameras
    {
	my $compressedFile = $trailDir . '/raw/' . $camNames[$i] . '.jpg';
	my $image = Image::Magick->new;
	$image->Read("$origFile");
	$image->Crop(width => ($mhdWidth / 2), height => ($mhdHeight / 2), x=> $x[$i], y=> $y[$i]);
	$image->Resize(geometry=>"${mldWidth}x${mldHeight}");
	$image->Write(filename=> "$compressedFile", quality=> $mldQuality);
	undef $image;
    }
}

sub buildCoords()  # create string like: 76.541,104.223,48.2|76.111,103.8387,53.9
{
    my ($coords, $count) = ('', 0);
    my $sth = $dbh->prepare("SELECT lat, lon, elevation FROM spots WHERE sessionID = $sessionID AND delivered = 0 ORDER BY timestamp");
    $sth->execute();
    while (my $ref = $sth->fetchrow_hashref())
    {
	if ($count > 0) { $coords .= '|'; }
	$coords .= $ref->{'lat'} . ',' . $ref->{'lon'} . ',' . $ref->{'elevation'};
	$count++;
    }
    $sth->finish();

    print "Built coords [session $sessionID] with $count points.\n";
    return $coords;
}

sub saveSpot
{
    $dbh->do('INSERT INTO spots (sessionID, lat, lon, elevation, speed, temperature) VALUES (?, ?, ?, ?, ?, ?)', undef, $sessionID, $lat, $lon, $ele, $speed, $tmpt);
}

sub savePhotos
{
    $dbh->do('INSERT INTO pics (sessionID, timestamp, lat, lon, mpv) VALUES (?, ?, ?, ?, ?)', undef, $sessionID, $timestamp, $lat, $lon, $mpv);
    return $dbh->selectrow_array("SELECT last_insert_rowid()");
}

sub changeSession
{
    my $newPinID = shift;
    $dbh->do("UPDATE sessions SET pinID = $newPinID WHERE ID = $sessionID");
}

sub loadConfiguration
{
    my $sth = $dbh->prepare("SELECT * FROM device WHERE ID = 1");
    $sth->execute();
    if (my $ref = $sth->fetchrow_hashref())
    {  
	$deviceType = $ref->{'deviceType'}; $isOnline = $ref->{'isOnline'};
	$mode = $ref->{'isTest'}; $checkDark = $ref->{'checkDark'};
	$locFreq = $ref->{'locFreq'}; $picFreq = $ref->{'picFreq'};
	$mhdQuality = $ref->{'mhdQuality'}; $mhdWidth = $ref->{'mhdWidth'};
	$mhdHeight = $ref->{'mhdHeight'}; $mldQuality = $ref->{'mldQuality'};
	$mldWidth = $ref->{'mldWidth'}; $mldHeight = $ref->{'mldHeight'};
    }
    $sth->finish();
    
    $sth = $dbh->prepare("SELECT * FROM sessions WHERE ID = $sessionID");
    $sth->execute();
    if (my $ref = $sth->fetchrow_hashref())
    {  
	$mapID = $ref->{'mapID'}; $pinID = $ref->{'pinID'};
	$locFreq = $ref->{'locFreq'}; $picFreq = $ref->{'picFreq'};
    }
    $sth->finish();
    
    $sessionDir = '/home/pi/trail/raw/' . $sessionID;
    mkdir($sessionDir);
}

sub saveSession
{
    $dbh->do("UPDATE sessions SET pinID = $pinID, locFreq = $locFreq, picFreq = $picFreq WHERE ID = $sessionID");
}

sub writeCamConfig
{
    print "Writing camconfig.txt (ts $timestamp]...\n";
    open(my $fh, '>', $configFile) or do { print "CamConfig write FAILED\n"; return; };
    $fh->autoflush;
    print $fh "$sessionID\n";
    print $fh "$timestamp\n";
    close $fh;    
}