# -*- 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>

use strict;
use lib ".";

# This module represents a chart.
#
# Note that it is perfectly legal for the 'lines' member variable of this
# class (which is an array of Bugzilla::Series objects) to have empty members
# in it. If this is true, the 'labels' array will also have empty members at
# the same points.
package Bugzilla::Chart;

use Bugzilla::Util;
use Bugzilla::Series;

sub new {
    my $invocant = shift;
    my $class = ref($invocant) || $invocant;
  
    # Create a ref to an empty hash and bless it
    my $self = {};
    bless($self, $class);

    if ($#_ == 0) {
        # Construct from a CGI object.
        $self->init($_[0]);
    } 
    else {
        die("CGI object not passed in - invalid number of args \($#_\)($_)");
    }

    return $self;
}

sub init {
    my $self = shift;
    my $cgi = shift;

    # The data structure is a list of lists (lines) of Series objects. 
    # There is a separate list for the labels.
    #
    # The URL encoding is:
    # line0=67&line0=73&line1=81&line2=67...
    # &label0=B+/+R+/+NEW&label1=...
    # &select0=1&select3=1...    
    # &cumulate=1&datefrom=2002-02-03&dateto=2002-04-04&ctype=html...
    # &gt=1&labelgt=Grand+Total    
    foreach my $param ($cgi->param()) {
        # Store all the lines
        if ($param =~ /^line(\d+)$/) {
            foreach my $series_id ($cgi->param($param)) {
                detaint_natural($series_id) 
                                     || &::ThrowCodeError("invalid_series_id");
                push(@{$self->{'lines'}[$1]}, 
                     new Bugzilla::Series($series_id));
            }
        }

        # Store all the labels
        if ($param =~ /^label(\d+)$/) {
            $self->{'labels'}[$1] = $cgi->param($param);
        }        
    }
    
    # Store the miscellaneous metadata
    $self->{'cumulate'} = $cgi->param('cumulate') ? 1 : 0;
    $self->{'gt'}       = $cgi->param('gt') ? 1 : 0;
    $self->{'labelgt'}  = $cgi->param('labelgt');
    $self->{'datefrom'} = $cgi->param('datefrom');
    $self->{'dateto'}   = $cgi->param('dateto');
    
    # If we are cumulating, a grand total makes no sense
    $self->{'gt'} = 0 if $self->{'cumulate'};
    
    # Make sure the dates are ones we are able to interpret
    foreach my $date ('datefrom', 'dateto') {
        if ($self->{$date}) {
            $self->{$date} = &::str2time($self->{$date}) 
              || &::ThrowUserError("illegal_date", { date => $self->{$date}});
        }
    }

    # datefrom can't be after dateto
    if ($self->{'datefrom'} && $self->{'dateto'} && 
        $self->{'datefrom'} > $self->{'dateto'}) 
    {
          &::ThrowUserError("misarranged_dates", 
                                         {'datefrom' => $cgi->param('datefrom'),
                                          'dateto' => $cgi->param('dateto')});
    }    
}

# Alter Chart so that the selected series are added to it.
sub add {
    my $self = shift;
    my @series_ids = @_;

    # If we are going from < 2 to >= 2 series, add the Grand Total line.
    if (!$self->{'gt'}) {
        my $current_size = scalar($self->getSeriesIDs());
        if ($current_size < 2 &&
            $current_size + scalar(@series_ids) >= 2) 
        {
            $self->{'gt'} = 1;
        }
    }
        
    # Create new Series and push them on to the list of lines.
    # Note that new lines have no label; the display template is responsible
    # for inventing something sensible.
    foreach my $series_id (@series_ids) {
        my $series = new Bugzilla::Series($series_id);
        push(@{$self->{'lines'}}, [$series]);
        push(@{$self->{'labels'}}, "");
    }
}

# Alter Chart so that the selections are removed from it.
sub remove {
    my $self = shift;
    my @line_ids = @_;
    
    foreach my $line_id (@line_ids) {
        if ($line_id == 65536) {
            # Magic value - delete Grand Total.
            $self->{'gt'} = 0;
        } 
        else {
            delete($self->{'lines'}->[$line_id]);
            delete($self->{'labels'}->[$line_id]);
        }
    }
}

# Alter Chart so that the selections are summed.
sub sum {
    my $self = shift;
    my @line_ids = @_;
    
    # We can't add the Grand Total to things.
    @line_ids = grep(!/^65536$/, @line_ids);
        
    # We can't add less than two things.
    return if scalar(@line_ids) < 2;
    
    my @series;
    my $label = "";
    my $biggestlength = 0;
    
    # We rescue the Series objects of all the series involved in the sum.
    foreach my $line_id (@line_ids) {
        my @line = @{$self->{'lines'}->[$line_id]};
        
        foreach my $series (@line) {
            push(@series, $series);
        }
        
        # We keep the label that labels the line with the most series.
        if (scalar(@line) > $biggestlength) {
            $biggestlength = scalar(@line);
            $label = $self->{'labels'}->[$line_id];
        }
    }

    $self->remove(@line_ids);

    push(@{$self->{'lines'}}, \@series);
    push(@{$self->{'labels'}}, $label);
}

sub data {
    my $self = shift;
    $self->{'_data'} ||= $self->readData();
    return $self->{'_data'};
}

# Convert the Chart's data into a plottable form in $self->{'_data'}.
sub readData {
    my $self = shift;
    my @data;

    my $series_ids = join(",", $self->getSeriesIDs());

    # Work out the date boundaries for our data.
    my $dbh = Bugzilla->dbh;
    
    # The date used is the one given if it's in a sensible range; otherwise,
    # it's the earliest or latest date in the database as appropriate.
    my $datefrom = $dbh->selectrow_array("SELECT MIN(date) FROM series_data " .
                                         "WHERE series_id IN ($series_ids)");
    $datefrom = &::str2time($datefrom);

    if ($self->{'datefrom'} && $self->{'datefrom'} > $datefrom) {
        $datefrom = $self->{'datefrom'};
    }

    my $dateto = $dbh->selectrow_array("SELECT MAX(date) FROM series_data " .
                                       "WHERE series_id IN ($series_ids)");
    $dateto = &::str2time($dateto); 

    if ($self->{'dateto'} && $self->{'dateto'} < $dateto) {
        $dateto = $self->{'dateto'};
    }

    # Prepare the query which retrieves the data for each series
    my $query = "SELECT TO_DAYS(date) - TO_DAYS(FROM_UNIXTIME($datefrom)), " . 
                "value FROM series_data " .
                "WHERE series_id = ? " .
                "AND date >= FROM_UNIXTIME($datefrom)";
    if ($dateto) {
        $query .= " AND date <= FROM_UNIXTIME($dateto)";
    }
    
    my $sth = $dbh->prepare($query);

    my $gt_index = $self->{'gt'} ? scalar(@{$self->{'lines'}}) : undef;
    my $line_index = 0;

    foreach my $line (@{$self->{'lines'}}) {        
        # Even if we end up with no data, we need an empty arrayref to prevent
        # errors in the PNG-generating code
        $data[$line_index] = [];

        foreach my $series (@$line) {

            # Get the data for this series and add it on
            $sth->execute($series->{'series_id'});
            my $points = $sth->fetchall_arrayref();

            foreach my $point (@$points) {
                my ($datediff, $value) = @$point;
                $data[$line_index][$datediff] ||= 0;
                $data[$line_index][$datediff] += $value;

                # Add to the grand total, if we are doing that
                if ($gt_index) {
                    $data[$gt_index][$datediff] ||= 0;
                    $data[$gt_index][$datediff] += $value;
                }
            }
        }

        $line_index++;
    }

    # Add the x-axis labels into the data structure
    my $date_progression = generateDateProgression($datefrom, $dateto);
    unshift(@data, $date_progression);

    if ($self->{'gt'}) {
        # Add Grand Total to label list
        push(@{$self->{'labels'}}, $self->{'labelgt'});

        $data[$gt_index] ||= [];
    }

    return \@data;
}

# Flatten the data structure into a list of series_ids
sub getSeriesIDs {
    my $self = shift;
    my @series_ids;

    foreach my $line (@{$self->{'lines'}}) {
        foreach my $series (@$line) {
            push(@series_ids, $series->{'series_id'});
        }
    }

    return @series_ids;
}

# Class method to get the data necessary to populate the "select series"
# widgets on various pages.
sub getVisibleSeries {
    my %cats;

    # Get all visible series
    my $dbh = Bugzilla->dbh;
    my $serieses = $dbh->selectall_arrayref("SELECT cc1.name, cc2.name, " .
                        "series.name, series.series_id " .
                        "FROM series " .
                        "LEFT JOIN series_categories AS cc1 " .
                        "    ON series.category = cc1.category_id " .
                        "LEFT JOIN series_categories AS cc2 " .
                        "    ON series.subcategory = cc2.category_id " .
                        "LEFT JOIN user_series_map AS ucm " .
                        "    ON series.series_id = ucm.series_id " .
                        "WHERE ucm.user_id = 0 OR ucm.user_id = $::userid");

    foreach my $series (@$serieses) {
        my ($cat, $subcat, $name, $series_id) = @$series;
        $cats{$cat}{$subcat}{$name} = $series_id;
    }

    return \%cats;
}

sub generateDateProgression {
    my ($datefrom, $dateto) = @_;
    my @progression;

    $dateto = $dateto || time();
    my $oneday = 60 * 60 * 24;

    # When the from and to dates are converted by str2time(), you end up with
    # a time figure representing midnight at the beginning of that day. We
    # adjust the times by 1/3 and 2/3 of a day respectively to prevent
    # edge conditions in time2str().
    $datefrom += $oneday / 3;
    $dateto += (2 * $oneday) / 3;

    while ($datefrom < $dateto) {
        push (@progression, &::time2str("%Y-%m-%d", $datefrom));
        $datefrom += $oneday;
    }

    return \@progression;
}

sub dump {
    my $self = shift;

    # Make sure we've read in our data
    my $data = $self->data;
    
    require Data::Dumper;
    print "<pre>Bugzilla::Chart object:\n";
    print Data::Dumper::Dumper($self);
    print "</pre>";
}

1;
