package Analizo::GlobalMetrics;
use strict;
use parent qw(Class::Accessor::Fast);

use Analizo::GlobalMetric::TotalAbstractClasses;
use Analizo::GlobalMetric::MethodsPerAbstractClass;
use Analizo::GlobalMetric::ChangeCost;

use Statistics::Descriptive;


__PACKAGE__->mk_accessors(qw(
    model
    calculators
    metric_report
    values_lists
    module_metrics_list
));

sub new {
  my ($package, %args) = @_;
  my @instance_variables = (
    model => $args{model},
    calculators => _initialize_calculators($args{model}),
    metric_report => _initialize_metric_report(),
    values_lists => {},
  );
  return bless { @instance_variables }, $package;
}

sub _initialize_calculators {
  my ($model) = @_;
  my %calculators = (
    total_abstract_classes            => Analizo::GlobalMetric::TotalAbstractClasses->new(model => $model),
    total_methods_per_abstract_class  => Analizo::GlobalMetric::MethodsPerAbstractClass->new(model => $model),
    change_cost                       => Analizo::GlobalMetric::ChangeCost->new(model => $model),
  );
  return \%calculators;
}

sub _initialize_metric_report {
  my %metric_report = (
    total_modules => 0,
    total_modules_with_defined_methods => 0,
    total_modules_with_defined_attributes => 0,
    total_nom => 0,
    total_loc => 0,
    total_cof => 0
  );
  return \%metric_report;
}

sub list {
  my ($self) = @_;
  my %list = (
    total_cof => "Total Coupling Factor",
    total_modules => "Total Number of Modules",
    total_nom => "Total Number of Methods",
    total_loc => "Total Lines of Code",
    total_modules_with_defined_methods => "Total number of modules with at least one defined method",
    total_modules_with_defined_attributes => "Total number of modules with at least one defined attributes"
  );
  for my $metric (keys %{$self->calculators}) {
    $list{$metric} = $self->calculators->{$metric}->description;
  }
  return %list;
}

sub add_module_values {
  my ($self, $values) = @_;

  $self->_update_metric_report($values);
  $self->_add_values_to_values_lists($values);
}

sub _update_metric_report {
  my ($self, $values) = @_;
  $self->metric_report->{'total_modules'} += 1;
  $self->metric_report->{'total_modules_with_defined_methods'} += 1 if $values->{'nom'} > 0;
  $self->metric_report->{'total_modules_with_defined_attributes'} += 1 if $values->{'noa'} > 0;
  $self->metric_report->{'total_nom'} += $values->{'nom'};
  $self->metric_report->{'total_loc'} += $values->{'loc'};
}

sub _add_values_to_values_lists {
  my ($self, $values) = @_;
  for my $metric (keys %{$values}) {
    $self->_add_metric_value_to_values_list($metric, $values->{$metric});
  }
}

sub _add_metric_value_to_values_list {
  my ($self, $metric, $metric_value) = @_;
  if( $metric ne '_module' && $metric ne '_filename' ) {
    $self->values_lists->{$metric} = [] unless ($self->values_lists->{$metric});
    push @{$self->values_lists->{$metric}}, $metric_value;
  }
}

sub report {

  my ($self) = @_;

  $self->_include_metrics_from_calculators;
  $self->_add_statistics;
  $self->_add_total_coupling_factor;

  return \%{$self->metric_report};
}

sub _include_metrics_from_calculators {
  my ($self) = @_;
  for my $metric (keys %{$self->calculators}) {
    $self->metric_report->{$metric} = $self->calculators->{$metric}->calculate();
  }
}

sub _add_statistics {
  my ($self) = @_;

  for my $metric (keys %{$self->values_lists}) {
    my $statistics = Statistics::Descriptive::Full->new();
    $statistics->add_data(@{$self->values_lists->{$metric}});

    $self->_add_descriptive_statistics($metric, $statistics);
    $self->_add_distributions_statistics($metric, $statistics);
  }
}

sub _add_descriptive_statistics {
  my ($self, $metric, $statistics) = @_;
  $self->metric_report->{$metric . "_mean"} = $statistics->mean();
  $self->metric_report->{$metric . "_mode"} = $statistics->mode();
  $self->metric_report->{$metric . "_standard_deviation"} = $statistics->standard_deviation();
  $self->metric_report->{$metric . "_sum"} = $statistics->sum();
  $self->metric_report->{$metric . "_variance"} = $statistics->variance();

  $self->metric_report->{$metric . "_quantile_min"}   = $statistics->min(); #minimum
  $self->metric_report->{$metric . "_quantile_lower"}   = $statistics->quantile(1); #lower quartile
  $self->metric_report->{$metric . "_quantile_median"}   = $statistics->median(); #median
  $self->metric_report->{$metric . "_quantile_upper"}   = $statistics->quantile(3); #upper quartile
  $self->metric_report->{$metric . "_quantile_ninety_five"}  = $statistics->percentile(95); #95th percentile
  $self->metric_report->{$metric . "_quantile_max"} = $statistics->max(); #maximum
}

sub _add_distributions_statistics {
  my ($self, $metric, $statistics) = @_;

  if (($statistics->count >= 4) && ($statistics->variance() > 0)) {
    $self->metric_report->{$metric . "_kurtosis"} = $statistics->kurtosis();
    $self->metric_report->{$metric . "_skewness"} = $statistics->skewness();
  }
  else {
    $self->metric_report->{$metric . "_kurtosis"} = 0;
    $self->metric_report->{$metric . "_skewness"} = 0;
  }
}

sub _add_total_coupling_factor {
  my ($self) = @_;
  my $total_modules = $self->metric_report->{'total_modules'};
  my $total_acc = $self->metric_report->{'acc_sum'};

  $self->metric_report->{"total_cof"} = $self->coupling_factor($total_acc, $total_modules);
}

sub coupling_factor {
  my ($self, $total_acc, $total_modules) = @_;
  return ($total_modules > 1) ? $total_acc / _number_of_combinations($total_modules) : 1;
}

sub _number_of_combinations {
  my ($total_modules) = @_;
  return $total_modules * ($total_modules - 1);
}



1;