#!/usr/bin/perl

use strict;
use warnings;
use Getopt::Long    qw( :config no_ignore_case );
use List::Util      qw( shuffle                );
use List::MoreUtils qw( none any               );
use Readonly;
use Config::General;
use File::HomeDir;
use Path::Class;
#use Data::Dumper;

local $|     = 1;

our $VERSION = '0.09';

sub print_usage {
	print <<"_END_HELP";
$0 [-c | --config FILE] [--read-config | --no-read-config] [-l | --length LENGTH] [-s | -special | --no-special | -n] [-r | --repeat NUMBER] [-e | --readable | --no-readable] [--verify | --no-verify]

(for full help, view perldoc of this distribution)
_END_HELP

	exit 0;
}

sub print_version {
	print "$VERSION\n" or die "Can't print: $!\n";

	exit 0;
}

Readonly my $EMPTY     => q{};

my $config_file        = file( File::HomeDir->my_home, '.genpassrc' )->stringify;
my $num_of_types       = 0;
my $read_config        = 1;

my ( @chars, %cmd_opts );

my %config_opts = (
        'lowercase_chars',  [ ( 'a'..'z' )                ],
        'uppercase_chars',  [ ( 'A'..'Z' )                ],
        'numerical_chars',  [ ( '0'..'9' )                ],
        'unreadable_chars', [ split $EMPTY, q{oO0l1I}     ],
        'special_chars',    [ split $EMPTY, q{!@#$%^&*()} ],

        'readable',          1,
        'special',           1,
        'verify',            1,
        'repeat',           10,
        'length',           15,
);

GetOptions(
        'read-config!'  => \$read_config,
        'c|config=s'    => \$config_file,
        'l|length=i'    => \$cmd_opts{'length'},
        'r|repeat=i'    => \$cmd_opts{'repeat'},
        'verify!'       => \$cmd_opts{'verify'},
        's|special!'    => \$cmd_opts{'special'},
        'e|readable!'   => \$cmd_opts{'readable'},
        'n'             => sub { $cmd_opts{'special'}  = 0; },
        'h|help'        => sub { print_usage();   },
        'V|version'     => sub { print_version(); },
) or exit 2;

# read the configuration from the configuration file
if ($read_config) {
	my $conf = new Config::General(
				-ConfigFile            => $config_file,
				-DefaultConfig         => \%config_opts,
				-MergeDuplicateOptions => 1,
			) or die "failed or parse $config_file: $@\n";

	%config_opts = $conf->getall();
}

while ( my ( $key, $val ) = each %cmd_opts ) {
	# GetOptions creates the keys, even if they are not given a value
	# so we're weeding out the undefs
	if ( defined $cmd_opts{$key} ) {
		$config_opts{$key} = $val;
	}
}

# allow for code refactoring later on
# creating a backwards entry to 'readable'
$config_opts{'unreadable'} = ( $config_opts{'readable'} ? 0 : 1 );

push @chars, @{$config_opts{'lowercase_chars'}}, @{$config_opts{'uppercase_chars'}}, @{$config_opts{'numerical_chars'}};
$num_of_types += 3;


if ($config_opts{'special'}) {
	push @chars, @{$config_opts{'special_chars'}};
	$num_of_types++;
}

# if we care for readable characters
# we remove using List::MoreUtils' none{} and grep{}
# the characters from @chars which match @unreadable_chars
if ($config_opts{'readable'}) {
	@chars = grep {
		    local $a = $_;
		    none { $a eq $_ } @{$config_opts{'unreadable_chars'}};
		  } @chars;
}

if ($num_of_types > $config_opts{'length'}) {
	die "You wanted a longer string that the variety of characters you've selected.\n"
	  . "You requested $num_of_types types of characters but only have $config_opts{'length'} length.\n";
}

for (1 .. $config_opts{'repeat'}) {
	my $password = $EMPTY;

	# this hash helps us make sure we have every type requested
	my %verified = (
		'lowercase',    $EMPTY,
		'uppercase',    $EMPTY,
		'numerical',    $EMPTY,
	);

	# adding "special" and "unreadable" if they were requested
	foreach my $type ( qw( special unreadable ) ) {
		if ( $config_opts{$type} ) {
			$verified{$type} = $EMPTY;
		}
	}

CHAR:	while ($config_opts{'length'} > length $password) {
		my $char = $chars[int rand @chars];

		# for verifying, we just check that it has small capital letters
		# if that doesn't work, we keep asking it to get a new random one till it does
		# the check if it has large capital letters and so on
		if ($config_opts{'verify'}) {
			# very comfortable for debugging
			#print "-> CHAR: $char\n";
			#print "(order: small, large, numerical, special, unreadable)\n";
			#use Data::Dumper; print Dumper(\%verified);
			#<>;

			foreach my $type ( keys %verified ) {
				my $entry = $type . '_chars';
				if ( !$verified{$type} ) {
					if ( any { $char eq $_ } @{$config_opts{$entry}} ) {
						$password .= $char;
						$verified{$type}++;
					}

					next CHAR;
				}
			}
		}

		$password .= $char;
	}

	# since the verification process creates a situation of ordered types
	# (lowercase, uppercase, numerical, special, unreadable)
	# we need to fix that by shuffling the string
	# also, it's generally good to shuffle whatever outcome there is
	print shuffle( split //, $password ) , "\n";
}


__END__

#################### main pod documentation begin ###################
=head1 NAME

genpass - Quickly create secure passwords

=head1 SYNOPSIS

genpass [-c | --config FILE] [--read-config | --no-read-config] [-l | --length LENGTH] [-s | --special | --no-special | -n] [-r | --repeat NUMBER] [-e | --readable | --no-readable] [--verify | --no-verify]

    -c | --config FILE     read config file from FILE (default is $HOME/.genpassrc)
         --read-config     read the configuration file (this is the default behavior)
         --no-read-config  do not read the configuration file, opposite of the above option
    -l | --length          password length
    -r | --repeat NUMBER   NUMBER of passwords to output
         --special         include special characters: '!','@','#','$','%','^','&','*','(',')', default behavior
    -n | --no-special      do NOT include special characters, opposite of the above option
    -e | --readable        print only easily readable characters (no "o", "O", "0", "l", "1", "I")
       | --verify          makes sure it's got every type of char (a tad slower), default behavior
       | --no-verify       doesn't make sure you get every type of char (a tad faster)
    -h | --help            print a small usage line

=head1 DESCRIPTION

If you've ever needed to create 10 (or even 10,000) passwords on the fly with varying preferences (lowercase, uppercase, no confusing characters, special characters, minimum length, etc.), you know it can become a pretty pesky task.

This script makes it possible to create flexible and secure passwords, quickly and easily.

Starting version 0.07 it also supports a configuration file (default is .genpassrc is your home folder) across multiple operating systems (thanks to Ken Williams and Adam Kennedy's great modules: Path::Class and File::HomeDir, respectively).


You can configure the following things in the configuration file:

=over

=item *

lowercase_chars ( default is 'a'..'z' )

=item *

uppercase_chars ( deafult is 'A'..'Z' )

=item *

numerical_chars ( default is '0'..'9' )

=item *

unreadable_chars ( default is o,O,0,l,1,I )

=item *

special_chars ( default is !,@,#,$,%,^,&,*,(,) )

=item *

readable ( default is 0 )

=item *

special ( default is 1 )

=item *

verify ( default is 1 )

=item *

repeat ( default is 10 )

=item *

length ( default is 15 )

=back

See POD of Config::General (perldoc Config::General) for more information on the file format.

=head1 BUGS

None that I know of. Please report if and when you find any.

=head1 SUPPORT

If you have any problems or questions, contact me using the details below.

=head1 AUTHOR

    Sawyer X
    CPAN ID: XSAWYERX
    xsawyerx@cpan.org

=head1 COPYRIGHT

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

The full text of the license can be found in the
LICENSE file included with this module.

=head1 A Word on Moral

Our lives depend on the decisions of others in the world, and so, other lives depend on our decisions.
When we decide to consume animals, we dedicate the death of others, and it is something to consider giving up.
Please review http://www.meatstinks.com and http://www.milksucks.com .
Thank you.

=head1 SEE ALSO

perl(1).

=cut

#################### main pod documentation end ###################

