package IO::Iron::PolicyBase;

## no critic (Documentation::RequirePodAtEnd)
## no critic (Documentation::RequirePodSections)
## no critic (Subroutines::RequireArgUnpacking)

use 5.010_000;
use strict;
use warnings;

# Global creator
BEGIN {
    # Inherit nothing
}

# Global destructor
END {
}


# ABSTRACT: Base package (inherited) for IO::Iron::IronMQ/Cache/Worker::Policy packages.

our $VERSION = '0.13'; # VERSION: generated by DZP::OurPkgVersion


use Log::Any  qw{$log};
use Hash::Util 0.06 qw{lock_keys unlock_keys};
use Carp::Assert;
use Carp::Assert::More;
use English '-no_match_vars';
use File::Spec ();
use Params::Validate qw(:all);
use Exception::Class (
      'IronPolicyException' => {
        fields => ['policy', 'candidate'],
      },
      'NoIronPolicyException' => {
        fields => [],
      },
      'CharacterGroupNotDefinedIronPolicyException' => {
        fields => [],
      }
  );

use IO::Iron::Common ();
use IO::Iron::PolicyBase::CharacterGroup ();


# INTERNAL METHODS
# For use in the inheriting subclass


# TODO policy character set, list possible alternatives:
sub IRON_CLIENT_DEFAULT_POLICIES {
    my %default_policies =
            (
            'definition' => {
                'character_set' => 'ascii', # The only supported character set!
                'character_group' => {
                },
                'no_limitation' => 1, # There is an unlimited number of alternatives.
            },
            'queue' => { 'name' => [ '[:word:]{1,}' ], },
            'cache' => { 'name' => [ '[:word:]{1,}' ], 'item_key' => [ '[:word:]{1,}' ]},
            'worker' => { 'name' => [ '[:word:]{1,}' ], },
            );
    return %default_policies;
}

sub _do_alt {
    my $self = shift;
    my %params = validate(
        @_, {
            'str' => { type => SCALAR, }, # name/key name.
        }
    );
    my $str = $params{'str'};
    $log->tracef('Entering _do_alt(%s)', $str);
    assert(length $str > 0, 'String length > 0.');
    my @processed_alts;
    if( $str =~ /^([[:graph:]]*)(\[:[[:graph:]]+:\]\{[[:digit:]]+\,[[:digit:]]+\})([[:graph:]]*)$/sx
            || ($str =~ /^([[:graph:]]*)(\[:[[:graph:]]+:\]\{([[:digit:]]+)\})([[:graph:]]*)$/sx && $3 > 1)
            ) {
        $log->tracef('We need to do recursion.', $str);
        my $preceeding_part = $1;
        my $group_part = $2;
        my $succeeding_part = defined $4 ? $4 : $3;
        $log->tracef('$preceeding_part=%s;$group_part=%s;$succeeding_part=%s;',
            $preceeding_part, $group_part, $succeeding_part);
        my @alternatives = _make_ones($preceeding_part, $group_part, $succeeding_part);
        foreach (@alternatives) {
            push @processed_alts, $self->_do_alt('str' => $_);
        }
    }
    else {
        $log->tracef('We need to create the alternatives.', $str);
        if( $str =~ /^([[:graph:]]*)(\[:[[:graph:]]+:\]\{1\})([[:graph:]]*)$/sx ) {
            my @alts;
            my $preceeding_part = $1;
            my $group_part = $2;
            my $succeeding_part = $3;
            $log->tracef('$preceeding_part=%s;$group_part=%s;$succeeding_part=%s;',
                $preceeding_part, $group_part, $succeeding_part);
            if($group_part =~ /^(\[:[[:graph:]]+:\])\{([[:digit:]]+)\}$/sx) {
                my $group = $1;
                my $lowest_amount = $2;
                my $highest_amount = $3;
                $log->tracef('$group=%s;$lowest_amount=%s;$highest_amount=%s;',
                    $group, $lowest_amount, $highest_amount);
                foreach ($self->_get_character_group_alternatives('character_group' => $group)) {
                    push @alts, $preceeding_part . $_ . $succeeding_part;
                }
            }
            $log->tracef('@alts=%s;', \@alts);
            foreach (@alts) {
                push @processed_alts, $self->_do_alt('str' => $_);
            }
        }
        else {
            push @processed_alts, $str;
        }
    }
    $log->tracef('Exiting _do_alt():%s', \@processed_alts);
    return @processed_alts;
}

sub _make_ones {
    my $preceeding_part = $_[0];
    my $group_part = $_[1];
    my $succeeding_part = $_[2];
    $log->tracef('_make_ones():$preceeding_part=%s;$group_part=%s;$succeeding_part=%s;',
        $preceeding_part, $group_part, $succeeding_part);
    $log->tracef('$group_part=%s;', $group_part);
    my @alternatives;
    if($group_part =~ /^(\[:[[:graph:]]+:\])\{([[:digit:]]+)\,([[:digit:]]+)\}$/msx) {
        my $group = $1;
        my $lowest_amount = $2;
        my $highest_amount = $3;
        $log->tracef('$group=%s;$lowest_amount=%s;$highest_amount=%s;',
            $group, $lowest_amount, $highest_amount);
        for($lowest_amount..$highest_amount) {
            my $group_str = $group . '{1}';
            push @alternatives, $preceeding_part . $group_str x $_ . $succeeding_part;
        }
    }
    elsif($group_part =~ /^(\[:[[:graph:]]+:\])\{([[:digit:]]+)\}$/msx) {
        my $group = $1;
        my $lowest_amount = $2;
        my $highest_amount = $2;
        $log->tracef('$group=%s;$lowest_amount=%s;$highest_amount=%s;',
            $group, $lowest_amount, $highest_amount);
        for(my $i = $lowest_amount; $i < $highest_amount + 1; $i++) {
            my $group_str = $group . '{1}';
            push @alternatives, $preceeding_part . $group_str x $i . $succeeding_part;
        }
    }
    else {
        $log->fatalf('Illegal string \'%s\'.', $group_part);
    }
    $log->tracef('@alternatives=%s;', \@alternatives);
    return @alternatives;
}

sub _get_character_group_alternatives {
    my $self = shift;
    my %params = validate(
        @_, {
            'character_group' => { type => SCALAR, regex => qr/^[[:graph:]]+$/msx, }, # name/key name.
            'keep_posix_group' => { type => BOOLEAN, optional => 1, }, # Keep POSIX (subset) group name and return it.
        },
    );
    my $chars;

    # Predefined groups (subset of POSIX) first!
    $chars = IO::Iron::PolicyBase::CharacterGroup::group(
            'character_group' => $params{'character_group'});
    if($chars && $params{'keep_posix_group'}) {
        $chars = $params{'character_group'}; # Put the group name back.
    }
    if(!$chars) {
        $chars = $self->{'policy'}->{'definition'}->{'character_group'}
                ->{$params{'character_group'}};
    }
    if($chars) {
        $log->tracef('$chars=%s;', $chars);
    }
    else {
        $log->fatalf('Character group \'%s\' not defined.', $params{'character_group'});
        CharacterGroupNotDefinedIronPolicyException->throw(
                error => 'CharacterGroupNotDefinedIronPolicyException: Character group \'' . $params{'character_group'} . '\' not defined!',
                );
    }
    return split //msx, $chars;
}


sub alternatives {
    my $self = shift;
    my %params = validate(
        @_, {
            'required_policy' => { type => SCALAR, }, # name/key name.
        }
    );
    assert_hashref( $self->{'policy'}, 'self->{required_policy} is a reference to a list.');
    $log->tracef('Entering alternatives(%s)', \%params);

    if(defined $self->{'policy'}->{'definition'}->{'no_limitation'} &&
            $self->{'policy'}->{'definition'}->{'no_limitation'} == 1) {
        NoIronPolicyException->throw(
                error => 'NoIronPolicyException: Cannot list alternatives, unlimited number!',
                );
    }
    my $templates = $self->{'policy'}->{$params{'required_policy'}};
    assert_listref($templates, 'templates is a reference to a list');
    my @template_alternatives;
    foreach (@{$templates}) {
        $log->tracef('alternatives(): Template:\"%s\".)', $_);
        push @template_alternatives, $self->_do_alt('str' => $_);
    }
    $log->tracef('Exiting alternatives():%s', \@template_alternatives);
    return @template_alternatives;
}

sub _get_chars_or_remain_posix_group {
    my $self = shift;
    $log->tracef('Entering _get_chars_or_remain_posix_group(%s)', \@_);
    my $group = $_[0];
    $log->tracef('_get_chars_or_remain_posix_group(): Ask for Group alternatives for :%s', $group);
    my @chars = $self->_get_character_group_alternatives('character_group' => $group, 'keep_posix_group' => 1);
    $log->tracef('_get_chars_or_remain_posix_group(): Group alternatives:%s', \@chars);
    my $group_chars = join q{}, @chars;
    $log->tracef('Exiting _get_chars_or_remain_posix_group():%s', ('[' . $group_chars . ']') );
    return '[' . $group_chars . ']';
}

sub _convert_policy_to_normal_regexp {
    my $self = shift;
    $log->tracef('Entering _convert_policy_to_normal_regexp(%s)', \@_);
    my $policy_regexp = $_[0];
    $policy_regexp =~ s/(\[:[[:graph:]]+?:\])/$self->_get_chars_or_remain_posix_group($1)/egsx;
    $log->tracef('Exiting _convert_policy_to_normal_regexp():%s', $policy_regexp);
    return $policy_regexp;
}


sub is_valid_policy {
    my $self = shift;
    my %params = validate(
        @_, {
            'policy' => { type => SCALAR, }, # name/key name.
            'candidate' => { type => SCALAR, }, # string to check.
        }
    );
    assert_listref( $self->{'policy'}, 'self->{policy} is a reference to a list.');
    $log->tracef('Entering is_valid_policy(%s)', \%params);

    my $validity = 0;
    if(defined $self->{'policy'}->{'definition'}->{'no_limitation'}
            && $self->{'policy'}->{'definition'}->{'no_limitation'} == 1) {
        $log->trace('is_valid_policy', 'no_limitation: no policy check!');
        $validity = 1;
    }
    else {
        my $templates = $self->{'policy'}->{$params{'policy'}};
        assert_listref($templates, "templates is a reference to a list");
        foreach (@{$templates}) {
            $log->tracef('is_valid_policy(): Going to comparing with raw template:\"%s\".)', $_);
            my $template = $self->_convert_policy_to_normal_regexp($_);
            $log->tracef('is_valid_policy(): Comparing with template:\"%s\".)', $template);
            if($params{'candidate'} =~ /^$template$/xgsm) {
                $validity = 1;
                last;
            }
        }
    }
    $log->tracef('Exiting is_valid_policy():%d', $validity);
    return $validity;
}

# This method throws an exception of type IronPolicyException.


sub validate_with_policy {
    my $self = shift;
    my %params = validate(
        @_, {
            'policy' => { type => SCALAR, }, # name/key name.
            'candidate' => { type => SCALAR, }, # string to check.
        }
    );
    assert_hashref( $self->{'policy'}, 'self->{policy} is a reference to a hash.');
    $log->tracef('Entering validate_with_policy(%s)', \%params);
    my $validity = 0;
    my $templates = $self->{'policy'}->{$params{'policy'}};
    assert_listref($templates, 'templates is a reference to a list');
    foreach (@{$templates}) {
        my $template = $self->_convert_policy_to_normal_regexp($_);
        $log->tracef('validate_with_policy(): Comparing with template:\"%s\".)', $template);
        if($params{'candidate'} =~ /^$template$/xgsm) {
            $validity = 1;
            last;
        }
    }
    if($validity == 0) {
        $log->tracef('Throwing exception in validate_with_policy(): policy=%s, candidate=%s', $params{'policy'}, $params{'candidate'});
        IronPolicyException->throw(
                policy => $params{'policy'},
                candidate => $params{'candidate'},
                error => 'IronPolicyException: policy=' . $params{'policy'}
                    . ' candidate=' . $params{'candidate'},
                );
    }
    $log->tracef('Exiting validate_with_policy():%d', $validity);
    return $validity;
}


sub get_policies { ## no critic (Subroutines::RequireArgUnpacking)
    my $self = shift;
    my %params = validate(
        @_, {
            'policies' => { type => SCALAR|UNDEF, optional => 0, },
        }
    );
    $log->tracef('Entering get_policies(%s)', \%params);
    my %all_policies = IRON_CLIENT_DEFAULT_POLICIES(); ## Preset default policies.
    $log->tracef('Default policies: %s', \%all_policies);
    if(defined $params{'policies'}) { # policies file specified when creating the object, if given.
        IO::Iron::Common::_read_iron_config_file(\%all_policies,
                File::Spec->file_name_is_absolute($params{'policies'})
                ? $params{'policies'} : File::Spec->catfile(File::Spec->curdir(), $params{'policies'})
                );
    }
    my %policies = %{$all_policies{$self->_THIS_POLICY()}};
    $policies{'definition'} = $all_policies{'definition'};
    $log->tracef('Exiting get_policies: %s', \%policies);
    return \%policies;
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

IO::Iron::PolicyBase - Base package (inherited) for IO::Iron::IronMQ/Cache/Worker::Policy packages.

=head1 VERSION

version 0.13

=head1 SYNOPSIS

This class is for internal use only.

=for stopwords Iron.io Params params API Mikko Koivunalho TODO

=for stopwords NoIronPolicyException IronPolicyException

=head1 METHODS

=head2 IRON_CLIENT_DEFAULT_POLICIES

Default policies for all clients.
These policies allow everything.

=head2 alternatives

Return all possible alternatives.

Parameters:

=over 8

=item required_policy, name/key name

=back

Return: List of possible alternatives if validation is successful.
If the policy is not set, throws a NoIronPolicyException.

=head2 is_valid_policy

Is this policy valid?

Parameters:

=over 8

=item policy, name/key name.

=item candidate, proposed string.

=back

Return: Boolean.

=head2 validate_with_policy

Validate a candidate string. Same as method is_valid_policy() but this method throws an exception of type IronPolicyException if the validation fails.

Parameters:

=over 8

=item policy, name/key name.

=item candidate, proposed string.

=back

Return: Boolean True if validation is successful, otherwise throws an exception.

=head2 get_policies

Get the policies from file or use the defaults. This function is for internal use.

The configuration is constructed as follows:

=over 8

=item 1. The global defaults.

=item 5. The policies file specified when instantiating the client library overwrites everything before it according to the file hierarchy.

=item 6. Return only the policies connected to this client (specify in derived class with method _THIS_POLICY).

=back

Return: ref to policies.

=head1 AUTHOR

Mikko Koivunalho <mikko.koivunalho@iki.fi>

=head1 BUGS

Please report any bugs or feature requests to bug-io-iron@rt.cpan.org or through the web interface at:
 http://rt.cpan.org/Public/Dist/Display.html?Name=IO-Iron

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2017 by Mikko Koivunalho.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

The full text of the license can be found in the
F<LICENSE> file included with this distribution.

=cut
