#!/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 );
use Readonly;

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

(for full help, view perldoc of this distribution)
_END_HELP
	exit 0;
}

our $VERSION = '0.06';

Readonly my $EMPTY     => q{};

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

my $num_of_types       = 0;
my $special            = 1;
my $length             = 15;
my $repeat             = 10;
my $readable           = 0;
my $verify             = 1;

GetOptions(
        'l|length=i'   => \$length,
        'r|repeat=i'   => \$repeat,
        'n|no-special' => sub { $special = 0; },
        'e|readable'   => \$readable,
        'verify!'      => \$verify,
        'h|help'       => sub { print_usage(); },
) or exit 2;

push @chars, @lowercase_chars, @uppercase_chars, @numerical_chars;
$num_of_types += 3;

if ($special) {
	push @chars, @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 ($readable) {
	@chars = grep {
		    local $a = $_;
		    none { $a eq $_ } @unreadable_chars;
		  } @chars;
} else {
	$num_of_types++;
}

if ($num_of_types > $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 $length length.\n";
}

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

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

	while ($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 ($verify) {
			# very comfortable for debugging
			#print "-> CHAR: $char\n";
			#print "(order: small, large, numerical, special, unreadable)\n";
			#use Data::Dumper; print Dumper(\%verified);

			# verify lowercase characters
			if ( !$verified{lowercase} ) {
				if ( none { $char eq $_ } @lowercase_chars ) {
					next;
				} else {
					$password .= $char;
					$verified{lowercase}++;
					next;
				}
			}

			# verify uppercase characters
			if ( !$verified{uppercase} ) {
				if ( none { $char eq $_ } @uppercase_chars ) {
					next;
				} else {
					$password .= $char;
					$verified{uppercase}++;
					next;
				}
			}

			# verify numerical characters
			if ( !$verified{numerical} ) {
				if ( none { $char eq $_ } @numerical_chars ) {
					next;
				} else {
					$password .= $char;
					$verified{numerical}++;
					next;
				}
			}

			# verify special characters
			if ( !$verified{special} ) {
				if ( ($special) && ( none { $char eq $_ } @special_chars ) ) {
					next;
				} else {
					$password .= $char;
					$verified{special}++;
					next;
				}
			}

			# verify unreadable characters
			if ( !$verified{unreadable} ) {
				if ( (!$readable) && ( none { $char eq $_ } @unreadable_chars ) ) {
					next;
				} else {
					$password .= $char;
					$verified{unreadable}++;
					next;
				}
			}

			$password .= $char;
		} else {
			$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 good generally
	print shuffle( split //, $password ) , "\n";
}


__END__

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

genpass - Quickly create secure passwords

=head1 SYNOPSIS

genpass [-l | --length LENGTH] [-n | --no-special] [-r | --repeat NUMBER] [--verify | --no-verify]

    -l | --length          password length
    -r | --repeat NUMBER   NUMBER of passwords to output
    -n | --no-special      do NOT include special characters: '!','@','#','$','%','^','&','*','(',')' 
    -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.

=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 ###################

