#!/usr/bin/perl -wT
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
# Contributor(s): Gervase Markham <gerv@gerv.net>

# Glossary:
# series:  An individual, defined set of data plotted over time.
# line:    A set of one or more series, to be summed and drawn as a single
#          line when the series is plotted.
# chart:   A set of lines
# So when you select rows in the UI, you are selecting one or more lines, not
# series.

# Generic Charting TODO:
#
# JS-less chart creation - hard.
# Broken image on error or no data - need to do much better.
# Centralise permission checking, so UserInGroup('editbugs') not scattered
#   everywhere.
# Better protection on collectstats.pl for second run in a day
# User documentation :-)
#
# Bonus:
# Offer subscription when you get a "series already exists" error?

use strict;
use lib qw(.);

require "CGI.pl";
use Bugzilla::Chart;
use Bugzilla::Series;

use vars qw($cgi $template $vars);

# Go back to query.cgi if we are adding a boolean chart parameter.
if (grep(/^cmd-/, $cgi->param())) {
    my $params = $cgi->canonicalise_query("format", "ctype", "action");
    print "Location: query.cgi?format=" . $cgi->param('query_format') .
                                          ($params ? "&$params" : "") . "\n\n";
    exit;
}

my $template = Bugzilla->template;
my $action = $cgi->param('action');
my $series_id = $cgi->param('series_id');

# Because some actions are chosen by buttons, we can't encode them as the value
# of the action param, because that value is localisation-dependent. So, we
# encode it in the name, as "action-<action>". Some params even contain the
# series_id they apply to (e.g. subscribe, unsubscribe.)
my @actions = grep(/^action-/, $cgi->param());
if ($actions[0] && $actions[0] =~ /^action-([^\d]+)(\d*)$/) {
    $action = $1;
    $series_id = $2 if $2;
}

$action ||= "assemble";

# Go to buglist.cgi if we are doing a search.
if ($action eq "search") {
    my $params = $cgi->canonicalise_query("format", "ctype", "action");
    print "Location: buglist.cgi" . ($params ? "?$params" : "") . "\n\n";
    exit;
}

ConnectToDatabase();

confirm_login();

# All these actions relate to chart construction.
if ($action =~ /^(assemble|add|remove|sum|subscribe|unsubscribe)$/) {
    # These two need to be done before the creation of the Chart object, so
    # that the changes they make will be reflected in it.
    if ($action =~ /^subscribe|unsubscribe$/) {
        my $series = new Bugzilla::Series($series_id);
        $series->$action($::userid);
    }

    my $chart = new Bugzilla::Chart($cgi);

    if ($action =~ /^remove|sum$/) {
        $chart->$action(getSelectedLines());
    }
    elsif ($action eq "add") {
        my @series_ids = getAndValidateSeriesIDs();
        $chart->add(@series_ids);
    }

    view($chart);
}
elsif ($action eq "plot") {
    plot();
}
elsif ($action eq "wrap") {
    # For CSV "wrap", we go straight to "plot".
    if ($cgi->param('ctype') && $cgi->param('ctype') eq "csv") {
        plot();
    }
    else {
        wrap();
    }
}
elsif ($action eq "create") {
    assertCanCreate($cgi);
    my $series = new Bugzilla::Series($cgi);

    if (ref($series)) {
        $vars->{'message'} = "series_created";
    }
    else {
        $vars->{'message'} = "series_already_exists";
        $series = new Bugzilla::Series($series);
    }

    $vars->{'series'} = $series;

    print "Content-Type: text/html\n\n";
    $template->process("global/message.html.tmpl", $vars)
      || ThrowTemplateError($template->error());
}
elsif ($action eq "edit") {
    $series_id || ThrowCodeError("invalid_series_id");
    assertCanEdit($series_id);

    my $series = new Bugzilla::Series($series_id);
    edit($series);
}
elsif ($action eq "alter") {
    $series_id || ThrowCodeError("invalid_series_id");
    assertCanEdit($series_id);

    my $series = new Bugzilla::Series($series_id);
    $series->alter($cgi);
    edit($series);
}
else {
    ThrowCodeError("unknown_action");
}

exit;

# Find any selected series and return either the first or all of them.
sub getAndValidateSeriesIDs {
    my @series_ids = grep(/^\d+$/, $cgi->param("name"));

    return wantarray ? @series_ids : $series_ids[0];
}

# Return a list of IDs of all the lines selected in the UI.
sub getSelectedLines {
    my @ids = map { /^select(\d+)$/ ? $1 : () } $cgi->param();

    return @ids;
}

# Check if the user is the owner of series_id or is an admin. 
sub assertCanEdit {
    my ($series_id) = @_;
    
    return if UserInGroup("admin");

    my $dbh = Bugzilla->dbh;
    my $iscreator = $dbh->selectrow_array("SELECT creator = ? FROM series " .
                                          "WHERE series_id = ?", undef,
                                          $::userid, $series_id);
    $iscreator || ThrowUserError("illegal_series_edit");
}

# Check if the user is permitted to create this series with these parameters.
sub assertCanCreate {
    my ($cgi) = shift;
    
    UserInGroup("editbugs") || ThrowUserError("illegal_series_creation");

    # Only admins may create public queries
    UserInGroup('admin') || $cgi->delete('public');
    
    # Check permission for frequency
    my $min_freq = 7;
    if ($cgi->param('frequency') < $min_freq && !UserInGroup("admin")) {
        ThrowUserError("illegal_frequency", { 'minimum' => $min_freq });
    }    
}

sub validateWidthAndHeight {
    $vars->{'width'} = $cgi->param('width');
    $vars->{'height'} = $cgi->param('height');

    if (defined($vars->{'width'})) {
       (detaint_natural($vars->{'width'}) && $vars->{'width'} > 0)
         || ThrowCodeError("invalid_dimensions");
    }

    if (defined($vars->{'height'})) {
       (detaint_natural($vars->{'height'}) && $vars->{'height'} > 0)
         || ThrowCodeError("invalid_dimensions");
    }

    # The equivalent of 2000 square seems like a very reasonable maximum size.
    # This is merely meant to prevent accidental or deliberate DOS, and should
    # have no effect in practice.
    if ($vars->{'width'} && $vars->{'height'}) {
       (($vars->{'width'} * $vars->{'height'}) <= 4000000)
         || ThrowUserError("chart_too_large");
    }
}

sub edit {
    my $series = shift;

    $vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
    $vars->{'creator'} = new Bugzilla::User($series->{'creator'});

    # If we've got any parameters, use those in preference to the values
    # read from the database. This is a bit ugly, but I can't see a better
    # way to make this work in the no-JS situation.
    if ($cgi->param('category') || $cgi->param('subcategory') ||
        $cgi->param('name') || $cgi->param('frequency') ||
        $cgi->param('public'))
    {
        $vars->{'default'} = new Bugzilla::Series($series->{'series_id'},
          $cgi->param('category')    || $series->{'category'},
          $cgi->param('subcategory') || $series->{'subcategory'},
          $cgi->param('name')        || $series->{'name'},
          $series->{'creator'},
          $cgi->param('frequency')   || $series->{'frequency'});

        $vars->{'default'}{'public'}
                                = $cgi->param('public') || $series->{'public'};
    }
    else {
        $vars->{'default'} = $series;
    }

    print "Content-Type: text/html\n\n";
    $template->process("reports/edit-series.html.tmpl", $vars)
      || ThrowTemplateError($template->error());
}

sub plot {
    validateWidthAndHeight();
    $vars->{'chart'} = new Bugzilla::Chart($cgi);

    my $format = &::GetFormat("reports/chart",
                              "",
                              $cgi->param('ctype'));

    # Debugging PNGs is a pain; we need to be able to see the error messages
    if ($cgi->param('debug')) {
        print "Content-Type: text/html\n\n";
        $vars->{'chart'}->dump();
    }

    print "Content-Type: $format->{'ctype'}\n\n";
    $template->process($format->{'template'}, $vars)
      || ThrowTemplateError($template->error());
}

sub wrap {
    validateWidthAndHeight();
    
    # We create a Chart object so we can validate the parameters
    my $chart = new Bugzilla::Chart($cgi);
    
    $vars->{'time'} = time();

    $vars->{'imagebase'} = $cgi->canonicalise_query(
                "action", "action-wrap", "ctype", "format", "width", "height");

    print "Content-Type:text/html\n\n";
    $template->process("reports/chart.html.tmpl", $vars)
      || ThrowTemplateError($template->error());
}

sub view {
    my $chart = shift;

    # Set defaults
    foreach my $field ('category', 'subcategory', 'name', 'ctype') {
        $vars->{'default'}{$field} = $cgi->param($field) || 0;
    }

    # Pass the state object to the display UI.
    $vars->{'chart'} = $chart;
    $vars->{'category'} = Bugzilla::Chart::getVisibleSeries();

    print "Content-Type: text/html\n\n";

    # If we have having problems with bad data, we can set debug=1 to dump
    # the data structure.
    $chart->dump() if $cgi->param('debug');

    $template->process("reports/create-chart.html.tmpl", $vars)
      || ThrowTemplateError($template->error());
}
