#!/usr/bin/perl -w
#
# mptc -- mobile phone tariff calculator
#
# Copyright (c) 1999 Adam Spiers <adam@spiers.net>. All rights
# reserved. This program is free software; you can redistribute it and/or
# modify it under the same terms as Perl itself.
#

use strict;

use Getopt::Std;
use Time::Local;
use Date::Calc qw{Day_of_Week};

my $VERSION = '0.10';

my %opts = ();
my %config = ();
my @calls = ();
my %bill_tariff = ();

my ($bill_file, $bill_tariff_file, @new_tariffs) = process_options();

parse_config_file();

parse_tariff(\%bill_tariff, $bill_tariff_file);
parse_bill($bill_file, \%bill_tariff, \@calls);
display_bill(\%bill_tariff, \@calls, \&by_category_date_time) unless $opts{i};

exit 0 if $opts{o};

foreach my $new_tariff_file (@new_tariffs) {
  my %new_tariff = ();
  parse_tariff(\%new_tariff, $new_tariff_file);

  my $first = 0;
  foreach my $guess_landlines (qw/national/) {
    $new_tariff{name}[2] = " guessing unknown landlines as $guess_landlines";

    my @new_calls = ();
    create_new_bill(\%bill_tariff, \%new_tariff,
                    \@calls, \@new_calls,
                    $guess_landlines);
    
    print "\n" if ! $opts{i} or $first++;
    display_bill(\%new_tariff, \@new_calls, \&by_time);
  }
}

exit 0;

##############################################################################

sub process_options {
  getopts('deiot', \%opts);

  if (@ARGV < 3 or $opts{h}) {
    die <<EOF;
Mobile Phone Tariff Calculator version $VERSION
(c) 1999 Adam Spiers <adam\@spiers.net>

Usage: mptc <bill> <bill tariff> <new tariff> [ <new tariff> ] ...

    -h    Show this help
    -i    Don't display input bill
    -o    Don't display output bill
    -e    Don't display details of each call 
    -t    Don't display call category totals
EOF
  }

  return @ARGV;
}

##

sub parse_config_file {
  my $config_file = 'mptc.conf';

  open(CONFIG, $config_file)
    or die "Couldn't open config file $config_file: $!\n";

  while (<CONFIG>) {
    next if /^\s*$/ || /^\s*#/;
    chomp;
    s/^\s*(.*?)\s*$/$1/;

    if (/^intl home code \+?(\d+)$/) {
      $config{intl_home_code} = $1;
      next;
    }

    if (/^VAT ([\d.]+)%?$/) {
      $config{VAT} = 1 + ($1 / 100);
      next;
    }

    if (/^type_code\s+{$/) {
      my $code = '';
      while (<CONFIG>) {
        last if /^}$/;
        $code .= $_;
      }
      $code = <<EOF;
sub {
  my (\$call, \$tariff) = \@_;
$code}
EOF
      $code =~ s/
                 \$([a-z_]+)
                /
                 my $ident = $1;
                 $ident =~ m{^(tariff|call)$} ? "\$$ident" : "\$call->{$ident}";
                /egx;

      $config{type_code} = eval $code;
      if ($@) {
        die "Code in type_code section of config file broke:\n$@\n";
      }
    }
  }

  close(CONFIG);
}

##

sub parse_tariff {
  my ($tariff, $tariff_file) = @_;

  open(TARIFF, $tariff_file)
    or die "Couldn't open tariff file $tariff_file: $!\n";

  $tariff->{name}[0] = '';
  $tariff->{name}[1] = '';
  $tariff->{min_call_length} = 0;
  $tariff->{max_cost_error} = 0;
  $tariff->{inclusive} = 0;
  $tariff->{inclusive_types} = {};
  $tariff->{bands} = [];
  $tariff->{band_code} = sub { die "No band_code in $tariff_file\n" };

  while (<TARIFF>) {
    next if /^\s*$/ || /^\s*#/;
    chomp;
    s/^\s*(.*?)\s*$/$1/;

    if (/^name (.+)$/i) {
      $tariff->{name}[0] = $1;
      next;
    }

    if (/^name2 (.+)$/i) {
      $tariff->{name}[1] = " $1";
      next;
    }

    if (/^network (.+)$/i) {
      $tariff->{network} = $1;
      next;
    }

    if (/^connection \s+ fee \s+ ([\d.]+) $/ix) {
      $tariff->{'connection fee'} = $1;
      next;
    }

    if (/^monthly \s+ ([\w ]+) \s+ ([\d.]+) $/ix) {
      $tariff->{"monthly $1"} = $2;
      next;
    }

    if (/^min call length (\d+)$/i) {
      $tariff->{min_call_length} = $1;
      next;
    }
    
    if (/^max cost error ([\d.]+)$/i) {
      $tariff->{max_cost_error} = $1;
      next;
    }
    
    if (/^bands (.+)$/) {
      my @bands = split /,\s*/, $1;
      my %bands = map { $_ => {} } @bands;
      $tariff->{bands} = \%bands;
      next;
    }

    if (/^band_code\s*{$/) {
      my $band_code = '';
      while (<TARIFF>) {
        last if /^}$/;
        $band_code .= $_;
      }
      $band_code =~ s/\$([a-z]+)/\$call->{$1}/g;
      $band_code = <<EOF;
sub {
  my \$call = shift;
$band_code}
EOF
      $tariff->{band_code} = eval $band_code;
      if ($@) {
        die <<EOF;
The code in the band_code entry for tariff $tariff->{name} is broken:
$@
EOF
      }
      next;
    }
    
    if (/^(.+) \s+ inclusive \s+ monthly \s+ (NOT \s+)? for \s+ (.+) $/ix) {
      die "bands must be specified before inclusive monthly information\n"
        unless exists $tariff->{bands};

      my ($what_inclusive, $not, $inclusive_types) = ($1, $2, $3);

      $tariff->{complement_inclusive} = $not ? 'not' : '';

      if ($what_inclusive !~ /^(\d+) \s+ mins$/ix) {
        die "$what_inclusive not currently supported as inclusive amount\n";
      }
      else {
        $tariff->{inclusive} = $1;
        
        my @inclusive_types = split /,\s*/, $inclusive_types;
        foreach my $inclusive_type (@inclusive_types) {
          if ($inclusive_type =~ /:/) {
            #huh?
            $tariff->{inclusive_types}{$inclusive_type}++;
          }
          else {
            foreach my $band (keys %{$tariff->{bands}}) {
              $tariff->{inclusive_types}{"$band:$inclusive_type"}++;
            }
          }
        }
      }
        
      next;
    }

    if (/^(pre-?VAT \s+)? rate \s+ (.+)$/ix) {
      my ($preVAT, $rate) = ($1, $2);

      my $VAT = $preVAT ? $config{VAT} : 1;

      if ($rate =~ /^([\w ]+?)\s+(\w+=[\d.]+(,\s*\w+=[\d.]+)*)/) {
        my ($rate_name, $values) = ($1, $2);
        my @values = split /,\s*/, $values;
        foreach my $value (@values) {
          my ($band, $cost) = split /=/, $value;
          die "Band $band not specified in 'band' line\n"
            unless exists $tariff->{bands}{$band};
          $tariff->{rates}{$rate_name}{$band} = $cost * $VAT;
        }
      }
      elsif ($rate =~ /^([\w ]+?)\s+([\d.]+)$/) {
        my ($rate_name, $cost) = ($1, $2);

        foreach my $band (keys %{$tariff->{bands}}) {
          $tariff->{rates}{$rate_name}{$band} = $cost * $VAT;
        }
      }
      else {
        die "Huh? at line $.:\n$_\n";
      }

      next;
    }

    die "Couldn't parse line $. of tariff file $tariff_file:\n$_\n";
  }

  close(TARIFF);

  foreach my $rate_name (keys %{$tariff->{rates}}) {
    foreach my $band (keys %{$tariff->{rates}{$rate_name}}) {
      my $rate = $tariff->{rates}{$rate_name}{$band};
      $tariff->{rate_lookup}{$rate} = [ $rate_name, $band ];
      $tariff->{sms_rate_lookup}{$rate} = [ $rate_name, $band ]
        if $rate_name =~ /sms/i;
    }
  }
}

##

sub parse_bill {
  my ($bill_file, $tariff, $calls) = @_;

  my $remaining_inclusive = $tariff->{inclusive} * 60;

  open(BILL, $bill_file)
    or die "Couldn't open bill file $bill_file: $!\n";

  my ($year, $month, $mday);

  while (<BILL>) {
    next if /^\s*$/ || /^\s*#/;
    chomp;
    s/^\s*(.*?)\s*$/$1/;

    if (m{^(\d+)/(\d+)$}) {
      ($month, $year) = ($1, $2);
      next;
    }

    if (m/^(\d+)$/) {
      $mday = $1;
      next;
    }

    if (m/^
           (\d\d?) :? (\d\d)          # time
           \s+
           ( \+?\d+ )                 # number
           \s+
           ( (?:\d*:)? \d\d | 0 )     # duration
           \s+
           (b | 0 | (?:\d*\.)? \d+ (?:pb)? )      # cost
         $/ix) {
      die "Full date not defined for line $. of bill file $bill_file: $_\n"
        unless defined $mday &&
               defined $month &&
               defined $year;

      my ($hour, $min, $number, $duration, $cost) = ($1, $2, $3, $4, $5);

      my ($mins, $secs);
      if ($duration =~ /:/) {
        ($mins, $secs) = split /:/, $duration;
        $mins ||= 0;
        $duration = $mins * 60 + $secs;
      }
      else {
        ($mins, $secs) = (0, $duration);
      }

      my $call_time = timelocal(0, $min, $hour, $mday, $month, $year);
      my $day_of_week = Day_of_Week($year, $month, $mday);

      my $new_call =
        {
         time     => $call_time,
         year     => $year,
         month    => $month,
         mday     => $mday,
         dow      => $day_of_week,
         hour     => $hour,
         min      => $min,
         number   => $number,
         mins     => $mins,
         secs     => $secs,
         duration => $duration,
         cost     => $cost,
        };

      analyse_call($new_call, $tariff, \$remaining_inclusive);

      push @$calls, $new_call;
      next;
    }

    die "Couldn't parse line $. of bill file $bill_file:\n$_\n";
  }

  close(BILL);
}

##

sub analyse_call {
  my ($call, $tariff, $remaining_inclusive) = @_;

  # Deal with inclusive call discounts
  if ($call->{cost} =~ /pb/i) {
    $call->{bundle} = 'part bundle';
    $call->{cost} =~ s/pb//i;
  }
  elsif ($call->{cost} =~ /b/i) {
    $call->{bundle} = 'bundle';
    $call->{cost} = 0;
  }
  else {
    $call->{bundle} = '';
  }

  # Which call band are we in?
  $call->{band} = $tariff->{band_code}->($call);

  # Calculate post VAT cost
  $call->{post_vat_cost} = $call->{cost} * $config{VAT};


  #
  # Figure out the call type
  #

  $call->{number} =~ s/^\+?$config{intl_home_code}/0/;

  $config{type_code}->($call, $tariff);

  $call->{type} ||= 'unknown';

  # Fudge factor per call

  $call->{call_charge} = 0;
  if ($call->{type} eq 'mobile') {
    if ($call->{band} eq 'peak') {
#      $call->{call_charge} = 0.001;
    }
    elsif ($call->{band} eq 'offpeak') {
#      $call->{call_charge} = 0.002;
    }
  }
  elsif ($call->{type} eq 'landline') {
    if ($call->{band} eq 'offpeak') {
#      $call->{call_charge} = 0.0001;
    }
    elsif ($call->{band} eq 'peak') {
    }
  }


  #
  # Calculate rates
  #

  $call->{max_rate_error} = 0;
  $call->{remaining_inclusive} = $$remaining_inclusive;

  if ($call->{duration} == 0) {
    @$call{qw/rate post_vat_rate/} = ( 'n/a' ) x 2;
  }
  elsif ($call->{bundle} eq 'bundle') {
    @$call{qw/rate post_vat_rate/} = ( 'bundle' ) x 2;
    $$remaining_inclusive -= $call->{duration};
  }    
  elsif ($call->{bundle} eq 'part bundle') {
    @$call{qw/rate post_vat_rate/} = ( 'pb' . $$remaining_inclusive ) x 2;
    $$remaining_inclusive = 0;
  }
  else {
    # Allow for minimum call length
    my $duration = ($call->{duration} <= $tariff->{min_call_length}) ?
      $tariff->{min_call_length} : $call->{duration};

    $call->{rate} = $call->{cost} * 60 / $call->{duration} * 100;
    $call->{post_vat_rate} = $call->{rate} * $config{VAT};

    $call->{max_rate_error} 
      = $tariff->{max_cost_error} * 60 / $duration * 100;
  }


  # Figure out whether mobile calls and SMS messages are cross-network
  # by comparing rate with given tariff rate.
  if ($call->{type} =~ /(?:(\w+)\s+)?(mobile|sms)/) {
    my ($prefix, $mobile_or_sms) = ($1, $2);
    my $rate_or_cost = $mobile_or_sms eq 'mobile' ?
      $call->{post_vat_rate} : $call->{cost};
    my $band = $call->{band};

    if ($rate_or_cost !~ /[^0-9.]/) {
      my $network = $tariff->{rates}{"network $mobile_or_sms"}{$band};
      my $crossnet = $tariff->{rates}{"crossnet $mobile_or_sms"}{$band};

      if ($network == $crossnet) {
        # we can't distinguish between the two
      }
      else {
        if (abs($rate_or_cost - $network) < abs($rate_or_cost - $crossnet)) {
          # Actual rate is closer to network rate than to crossnet rate
          if (squashed_type($call->{type}, $tariff) ne "network $mobile_or_sms") {
            warn "$call->{type} changed to network $mobile_or_sms"
              unless $call->{type} eq $mobile_or_sms;
            $call->{type} = "network $mobile_or_sms";
          }
        }
        else {
          if (squashed_type($call->{type}, $tariff) ne "crossnet $mobile_or_sms") {
            warn "$call->{type} changed to crossnet $mobile_or_sms"
              unless $call->{type} eq $mobile_or_sms;
            $call->{type} = "crossnet $mobile_or_sms";
          }
        }
      }
    }

    # If we still haven't figured out whether it's network or crossnet,
    # if it's an inclusive call, maybe that will tell us.
    if ($call->{type} eq $mobile_or_sms and $call->{bundle} and
        (is_inclusive($tariff, "network $mobile_or_sms", $band) xor
         is_inclusive($tariff, "crossnet $mobile_or_sms", $band))) {
      # One or the other of network/crossnet calls are inclusive
      if (is_inclusive($tariff, "network $mobile_or_sms", $band)) {
        # This call is inclusive so it must be a network call
        if (squashed_type($call->{type}, $tariff) ne "network $mobile_or_sms") {
          warn "$call->{type} changed to network $mobile_or_sms"
            unless $call->{type} eq $mobile_or_sms;
          $call->{type} = "network $mobile_or_sms";
        }
      }
      else {
        if (squashed_type($call->{type}, $tariff) ne "crossnet $mobile_or_sms") {
          warn "$call->{type} changed to crossnet $mobile_or_sms"
            unless $call->{type} eq $mobile_or_sms;
          $call->{type} = "crossnet $mobile_or_sms";
        }
      }
    }
  }

  # Figure out whether landline calls are local or national by comparing
  # rate with given tariff rate.
  if ($call->{type} eq 'landline') {
    my $rate = $call->{post_vat_rate};
    my $band = $call->{band};

    if ($call->{post_vat_rate} !~ /[^0-9.]/) {
      my $local = $tariff->{rates}{'local landline'}{$band};
      my $national = $tariff->{rates}{'national landline'}{$band};

      if ($local == $national) {
        # we can't distinguish between the two
      }
      else {
        if (abs($rate - $local) < abs($rate - $national)) {
          # Actual rate is closer to local rate than to national rate
          $call->{type} = 'local landline';
        }
        else {
          $call->{type} = 'national landline';
        }
      }
    }

    # If we still haven't figured out whether a landline call is local
    # or national, if it's an inclusive call, maybe that will tell us.
    # (It won't usually on most, if not all, tariffs.)
    if ($call->{type} eq 'landline' and $call->{bundle} and
        (is_inclusive($tariff, 'local landline', $band) xor
         is_inclusive($tariff, 'national landline', $band))) {
      # One or the other of local/national calls are inclusive
      if (is_inclusive($tariff, 'local landline', $band)) {
        # This call is inclusive so it must be a local call
        $call->{type} = 'local landline';
      }
      else {
        $call->{type} = 'national landline';
      }
    }
  }
}

##

sub display_bill {
  my ($tariff, $calls, $sorter) = @_;

  my %total = ();

  my @text = @{$tariff->{name}};
  my $prefix = shift @text;

  die unless defined @text;
  my $suffix = @text ? (join '', @text) : '';

  print "Bill for $prefix tariff$suffix:\n";

  display_calls($calls, $tariff, $sorter, \%total);

  my $total_cost = 0;
  
  display_category_totals(\%total, $tariff, \$total_cost);
  display_totals($tariff, \$total_cost);

  print "\n", '=' x 90, "\n";
}

##

sub display_calls {
  my ($calls, $tariff, $sorter, $total) = @_;

  my $hformat = "%-9s %5s  %-7s%-6s  %6s  %6s  %6s  %6s  %s\n";

  print "\n" unless $opts{e};
  printf $hformat, qw/Date Time Number Length Cost Rate w\/VAT R_err Type/
    unless $opts{e};

  my $remaining_inclusive;

  foreach my $call (sort { $sorter->($tariff) } @$calls) {
    my $type = squashed_type($call->{type}, $tariff);
    my $band = $call->{band};

    $total->{calls_made}{both}{"all $band"}++;
    $total->{calls_made}{both}{"$band $type"}++;
    $total->{calls_made}{band}{$band}++;
    $total->{calls_made}{type}{$type}++;

    $total->{secs_called}{both}{"all $band"} += $call->{duration};
    $total->{secs_called}{both}{"$band $type"} += $call->{duration};
    $total->{secs_called}{band}{$band} += $call->{duration};
    $total->{secs_called}{type}{$type} += $call->{duration};

    if ($call->{rate} !~ /[^0-9.]/) {
      $total->{rate}{both}{"all $band"} += $call->{rate};
      $total->{rate}{both}{"$band $type"} += $call->{rate};
      $total->{rate}{band}{$band} += $call->{rate};
      $total->{rate}{type}{$type} += $call->{rate};
    }

    $total->{cost}{both}{"all $band"} += $call->{cost};
    $total->{cost}{both}{"$band $type"} += $call->{cost};
    $total->{cost}{band}{$band} += $call->{cost};
    $total->{cost}{type}{$type} += $call->{cost};

    display_call($call) unless
         $opts{e}
#      or $type =~ /^free|\b(battery|operator|voice|service)\b/i
#      or $call->{bundle}
#      or $call->{type} !~ /mobile|sms/ or $call->{type} =~ /received sms/
           ;

    if ($call->{bundle} eq 'bundle') {
      $remaining_inclusive = $call->{remaining_inclusive} - $call->{duration};
#      print "---------- inclusive remaining: $remaining_inclusive\n";
    }
  }

  if (($remaining_inclusive || 0) > 0) {
    my ($mins, $secs) = ($remaining_inclusive / 60, $remaining_inclusive % 60);
    printf "\n     %2d:%02d mins of the inclusive free calls were remaining.\n",
      $mins, $secs;
  }

  print "\n", '-' x 90, "\n" unless $opts{e};
}

##

sub display_call {
  my ($call) = @_;

  my $format  = "%3s %2d/%02d %2d:%02d  %-7s %2.0d:%02d  %6.4f  %6s  %6s  %6.4f  %s %s\n";

  my $day_of_week = (qw/Mon Tue Wed Thu Fri Sat Sun/)[$call->{dow} - 1];

  my ($rate, $post_vat_rate) = @$call{qw/rate post_vat_rate/};

  $rate = sprintf '%6.3f', $call->{rate} if $call->{rate} !~ /[^0-9.]/; 

  $post_vat_rate = sprintf '%6.3f', $call->{post_vat_rate}
    if $call->{post_vat_rate} !~ /[^0-9.]/;  

  (my $type = $call->{type}) =~ s/sms/SMS/gi;

  printf $format, $day_of_week,
                  @$call{qw/
                            mday month
                            hour min
                            number
                            mins secs
                            cost
                           /},
                  $rate,
                  $post_vat_rate,
                  @$call{qw/
                            max_rate_error
                            band
                           /},
                  $type;
}

##

sub display_category_totals {
  my ($total, $tariff, $total_cost) = @_;

  print "\n" unless $opts{t};

  my $catcat = 'both';          # which category of category to use :-)
  foreach my $category (( map { "all $_" } keys %{$tariff->{bands}} ),
                        grep ! /\ball\b/,
                             sort { by_category_cost($total->{cost}{$catcat}) }
                                  keys %{$total->{calls_made}{$catcat}})
  {
    display_category_total($total, $catcat, $category, $total_cost);
  }
}

sub display_totals {
  my ($tariff, $total_cost) = @_;

  print "\n";

  my @charges = ();
  push @charges, [ 'Call charges', $$total_cost ];

  foreach my $charge (sort keys %$tariff) {
    if ($charge =~ /^monthly\b/i) {
      push @charges, [ $charge, $tariff->{$charge} / $config{VAT} ];
    }
  }

  my $indent = ' ' x 10;
  my $format = "$indent%30s      %5.2f     %5.2f";
  printf "$indent%30s      %5s   %7s\n", 'Type of charge', 'Cost', 'Inc VAT';
  printf "$indent%30s----------------------\n", '----------------------------';

  my $subtotal = 0;
  foreach my $charge (sort { $b->[1] <=> $a->[1] } @charges) {
    printf "$format\n",
      ucfirst($charge->[0]), $charge->[1], ($charge->[1] * $config{VAT});
    $subtotal += $charge->[1];
  }

  printf "$indent%30s    ------------------\n";
  printf "$format\n",
    'Total', $subtotal, $subtotal * $config{VAT};
}

##

sub display_category_total {
  my ($total, $catcat, $category, $total_cost_all) = @_;
  
  $$total_cost_all += $total->{cost}{$catcat}{$category} if $category =~ /\ball\b/;

  my $total_secs = $total->{secs_called}{$catcat}{$category};
  my ($mins, $secs) = ($total_secs / 60, $total_secs % 60);

  (my $category2 = $category) =~ s/sms/SMS/gi;

  my $calls_made = $total->{calls_made}{$catcat}{$category};
  my $average_rate = (defined $total->{rate}{$catcat}{$category}) ?
    $total->{rate}{$catcat}{$category} / $calls_made : 'n/a';
  my $total_cost = $total->{cost}{$catcat}{$category};

  printf "%25s calls: %3d:%02d mins cost %5.2f, %2d calls, average rate " .
           (($average_rate =~ /\d/) ? '%6.3f' : '%6s') . "\n",
         ucfirst $category2, $mins, $secs, $total_cost, $calls_made, $average_rate
    unless $opts{t};
}

##

sub create_new_bill {
  my ($old_tariff, $tariff, $old_calls, $new_calls, $guess_landlines) = @_;

  my $remaining_inclusive = $tariff->{inclusive} * 60;

  my $guessed = 0;

  foreach my $old_call (sort by_time @$old_calls) {
    my $call = { %$old_call };
    push @$new_calls, $call;
    
    $call->{band} = $tariff->{band_code}->($call);
    my $type = squashed_type($call->{type}, $tariff);

    if ($call->{type} eq 'landline') {
#      print "Guessing type $guess_landlines landline for:\n" unless $guessed;
#      display_call($call);
      $call->{type} = $guess_landlines . ' landline';
      $guessed++;
    }

    # If we're moving network, we know that calls to the old network
    # are now crossnet calls.  Unfortunately we don't know that old
    # crossnet calls are now calls to the same network.
    if ($call->{type} eq 'network mobile' and
        $old_tariff->{name} ne $tariff->{name}) {
      $call->{type} = 'crossnet mobile';
    }

    # Work out new rates and costs
    if (exists $tariff->{rates}{$type}{$call->{band}}) {
      if ($call->{type} =~ /sms/i) {
        # SMS calls are flat rate per call
        $call->{cost} = $tariff->{rates}{$type}{$call->{band}}
                          / 100 / $config{VAT};
        @$call{qw/rate post_vat_rate/} = ( 'n/a' ) x 2;
      }
      else {
        $call->{post_vat_rate} = $tariff->{rates}{$type}{$call->{band}};
        $call->{rate} = $call->{post_vat_rate} / $config{VAT};
        $call->{cost} = $call->{rate} * ($call->{duration} / 60) / 100;
      }
    }
    else {
      # Assume same cost as on original bill
#      print "Assuming same cost for:\n";
#      display_call($call);
      $call->{rate} = $call->{cost} * 60 / $call->{duration} * 100;
      $call->{post_vat_rate} = $call->{rate} * $config{VAT};
    }


    $call->{bundle} = '';
    $call->{remaining_inclusive} = $remaining_inclusive;

    if (is_inclusive($tariff, $type, $call->{band}) and
        $call->{duration} > 0)
    {
      if ($remaining_inclusive > $call->{duration}) {
        $call->{bundle} = 'bundle';
        $call->{cost} = 0;
        @$call{qw/rate post_vat_rate/} = ( 'bundle' ) x 2;
        $call->{remaining_inclusive} = $remaining_inclusive;
        $remaining_inclusive -= $call->{duration};
      } elsif ($remaining_inclusive > 0) {
        $call->{bundle} = 'part bundle';
        $call->{cost} = ($call->{rate} || 0)
                          * (($call->{duration} - $remaining_inclusive) / 60)
                          / 100;
        @$call{qw/rate post_vat_rate/} = ( 'pb' . $remaining_inclusive ) x 2;
        $remaining_inclusive = 0;
      }
    }
  }

#  print "\n", '-' x 90, "\n\n" if $guessed;
}

##

sub is_inclusive {
  my ($tariff, $type, $band) = @_;

  return 0 if $type =~ /^free|\b(battery)\b/i;

  if ($tariff->{complement_inclusive}) {
    return ! exists $tariff->{inclusive_types}{"$band:$type"};
  }
  else {
    return exists $tariff->{inclusive_types}{"$band:$type"};
  }
}

##

sub by_category_date_time {
  my ($tariff) = @_;
  my $type1 = squashed_type($a->{type}, $tariff);
  my $type2 = squashed_type($b->{type}, $tariff);
  my $band_and_type1 = "$type1$a->{band}";
  my $band_and_type2 = "$type2$b->{band}";

  return ($band_and_type1 cmp $band_and_type2)
                          ||
          ($a->{time} <=> $b->{time});
}

##

sub by_time {
  return $a->{time} <=> $b->{time};
}

##

sub by_category_cost {
  my $cost = shift;

  return -1 if $a =~ /crossnet mobile/;
  return  1 if $b =~ /crossnet mobile/;
  return -1 if $a =~ /network mobile/;
  return  1 if $b =~ /network mobile/;
  return -1 if $a =~ /mobile/;
  return  1 if $b =~ /mobile/;

  return -1 if $a =~ /national landline/;
  return  1 if $b =~ /national landline/;
  return -1 if $a =~ /local landline/;
  return  1 if $b =~ /local landline/;
  return -1 if $a =~ /landline/;
  return  1 if $b =~ /landline/;

  return -1 if $a =~ /voicemail/;
  return  1 if $b =~ /voicemail/;

  return -1 if $a =~ /sms/;
  return  1 if $b =~ /sms/;

  return $cost->{$a} <=> $cost->{$b};
}

##

sub squashed_type {
  my ($type, $tariff) = @_;

  if ($type =~ /(.+?)\s+(mobile|sms)$/i) {
    my $network = $1;
    if ($network eq $tariff->{network}) {
      return "network $2";
    }
    else {
      return "crossnet $2";
    }
  }

  return $type;
}
