#!/usr/bin/perl -T

# This is a library of functions used by Captrap's view generation programs.

# Copyright 2009 Corey Hickey


# This file is part of Captrap.
#
# Captrap 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 of the License, or
# (at your option) any later version.
#
# Captrap 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 Captrap.  If not, see <http://www.gnu.org/licenses/>.

=head1 NAME

Captrap::View - a library of functions used by Captrap's view generation
programs.

=head1 SYNOPSIS

use Captrap::View qw(:view :param);

=head1 DESCRIPTION

As of yet, this library is not intended to be used outside of Captrap; few of
the included functions can be used in a self-contained manner. This text is
intended as a quick reference for use in Captrap development.

No functions are exported by default.

=head2 Tags

The following groups of functions are available; contents may change in future
versions.

=over

=item :view

mk_view_info, mk_views, cgi_list_views

=item :param

mk_param_info

=back

=head2 Functions

=over

=cut


use 5.010; # we need Perl >= 5.10 for given/when
use strict;
use warnings FATAL => 'all';
no warnings "experimental::smartmatch";
use feature 'switch';

package Captrap::View;
require Exporter;
our @ISA = ('Exporter');
our @EXPORT = ();
our %EXPORT_TAGS = (
  view => [ qw(
    mk_view_info
    mk_views
    cgi_list_views
  ) ],
  param => [ qw(
    mk_param_info
  ) ],
);
# all the tagged functions
our @EXPORT_OK = map({ @$_ } values(%EXPORT_TAGS));

# for development using a different Captrap module
use lib "lib";
use Captrap qw(:misc :cgi :db);


# -----------------------------------------------------------------------------
# misc.
# -----------------------------------------------------------------------------

# initialize priv hash according to what keys need to be filled;
# keys with defined values are ignored
sub init_priv {
  my $common = shift;
  my $needed = shift;
  $common->{priv} = {} unless defined($common->{priv});
  my $priv = $common->{priv};
  foreach my $key (@{$needed}) {
    next if defined($priv->{$key}); # already done
    my $val;
    given ($key) {
      when ("avg_data") {
        $val = calc_avg_data($common);
      }
      when ("times") {
        # $common->{priv}->{now} may be undef
        $val = get_times($common, $common->{priv}->{now});
      }
      when ("lengths") {
        $val = get_lengths($common);
      }
      when ("mtpred_strings") {
        init_priv($common, [ qw(avg_data intervals states) ]);
        $val = format_mtpred_strings($priv->{avg_data}, $priv->{intervals},
            $priv->{states});
      }
      when ("states") {
        $val = [ qw(total up down bcast bogey) ];
      }
      when ("intervals") {
        $val = [ qw(year month day hour) ];
      }
      when ("data_current") {
	init_priv($common, [ qw(times) ]);
        $val = get_data_from_current_hour($common, $priv->{times}->{now});
      }
      when ("data_last") {
        init_priv($common, [ qw(times) ]);
        $val = get_data_from_last_intervals($common,
            floor_unit("hour", $priv->{"times"}->{now}));
      }
      when ("per_starts") {
        $val = {
          year  => "first",
          month => "lastyear",
          day   => "lastmonth",
          hour  => "lastday",
        };
      }
      when ("unknown_macs") {
        $val = get_unknown_macs($common);
      }
      default { die "init_priv: don't know what to do with key '$key'" }
    }
    $priv->{$key} = $val;
  }
  return $priv;
}


# round to nearest int
sub round {
  my $number = shift;
  return int($number + 0.5);
}


# calculate the average bytes/packets per day for each state and interval
sub calc_avg_data {
  my $common = shift;
  my $priv = init_priv($common, [ qw(data_last lengths states intervals) ]);
  my $data = $priv->{data_last};
  my $lengths = $priv->{lengths};
  my $states = $priv->{states};
  my $intervals = $priv->{intervals};
  my $avg = {};
  foreach my $interval (@{$intervals}) {
    my $length = $lengths->{$interval};
    foreach my $state (@{$states}) {
      my $bytes   = $data->{$interval}->{bytes}->{$state};
      my $packets = $data->{$interval}->{packets}->{$state};
      $avg->{$interval}->{bytes}->{$state}   = round($bytes/$length);
      $avg->{$interval}->{packets}->{$state} = round($packets/$length);
    }
  }
  return $avg;
}

# -----------------------------------------------------------------------------
# formatting of data for CGI
# -----------------------------------------------------------------------------

# make a table row out of data/state data
sub mk_data_table_row {
  my $cgi = shift;
  my $item = shift;
  my $data = shift; # hash ref
  my $states = shift; # array ref
  my $row_label = shift;
  my $colors = shift; # hash ref; may be undef
  my $row = $cgi->td($row_label);
  foreach my $state (@{$states}) {
    my $color = $colors->{$state} // '000000';
    $row .= $cgi->td({ "-style" => "color: #$color" },
        format_data($cgi, $data->{$state}, $item));
  }
  return $cgi->Tr($row);
}


# make a table out of data/interval/state data
sub mk_data_table {
  my $cgi = shift;
  my $data = shift; # hash ref
  my $intervals = shift; # array ref
  my $item = shift; # bytes or packets
  my $states = shift; # array ref
  my $colors = shift; # hash ref; may be undef
  # first make the first row of labels
  my $table = $cgi->td("");
  foreach my $state (@{$states}) {
    $table .= $cgi->td($state);
  }
  $table = $cgi->Tr($table);
  # now fill in the data rows
  foreach my $interval (@{$intervals}) {
    $table .= mk_data_table_row($cgi, $item, $data->{$interval}->{$item},
        $states, $interval, $colors);
  }
  return $cgi->table($table);
}


# make strings of parameters for the mtpred graph
sub format_mtpred_strings {
  my $data = shift; # hash ref
  my $intervals = shift; # array ref
  my $states = shift; # array ref
  my $mtpred = {};
  foreach my $item (qw(bytes packets)) {
    foreach my $state (@{$states}) {
      my @strings;
      foreach my $interval (@{$intervals}) {
        # make something like "12345:last_day"
        push(@strings, "$data->{$interval}->{$item}->{$state}:last_$interval");
      }
      $mtpred->{$item}->{$state} = join(",", @strings);
    }
  }
  return $mtpred;
}


# make an image with a border
sub mk_graph_image {
  my $common = shift; # hash ref
  my $params = shift;
  my $alt = shift;
  my $grapher = $common->{config}->{grapher};
  my ($w, $h) = @{$common->{config}}{"default_graph_w", "default_graph_h"};
  my $cgi = $common->{cgi};
  my $src = mk_link($common, $grapher, $params);
  return $cgi->img({'src' => $src, 'width' => $w,
      'height' => $h, 'alt' => $alt, 'class' => 'graph'});
}


# wrapper for making the usual graphs per interval
sub mk_graph_links_per {
  my $common = shift; # hash ref
  my $params = shift;
  my $states = shift; # may be undef
  my $cgi = $common->{cgi};
  my $priv = init_priv($common, [ qw(intervals per_starts) ]);
  my $text;
  foreach my $interval (@{$priv->{intervals}}) {
    my $start = $priv->{per_starts}->{$interval};
    $text .= mk_graph_set($common, $params, "interval graph", "per", $interval,
        $start, $states);
  }
  return $text;
}


# wrapper for making the usual moving total graphs
sub mk_graph_links_mt {
  my $common = shift; # hash ref
  my $params = shift;
  my $states = shift; # may be undef
  my $cgi = $common->{cgi};
  my $priv = init_priv($common, [ qw(intervals per_starts) ]);
  my $text;
  # skip the first interval (year)
  for (my $i = 1; $i < @{$priv->{intervals}}; ++$i) {
    my $interval = $priv->{intervals}->[$i];
    my $start = $priv->{per_starts}->{$interval};
    my $sum_unit = $priv->{intervals}->[$i-1];
    my $extra = { sum_unit => [ $sum_unit ], sum_interval => [ 1 ] };
    $text .= mk_graph_set($common, $params, "moving-total graph", "mt",
        $interval, $start, $states, $extra);
  }
  return $text;
}


# make some mtpred graphs
sub mk_graph_links_mtpred {
  my $common = shift;
  my $params = shift;
  my $states = shift; # array ref; may be undef
  my $priv = init_priv($common, [ "mtpred_strings" ]);
  unless (@$states) {
    $states = [ "total" ];
  }
  my $text;
  my $item = $params->{item};
  foreach my $state (@$states) {
    my $mtpred_string = $priv->{mtpred_strings}->{$item}->{$state};
    $state = undef if $state eq "total";
    $text .= mk_graph_set($common, $params, "predicted moving-total graph",
        "mtpred", "day", "lastmonth", $state, { pred => [ $mtpred_string ] });
  }
  return $text;
}


# make a set of graph links and an image
# end time is hardcoded to "now"; if I need a more general function later I'll
# write it then.
sub mk_graph_set {
  my $common = shift;
  my $params = shift;
  my $alttext = shift;
  my $graph = shift;
  my $per = shift;
  my $start = shift;
  my $states = shift;
  my $extra = shift;
  my $priv = init_priv($common, [ qw(times) ]);
  my $cgi = $common->{cgi};
  $extra = {} unless defined($extra);
  my $patterns = $params->{patterns} // $common->{config}->{patterns_default};
  my $graph_params = mk_ixhash();
  %$graph_params = (
    graph    => [ $graph ],
    item     => [ $params->{item} ],
    per      => [ $per ],
    start    => [ $priv->{"times"}->{$start} ],
    end      => [ $priv->{"times"}->{now} ],
    patterns => [ $patterns ],
    %$extra,
  );
  if (defined($states)) {
    $graph_params->{states} = [ $states ];
  }
  my $caps = get_caps($common, $states, $graph_params);
  if (defined($caps)) {
    $graph_params->{caps} = [ $caps ];
  }
  my $text = mk_graph_image($common, $graph_params, $alttext) . $cgi->br();
  my $grapher = $common->{config}->{grapher};
  if ($patterns) {
    $graph_params->{patterns} = [ 0 ];
    $text .= mk_href($common, $grapher, $graph_params, "[solid bars]");
    $graph_params->{patterns} = [ 1 ];
  } else {
    $graph_params->{patterns} = [ 1 ];
    $text .= mk_href($common, $grapher, $graph_params, "[patterned bars]");
    $graph_params->{patterns} = [ 0 ];
  }
  foreach my $output (qw(svg text csv)) {
    $graph_params->{output} = [ $output ];
    $text .= mk_href($common, $grapher, $graph_params, "[$output]");
  }
  return $cgi->p({'-class' => 'graphset'}, $text);
}


# get a list of transfer caps according to states and $per
sub get_caps {
  my $common = shift;
  my $states = shift; # array ref
  my $params = shift; # hash ref
  my $caps = $common->{config}->{caps};
  my $cap_unit;
  given ($params->{graph}->[0]) {
    when (/^per$/i) {
      $cap_unit = $params->{per}->[0];
    }
    when (/^mt$/i) {
      $cap_unit = $params->{sum_unit}->[0];
    }
    when (/^mtpred$/i) {
      $cap_unit = "month";
    }
    default {
      die "unknown graph type: $params->{graph}";
    }
  }
  my $pos;
  given ($cap_unit) {
    when (/^year$/i) {
      $pos = 0;
    }
    when (/^month$/i) {
      $pos = 1;
    }
    when (/^day$/i) {
      $pos = 2;
    }
    when (/^hour$/i) {
      $pos = 3;
    }
    default {
      die "unrecognized cap unit '$cap_unit'";
    }
  }
  unless (defined($states)) {
    return $caps->{total}->[$pos];
  }
  my @list;
  my $empty = 1;
  foreach my $state (l2a($states)) {
    # year,month,day,hour
    my $cap = $caps->{$state}->[$pos];
    if (defined($cap)) {
      push(@list, $cap);
      $empty = 0;
    } else {
      # use a -1 to take up space in case we get a real value later
      push(@list, -1);
    }
  }
  return undef if $empty;
  return join(',', @list);
}


# return a string to print
sub format_data {
  my $cgi = shift;
  my $data = shift;
  my $item = shift;
  my ($factor, $suffix) = choose_factor($data, $item);
  my $adjusted = $data / $factor;
  # 32-bit perl's sprintf doesn't seem to understand "%lld" properly,
  # so just let $data go in the format string
  return sprintf("$data%s(%.02f %s)", $cgi->br, $adjusted, $suffix);
}


# -----------------------------------------------------------------------------
# database fetching
# -----------------------------------------------------------------------------

# get lengths (in days) of various intervals
sub get_lengths {
  my $common = shift;
  my $priv = init_priv($common, [ "times" ]);
  # days/year and days/month are variable,
  # days/day and days/hour should be constant
  my $times = $priv->{times};
 return {
    year  => time_diff($times->{yearago},  $times->{now}, "day"),
    month => time_diff($times->{monthago}, $times->{now}, "day"),
    day   => 1,
    hour  => 1/24,
  };
}


# Get the data transferred over the most recent hour/day/month/year intervals.
# returns a hash reference:
# $data = {
#   interval1 => {
#     bytes => {
#       state1  => 1234,
#       state2  => 456,
#     },
#     packets => {
#       ...
#     },
#   },
#   interval2 => {
#      ...
#   }
#   ...
# }
sub get_data_from_last_intervals {
  my $common = shift;
  my $end = shift;
  my $priv = init_priv($common, [ qw(states intervals) ]);
  my $data = {};
  foreach my $interval (@{$priv->{intervals}}) {
    $data->{$interval} =
        get_data_from_interval($common, $end, 1, $interval, $priv->{states});
  }
  return $data;
}


# get the data transferred within an interval, separated by state
# returns a hash reference
# $data = {
#   bytes = {
#     state1 => 1234,
#     state2 => 4567,
#     ...
#     total  => 141234,
#   },
#   packets = {
#     ...
#   },
# }
sub get_data_from_interval {
  my $common = shift;
  my $end = shift;
  my $num = shift;
  my $interval = shift;
  my $states = shift;
  my $data = {};
  foreach my $state (@$states) {
    # default 0
    $data->{bytes}->{$state}   = 0;
    $data->{packets}->{$state} = 0;
  }
  die "invalid interval unit" unless check_interval_unit($interval);
  my ($acct, $macs) = @{$common->{config}}{"db_table_acct","db_table_macs"};
  my $states_ignore = $common->{config}->{states_ignore};
  my $ph = placeholder_list($states_ignore);
  # This query generates sums for each mac, then joins to the macs table, then
  # generates sums for each state. Even though we're summing twice, the join
  # is run on a MUCH smaller table, so the query is quite a bit faster (75%
  # faster over a year of data).
  my $sel = "
    SELECT $macs.state, SUM(t_sums.bytes), SUM(t_sums.packets)
    FROM
      (
        SELECT
          $acct.mac_dst,
          SUM($acct.bytes) AS bytes,
          SUM($acct.packets) AS packets
        FROM $acct
        WHERE
          $acct.stamp_inserted < ? AND
          $acct.stamp_inserted >= ? - INTERVAL ? $interval
        GROUP BY $acct.mac_dst
      ) AS t_sums LEFT JOIN
      $macs ON t_sums.mac_dst = $macs.mac
    WHERE ($macs.state <=> NULL OR $macs.state NOT IN ($ph))
    GROUP BY $macs.state
  ";
  my $sth = $common->{dbh}->prepare($sel);
  $sth->dbcache_execute($end, $end, $num, @$states_ignore);
  $data->{bytes}->{total}   = 0;
  $data->{packets}->{total} = 0;
  while (defined(my $a = $sth->fetchrow_arrayref())) {
    my ($state, $bytes, $packets) = @$a;
    # add everything to the totals
    $data->{bytes}->{total}   += $bytes;
    $data->{packets}->{total} += $packets;
    # bytes/packets with unknown state will have state == NULL; skip them
    next unless defined($state);
    # don't record states that weren't specified
    next unless defined($data->{bytes}->{$state});
    $data->{bytes}->{$state}   = $bytes;
    $data->{packets}->{$state} = $packets;
  }
  return $data;
}


# get the data transferred during the current hour, for each state
sub get_data_from_current_hour {
  my $common = shift;
  my $priv = init_priv($common, [ qw(states) ]);
  my $now = shift;
  my $start = floor_unit("hour", $now);
  my $data = {
    current =>
        get_data_from_interval($common, $now, 1, "hour", $priv->{states}),
  };
  return $data;
}

# -----------------------------------------------------------------------------
# view generation
# -----------------------------------------------------------------------------

# make a dispatch table relating views to functions
sub mk_view_funcs {
  return {
    basic              => \&mk_view_basic,
    graph_inter        => \&mk_view_graph_inter,
    graph_inter_updown => \&mk_view_graph_inter_updown,
    graph_inter_bb     => \&mk_view_graph_inter_bb,
    graph_adv          => \&mk_view_graph_adv,
    graph_adv_updown   => \&mk_view_graph_adv_updown,
    graph_adv_bb       => \&mk_view_graph_adv_bb,
  }
}


=item mk_view_info()

Make a hash of available views and brief descriptions.

=cut

sub mk_view_info () {
  my $views = mk_ixhash();
  %$views = (
    basic              => {
      ttl => "Basic Information",
      txt => "tables of transfer information",
    },
    graph_inter        => {
      ttl => "Interval Graphs",
      txt => "graphs of traffic transferred over last intervals",
    },
    graph_inter_updown => {
      ttl => "Interval Graphs (up/down)",
      txt => "interval graphs for upload and download traffic",
    },
    graph_inter_bb     => {
      ttl => "Interval Graphs (bcast/bogey)",
      txt => "interval graphs for broadcast and bogey traffic",
    },
    graph_adv          => {
      ttl => "Advanced Graphs",
      txt => "advanced graphs",
    },
    graph_adv_updown   => {
      ttl => "Advanced Graphs (up/down)",
      txt => "advanced graphs for upload and download traffic",
    },
    graph_adv_bb       => {
      ttl => "Advanced Graphs (bcast/bogey)",
      txt => "advanced graphs for broadcast and bogey traffic",
    },
  );
  return $views;
}


=item mk_views($common, $params, $extension)

Generate one or more views and return the contents. $extension is a scalar
reference; the referent will be set to the suggested filename extension if the
output were to be written to a file.

=cut

sub mk_views {
  my $common = shift; # hash ref
  my $params = shift; # hash ref
  my $extension = shift; # scalar ref
  my $cgi = $common->{cgi};
  $$extension = "html";
  my $text;
  # break out early if there are no parameters
  if (! keys(%$params)) {
    my $me = "View";
    $text .= cgi_start_xhtml($cgi, "$me usage");
    $text .= $cgi->h1("Captrap Viewer Help");
    $text .= cgi_list_params($cgi, $me, mk_param_info());
    $text .= cgi_list_views($common, mk_view_info());
    $text .= $cgi->end_html();
    return $text;
  }
  # must set this before init_priv() gets a chance to
  if (defined($params->{now})) {
    $common->{priv}->{now} = $params->{now};
  }
  my $item = $params->{item};
  my $views = mk_view_info();
  my $funcs = mk_view_funcs();
  $text .= cgi_start_xhtml($cgi, 'Captrap');
  $text .= $cgi->h1("Selected Captrap Views (data: $item)");
  foreach my $view (l2a($params->{views})) {
    if ($view eq "all") {
      foreach my $v (keys %$views) {
        $text .= $cgi->h2($views->{$v}->{ttl});
        $text .= &{$funcs->{$v}}($common, $params);
      }
    } elsif ($view eq "list") {
      $text .= cgi_list_views($common, $views);
    } elsif (defined($views->{$view})) {
      $text .= $cgi->h2($views->{$view}->{ttl});
      $text .= &{$funcs->{$view}}($common, $params);
    } else {
      mk_view_error($common->{cgi}, $view);
    }
  }
  $text .= $cgi->end_html();
  return $text;
}


=item cgi_list_views($common, $views)

Generate and return an HTML list of supported views. $views is the hash
returned by mk_view_info().

=cut

sub cgi_list_views {
  my $common = shift;
  my $views = shift; # hash ref
  my $cgi = $common->{cgi};
  my $text;
  $text .= $cgi->h2('Supported Views');
  $text .= $cgi->p("Multiple views can be retrieved in the same request by
      joining them with a comma. For example: \"views=basic,adv\"");
  $text .= $cgi->h3('Regular Views');
  foreach my $view (keys %$views) {
    $text .= $cgi->h5($view);
    $text .= $cgi->p($views->{$view}->{txt});
  }
  $text .= $cgi->h3('Meta-views');
  $text .= $cgi->h5("all");
  $text .= $cgi->p("all supported views (slow, useful for debug)");
  $text .= $cgi->h5("list");
  $text .= $cgi->p("this list of views");
  return $text;
}


# make the basic information view
sub mk_view_basic {
  my $common = shift;
  my $params = shift;
  my $cgi = $common->{cgi};
  my $config = $common->{config};
  my $item = $params->{item};
  my $needed = [ qw(
    avg_data
    data_current
    data_last
    intervals
    states
    times
    unknown_macs
   ) ];
  my $priv = init_priv($common, $needed);
  my $text;
    
  if (defined($priv->{now})) {
    $text .= $cgi->p("Specified current time: "
        . print_time($priv->{"times"}->{now}));
  } else {
    $text .= $cgi->p("Current time from database server: "
        . print_time($priv->{"times"}->{now}));
  }
  $text .= $cgi->p("Statistics are listed for interface " .
      "$config->{interface}.");

  $text .= $cgi->h4('Unknown MAC Addresses');
  my $macs = $priv->{unknown_macs};
  if (@$macs) {
    # todo: how?
    $text .= $cgi->p("The following MAC addresses are unrecognized. They " .
      "will be accounted for in totals but not in individual traffic states.");
    my $table = $cgi->Tr($cgi->td("MAC"), $cgi->td("bytes"),
        $cgi->td("first seen"), $cgi->td("last seen"));
    foreach my $row (@$macs) {
      my $tr;
      foreach my $td (@$row) {
        $tr .= $cgi->td($td);
      }
      $table .= $cgi->Tr($tr);
    }
    $text .= $cgi->table($table);
  } else {
    $text .= $cgi->p("No unknown MAC addresses found (this is good).");
  }

  # if "now" is specified, it gets rounded down, so there won't be any
  # current bytes
  unless (defined($priv->{now})) {
    $text .= $cgi->h4(ucfirst($item) . " Transferred During Current Hour");
    $text .= mk_data_table($cgi, $priv->{data_current},
        [ "current" ], $item, $priv->{states}, $config->{state_colors});
  }
  $text .= $cgi->h4(ucfirst($item) . " Transferred Over Last Intervals");
  unless (defined($priv->{now})) {
    $text .= $cgi->p("Note: this does not include $item transferred during " .
        "the current hour");
  }
  $text .= mk_data_table($cgi, $priv->{data_last}, $priv->{intervals}, $item,
      $priv->{states}, $config->{state_colors});
  $text .= $cgi->h4("Average Bytes per Day Over Last Intervals");
  $text .= mk_data_table($cgi, $priv->{avg_data}, $priv->{intervals}, $item,
      $priv->{states}, $config->{state_colors});
  return $text;
}


# make interval graphs
sub mk_view_graph_inter {
  my $common = shift;
  my $params = shift;
  return mk_graph_links_per($common, $params);
}


# make interval graphs with separate up/down bars
sub mk_view_graph_inter_updown {
  my $common = shift;
  my $params = shift;
  return mk_graph_links_per($common, $params, "up,down");
}


# make interval graphs with separate bcast/bogey bars
sub mk_view_graph_inter_bb {
  my $common = shift;
  my $params = shift;
  return mk_graph_links_per($common, $params, "bcast,bogey");
}


# make advanced graphs
sub mk_view_graph_adv {
  my $common = shift;
  my $params = shift;
  return mk_graph_links_mt($common, $params) .
      mk_graph_links_mtpred($common, $params, [ ]);
}


# make advanced graphs with separate up/down bars
sub mk_view_graph_adv_updown {
  my $common = shift;
  my $params = shift;
  return mk_graph_links_mt($common, $params, "up,down") .
      mk_graph_links_mtpred($common, $params, [ qw(up down) ]);
}


# make advanced graphs with separate bcast/bogey bars
sub mk_view_graph_adv_bb {
  my $common = shift;
  my $params = shift;
  return mk_graph_links_mt($common, $params, "bcast,bogey") .
      mk_graph_links_mtpred($common, $params, [ qw(bcast bogey) ]);
}


# print a useful error when a view doesn't exist
sub mk_view_error {
  my $cgi = shift;
  my $view = shift;
  print $cgi->h2('Error');
  print $cgi->p("No such view: $view");
}

# -----------------------------------------------------------------------------
# param info
# -----------------------------------------------------------------------------


=item mk_param_info()

Set up a hash of all parameters the viewer accepts, along with their defaults.

=cut

# each parameter MUST have a key here
sub mk_param_info () {
  my $info = mk_ixhash();
  # don't forget to start regexes with a ^ and end with a $
  %$info = (
    views => {
      def => "basic",
      # Just check for valid characters;
      # mk_views will handle unknown views gracefully
      reg => qr/^[[:word:],]{1,100}$/,
      txt => "Comma-separated list of views to display. Use 'views=list' " .
          "for information about available views.",
      var => 's',
    },
    item => {
      def => "bytes",
      reg => qr/^(bytes|packets)$/,
      txt => "Item to be viewed ('bytes' or 'packets').",
      var => 's',
    },
    now => {
      def => undef,
      reg => time_re("second", "T"),
      txt => "Current time. This will be rounded down to the nearest hour " .
          "and used as a maximum time for data retrieval.",
      var => 's',
    },
    patterns => {
      def => undef,
      reg => qr/^[01]$/,
      txt => "Set to 1 to draw graphs with patterned bars, or 0 to\n" .
          "disable. See the \"patterns_default\" configuration parameter.",
      var => 's',
    },
  );
  return $info;
}


1;

=back

=head1 AUTHOR

Corey Hickey <bugfood-c@fatooh.org>

This library is free software; you may redistribute and/or modify it under the
terms of the GNU General Public License, version 3. See the source file for the
usual GPL preamble and the COPYING file for a copy of the GPL.

=head1 SEE ALSO

Captrap, Captrap::Graph, Captrap::Main, captrap_view, viewer.pl

=cut
