<%doc>

 This is part of the YARRG website.  YARRG is a tool and website
 for assisting players of Yohoho Puzzle Pirates.

 Copyright (C) 2009 Ian Jackson <ijackson@chiark.greenend.org.uk>
 Copyright (C) 2009 Clare Boothby

  YARRG's client code etc. is covered by the ordinary GNU GPL (v3 or later).
  The YARRG website is covered by the GNU Affero GPL v3 or later, which
   basically means that every installation of the website will let you
   download the source.

 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/>.

 Yohoho and Puzzle Pirates are probably trademarks of Three Rings and
 are used without permission.  This program is not endorsed or
 sponsored by Three Rings.


 This Mason component is the core trade planner for a specific route.


</%doc>
<%args>
$dbh
@islandids
@archipelagoes
$qa
$routeparams
$reset_suppressions
$quri
</%args>
<& query_age:pageload &>

<%perl>

my $loss_per_league= defined $routeparams->{LossPerLeaguePct}
	? $routeparams->{LossPerLeaguePct}*0.01 : 1e-7;
my $loss_per_delay_slot= 1e-8;
my $max_gems= 24;

my $minprofit= $routeparams->{MinProfit} || 0;

my $now= time;

my @flow_conds;
my @query_params;
my %dists;
my $expected_total_profit;

my $sd_condition= sub {
	my ($bs, $ix) = @_;
	my $islandid= $islandids[$ix];
	if (defined $islandid) {
		return "${bs}.islandid = $islandid";
	} else {
		push @query_params, $archipelagoes[$ix];
		return "${bs}_islands.archipelago = ?";
	}
};

my $specific= !grep { !defined $_ } @islandids;

my %ipair2subflowinfs;
# $ipair2subflowinfs{$orgi,$dsti}= [ [$orgix,$distix], ... ]

my @subflows;
# $subflows[0]{Flow} = { ... }
# $subflows[0]{Org} = $orgix
# $subflows[0]{Dst} = $dstix

foreach my $org_i (0..$#islandids) {
	my $org_isle= $islandids[$org_i];
	my $org_cond= $sd_condition->('sell',$org_i);
	my @dst_conds;
	foreach my $dst_i ($org_i..$#islandids) {
		my $dst_isle= $islandids[$dst_i];
		# Don't ever consider sailing things round the houses:
		next if defined $dst_isle and
			grep { $dst_isle == $_ } @islandids[$org_i..$dst_i-1];
		next if defined $org_isle and
			grep { $org_isle == $_ } @islandids[$org_i+1..$dst_i];
		my $dst_cond;
		if ($dst_i==$org_i and !defined $org_isle) {
			# we always want arbitrage, but mentioning an arch
			# once shouldn't produce intra-arch trades
			$dst_cond= "sell.islandid = buy.islandid";
		} else {
			$dst_cond= $sd_condition->('buy',$dst_i);
		}
		push @dst_conds, $dst_cond;

		if ($specific) {
			push @{ $ipair2subflowinfs{$org_isle,$dst_isle} },
				[ $org_i, $dst_i ];
		}
	}
	push @flow_conds, "$org_cond AND (
			".join("
		     OR ",@dst_conds)."
		)";
}

my $stmt= "		
	SELECT  sell_islands.islandname				org_name,
		sell_islands.islandid				org_id,
		sell.price					org_price,
		sell.qty 					org_qty_stall,
		sell_stalls.stallname				org_stallname,
		sell.stallid					org_stallid,
		sell_uploads.timestamp				org_timestamp,
		buy_islands.islandname				dst_name,
		buy_islands.islandid				dst_id,
		buy.price					dst_price,
		buy.qty 					dst_qty_stall,
		buy_stalls.stallname				dst_stallname,
		buy.stallid					dst_stallid,
		buy_uploads.timestamp				dst_timestamp,
".($qa->{ShowStalls} ? "
		sell.qty 					org_qty_agg,
		buy.qty 					dst_qty_agg,
" : "
		(SELECT sum(qty) FROM sell AS sell_agg
		  WHERE	sell_agg.commodid = commods.commodid
		  AND	sell_agg.islandid = sell.islandid
		  AND	sell_agg.price = sell.price)		org_qty_agg,
		(SELECT sum(qty) FROM buy AS buy_agg
		  WHERE	buy_agg.commodid = commods.commodid
		  AND	buy_agg.islandid = buy.islandid
		  AND	buy_agg.price = buy.price)		dst_qty_agg,
")."
		commods.commodname				commodname,
		commods.commodid				commodid,
		commods.unitmass				unitmass,
		commods.unitvolume				unitvolume,
		commods.ordval					ordval,
		commods.posinclass				posinclass,
		commods.commodclassid				commodclassid,
		commods.flags					flags,
		dist						dist,
		buy.price - sell.price				unitprofit
	FROM commods
	JOIN sell ON commods.commodid = sell.commodid
	JOIN buy  ON commods.commodid = buy.commodid
	JOIN islands AS sell_islands ON sell.islandid = sell_islands.islandid
	JOIN islands AS buy_islands  ON buy.islandid  = buy_islands.islandid
	JOIN uploads AS sell_uploads ON sell.islandid = sell_uploads.islandid
	JOIN uploads AS buy_uploads  ON buy.islandid  = buy_uploads.islandid
	JOIN stalls  AS sell_stalls  ON sell.stallid  = sell_stalls.stallid
	JOIN stalls  AS buy_stalls   ON buy.stallid   = buy_stalls.stallid
	JOIN dists ON aiid = sell.islandid AND biid = buy.islandid
	WHERE	(
		".join("
	   OR	", @flow_conds)."
	)
	  AND	buy.price > sell.price
	ORDER BY org_name, dst_name, commodname, unitprofit DESC,
		 org_price, dst_price DESC,
		 org_stallname, dst_stallname
     ";

my $sth= $dbh->prepare($stmt);
$sth->execute(@query_params);
my @flows;

my $distquery= $dbh->prepare("
		SELECT dist FROM dists WHERE aiid = ? AND biid = ?
		");
my $distance= sub {
	my ($from,$to)= @_;
	my $d= $dists{$from}{$to};
	return $d if defined $d;
	$distquery->execute($from,$to);
	$d = $distquery->fetchrow_array();
	defined $d or die "$from $to ?";
	$dists{$from}{$to}= $d;
	return $d;
};

my @cols= ({ NoSort => 1 });

my $addcols= sub {
	my $base= shift @_;
	foreach my $name (@_) {
		my $col= { Name => $name, %$base };
		$col->{Numeric}=1 if !$col->{Text};
		push @cols, $col;
	}
};

if ($qa->{ShowStalls}) {
	$addcols->({ Text => 1 }, qw(
		org_name org_stallname
		dst_name dst_stallname
	));
} else {
	$addcols->({Text => 1 }, qw(
		org_name dst_name
	));
}
$addcols->({ Text => 1 }, qw(commodname));
$addcols->({ DoReverse => 1 },
	qw(	org_price org_qty_agg dst_price dst_qty_agg
	));
$addcols->({ DoReverse => 1, SortColKey => 'MarginSortKey' },
	qw(	Margin
	));
$addcols->({ DoReverse => 1 },
	qw(	unitprofit MaxQty MaxCapital MaxProfit dist
	));
foreach my $v (qw(MaxMass MaxVolume)) {
   $addcols->({
	DoReverse => 1, Total => 0, SortColKey => "${v}SortKey" }, $v);
}

</%perl>

% if ($qa->{'debug'}) {
<pre>
<% $stmt |h %>
<% join(' | ',@query_params) |h %>
</pre>
% }

<& dumptable:start, qa => $qa, sth => $sth &>
% {
%   my $got;
%   while ($got= $sth->fetchrow_hashref()) {
<%perl>

	my $f= $flows[$#flows];
	if (	!$f ||
		$qa->{ShowStalls} ||
		grep { $f->{$_} ne $got->{$_} }
			qw(org_id org_price dst_id dst_price commodid)
	) {
		# Make a new flow rather than adding to the existing one

		$f= {
			Ix => scalar(@flows),
			%$got
		};
		$f->{"org_stallid"}= $f->{"dst_stallid"}= 'all'
			if !$qa->{ShowStalls};
		push @flows, $f;
	}
	foreach my $od (qw(org dst)) {
		$f->{"${od}Stalls"}{
			$got->{"${od}_stallname"}
		    } =
			$got->{"${od}_qty_stall"}
		    ;
	}

</%perl>
<& dumptable:row, qa => $qa, sth => $sth, row => $f &>
%    }
<& dumptable:end, qa => $qa &>
% }

% if (@islandids==1) {
% 	if (defined $islandids[0]) {
Searched for arbitrage trades only.
%	} else {
Searched for arbitrage trades only, in <% $archipelagoes[0] |h %>
<a href="docs#arbitrage">[?]</a>.
%	}
% }

<%perl>

if (!@flows) {
	print 'No profitable trading opportunities were found.';
	return;
}

my @sail_total;
my %opportunity_value;

my $oppo_key= sub {
	my ($f) = @_;
	return join '_', map { $f->{$_} } qw(org_id dst_id commodid);
};

my $any_previous_suppression= 0;

foreach my $f (@flows) {

	$f->{MaxQty}= $f->{'org_qty_agg'} < $f->{'dst_qty_agg'}
		? $f->{'org_qty_agg'} : $f->{'dst_qty_agg'};
	$f->{MaxProfit}= $f->{MaxQty} * $f->{'unitprofit'};
	$f->{MaxCapital}= $f->{MaxQty} * $f->{'org_price'};

	$f->{MaxMassSortKey}= $f->{MaxQty} * $f->{'unitmass'};
	$f->{MaxVolumeSortKey}= $f->{MaxQty} * $f->{'unitvolume'};
	foreach my $v (qw(Mass Volume)) {
		$f->{"Max$v"}= sprintf "%.1f", $f->{"Max${v}SortKey"} * 1e-6;
	}

	$f->{MarginSortKey}= sprintf "%d",
		$f->{'dst_price'} * 10000 / $f->{'org_price'};
	$f->{Margin}= sprintf "%3.1f%%",
		$f->{'dst_price'} * 100.0 / $f->{'org_price'} - 100.0;

	$f->{ExpectedUnitProfit}=
		$f->{'dst_price'} * (1.0 - $loss_per_league) ** $f->{'dist'}
		- $f->{'org_price'};

	$dists{'org_id'}{'dst_id'}= $f->{'dist'};

	$opportunity_value{ $oppo_key->($f) } += $f->{MaxProfit};

	my @uid= $f->{commodid};
	foreach my $od (qw(org dst)) {
		push @uid,
			$f->{"${od}_id"},
			$f->{"${od}_price"};
		push @uid,
			$f->{"${od}_stallid"}
				if $qa->{ShowStalls};
	}
	$f->{UidLong}= join '_', @uid;

	my $base= 31;
	my $cmpu= '';
	map {
		my $uue= $_;
		my $first= $base;
		do {
			my $this= $uue % $base;
#print STDERR "uue=$uue this=$this ";
			$uue -= $this;
			$uue /= $base;
			$this += $first;
			$first= 0;
			$cmpu .= chr($this + ($this < 26 ? ord('a') :
					      $this < 52 ? ord('A')-26
							 : ord('0')-52));
#print STDERR " uue=$uue this=$this cmpu=$cmpu\n";
			die "$cmpu $uue ?" if length $cmpu > 20;
		} while ($uue);
		$cmpu;
	} @uid;
	$f->{UidShort}= $cmpu;

	if ($qa->{'debug'}) {
		my @outuid;
		$_= $f->{UidShort};
		my $mul;
		while (m/./) {
			my $v= m/^[a-z]/ ? ord($&)-ord('a') :
			       m/^[A-Z]/ ? ord($&)-ord('A')+26 :
			       m/^[0-9]/ ? ord($&)-ord('0')+52 :
			       die "$_ ?";
			if ($v >= $base) {
				push @outuid, 0;
				$v -= $base;
				$mul= 1;
#print STDERR "(next)\n";
			}
			die "$f->{UidShort} $_ ?" unless defined $mul;
			$outuid[$#outuid] += $v * $mul;

#print STDERR "$f->{UidShort}  $_  $&  v=$v  mul=$mul  ord()=".ord($&).
#			"[vs.".ord('a').",".ord('A').",".ord('0')."]".
#			"  outuid=@outuid\n";

			$mul *= $base;
			s/^.//;
		}
		my $recons_long= join '_', @outuid;
		$f->{UidLong} eq $recons_long or
			die "$f->{UidLong} = $f->{UidShort} = $recons_long ?";
	}
}

foreach my $f (@flows) {

	if ($reset_suppressions || !defined $qa->{"R$f->{UidShort}"}) {
		if ($opportunity_value{ $oppo_key->($f) } < $minprofit) {
			$f->{Suppress}= 1;
		}
	} else {
		if (!defined $qa->{"T$f->{UidShort}"}) {
			$any_previous_suppression= 1;
			$f->{Suppress}= 1;
		}
	}
	if (!$f->{Suppress}) {
		my $sfis= $ipair2subflowinfs{$f->{'org_id'},$f->{'dst_id'}};
		foreach my $sfi (@$sfis) {
			my $subflow= {
				Flow => $f,
				Org => $sfi->[0],
				Dst => $sfi->[1],
				Var => sprintf "f%ss%s_c%d_p%d_%d_p%d_%d",
					$f->{Ix}, $sfi->[0],
					$f->{'commodid'},
					$sfi->[0], $f->{'org_price'},
					$sfi->[1], $f->{'dst_price'}
			};
			push @{ $f->{Subflows} }, $subflow;
			push @subflows, $subflow;
		}
	}
}
</%perl>

% my $optimise= 1;
% my $opt_how;

% if (!$specific) {
%	$optimise= 0;
Route contains archipelago(es), not just specific islands.
% } elsif (!@subflows) {
%	$optimise= 0;
%	if ($any_previous_suppression) {
All available trades deselected.
%	} else {
No available trades meet the specified minimum trade value, so
all available trades deselected.
%	}
% }

% if (!$optimise) {

<p>
Therefore, optimal voyage trade plan not calculated.

% } else { # ========== OPTMISATION ==========
<%perl>

my $cplex= "
Maximize

  totalprofit:
";

my %stall_poe_limits;

foreach my $sf (@subflows) {
	my $eup= $sf->{Flow}{ExpectedUnitProfit};
	$eup *= (1.0-$loss_per_delay_slot) ** $sf->{Org};
	$cplex .= sprintf "
		%+.20f %s", $eup, $sf->{Var};
	if ($qa->{ShowStalls}>=2) {
		my $stall= $sf->{Flow}{'dst_stallid'};
		push @{ $stall_poe_limits{$stall} }, $sf;
	}
}
$cplex .= "

Subject To
";

my %avail_lims;
foreach my $flow (@flows) {
	next if $flow->{Suppress};
	foreach my $od (qw(org dst)) {
		my $limname= join '_', (
			$od,
			'i'.$flow->{"${od}_id"},
			'c'.$flow->{'commodid'},
			$flow->{"${od}_price"},
			$flow->{"${od}_stallid"},
		);

		push @{ $avail_lims{$limname}{SubflowVars} },
			map { $_->{Var} } @{ $flow->{Subflows} };
		$avail_lims{$limname}{Qty}= $flow->{"${od}_qty_agg"};
	}
}
foreach my $limname (sort keys %avail_lims) {
	my $c= $avail_lims{$limname};
	$cplex .=
		sprintf("    %-30s","$limname:")." ".
			join("+", @{ $c->{SubflowVars} }).
			" <= ".$c->{Qty}."\n";
}

foreach my $ci (0..($#islandids-1)) {
	my @rel_subflows;

	foreach my $f (@flows) {
		next if $f->{Suppress};
		my @relsubflow= grep {
			$_->{Org} <= $ci &&
			$_->{Dst} > $ci;
		} @{ $f->{Subflows} };
		next unless @relsubflow;
		die unless @relsubflow == 1;
		push @rel_subflows, @relsubflow;
#print " RELEVANT $ci $relsubflow[0]->{Var} ";
	}
#print " RELEVANT $ci COUNT ".scalar(@rel_subflows)."  ";
	if (!@rel_subflows) {
		foreach my $mv (qw(mass volume capital)) {
			$sail_total[$ci]{$mv}= 0;
		}
		next;
	}

	my $applylimit= sub {
		my ($mv, $f2val) = @_;
		my $max= $routeparams->{"Max".ucfirst $mv};
		$max= 1e9 unless defined $max;
#print " DEFINED MAX $mv $max ";
		$cplex .= "
   ". sprintf("%-10s","${mv}_$ci:")." ".
		join(" + ", map {
#print " PART MAX $_->{Var} $_->{Flow}{Ix} ";
			$f2val->($_->{Flow}) .' '. $_->{Var};
		} @rel_subflows).
		" <= $max";
	};

	$applylimit->('mass',    sub { $_[0]{'unitmass'}  *1e-3 });
	$applylimit->('volume',  sub { $_[0]{'unitvolume'}*1e-3 });
	$applylimit->('capital', sub { $_[0]{'org_price'}       });

	my @gem_subflows= grep { $_->{Flow}{flags} =~ m/g/ } @rel_subflows;
	if (@gem_subflows) {
		$cplex .= "
   ". sprintf("%-10s","gems_$ci:")." ".
		join(" + ", map { $_->{Var} } @gem_subflows). " <= $max_gems";
	}

	$cplex.= "\n";
}

if ($qa->{ShowStalls}>=2) {
	my $stallpoe= $dbh->prepare(<<END);
SELECT max(qty*price) FROM buy WHERE stallid=?
END
	foreach my $stallid (sort { $a <=> $b } keys %stall_poe_limits) {
		$stallpoe->execute($stallid);
		my ($lim)= $stallpoe->fetchrow_array();
		$stallpoe->finish();
		$cplex.= "
    ". sprintf("%-15s","poe_$stallid:")." ".
		join(" + ", map {
			sprintf "%d %s", $_->{Flow}{'dst_price'}, $_->{Var};
		} @{ $stall_poe_limits{$stallid} }).
		" <= $lim";
	}
	$cplex.= "\n";
}

$cplex.= "
Bounds
        ".(join "
        ", map { "$_->{Var} >= 0" } @subflows)."

";

$cplex.= "
Integer
	".(join "
	", map { $_->{Var} } @subflows)."

End
";

if ($qa->{'debug'}) {
</%perl>
<pre>
<% $cplex |h %>
</pre>
<%perl>
}

my $try_solve= sub {
	my ($how, @opts) = @_;
	my $input= pipethrough_prep();
	print $input $cplex or die $!;
	my $output= pipethrough_run_along($input, undef, 'glpsol',
		qw(glpsol --tmlim 5 --memlim 20), @opts,
		qw( --cpxlp /dev/stdin -o /dev/stdout));
	if ($qa->{'debug'}) {
		print "<h3>@opts</h3>\n<pre>\n";
	}
	$expected_total_profit= undef;
	$_->{OptQty}= undef foreach @subflows;
	my $found_section= 0;
	my $glpsol_out= '';
	my $continuation='';
	my $timelimit= 0;
	my $somemip= 0;
	while (<$output>) {
		$glpsol_out.= $_;
		print encode_entities($_) if $qa->{'debug'};
		if (m/^\s*No\.\s+(Row|Column) name\s+(?:St\s+)?Activity\s/) {
			die "$_ $found_section ?" if $found_section>0;
			$found_section= 1;
			next;
		}
		if ((m/^Integer optimization begins/ .. 0) &&
		    m/^\+ \s* \d+\: \s* mip \s* = \s* \d/) {
			$somemip= 1;
			next;
		}
		if (m/^TIME LIMIT EXCEEDED/) {
			$timelimit= 1;
		}
		if (m/^Objective:\s+totalprofit = (\d+(?:\.\d*)?) /) {
			$expected_total_profit= $1;
		}
		next unless $found_section==1;
		if (!length $continuation) {
			next if !$continuation &&  m/^[- ]+$/;
			if (!/\S/) {
				$found_section= 0;
				next;
			}
			if (m/^ \s* \d+ \s+ \w+ $/x) {
				$continuation= $&;
				next;
			}
		}
		$_= $continuation.$_;
		$continuation= '';
		my ($varname, $qty) = m/^
			\s* \d+ \s+
			(\w+) \s+ (?: [A-Z*]+ \s+ )?
			([-+0-9]+)(?: [.e][-+e0-9.]* )? \s
			/x or die "$cplex \n==\n $glpsol_out $_ ?";
		if ($varname =~ m/^f(\d+)s(\d+)_/) {
			my ($ix,$orgix) = ($1,$2);
			my $flow= $flows[$ix] or die;
			my @relsubflow= grep { $_->{Org} == $orgix }
				@{ $flow->{Subflows} };
			die "$ix $orgix @relsubflow" unless @relsubflow == 1;
			my $sf= $relsubflow[0];
			$sf->{OptQty}= $qty;
			$sf->{OptProfit}= $qty * $flow->{'unitprofit'};
			$sf->{OptCapital}= $qty * $flow->{'org_price'};
		} elsif ($varname =~ m/^(mass|volume|capital)_(\d+)$/) {
			my ($mv,$ix) = ($1,$2);
			$sail_total[$ix]{$mv}= $qty;
		}
	}
	print "</pre>\n" if $qa->{'debug'};
	my $prerr= "\n=====\n$cplex\n=====\n$glpsol_out\n=====\n ";
	pipethrough_run_finish($output,$prerr);
	map { defined $_->{OptQty} or die "$prerr $_->{Flow}{Ix}" } @subflows;
	defined $expected_total_profit or die "$prerr ?";
	return 0 unless $somemip || !$timelimit;
	$opt_how= $how;
	return 1;
};

unless ($try_solve->('Optimisation successful',
		     qw( --intopt --cuts --bfs )) or
	$try_solve->('<strong>Complex problem, downgraded</strong>'.
		     ' to rounded-down LP.',
		     qw( --nomip ))) {
</%perl>
<h2>Optimisation failed</h2>
The linear/mixed-integer optimisation failed.
Please report this problem.

<pre>
<% $cplex |h %>
</pre>
<%perl>
	return;
}

$addcols->({ DoReverse => 1, TotalSubflows => 1, Special => sub {
	my ($flow,$col,$v,$spec) = @_;
	if ($flow->{ExpectedUnitProfit} < 0) {
		$spec->{Span}= 3;
		$spec->{String}= '(Small margin)';
		$spec->{Align}= 'align=center';
	}
} }, qw(
		OptQty
	));
$addcols->({ Total => 0, DoReverse => 1, TotalSubflows => 1 }, qw(
		OptCapital OptProfit
	));

</%perl>

% } # ========== OPTIMISATION ==========

% if (!printable($m)) {
<h2>Contents</h2>
<ul>
% if ($optimise) {
 <li><a href="#summary">Summary</a>
 <li><a href="#plan">Voyage trading plan</a>
  <ul>
   <li>Printable:
         <input type=submit name=printable_pdf value="PDF">
         <input type=submit name=printable_html value="HTML">
         <input type=submit name=printable_ps value="PostScript">
         <input type=submit name=printable_pdf2 value="PDF 2-up">
         <input type=submit name=printable_ps2 value="PostScript 2-up">
  </ul>
% }
 <li><a href="#dataage">Relevant data ages</a>
 <li><a href="#trades">Relevant trades</a>
</ul>
% } else {
%	my @tl= gmtime $now or die $!;
<p>
Generated by YARRG at <strong><%
	sprintf "%04d-%02d-%02d %02d:%02d:%02d UTC",
		$tl[5]+1900, @tl[4,3,2,1,0]
			|h %></strong>.
% }

% if ($optimise) { # ========== TRADING PLAN ==========
<%perl>
my $iquery= $dbh->prepare('SELECT islandname FROM islands
				WHERE islandid = ?');
my %da_ages;
my $total_total= 0;
my $total_dist= 0;
my @oldest= (-1, 'nowhere');
my $plan_html= '';

my $plan_table_info= printable($m) ? 'width=100%' : '';
$plan_html .= <<END;
<table class="data" rules=groups $plan_table_info >
END

my $tbody= sub {
	if (!printable($m)) { return '<tbody>'; }
	my ($c)= qw(40 00)[$_[0]];
	return "<tr><td bgcolor=\"#${c}${c}${c}\" height=1 colspan=7>";
};

foreach my $i (0..$#islandids) {
     $plan_html .= $tbody->(1);
     $plan_html .= "<tr>\n";
     $iquery->execute($islandids[$i]);
     my ($islandnamepr)= encode_entities( $iquery->fetchrow_array() );
	
     if (!$i) {
		$plan_html .= <<END;
<td colspan=2>
<strong>Start at $islandnamepr</strong>
<td colspan=2><a href="docs#posinclass">[what are these codes?]</a>
<td>
END
     } else {
		my $this_dist= $distance->($islandids[$i-1],$islandids[$i]);
		$total_dist += $this_dist;
		$plan_html .= <<END;
<td colspan=5>
END
		my $total_value= 0;
		foreach my $sf (@subflows) {
			next unless $sf->{Org} < $i && $sf->{Dst} >= $i;
			$total_value +=
				$sf->{OptQty} * $sf->{Flow}{'dst_price'};
		}
		$plan_html .= <<END;
<strong>Sail to $islandnamepr</strong>
- $this_dist leagues, $total_value poe at risk
 </td>
END
     }
     my $age_reported= 0;
     my %flowlists;
     #print "<tr><td colspan=7>" if $qa->{'debug'};
     foreach my $od (qw(org dst)) {
	#print " [[ i $i od $od " if $qa->{'debug'};
	foreach my $sf (@subflows) {
		my $f= $sf->{Flow};
		next unless $sf->{ucfirst $od} == $i;
		#print " FLOW $f->{Ix} SUB #$sf->{Org}..$sf->{Dst} "
		#	if $qa->{'debug'};
		next unless $sf->{OptQty};
		my $arbitrage= $f->{'org_id'} == $f->{'dst_id'};
		die if $arbitrage and $sf->{Org} != $sf->{Dst};
		my $price= $f->{"${od}_price"};
		my $stallname= $f->{"${od}_stallname"};
		my $todo= \$flowlists{$od}{
				(sprintf "%010d", $f->{'ordval'}),
				$f->{'commodname'},
				(sprintf "%07d", ($od eq 'dst' ?
						9999999-$price : $price)),
				$stallname
			};
		$$todo= {
			Qty => 0,
			orgArbitrage => 0,
			dstArbitrage => 0,
		} unless $$todo;
		$$todo->{'commodid'}= $f->{'commodid'};
		$$todo->{'commodname'}= $f->{'commodname'};
		$$todo->{'posinclass'}= '';
		my $incl= $f->{'posinclass'};

		my $findclass= $dbh->prepare(<<END);
SELECT commodclass, maxposinclass FROM commodclasses WHERE commodclassid = ?
END
		$findclass->execute($f->{'commodclassid'});
		my $classinfo= $findclass->fetchrow_hashref();
		if ($classinfo) {
			my $clname= $classinfo->{'commodclass'};
			my $desc= encode_entities(sprintf "%s is under %s",
					$f->{'commodname'}, $clname);
			my $abbrev= substr($clname,0,1);
			if ($incl) {
				my $maxpic= $classinfo->{'maxposinclass'};
				$desc.= (sprintf ", commodity %d of %d",
					$incl, $maxpic);
				if ($classinfo->{'maxposinclass'} >= 8) {
					my @tmbs= qw(0 1 2 3 4 5 6 7 8 9);
					my $tmbi= ($incl+0.5)*$#tmbs/$maxpic;
					$abbrev.= " ".$tmbs[$tmbi]."&nbsp;";
				}
			}
			$$todo->{'posinclass'}=
				"<div class=mouseover title=\"$desc\">"
				.$abbrev."</div>";
		}
		$$todo->{'stallname'}= $stallname;
		$$todo->{Price}= $price;
		$$todo->{Timestamp}= $f->{"${od}_timestamp"};
		$$todo->{Qty} += $sf->{OptQty};
		$$todo->{Total}= $$todo->{Price} * $$todo->{Qty};
		$$todo->{Stalls}= $f->{"${od}Stalls"};
		$$todo->{"${od}Arbitrage"}= 1 if $arbitrage;
	}
	#print "]] " if $qa->{'debug'};
     }
     #print "</tr>" if $qa->{'debug'};

     my ($total, $total_to_show);
     my $dline= 0;
     my $show_total= sub {
	my ($totaldesc, $sign) = @_;
	if (defined $total) {
		die if defined $total_to_show;
		$total_total += $sign * $total;
		$total_to_show= [ $totaldesc, $total ];
		$total= undef;
	}
	$dline= 0;
     };
     my $show_total_now= sub {
	my ($xinfo) = @_;
	return unless defined $total_to_show;
	my ($totaldesc,$totalwas) = @$total_to_show;
	$plan_html .= <<END;
<tr>
<td colspan=1>
<td colspan=3>$xinfo
<td colspan=2 align=right>$totaldesc
<td align=right>$totalwas total
END
	$total_to_show= undef;
     };
     my $show_flows= sub {
	my ($od,$arbitrage,$collectdeliver) = @_;
	my $todo= $flowlists{$od};
	return unless $todo;
	foreach my $tkey (sort keys %$todo) {
		my $t= $todo->{$tkey};
		next if $t->{"${od}Arbitrage"} != $arbitrage;
		$show_total_now->('');
		if (!$age_reported++) {
			my $age= $now - $t->{Timestamp};
			@oldest= ($age,$islandnamepr) if $oldest[0] < $age;
			my $cellid= "da_${i}";
			my $agepr= prettyprint_age($age);
			$da_ages{$cellid}= $age;
			$plan_html .= <<END
<td colspan=2>(Data age: <span id="$cellid">$agepr</span>)
END
		} elsif (!defined $total) {
			$total= 0;
			$plan_html .= $tbody->(0);
		}
		$total += $t->{Total};
		my $span= 0 + keys %{ $t->{Stalls} };
		my $td= "td rowspan=$span";
		my %linkqf= (%{ $qa->{'baseqf'} }, %{ $qa->{'queryqf'} });
		$linkqf{'query'}= 'commod';
		$linkqf{'commodstring'}= $t->{'commodname'};
		$linkqf{'commodid'}= $t->{'commodid'};
		my $linkqfpr= encode_entities( $quri->(%linkqf) );
		my $commodnamepr= encode_entities($t->{'commodname'});
		$plan_html .= tr_datarow_s($m,$dline) . <<END;
<$td>$collectdeliver
<$td><a href="$linkqfpr">$commodnamepr</a>
<$td>$t->{'posinclass'}
END
		my @stalls= sort keys %{ $t->{Stalls} };
		my $pstall= sub {
			my $namepr= encode_entities( $stalls[$_[0]] );
			$plan_html .= <<END;
<td>$namepr
END
		};

		$pstall->(0);
		$plan_html .= <<END;
<$td align=right>$t->{Price} poe ea.
<$td align=right>$t->{Qty} unit(s)
<$td align=right>$t->{Total} total
END
		foreach my $stallix (1..$#stalls) {
			$plan_html .= tr_datarow_s($m,$dline);
			$pstall->($stallix);
		}

		$dline ^= 1;
	}
     };

     $show_flows->('dst',0,'Deliver'); $show_total->('Proceeds',1);
     $show_flows->('org',1,'Collect'); $show_total->('(Arbitrage) outlay',-1);
     $show_flows->('dst',1,'Deliver'); $show_total->('(Arbitrage) proceeds',1);
     $show_flows->('org',0,'Collect'); $show_total->('Outlay',-1);
     my $totals= '';
     if ($i < $#islandids) {
	$totals .= 	"In hold $sail_total[$i]{mass}kg,".
			" $sail_total[$i]{volume} l";
	my $delim= '; spare ';
	my $domv= sub {
		my ($max, $got, $units) = @_;
		return unless defined $max;
		$totals .= $delim;
		$totals .= sprintf "%g %s", ($max-$got), $units;
		$delim= ', ';
	};
	$domv->($routeparams->{MaxMass},   $sail_total[$i]{mass},   'kg');
	$domv->($routeparams->{MaxVolume}, $sail_total[$i]{volume}, 'l');
	$totals .= ".\n";
     }
     $show_total_now->($totals);
}

my $cashflowpr= $total_total < 0
		? -$total_total." loss"
		: $total_total." gain";

my $max_capital= 0;
foreach my $cap (map { $_->{capital} } @sail_total) {
	$max_capital= $cap if $cap > $max_capital;
}

$da_ages{'oldest'}= $oldest[0];

$plan_html .= $tbody->(1) . <<END;
<tr>
<td colspan=3>Total distance: $total_dist leagues.
<td colspan=3 align=right>Overall net cash flow
<td align=right><strong>$cashflowpr</strong>
</table>
END

</%perl>
% if (!printable($m)) {
<h2><a name="summary">Summary</a></h2>
% }

<table>
<tr>
 <td>Distance:
 <td><strong><% $total_dist %></strong> leagues,
     <strong><% scalar(@islandids) %></strong> island(s)
<tr>
 <td>Planned net cash flow:
 <td><strong><% $cashflowpr %></strong>
<tr>
 <td>Expected profit on average: approx.
 <td>
  <strong><% sprintf "%d", $expected_total_profit %></strong> poe
  (considering expected losses, but ignoring rum consumed)
<tr>
 <td>Capital required:
 <td>
  <strong><% $max_capital %></strong> poe or less
<tr>
 <td>Oldest market data used:
 <td><strong id="oldest"><% prettyprint_age($oldest[0]) %></strong>
     (<% $oldest[1] %>)
<tr>
 <td colspan=2><% $opt_how %>
</table>
<p>

<h2><a name="plan">Voyage trading plan</a></h2>
<% $plan_html %>
<& query_age:dataages, id2age => \%da_ages &>
%
% } # ========== TRADING PLAN ==========

% if (!printable($m)) {
<h2><a name="dataage">Relevant data ages</a></h2>
<%perl>
	my $sth_i= $dbh->prepare(<<END);
		SELECT archipelago, islandid, islandname, timestamp
			FROM uploads JOIN islands USING (islandid)
			WHERE islandid = ?
END
	my $sth_a= $dbh->prepare(<<END);
		SELECT archipelago, islandid, islandname, timestamp
			FROM uploads JOIN islands USING (islandid)
			WHERE archipelago = ?
			ORDER BY islandname
END
	my $ix=$#islandids;
	my $sth_current;
	my %idone;
	my $fetchrow= sub {
		for (;;) {
			if ($sth_current) {
				my $row= $sth_current->fetchrow_hashref();
				if ($row) {
					next if $idone{$row->{'islandid'}}++;
					return $row;
				}
			}
			return undef if $ix < 0;
			my $iid= $islandids[$ix];
			if (defined $iid) {
				$sth_i->execute($iid);
				$sth_current= $sth_i;
			} else {
				my $arch= $archipelagoes[$ix];
				die unless defined $arch && length $arch;
				$sth_a->execute($arch);
				$sth_current= $sth_a;
			}
			$ix--;
		}
	};
</%perl>
<&| query_age:agestable, now => $now, fetchrow => $fetchrow &>
Islands shown in reverse order of visits.<br>
</&>
% }

% if (!printable($m)) {
%   my %ts_sortkeys;
%   {
%	my $cdspan= $qa->{ShowStalls} ? ' colspan=2' : '';
%	my $cdstall= $qa->{ShowStalls} ? '<th>Stall</th>' : '';
<h2><a name="trades">Relevant trades</a></h2>
<table class="data" id="trades" rules=groups>
<colgroup span=1>
<colgroup span=2>
<% $qa->{ShowStalls} ? '<colgroup span=2>' : '' %>
<colgroup span=1>
<colgroup span=2>
<colgroup span=2>
<colgroup span=2>
<colgroup span=3>
<colgroup span=3>
%	if ($optimise) {
<colgroup span=3>
%	}
<tr>
<th>
<th<% $cdspan %>>Collect
<th<% $cdspan %>>Deliver
<th>
<th colspan=2>Collect
<th colspan=2>Deliver
<th colspan=2>Profit
<th colspan=3>Max
<th colspan=1>
<th colspan=2>Max
%	if ($optimise) {
<th colspan=3>Planned
%	}

<tr>
<th>
<th>Island <% $cdstall %>
<th>Island <% $cdstall %>
<th>Commodity
<th>Price
<th>Qty
<th>Price
<th>Qty
<th>Margin
<th>Unit
<th>Qty
<th>Capital
<th>Profit
<th>Dist
<th>Mass
<th>Vol
%	if ($optimise) {
<th>Qty
<th>Capital
<th>Profit
%	}
%   }

<tr id="trades_sort">
%   foreach my $col (@cols) {
<th>
%   }

%   foreach my $flowix (0..$#flows) {
%	my $flow= $flows[$flowix];
%	my $rowid= "id_row_$flow->{UidShort}";
<tr id="<% $rowid %>" class="datarow<% $flowix & 1 %>">
<td><input type=hidden   name=R<% $flow->{UidShort} %> value="">
    <input type=checkbox name=T<% $flow->{UidShort} %> value=""
       <% $flow->{Suppress} ? '' : 'checked' %> >
%	my $ci= 1;
%	while ($ci < @cols) {
%		my $col= $cols[$ci];
%		my $spec= {
%			Span => 1,
%			Align => ($col->{Text} ? '' : 'align=right')
%		};
%		my $cn= $col->{Name};
%		my $v;
%		if (!$col->{TotalSubflows}) {
%			$v= $flow->{$cn};
%		} else {
%			$v= 0;
%			$v += $_->{$cn} foreach @{ $flow->{Subflows} };
%		}
%		if ($col->{Special}) { $col->{Special}($flow,$col,$v,$spec); }
%		$col->{Total} += $v
%			if defined $col->{Total} and not $flow->{Suppress};
%		$v='' if !$col->{Text} && !$v;
%		my $sortkey= $col->{SortColKey} ?
%			$flow->{$col->{SortColKey}} : $v;
%		$ts_sortkeys{$ci}{$rowid}= $sortkey;
<td <% $spec->{Span} ? "colspan=$spec->{Span}" : ''
 %> <% $spec->{Align}
 %>><% exists $spec->{String} ? $spec->{String} : $v |h %>
%		$ci += $spec->{Span};
%	}
%   }
<tr id="trades_total">
<th>
<th colspan=2>Total
%   foreach my $ci (3..$#cols) {
%	my $col= $cols[$ci];
<td align=right>
%	if (defined $col->{Total}) {
<% $col->{Total} |h %>
%	}
%   }
</table>

<&| tabsort, cols => \@cols, table => 'trades', rowclass => 'datarow',
	throw => 'trades_sort', tbrow => 'trades_total' &>
  ts_sortkeys= <% to_json_protecttags(\%ts_sortkeys) %>;
</&tabsort>
<p>
<input type=submit name=update value="Update">

% } # !printable

<%init>
use CommodsWeb;
use Commods;
</%init>
