package Perl::Critic::PolicyParameter;

use 5.010001;
use strict;
use warnings;
use Readonly;

use Exporter 'import';

Readonly::Array our @EXPORT_OK => qw{ $NO_DESCRIPTION_AVAILABLE };

use String::Format qw{ stringf };

use Perl::Critic::Exception::Fatal::PolicyDefinition
    qw{ throw_policy_definition };
use Perl::Critic::PolicyParameter::Behavior;
use Perl::Critic::PolicyParameter::Behavior::Boolean;
use Perl::Critic::PolicyParameter::Behavior::Enumeration;
use Perl::Critic::PolicyParameter::Behavior::Integer;
use Perl::Critic::PolicyParameter::Behavior::String;
use Perl::Critic::PolicyParameter::Behavior::StringList;

use Perl::Critic::Utils qw( :characters interpolate );

our $VERSION = '1.152';

Readonly::Scalar our $NO_DESCRIPTION_AVAILABLE => 'No description available.';

#-----------------------------------------------------------------------------

# Grrr... one of the OO limitations of Perl: you can't put references to
# subclases in a superclass (well, not nicely).  This map and method belong
# in Behavior.pm.
Readonly::Hash my %BEHAVIORS =>
    (
        'boolean'     => Perl::Critic::PolicyParameter::Behavior::Boolean->new(),
        'enumeration' => Perl::Critic::PolicyParameter::Behavior::Enumeration->new(),
        'integer'     => Perl::Critic::PolicyParameter::Behavior::Integer->new(),
        'string'      => Perl::Critic::PolicyParameter::Behavior::String->new(),
        'string list' => Perl::Critic::PolicyParameter::Behavior::StringList->new(),
    );

sub _get_behavior_for_name {
    my $behavior_name = shift;

    my $behavior = $BEHAVIORS{$behavior_name}
        or throw_policy_definition qq{There's no "$behavior_name" behavior.};

    return $behavior;
}

#-----------------------------------------------------------------------------

sub new {
    my ($class, $specification) = @_;
    my $self = bless {}, $class;

    defined $specification
        or throw_policy_definition
            'Attempt to create a ', __PACKAGE__, ' without a specification.';

    my $behavior_specification;

    my $specification_type = ref $specification;
    if ( not $specification_type ) {
        $self->{_name} = $specification;

        $behavior_specification = {};
    } else {
        $specification_type eq 'HASH'
            or throw_policy_definition
                'Attempt to create a ',
                __PACKAGE__,
                " with a $specification_type as a specification.",
                ;

        defined $specification->{name}
            or throw_policy_definition
                'Attempt to create a ', __PACKAGE__, ' without a name.';
        $self->{_name} = $specification->{name};

        $behavior_specification = $specification;
    }

    $self->_initialize_from_behavior($behavior_specification);
    $self->_finish_standard_initialization($behavior_specification);

    return $self;
}

# See if the specification includes a Behavior name, and if so, let the
# Behavior with that name plug in its implementations of parser, etc.
sub _initialize_from_behavior {
    my ($self, $specification) = @_;

    my $behavior_name = $specification->{behavior};
    my $behavior;
    if ($behavior_name) {
        $behavior = _get_behavior_for_name($behavior_name);
    } else {
        $behavior = _get_behavior_for_name('string');
    }

    $self->{_behavior} = $behavior;
    $self->{_behavior_values} = {};

    $behavior->initialize_parameter($self, $specification);

    return;
}

# Grab the rest of the values out of the specification, including overrides
# of what the Behavior specified.
sub _finish_standard_initialization {
    my ($self, $specification) = @_;

    my $description =
        $specification->{description} || $NO_DESCRIPTION_AVAILABLE;
    $self->_set_description($description);
    $self->_set_default_string($specification->{default_string});

    $self->_set_parser($specification->{parser});

    return;
}

#-----------------------------------------------------------------------------

sub get_name {
    my $self = shift;

    return $self->{_name};
}

#-----------------------------------------------------------------------------

sub get_description {
    my $self = shift;

    return $self->{_description};
}

sub _set_description {
    my ($self, $new_value) = @_;

    return if not defined $new_value;
    $self->{_description} = $new_value;

    return;
}

sub _get_description_with_trailing_period {
    my $self = shift;

    my $description = $self->get_description();
    if ($description) {
        if ( $PERIOD ne substr $description, ( length $description ) - 1 ) {
            $description .= $PERIOD;
        }
    } else {
        $description = $EMPTY;
    }

    return $description;
}

#-----------------------------------------------------------------------------

sub get_default_string {
    my $self = shift;

    return $self->{_default_string};
}

sub _set_default_string {
    my ($self, $new_value) = @_;

    return if not defined $new_value;
    $self->{_default_string} = $new_value;

    return;
}

#-----------------------------------------------------------------------------

sub _get_behavior {
    my $self = shift;

    return $self->{_behavior};
}

sub _get_behavior_values {
    my $self = shift;

    return $self->{_behavior_values};
}

#-----------------------------------------------------------------------------

sub _get_parser {
    my $self = shift;

    return $self->{_parser};
}

sub _set_parser {
    my ($self, $new_value) = @_;

    return if not defined $new_value;
    $self->{_parser} = $new_value;

    return;
}

#-----------------------------------------------------------------------------

sub parse_and_validate_config_value {
    my ($self, $policy, $config) = @_;

    my $config_string = $config->{$self->get_name()};

    my $parser = $self->_get_parser();
    if ($parser) {
        $parser->($policy, $self, $config_string);
    }

    return;
}

#-----------------------------------------------------------------------------

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

    return $self->_get_behavior()->generate_parameter_description($self);
}

#-----------------------------------------------------------------------------

sub _generate_full_description {
    my ($self, $prefix) = @_;

    my $description = $self->generate_full_description();

    if (not $description) {
        return $EMPTY;
    }

    if ($prefix) {
        $description =~ s/ ^ /$prefix/xmsg;
    }

    return $description;
}

#-----------------------------------------------------------------------------

sub to_formatted_string {
    my ($self, $format) = @_;

    my %specification = (
        n => sub { $self->get_name() },
        d => sub { $self->get_description() // $EMPTY },
        D => sub { $self->get_default_string() // $EMPTY },
        f => sub { $self->_generate_full_description(@_) },
    );

    return stringf( interpolate($format), %specification );
}

#-----------------------------------------------------------------------------

1;

__END__

#-----------------------------------------------------------------------------

=pod

=for stopwords parsable

=head1 NAME

Perl::Critic::PolicyParameter - Metadata about a parameter for a Policy.


=head1 DESCRIPTION

A provider of validation and parsing of parameter values and metadata
about the parameter.


=head1 INTERFACE SUPPORT

This is considered to be a public class.  Any changes to its interface
will go through a deprecation cycle.


=head1 METHODS

=over

=item C<get_name()>

Return the name of the parameter.  This is the key that will be looked
for in the F<.perlcriticrc>.


=item C<get_description()>

Return an explanation of the significance of the parameter, as
provided by the developer of the policy.


=item C<get_default_string()>

Return a representation of the default value of this parameter as it
would appear if it was specified in a F<.perlcriticrc> file.


=item C<parse_and_validate_config_value( $parser, $config )>

Extract the configuration value for this parameter from the overall
configuration and initialize the policy based upon it.


=item C<generate_full_description()>

Produce a more complete explanation of the significance of this
parameter than the value returned by C<get_description()>.

If no description can be derived, returns the empty string.

Note that the result may contain multiple lines.


=item C<to_formatted_string( $format )>

Generate a string representation of this parameter, based upon the
format.

The format is a combination of literal and escape characters similar
to the way C<sprintf> works.  If you want to know the specific
formatting capabilities, look at L<String::Format|String::Format>.
Valid escape characters are:

=over

=item C<%n>

The name of the parameter.

=item C<%d>

The description, as supplied by the programmer.

=item C<%D>

The default value, in a parsable form.

=item C<%f>

The full description, which is an extension of the value returned by
C<%d>.  Takes a parameter of a prefix for the beginning of each line.


=back


=back


=head1 SEE ALSO

L<Perl::Critic::DEVELOPER/"MAKING YOUR POLICY CONFIGURABLE">


=head1 AUTHOR

Elliot Shank <perl@galumph.com>

=head1 COPYRIGHT

Copyright (c) 2006-2023 Elliot Shank.

This program is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.  The full text of this license
can be found in the LICENSE file included with this module.

=cut

# Local Variables:
#   mode: cperl
#   cperl-indent-level: 4
#   fill-column: 78
#   indent-tabs-mode: nil
#   c-indentation-style: bsd
# End:
# ex: set ts=8 sts=4 sw=4 tw=78 ft=perl expandtab shiftround :
