###############################################################################
# Copyright (c) 2003 Jason Remillard.  All rights reserved.
#
# This program is free software; you can redistribute it and modify it under
# the terms of the GPL.

# Model object for handling metrics reports. This object is not an object, it
# is a normal module.

package Codestriker::Model::MetricStats;

use strict;
use Encode qw(decode_utf8);

use Codestriker::DB::DBI;

my $total_participants_header = 'Total Participants';
my $topic_size_lines_header = 'Topic Size In Lines';

# Returns the list of users that have participated in a Codestriker topics.
#
#
# Returns a collection of the following hash references.
# {
#   name
#   date_last_authored
#   date_last_participated
#   total_topics
# }

sub get_basic_user_metrics {
    # Gather basic user metrics for the past 16 weeks.
    my $last_n_days = 16*7;

    my $date = Codestriker->get_timestamp(time-($last_n_days*24*60*60));

    # Obtain a database connection.
    my $dbh = Codestriker::DB::DBI->get_connection();

    # Get the list of authors.
    my $author_list = $dbh->selectall_arrayref(
                                               'SELECT LOWER(author),MAX(modified_ts),COUNT(id)
         FROM topic
         WHERE modified_ts >= ?
         GROUP BY author
         ORDER BY 2 desc', {}, $date);

    my @users_metrics;

    foreach my $row (@$author_list) {
        my ($name, $last_authored_date, $count) = @$row;

        my $metrics =
          {
           name=>$name,
           date_last_authored=>
           int((time() - Codestriker->convert_date_timestamp_time($last_authored_date))/(60*60*24)),
           date_last_participated=>'',
           total_codestriker_time =>
           calculate_topic_view_time_for_user($date,$name),
           total_topics=>$count,
          };

        push(@users_metrics, $metrics);
    }

    # Get the list of participants from all these topics. You need to
    # submit at least one comment to be counted.
    my $participant_list = $dbh->selectall_arrayref(
                                                    'SELECT LOWER(commentdata.author),
            MAX(topic.modified_ts),
            COUNT(DISTINCT topic.id)
         FROM commentdata, commentstate, topic
         WHERE topic.modified_ts >= ? AND
           topic.id = commentstate.topicid AND
           LOWER(topic.author) <> LOWER(commentdata.author) AND
           commentdata.commentstateid = commentstate.id
         GROUP BY LOWER(commentdata.author)
         ORDER BY 2 desc',{}, $date);

    foreach my $row (@$participant_list) {
        my ($name, $last_participated_date, $count) = @$row;

        my $found = 0;
        foreach my $user (@users_metrics) {
            if ($user->{name} eq $name) {
                $user->{date_last_participated} =
                  int((time() -
                       Codestriker->convert_date_timestamp_time($last_participated_date))/(60*60*24));
                $user->{total_topics} += $count;
                $found = 1;
            }
        }

        if ($found == 0) {
            my $metrics =
              {
               name=>$name,
               date_last_authored=>'',
               date_last_participated=>
               int((time() -
                    Codestriker->convert_date_timestamp_time($last_participated_date))/(60*60*24)),
               total_topics=>$count,
               total_codestriker_time =>
               calculate_topic_view_time_for_user($date,$name),
              };

            push(@users_metrics, $metrics);
        }
    }

    # Close the connection, and check for any database errors.
    Codestriker::DB::DBI->release_connection($dbh, 1);

    return @users_metrics;
}

# Given the username, and the oldest date, calculate the total amount of
# codestriker time that was recorded in hours.
sub calculate_topic_view_time_for_user {
    my ($date,$user) = @_;

    # get the total time for this user.
    my $dbh = Codestriker::DB::DBI->get_connection();

    my $select_topic = $dbh->prepare_cached('SELECT creation_ts ' .
                                            'FROM topicviewhistory ' .
                                            'WHERE creation_ts > ? AND ' .
                                            'LOWER(email) = LOWER(?) ' .
                                            'ORDER BY creation_ts');

    $select_topic->execute($date,$user);

    my $total_time =
      Codestriker::Model::Metrics->calculate_topic_view_time($select_topic);

    Codestriker::DB::DBI->release_connection($dbh, 1);

    $total_time = sprintf("%1.1f",$total_time / (60*60));

    return $total_time;
}


# Returns a list of all the topic ids in the database.
sub get_topic_ids {
    # Obtain a database connection.
    my $dbh = Codestriker::DB::DBI->get_connection();

    # Get the list of authors.
    my $topicid_list  = $dbh->selectall_arrayref(
                                                 'SELECT id FROM topic ORDER BY creation_ts');

    # Close the connection, and check for any database errors.
    Codestriker::DB::DBI->release_connection($dbh, 1);

    return @$topicid_list;

}

# Returns in a hash the column names for the raw metric download feature.
sub get_download_headers {
    # Obtain a database connection.
    my $dbh = Codestriker::DB::DBI->get_connection();

    my @base_headers = qw(
                             TopicID
                             Author
                             Title
                             State
                             Creation
                             Project
                        );

    # Comment metric counts.
    my @comment_metric_headers;
    my $comment_metrics = $dbh->selectall_arrayref(
                                                   'SELECT DISTINCT name
         FROM commentstatemetric
         ORDER BY name
        ');
    foreach my $metric (@$comment_metrics) {
        push @comment_metric_headers, $metric->[0];
    }

    # Do the built-in comment metrics.
    push @comment_metric_headers, 'Comment Threads';
    push @comment_metric_headers, 'Submitted Comments';

    # Topic metrics counts.
    my @topic_metric_headers;
    my $topic_metrics = $dbh->selectall_arrayref(
                                                 'SELECT DISTINCT metric_name
         FROM topicmetric
         ORDER by metric_name
        ');

    foreach my $metric (@$topic_metrics) {
        push @topic_metric_headers, $metric->[0];
    }

    # Do the built in topic metrics.
    for (my $state = 0; $state < scalar(@Codestriker::topic_states);
         ++$state) {
        push @topic_metric_headers,
          'Time In ' . $Codestriker::topic_states[$state];
    }

    push @topic_metric_headers, $topic_size_lines_header;

    my @topic_user_metric_headers;

    # User topic metrics counts.
    my $user_topic_metrics  = $dbh->selectall_arrayref(
                                                       'SELECT DISTINCT metric_name
         FROM topicusermetric
         ORDER by metric_name
        ');

    foreach my $metric (@$user_topic_metrics) {
        push @topic_user_metric_headers, $metric->[0];
    }

    # Do the built in user metrics.
    push @topic_user_metric_headers, 'Codestriker Time';
    push @topic_user_metric_headers, $total_participants_header;

    my $headers =
      {
       base => \@base_headers,
       comment => \@comment_metric_headers,
       topic => \@topic_metric_headers,
       user => \@topic_user_metric_headers
      };

    # Close the connection, and check for any database errors.
    Codestriker::DB::DBI->release_connection($dbh, 1);

    return $headers;
}

# Given a topic id, and a header hash, return an list with all of the
# topics metric data returned in the order given by the header
# information.
sub get_raw_metric_data {
    my ($topicid, $headers) = @_;

    # Obtain a database connection.
    my $dbh = Codestriker::DB::DBI->get_connection();

    my @basic_topic_info = $dbh->selectrow_array('
        SELECT topic.id,
           LOWER(topic.author),
           topic.title,
           topic.state,
           topic.creation_ts,
           project.name
        FROM topic, project
        WHERE topic.id = ? AND
          topic.projectid = project.id',{}, $topicid);

    # Decode topic title and project name.
    $basic_topic_info[2] = decode_utf8($basic_topic_info[2]);
    $basic_topic_info[5] = decode_utf8($basic_topic_info[5]);

    if ($basic_topic_info[3] < @Codestriker::topic_states) {
        $basic_topic_info[3] =
          @Codestriker::topic_states[$basic_topic_info[3]];
    }

    $basic_topic_info[4] =
      Codestriker->format_date_timestamp($basic_topic_info[4]);

    $basic_topic_info[1] =
      Codestriker->filter_email($basic_topic_info[1]);

    my @row;

    push @row, @basic_topic_info;

    # Process the comment metric values.
    my $count_query =
      $dbh->prepare_cached('SELECT COUNT(*) ' .
                           'FROM topic, commentstate, commentstatemetric ' .
                           'WHERE topic.id = ? AND ' .
                           'topic.id = commentstate.topicid AND ' .
                           'commentstate.id = commentstatemetric.id AND '.
                           'commentstatemetric.name = ? ');
    foreach my $metric (@{$headers->{comment}}) {
        # Get the count for this metric name.
        $count_query->execute($topicid, $metric);
        my ($count) = $count_query->fetchrow_array();
        $count_query->finish();
        push @row, $count;
    }

    # Now process the 'Comment Threads' metric.
    my $comment_threads = $dbh->selectall_arrayref(
                                                   'SELECT COUNT(id)
         FROM commentstate
         WHERE topicid = ?
        ',{}, $topicid);
    push @row, $comment_threads->[0]->[0];

    # Now process the 'Submitted Comments' metric.
    my $submitted_comments = $dbh->selectall_arrayref(
                                                      'SELECT COUNT(id)
         FROM commentstate, commentdata
         WHERE commentstate.topicid = ?
             AND commentstate.id = commentdata.commentstateid
        ',{}, $topicid);
    push @row, $submitted_comments->[0]->[0];

    my $topic = Codestriker::Model::Topic->new($basic_topic_info[0]);

    my $metrics = $topic->get_metrics();

    my @topic_metrics = $metrics->get_topic_metrics();

    for (my $index = 0; $index < scalar(@{$headers->{topic}}); ++$index) {
        my $count = "";

        foreach my $metric ( @topic_metrics ) {
            $count = $metric->{value}
              if ($metric->{name} eq $headers->{topic}->[$index]);
        }

        if ($headers->{topic}->[$index] eq $topic_size_lines_header ) {
            $count = $topic->get_topic_size_in_lines();
        }

        push @row, $count;
    }

    # Get the list of users for this review.
    my @users = $metrics->get_complete_list_of_topic_participants();

    my @user_metrics = $metrics->get_user_metrics_totals(@users);

    for (my $index = 0; $index < scalar(@{$headers->{user}}); ++$index) {
        my $count = "";

        foreach my $metric ( @user_metrics ) {
            $count = $metric->{value}
              if ($metric->{name} eq $headers->{user}->[$index]);
        }

        if ($headers->{user}->[$index] eq $total_participants_header) {
            # Add the total number of participants in the topic.
            $count = scalar(@users);
        }

        push @row, $count;
    }


    # Close the connection, and check for any database errors.
    Codestriker::DB::DBI->release_connection($dbh, 1);

    return @row;
}

# Returns 12 months of comment metrics data.
#
# returns a collection of the following hash references
# {
#   name  = the comment metric name
#   results = collection ref to
#             {
#               name = the comment value name.
#               counts = array ref to metric counts per month
#               monthnames = array ref to month names
#             }
# }
sub get_comment_metrics {
    # Stores the collection results.
    my @results = ();

    # Obtain a database connection.
    my $dbh = Codestriker::DB::DBI->get_connection();

    # Get the comment metric totals.
    foreach my $metric_config (@{ $Codestriker::comment_state_metrics }) {
        my $metric_name = $metric_config->{name};
        my $query =
          "SELECT commentstatemetric.value, COUNT(commentstate.id) " .
            "FROM commentstate, commentstatemetric " .
              "WHERE commentstatemetric.id = commentstate.id AND " .
                "commentstatemetric.name = " . $dbh->quote($metric_name) . " AND " .
                  "commentstate.creation_ts > ? AND " .
                    "commentstate.creation_ts <= ? " .
                      "GROUP BY commentstatemetric.value " .
                        "ORDER BY commentstatemetric.value";

        my @metrics = _get_monthly_metrics(12, $query);
        my $months_ref = $metrics[0]->{monthnames};

        # Make sure all enumerated values are catered for.
        my %handled_value = ();
        foreach my $value (@metrics) {
            $handled_value{$value->{name}} = 1;
        }
        foreach my $value (@{ $metric_config->{values} }) {
            if (! defined $handled_value{$value}) {
                push @metrics, { name => $value,
                                 counts => [0,0,0,0,0,0,0,0,0,0,0,0],
                                 monthnames => $months_ref };
            }
        }

        my $result = { name => $metric_name, results => \@metrics };
        push @results, $result;
    }

    # Get comment thread totals.
    my @total_metrics = ();
    my @thread_total = _get_monthly_metrics(12,
                                            'SELECT \'Comment Threads\', COUNT(commentstate.id)
    FROM commentstate
    WHERE commentstate.creation_ts >  ? AND
          commentstate.creation_ts <= ?');
    push @total_metrics, @thread_total;

    # Get submitted comment totals.
    my @submitted_total = _get_monthly_metrics(12,
                                               'SELECT \'Submitted Comments\', COUNT(commentstate.id)
    FROM commentstate, commentdata
    WHERE commentstate.id = commentdata.commentstateid AND
              commentstate.creation_ts >  ? AND
          commentstate.creation_ts <= ?');
    push @total_metrics, @submitted_total;

    my $result = { name => 'Total', results => \@total_metrics };
    push @results, $result;

    # Close the connection, and check for any database errors.
    Codestriker::DB::DBI->release_connection($dbh, 1);

    return @results;
}

# Returns 12 months of data with a break down of topic metrics.
#
# Returns a collection of the following hash references:
# {
#   name  = the metric name
#   counts = array ref to metric counts per month
#   monthnames = array ref to month names
# }
sub get_topic_metrics {
    my @metrics;

    # Get total.
    my @total = _get_monthly_metrics(12,
                                     'SELECT \'Total Topics\', COUNT(topic.id)
    FROM topic
    WHERE topic.creation_ts >  ? AND
          topic.creation_ts <= ?');

    push @metrics, @total;

    # Get totals for the topic metrics.
    @total = _get_monthly_metrics(12,
                                  'SELECT topicmetric.metric_name, SUM(topicmetric.value)
    FROM topicmetric,topic
    WHERE topic.creation_ts >  ? AND
          topic.creation_ts <= ? AND
          topicmetric.topicid = topic.id
          GROUP BY topicmetric.metric_name
          ORDER BY topicmetric.metric_name');

    push @metrics, @total;

    # Get totals for the topic user metrics.
    @total = _get_monthly_metrics(12,
                                  'SELECT topicusermetric.metric_name, SUM(topicusermetric.value)
    FROM topicusermetric,topic
    WHERE topic.creation_ts >  ? AND
          topic.creation_ts <= ? AND
          topicusermetric.topicid = topic.id
          GROUP BY topicusermetric.metric_name
          ORDER BY topicusermetric.metric_name');

    push @metrics, @total;


    return @metrics;
}


# Returns $total_months of data for the given query. The query must return
# name, count collection of rows between two times.
#
# returns a collection of the following hash references
# {
#   name  = the metric name
#   counts = array ref to metric counts per month
#   monthnames = array ref to month names
# }
sub _get_monthly_metrics {
    my ($total_months, $dbi_query_string) = @_;

    # Obtain a database connection.
    my $dbh = Codestriker::DB::DBI->get_connection();

    my @month_dbi_times = _dbi_month_time_stamp($total_months);
    my @month_names = _user_month_name_list($total_months);

    my @metrics;

    # For the past year, get the metrics counts for each month.
    for (my $month_count = 0;
         $month_count+1 < @month_dbi_times;
         ++$month_count) {

        # Do the db query.
        my $comment_counts = $dbh->selectall_arrayref(
                                                      $dbi_query_string,
                                                      {
                                                      },
                                                      $month_dbi_times[$month_count],
                                                      $month_dbi_times[$month_count+1]);

        foreach my $row (@$comment_counts) {
            my ($db_metric_name, $db_count) = @$row;

            my $found = 0;

            # See if we can find the metric.
            foreach my $metric (@metrics) {
                if ($metric->{name} eq $db_metric_name) {
                    push @{$metric->{counts}}, $db_count;
                    $found = 1;
                    last;
                }
            }

            if ($found == 0) {
                my $metric =
                  {
                   name=>$db_metric_name,
                   counts=>[],
                   monthnames=>\@month_names
                  };

                # Catch up the collection of counts on any missed months.
                for ( my $missingmonths = 0;
                      $missingmonths < $month_count;
                      ++$missingmonths) {
                    push @{$metric->{counts}}, 0;
                }

                push @{$metric->{counts}}, $db_count;

                push @metrics, $metric;
            }
        }

        # Add zero's to any metrics not present.
        foreach my $metric (@metrics) {
            if (@{$metric->{counts}} eq $month_count) {
                push @{$metric->{counts}}, 0;
            }
        }
    }

    # Close the connection, and check for any database errors.
    Codestriker::DB::DBI->release_connection($dbh, 1);

    return @metrics;
}

# Return a list of dbi time stamp on 1 month boundaries back for n months. This
# is used to do db queries to get data cut up by month.
sub _dbi_month_time_stamp {
    my ($number_months_back) = @_;

    # Get the start time of this month

    my @month = _add_month(-$number_months_back+1,localtime(time()));

    my @month_dbi_ts;

    for (my $count = 0; $count < $number_months_back +1; ++$count) {
        # Calculate the start of this month dbi string.
        my $month_start = sprintf("%04d-%02d-01 00:00:00",
                                  $month[5]+1900,
                                  $month[4]+1);

        push @month_dbi_ts, $month_start;

        @month = _add_month(1, @month);
    }

    return @month_dbi_ts;
}

# Return a list of user displayable time stamps on 1 month bondaries
# back for n months.
sub _user_month_name_list {
    my ($number_months_back) = @_;

    # Get the start time of this month.
    my @month = _add_month(-$number_months_back+1,localtime(time()));

    my @month_names;

    for (my $count = 0; $count < $number_months_back; ++$count) {
        my $month_name = $Codestriker::short_months[$month[4]] .
          " " . ($month[5]+1900);

        push @month_names, $month_name;

        @month = _add_month(1, @month);
    }

    return @month_names;
}


# Add or substracts count months on to a time array.
sub _add_month {
    my ($count,@time) = @_;

    my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = @time;

    if ($count > 0) {
        for (my $i = 0; $i < $count; ++$i) {
            # Calculate the end of this month dbi string.
            ++$mon;

            if ($mon >= 12) {
                $mon = 0;
                ++$year;
            }
        }
    } elsif ($count < 0) {
        for (my $i = $count; $i < 0; ++$i) {
            # Calculate the end of this month dbi string.
            --$mon;

            if ($mon < 0) {
                $mon = 11;
                --$year;
            }
        }
    }

    $time[4] = $mon;
    $time[5] = $year;

    return @time;
}

1;
