use strict;

package CVS::Metrics;

use vars qw($VERSION);
$VERSION = '0.12';

use File::Basename;
use POSIX qw(mktime);

sub insertHead {
	my $cvs_log = shift;

	foreach my $file (values %{$cvs_log}) {
		my $head = $file->{head};
		my $state = $file->{description}->{$head}->{state};
		if ($state eq 'Exp') {
			$file->{'symbolic names'}->{HEAD} = $head;
		}
	}
}

sub getTagname {
	my $cvs_log = shift;
	my ($regex_ignore) = @_;

	my %tagname;
	while (my ($filename, $file) = each %{$cvs_log}) {
		next if ($regex_ignore and $filename =~ /$regex_ignore/);  
		foreach (keys %{$file->{'symbolic names'}}) {
			unless (exists $tagname{$_}) {
				$tagname{$_} = 1;
			}
		}
	}
	return keys %tagname;
}

sub getTimedTag {
	my $cvs_log = shift;
	my ($regex_ignore) = @_;

	my %timed;
#	open LOG, "> timed.log";
	while (my ($filename, $file) = each %{$cvs_log}) {
		next if ($regex_ignore and $filename =~ /$regex_ignore/);  
		while (my ($tag, $rev_name) = each %{$file->{'symbolic names'}}) {
			my $rev = $file->{description}->{$rev_name};
			next unless (exists $rev->{date});
			my $date = $rev->{date};
			if (exists $timed{$tag}) {
				if ($date gt $timed{$tag}) {
					$timed{$tag} = $date;
#					print LOG "$tag $filename $date\n";
				}
			} else {
				$timed{$tag} = $date;
#				print LOG "$tag $filename $date\n";
			}
		}
	}
#	close LOG;
	return \%timed;
}

sub getBranchname {
	my $cvs_log = shift;
	my ($regex_ignore) = @_;

	my %tagname;
	while (my ($filename, $file) = each %{$cvs_log}) {
		next if ($regex_ignore and $filename =~ /$regex_ignore/);  
		foreach (keys %{$file->{'symbolic names'}}) {
			my $rev = $file->{'symbolic names'}->{$_}; 
			next unless ($rev =~ /\.0\./);
			unless (exists $tagname{$_}) {
				$tagname{$_} = 1;
			}
		}
	}
	return keys %tagname;
}

sub _Energy {
	my $cvs_log = shift;
	my ($tags, $path) = @_;

	my @diffs;
	my @tags0 = @{$tags};
	my @tags1 = @{$tags};
	shift @tags1;
	foreach (@tags1) {
		my $diff = shift(@tags0) . "-" . $_;
		push @diffs, $diff;
	}

	my %size;
	foreach my $tag (@{$tags}) {
		$size{$tag} = 0;
	}

	my %delta;
	foreach my $diff (@diffs) {
		$delta{$diff} = 0;
	}

	while (my ($filename, $file) = each %{$cvs_log}) {
		next unless ($filename =~ /^$path/);
		my @rev0;
		foreach my $tag (@{$tags}) {
			if (exists $file->{'symbolic names'}->{$tag}) {
				$size{$tag} ++;
				push @rev0, $file->{'symbolic names'}->{$tag};
			} else {
				push @rev0, '';
			}
		}
		my @rev1 = @rev0;
		shift @rev1;
		foreach my $diff (@diffs) {
			my $rev0 = shift @rev0;
			my $rev1 = shift @rev1;
			if ($rev1 and $rev0 ne $rev1) {
				$delta{$diff} ++;
			}
		}
	}

	my %cumul;
	$cumul{$tags->[0]} = 0;
	@tags0 = @{$tags};
	@tags1 = @{$tags};
	shift @tags1;
	foreach my $tag1 (@tags1) {
		my $tag0 = shift @tags0;
		my $diff = $tag0 . "-" . $tag1;
		$cumul{$tag1} = $cumul{$tag0} + $delta{$diff};
	}

	my @data = ();
	foreach my $tag (@{$tags}) {
		push @data, $cumul{$tag};	# x
		push @data, $size{$tag};	# y
	}

	return \@data;
}

sub getTimedEvolution {
	my $cvs_log = shift;
	my ($path) = @_;
	my %evol;

	while (my ($filename, $file) = each %{$cvs_log}) {
		next if ($path ne "." and $filename !~ /^$path/);
		while (my ($rev_name, $rev) = each %{$file->{description}}) {
			my $date = substr $rev->{date}, 0, 10;		# aaaa/mm/jj
			$evol{$date} = [ 0, 0, 0 ] unless (exists $evol{$date});
			if ($rev->{state} eq "dead") {
				$evol{$date}->[2] ++;		# deleted
			} else {
				if ($rev_name =~ /^1(\.1)+$/) {
					$evol{$date}->[0] ++;	# added
				} else {
					$evol{$date}->[1] ++;	# modified
				}
			}
		}
	}
	return \%evol;
}

sub getDirEvolution {
	my $cvs_log = shift;
	my ($path, $tag_from, $tag_to) = @_;
	my %evol;

	while (my ($filename, $file) = each %{$cvs_log}) {
		next if ($path ne "." and $filename !~ /^$path/);
		my $rev_from = $file->{'symbolic names'}->{$tag_from} || '';
		my $rev_to = $file->{'symbolic names'}->{$tag_to} || '';
		if ($rev_from or $rev_to) {
			next if (cmp_rev($rev_from, $rev_to) == 0);
		} else {
			my $in = 0;
			foreach my $tag (sort keys %{$file->{'symbolic names'}}) {
				$in = 1 if ($tag gt $tag_from and $tag lt $tag_to);
			}
			next unless ($in);
		}
		my $dir = dirname($filename);
		$evol{$dir} = [ 0, 0, 0 ] unless (exists $evol{$dir});
		$evol{$dir}->[0] ++		# added
				unless ($rev_from);
		$evol{$dir}->[2] ++		# deleted
				unless ($rev_to);
		if ($rev_from and $rev_to) {
			next if (cmp_rev($rev_from, $rev_to) == 0);
			my $in = 0;
			while (my ($rev_name, $rev) = each %{$file->{description}}) {
				next if ($rev_from and cmp_rev($rev_name, $rev_from) <= 0);
				next if ($rev_to and cmp_rev($rev_name, $rev_to) > 0);
				$in = 1;
				last;
			}
			$evol{$dir}->[1] ++		# modified
					if ($in);
		}
	}
	return \%evol;
}

sub getEvolution {
	my $cvs_log = shift;
	my ($path, $tag_from, $tag_to) = @_;
	my %evol;

	while (my ($filename, $file) = each %{$cvs_log}) {
		next if ($path ne "." and $filename !~ /^$path/);
		my $rev_from = $file->{'symbolic names'}->{$tag_from} || '';
		my $rev_to = $file->{'symbolic names'}->{$tag_to} || '';
		if ($rev_from or $rev_to) {
			next if (cmp_rev($rev_from, $rev_to) == 0);
		} else {
			my $in = 0;
			foreach my $tag (sort keys %{$file->{'symbolic names'}}) {
				$in = 1 if ($tag gt $tag_from and $tag lt $tag_to);
			}
			next unless ($in);
		}
		my $dir = dirname($filename);
		$evol{$dir} = {} unless (exists $evol{$dir});

		my $trace = "$filename:";
		foreach (keys %{$file->{description}}) {
			$trace .= " " . $_;
		}
		$trace .= "\n";
		warn $trace;
		while (my ($rev_name, $rev) = each %{$file->{description}}) {
#			print "$filename $rev_name\n";
			next if ($rev_from and cmp_rev($rev_name, $rev_from) <= 0);
			next if ($rev_to and cmp_rev($rev_name, $rev_to) > 0);
			next if ($rev_name eq "1.1" and $rev->{state} eq "dead");	# file was initially added on branch 
			my $message = $rev->{message};
#			print "$rev_name $message\n";
			$message .= " " . $rev->{date} if ($message eq "no message");
			my @tags;
			if ($tag_to eq "HEAD") {
				foreach my $tag (keys %{$file->{'symbolic names'}}) {
					if (cmp_rev($file->{'symbolic names'}->{$tag}, $rev_name) == 0) {
						push @tags, $tag;
					}
				}
			}
			$evol{$dir}->{$message} = [] unless (exists $evol{$dir}->{$message});
			push @{$evol{$dir}->{$message}}, {
					filename	=> $filename,
					date		=> $rev->{date},
					author		=> $rev->{author},
					state		=> $rev->{state},
					revision	=> $rev_name,
					tags		=> \@tags,
			};
		}
	}
	return \%evol;
}

sub cmp_rev {
	my ($rev1, $rev2) = @_;

	return 0 unless ($rev1 or $rev2);
	return -1 unless ($rev1);
	return 1 unless ($rev2);
	return 0 if ($rev1 eq $rev2);
	my @l1 = split /\./, $rev1;
	my @l2 = split /\./, $rev2;
	foreach my $v1 (@l1) {
		my $v2 = shift @l2;
		return 1 unless (defined $v2);
		return 1 if ($v1 > $v2);
		return -1 if ($v1 < $v2);
	}
	return -1;
}

sub _Activity {
	my $cvs_log = shift;
	my ($path, $start_date) = @_;

	use POSIX qw(mktime);

	my $evol = $cvs_log->getTimedEvolution($path);

	my $start = _get_day($start_date) || 0;

	my %evol2;
	while (my ($date, $value) = each %{$evol}) {
		my $d = _get_day($date);
		if (defined $d and $d > $start) {
			$evol2{sprintf("%08d", $d)} = $value;
		}
	}

	my @days;
	my @data;
	my @key_evol2 = sort keys %evol2;
	my $last_day = $start ? $start : $key_evol2[0];
	foreach my $date (@key_evol2) {
		foreach ($last_day+1 .. $date-1) {
			push @days, $_;
			push @data, undef;
		}
		my $val = $evol2{$date};
		push @days, $date;
		push @data, (${$val}[0] + ${$val}[1]);		# added + modified
		$last_day = $date;
	}
	my $now = int(time() / 86400);
	foreach ($last_day+1 .. $now) {
		push @days, $_;
		push @data, undef;
	}

	return (\@days, \@data);
}

sub _get_day {
	my ($date) = @_;

	if ($date =~ /^(\d+)[\-\/](\d+)[\-\/](\d+)$/) {
		my $t = POSIX::mktime(0, 0, 0, $3, $2 - 1, $1 - 1900);
		return int($t / 86400);
	} else {
		warn "_get_day: $date\n";
		return undef;
	}
}

sub getRevByTag {
	my $cvs_log = shift;
	my ($tags, $path) = @_;
	my %evol;

	while (my ($filename, $file) = each %{$cvs_log}) {
		next if ($path ne "." and $filename !~ /^$path/);
		my $dir = dirname($filename);
		$evol{$dir} = {} unless (exists $evol{$dir});
#		my $trace = "$filename:";
#		foreach (keys %{$file->{description}}) {
#			$trace .= " " . $_;
#		}
#		$trace .= "\n";
#		warn $trace;
		my @rev;
		foreach (@{$tags}) {
			if ($_ eq "HEAD") {
				push @rev, $file->{'head'};
			} else {
				if (exists $file->{'symbolic names'}->{$_}) {
					push @rev, $file->{'symbolic names'}->{$_};  
				} else {
					push @rev, undef;
				}
			}
		}
		$evol{$dir}->{$filename} = \@rev;
	}
	return \%evol;
}

sub getBranch {
	my $cvs_log = shift;
	my ($path, $branch) = @_;
	my %evol;

	while (my ($filename, $file) = each %{$cvs_log}) {
		next if ($path ne "." and $filename !~ /^$path/);
		my $in = 0;
		foreach (keys %{$file->{'symbolic names'}}) {
			$in = 1 if ($_ eq $branch);
		}
		next unless ($in);
		my $dir = dirname($filename);
		$evol{$dir} = {} unless (exists $evol{$dir});

		my $trace = "$filename:";
		foreach (keys %{$file->{description}}) {
			$trace .= " " . $_;
		}
		$trace .= "\n";
		warn $trace;
		my $rev_base = $file->{'symbolic names'}->{$branch};
		$rev_base =~ s/\.0//;
		while (my ($rev_name, $rev) = each %{$file->{description}}) {
#			print "$filename $rev_name\n";
			next if ($rev_name !~ /$rev_base/);
			my $message = $rev->{message};
			$message .= " " . $rev->{date} if ($message eq "no message");
			my @tags;
			foreach my $tag (keys %{$file->{'symbolic names'}}) {
				if (cmp_rev($file->{'symbolic names'}->{$tag}, $rev_name) == 0) {
					push @tags, $tag;
				}
			}
			$evol{$dir}->{$message} = [] unless (exists $evol{$dir}->{$message});
			push @{$evol{$dir}->{$message}}, {
					filename	=> $filename,
					date		=> $rev->{date},
					author		=> $rev->{author},
					state		=> $rev->{state},
					revision	=> $rev_name,
					tags		=> \@tags,
			};
		}
	}
	foreach (values %evol) {
		foreach (values %{$_}) {
			foreach my $desc (@{$_}) {
				my $file = $cvs_log->{$desc->{filename}};
				my $rev_base = $desc->{revision};
				$rev_base =~ s/\.\d+$//;
				my $rev_orig = undef;
				for my $rev_name (keys %{$file->{description}}) {
					my $rev = $file->{description}->{$rev_name};  
					foreach (@{$rev->{branches}}) {
						if ($_ eq $rev_base) {
							$rev_orig = $rev_name;
							last;
						}
					}
					last if (defined $rev_orig);
				}
				if (defined $rev_orig) {
					my $state_orig = $file->{description}->{$rev_orig}->{state};
					$desc->{orig} = [$rev_orig, $state_orig];
				}
			}
		}
	}
	return \%evol;
}

sub getDirBranch {
	my $cvs_log = shift;
	my ($path, $branch) = @_;
	my %evol;

	while (my ($filename, $file) = each %{$cvs_log}) {
		next if ($path ne "." and $filename !~ /^$path/);
		my $in = 0;
		foreach (keys %{$file->{'symbolic names'}}) {
			$in = 1 if ($_ eq $branch);
		}
		next unless ($in);
		my $dir = dirname($filename);
		$evol{$dir} = 0 unless (exists $evol{$dir});
		my $rev_base = $file->{'symbolic names'}->{$branch};
		$rev_base =~ s/\.0//;
		while (my ($rev_name, $rev) = each %{$file->{description}}) {
#			print "$filename $rev_name\n";
			next if ($rev_name !~ /$rev_base/);
			$evol{$dir} ++;
			last;
		}
	}
	return \%evol;
}

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

use Data::Dumper;
use CVS::Metrics::Parser;

our $cvs_log = bless({}, "CVS::Metrics");
our $timestamp = 0;

sub _insert {
	my $self = shift;
	my ($filename, $file) = @_;
	$self->{$filename} = $file;
}

sub CvsLog {
	my %hash = @_;

	my $cache = ".cvs_log.pl";
	if ($hash{use_cache} and !$hash{force}) {
		if ( -e $cache) {
			$hash{refresh} = 24 * 60 * 60		# 1 day
					unless ($hash{refresh});
			require $cache;
			return $cvs_log
					if ($timestamp + $hash{refresh} > time());
		}
	}

	my $parser = new CVS::Metrics::Parser();
	if ($parser) {
		$cvs_log = $parser->parse($hash{stream});
		if ($hash{use_cache}) {
			$timestamp = time();
			if (open OUT, "> $cache") {
				print OUT "package CVS::Metrics;\n";
				print OUT "\$timestamp = $timestamp;\n";
				$Data::Dumper::Indent = 1;
				while (my ($filename, $file) = each %{$cvs_log}) {
					print OUT Dumper($file);
					print OUT "\$cvs_log->_insert('$filename', \$VAR1);\n";
				}
				close OUT;
			} else {
				warn "can't open $cache ($!).\n";
			}
		}
		return $cvs_log;
	}
	return undef;
}

1;

__END__


=head1 NAME

CVS::Metrics - Utilities for process B<cvs log>

=head1 SEE ALSO

L<cvs_activity.pl>, L<cvs_energy.pl>, L<cvs_tklog.pl>, L<cvs_wxlog.pl>, L<cvs_current.pl>

=head1 COPYRIGHT

(c) 2003-2005 Francois PERRAD, France. All rights reserved.

This library is distributed under the terms of the Artistic Licence.

=head1 AUTHOR

Francois PERRAD, francois.perrad@gadz.org

=cut
