The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# Download cost recording and calculations.

# Copyright 2007, 2008, 2009, 2010, 2011 Kevin Ryde

# This file is part of Chart.
#
# Chart is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3, or (at your option) any later version.
#
# Chart 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 General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with Chart.  If not, see <http://www.gnu.org/licenses/>.


package App::Chart::DownloadCost;
use 5.010;
use strict;
use warnings;
use Carp;
use List::Util qw(min max);

use App::Chart;
use App::Chart::Database;
use App::Chart::Download;


sub cost_store_h {
  my ($h) = @_;
  my $key = $h->{'cost_key'};
  my $value = $h->{'cost_value'};
  if (! defined $value && defined $h->{'resp'}) {
    $value = length ($h->{'resp'}->content);
  }
  if (! defined $value) {
    croak "DownloadCost: missing cost_value or resp for cost_key '$key'";
  }
  cost_store ($key, $value);
}

sub cost_store {
  my ($key, $value) = @_;
  App::Chart::Database->write_extra ('', $key, $value);
}

sub cost_get {
  my ($key, $default) = @_;
  my $cost = App::Chart::Database->read_extra ('', $key);
  return (defined $cost ? $cost : $default);
}


# going by day or by symbol
#
# Decide whether to use daily downloads or individual symbol downloads to
# update all of SYMBOL-LIST.  Download will be from the
# `download-start-tdate' of each symbol, up to AVAIL-TDATE.
#
# DAILY-COST is the cost (in bytes) to download a day.
# (SYMBOL-COST-PROC tdate) should return the cost of downloading from TDATE
# to now (ie. AVAIL-TDATE) for a symbol.
# Both costs will get `http-get-cost' added to them automatically.
#
# The return is two values (USE-SYMBOL-LIST TDATE), which is a list of
# symbols to do individually, and a tdate to start from for daily
# downloads.  That tdate is (1+ avail-tdate) when no dailies should be
# done.
#
# As an example, if there's a few symbols quite a bit behind but the rest
# needing just a day or two then the result can be to do the laggards
# individually, then whole days for the most recent.
#

sub by_day_or_by_symbol {
  my (%param) = @_;

  if (! exists $param{'available_tdate'}) { croak 'missing available_tdate'; }
  my $avail = $param{'available_tdate'};

  if (! exists $param{'symbol_list'})     { croak 'missing symbol_list'; }
  my $symbol_list = $param{'symbol_list'};

  my $whole_cost;
  if (exists $param{'whole_cost'}) {
    $whole_cost = $param{'whole_cost'};
  } elsif (exists $param{'whole_cost_key'}) {
    $whole_cost = cost_get ($param{'whole_cost_key'},
                            $param{'whole_cost_default'});
  } else {
    croak 'no whole day cost method';
  }

  my $indiv_cost_proc;
  if (exists $param{'indiv_cost_proc'}) {
    $indiv_cost_proc = $param{'indiv_cost_proc'};
  } elsif (exists $param{'indiv_cost_key'}) {
    my $cost = cost_get ($param{'indiv_cost_key'},
                         $param{'indiv_cost_default'});
    $indiv_cost_proc = sub { return $cost; };
  } elsif (exists $param{'indiv_cost_fixed'}) {
    my $cost = $param{'indiv_cost_fixed'};
    $indiv_cost_proc = sub { return $cost; };
  } elsif (exists $param{'indiv_cost_daily'}) {
    my $indiv_cost_daily = $param{'indiv_cost_daily'};
    $indiv_cost_proc = sub { my ($lo) = @_;
                             return $indiv_cost_daily * ($avail - $lo + 1); };
  } else {
    croak 'no indiv cost method';
  }

  $whole_cost += $App::Chart::option{http_get_cost};

  # tdate => [ symbol, symbol, ... ]
  my %hash = ();
  foreach my $symbol (@$symbol_list) {
    my $start = App::Chart::Download::start_tdate_for_update ($symbol);
    my $aref = ($hash{$start} ||= []);
    push @$aref, $symbol;
  }

  my $best_whole_tdate = min (keys %hash);
  my $best_cost = $whole_cost * ($avail - $best_whole_tdate + 1);
  my @best_indiv_list = ();
  ### whole ...
  ### $best_whole_tdate
  ### $avail
  ### none indiv ...
  ### $best_cost
  ### whole_cost per day: $whole_cost

  my @indiv_list = ();
  my $indiv_cost = 0;

  while (%hash) {
    my $new_tdate = min (keys %hash);
    my $new_list = delete $hash{$new_tdate};
    my $new_cost = scalar (@$new_list)
      * (($indiv_cost_proc->($new_tdate)) + $App::Chart::option{http_get_cost});
    push @indiv_list, @$new_list;
    $indiv_cost += $new_cost;

    my $whole_tdate = min (keys %hash);
    if (! defined $whole_tdate) { $whole_tdate = $avail + 1; }
    my $cost = $whole_cost * ($avail - $whole_tdate + 1) + $indiv_cost;
    # if (DEBUG) { print "  [",scalar(%hash),
    #                "] whole $whole_tdate indiv ",scalar(@indiv_list),
    #                ": $cost (indiv $indiv_cost)\n"; }

    if ($cost < $best_cost) {
      ### is new best ...
      $best_cost = $cost;
      @best_indiv_list = @indiv_list;
      $best_whole_tdate = $whole_tdate;
    }
  }

#     # same order as original symbol-list
#     (set! best-symbol-list (lset-intersection string=?
# 					      symbol-list best-symbol-list))

  return $best_whole_tdate, @best_indiv_list;
}


1;
__END__

# =head1 NAME
# 
# App::Chart::DownloadCost -- download cost recording and calculations
# 
# =head1 FUNCTIONS
# 
# =over 4
# 
# =item C<< App::Chart::DownloadCost::by_day_or_by_symbol (key=>value, ...) >>
# 
#     symbol_list
#     available_tdate
# 
#     whole_cost
#     whole_cost_key
#     whole_cost_default
# 
#     indiv_cost_proc
#     indiv_cost_fixed
#     indiv_cost_daily
#     indiv_cost_key
#     indiv_cost_default
# 
# =item C<App::Chart::DownloadCost::cost_store ($key, $data_body)>
# 
# ...
# 
# =item C<< App::Chart::DownloadCost::cost_get ($key, $default) >>
# 
# ...
# 
# =back
# 
# =cut