package NameMapper;

use strict;

use Logging;

use constant COUNTER_BASE => 2;
use constant COUNTER_ITERATIONS_LIMIT => 100000;
use constant SUFFIX_DELIMITER => '_';

=pod 

=for comment

NameMapper provides a name mapping facility for names unification.

Prerequisities: bunch of namespaces with namespace-unique names whithin.

Outcome: mapping to names globally unique.

Same PPCPL data generates the same mapping. Otherwise nothing is guaranteed 
(but precautions taken to minimize the consequences, e.g. name-specific counters).

=cut

my $oneTrueSelf;
 
sub instance {
  unless (defined $oneTrueSelf) {
    my $type = shift;
    my $this = {
      'nameMapping' => {},
      'seenNames' => {
        'root' => 1,
        'admin' => 1,
        'operator' => 1,
        'adm' => 1,
        'apache' => 1,
        'psa' => 1,
      },
      'counters' => {},
    };
    $oneTrueSelf = bless $this, $type;
  }
  return $oneTrueSelf;
}

# expected additional parameters:
# - maxlength (integer): maximum length of the mapped name
# - filter (coderef): filtering function for the original username
#     Must accept a string and return a string changed to fit some 
#     caller-specified rules. Filtering is done only once, before all
#     other modifications.
sub addName {
  my ($self, $originalNamespace, $originalName, %params) = @_;

  my $modifiedName = undef;
  my $nameMapping = $self->{'nameMapping'};
  my $seenNames = $self->{'seenNames'};
  my $counters = $self->{'counters'};

  my $maxlength = undef;
  my $filterRef = undef;
  if (exists($params{'maxlength'})) {
    $maxlength = $params{'maxlength'};
  }
  if (exists($params{'filter'}) and defined($params{'filter'}) and ref($params{'filter'}) eq 'CODE') {
    $filterRef = $params{'filter'};
  }

  # check if the name is already mapped
  if (exists($nameMapping->{$originalNamespace})) {
    my $namespace = $nameMapping->{$originalNamespace};
    if (exists($namespace->{$originalName})) {
      return $namespace->{$originalName};
    }
  }

  my $filteredOriginalName = $originalName;
  if (defined($filterRef)) {
    $filteredOriginalName = $filterRef->($filteredOriginalName);
  }
  if (defined($maxlength)) {
    $filteredOriginalName = substr($filteredOriginalName, 0, $maxlength);
  }

  $modifiedName = $filteredOriginalName;
  if (exists($seenNames->{$modifiedName})) {
    my $found = 0;
    
    # counter mode ($originalName_$i)
    $counters->{$originalName} = COUNTER_BASE if not exists $counters->{$originalName};
     
    for (my $cnt = 0; !$found && $cnt < COUNTER_ITERATIONS_LIMIT; $cnt++) {
      $modifiedName = $self->_appendSuffix($filteredOriginalName, $counters->{$originalName}, $maxlength);
      $counters->{$originalName} += 1;

      $found = 1 if not exists($seenNames->{$modifiedName});
    }
    if (not $found) {
      my $errorMessage = "Can't generate unique name mapping for '$originalName' after " . COUNTER_ITERATIONS_LIMIT . "iterations. Rare and fatal error, dying.";
      Logging::error($errorMessage);
      # still no luck, goodbye cruel world ('cause Dima told me so)
      die($errorMessage);
    }
  }

  $nameMapping->{$originalNamespace} = {} if not exists($nameMapping->{$originalNamespace});
  $nameMapping->{$originalNamespace}->{$originalName} = $modifiedName;
  $seenNames->{$modifiedName} = 1;
  Logging::trace("'$originalNamespace' '$originalName' renamed to '$modifiedName'");
  return $modifiedName;
}

# will return unmodified string if length($suffix)+1 >= $maxlength and $maxlength defined
sub _appendSuffix {
  my ($self, $string, $suffix, $maxlength) = @_;
  my $append = SUFFIX_DELIMITER . $suffix;
  if (defined($maxlength)) {
    if (length($append) < $maxlength) {
      return substr($string, 0, $maxlength - length($append)) . $append;
    } else {
      return $string;
    }
  } else {
    return $string . $append;
  }
}

sub getMappedName($$$) {
  my ($self, $originalNamespace, $originalName) = @_;
  Logging::trace("requested $originalNamespace, $originalName");
  my $nameMapping = $self->{'nameMapping'};
  if (exists($nameMapping->{$originalNamespace}) and exists($nameMapping->{$originalNamespace}->{$originalName})) {
    return $nameMapping->{$originalNamespace}->{$originalName};
  } else {
    return undef;
  }
}

1;
