package App::CrockfordBase32Utils;

use 5.010001;
use strict 'subs', 'vars';
use utf8;
use warnings;
use Log::ger;

use Exporter 'import';

our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
our $DATE = '2026-01-28'; # DATE
our $DIST = 'App-CrockfordBase32Utils'; # DIST
our $VERSION = '0.005'; # VERSION

our @EXPORT_OK = qw(
                       num_to_cfbase32
                       cfbase32_to_num
                       cfbase32_encode
                       cfbase32_decode
                       cfbase32_rand
               );

our %SPEC;

$SPEC{num_to_cfbase32} = {
    v => 1.1,
    summary => "Convert integer decimal number(s) to Crockford's Base 32 encoding",
    args => {
        nums => {
            schema => ['array*', of=>'int*'],
            pos => 0,
            slurpy => 1,
        },
    },
};
sub num_to_cfbase32 {
    require Encode::Base32::Crockford;

    my %args = @_;
    my $nums;
    defined($nums = $args{nums}) && @$nums or return [400, "Please specify one or more numbers"];

    my @res;
    for my $num (@{ $nums }) {
        $num = int($num);
        push @res, Encode::Base32::Crockford::base32_encode($num);
    }
    [200, "OK", \@res];
}

$SPEC{cfbase32_to_num} = {
    v => 1.1,
    summary => "Convert Crockford's Base 32 encoding to integer decimal number",
    args => {
        strs => {
            schema => ['array*', of=>'str*'],
            pos => 0,
            slurpy => 1,
        },
    },
};
sub cfbase32_to_num {
    require Encode::Base32::Crockford;

    my %args = @_;
    my $strs;
    defined($strs = $args{strs}) && @$strs or return [400, "Please specify one or more Base32 encoded strings"];

    my @res;
    for my $str (@{ $strs }) {
        push @res, Encode::Base32::Crockford::base32_decode($str);
    }
    [200, "OK", \@res];
}

$SPEC{cfbase32_encode} = {
    v => 1.1,
    summary => "Encode string to Crockford's Base32 encoding",
    args => {
        str => {
            schema => 'str*',
            pos => 0,
            cmdline_src => 'stdin_or_files',
        },
    },
};
sub cfbase32_encode {
    require Convert::Base32::Crockford;

    my %args = @_;
    my $str = $args{str};

    [200, "OK", Convert::Base32::Crockford::encode_base32($str)];
}

$SPEC{cfbase32_decode} = {
    v => 1.1,
    summary => "Decode Crockford's Base32-encoded string",
    args => {
        str => {
            schema => 'str*',
            pos => 0,
            cmdline_src => 'stdin_or_files',
        },
    },
};
sub cfbase32_decode {
    require Convert::Base32::Crockford;

    my %args = @_;
    my $str = $args{str};
    $str =~ s/[^A-TV-Z0-9]+//ig;

    [200, "OK", Convert::Base32::Crockford::decode_base32($str)];
}

my $cfbase32_digit_re    = qr/[0-9A-HJKMNP-TV-Z]/;
my $cfbase32_nondigit_re = qr/[^0-9A-HJKMNP-TV-Z]/;

my @cfbase32_digits = qw(0 1 2 3 4 5 6 7 8 9
                         A B C D E F G H J K
                         M N P Q R S T V W X Y Z);
my @cfbase32_digits_x0 = qw(1 2 3 4 5 6 7 8 9
                            A B C D E F G H J K
                            M N P Q R S T V W X Y Z);
sub _gen_rand_cfbase32 {
    require Math::Random::Secure;

    my ($min_len, $max_len, $zero_prefix) = @_;
    my $len = $min_len + Math::Random::Secure::irand($max_len - $min_len + 1);

    my @digits;
    for my $i (1..$len) {
        my $digit;
        if ($i == 1 && !$zero_prefix) {
            $digit = $cfbase32_digits_x0[Math::Random::Secure::irand(scalar @cfbase32_digits_x0)];
        } else {
            $digit = $cfbase32_digits[Math::Random::Secure::irand(scalar @cfbase32_digits)];
        }
        push @digits, $digit;
    }
    join "", @digits;
}

$SPEC{cfbase32_rand} = {
    v => 1.1,
    summary => "Generate one or more Crockford Base 32 numbers",
    description => <<'MARKDOWN',

This routine uses <pm:Math::Random::Secure> for cryptographically secure random
number generator.

MARKDOWN
    args => {
        zero_prefix => {
            schema => 'bool*',
            default => 1,
            summary => 'When generating random number of certain length range, whether the first digit is allowed to be zero',
        },
        min_int => {
            schema => 'int*',
            tags => ['category:range'],
        },
        max_int => {
            schema => 'int*',
            tags => ['category:range'],
        },
        min_base32 => {
            schema => 'str*',
            tags => ['category:range'],
        },
        max_base32 => {
            schema => 'str*',
            tags => ['category:range'],
        },
        min_len => {
            summary => 'Specify how many minimum number of digits to generate',
            description => <<'MARKDOWN',

Note that the first digit can still be 0 unless zero_prefix is set to false.

MARKDOWN
            schema => ['int*', min=>1],
            tags => ['category:range'],
        },
        max_len => {
            summary => 'Specify how many maximum number of digits to generate',
            description => <<'MARKDOWN',

Note that the first digit can still be 0 unless zero_prefix is set to false.

MARKDOWN
            schema => ['int*', min=>1],
            tags => ['category:range'],
        },
        len => {
            summary => 'Specify how many number of digits to generate for a number',
            description => <<'MARKDOWN',

Note that the first digit can still be 0 unless zero_prefix is set to false.

MARKDOWN
            schema => ['int*', min=>1],
            cmdline_aliases => {l=>{}},
            tags => ['category:range'],
        },

        num => {
            summary => 'Specify how many numbers to generate',
            schema => 'uint*',
            default => 1,
            cmdline_aliases => {n=>{}},
            tags => ['category:quantity'],
        },

        fill_char_template => {
            schema => 'str*',
            summary => 'Provide a template for formatting number, e.g. "###-###-###"',
            description => <<'MARKDOWN',

See <pm:String::FillCharTemplate> for more details.

MARKDOWN
            cmdline_aliases => {f=>{}},
            tags => ['category:output'],
        },
        unique => {
            schema => 'bool*',
            summary => 'Whether to avoid generating previously generated numbers',
            cmdline_aliases => {u=>{}},
            tags => ['category:output'],
        },
        prev_file => {
            schema => 'filename*',
            description => <<'MARKDOWN',

Load list of previous numbers from the specified file.

The file will be read per-line. Empty lines and lines starting with "#" will be
skipped. Non-digits will be removed first. Lowercase will be converted to
uppercase. I L will be normalized to 1, O will be normalized to 0.

MARKDOWN
            tags => ['category:output'],
            cmdline_aliases => {p=>{}},
        },
    },
    args_rels => {
        'choose_all&' => [
            [qw/from_int to_int/],
            [qw/from_base32 to_base32/],
            [qw/from_digits to_digits/],
        ],
        req_one => [
            qw/min_int min_base32 min_len len/,
        ],
    },
    examples => [
        {
            summary => 'Generate 35 random numbers from 12 digits each, first digit(s) can be 0',
            argv => [qw/--len 12 -n35/],
            test => 0,
        },
        {
            summary => 'Generate 35 random numbers from 12 digits each, first digit(s) CANNOT be 0',
            argv => [qw/--len 12 -n35 --nozero-prefix/],
            test => 0,
        },
        {
            summary => 'Generate a formatted random code',
            argv => ['-f', '###-###-###', '-l9'],
            test => 0,
        },
    ],
};
sub cfbase32_rand {
    require Encode::Base32::Crockford;
    require Math::Random::Secure;
    require String::FillCharTemplate;

    my %args = @_;
    my ($gen, $from, $to, $fmt);
    if ($args{len}) {
        if ($args{len} >= 9) {
            $gen = sub { _gen_rand_cfbase32($args{len}, $args{len}, $args{zero_prefix}) };
        } else {
            $from = 32 ** ($args{len} - 1); $from = 0 if $from == 1;
            $to = 32 ** ($args{len}) - 1;
        }
    } elsif (defined($args{min_len})) {
        if ($args{min_len} >= 9 || $args{max_len} >= 9) {
            $gen = sub { _gen_rand_cfbase32($args{min_len}, $args{max_len}, $args{zero_prefix}) };
        } else {
            $from = 32 ** int($args{min_len} - 1); $from = 0 if $from == 1;
            $to   = 32 ** int($args{max_len}) - 1;
        }
    } elsif (defined($args{min_int})) {
        $from = int($args{min_int});
        $to   = int($args{max_int});
    } elsif (defined($args{min_base32})) {
        $from = Encode::Base32::Crockford::base32_decode($args{min_base32});
        $to   = Encode::Base32::Crockford::base32_decode($args{max_base32});
    } else {
        return [400, "Please specify range"];
    }
    log_trace "from: %s   to: %s", $from, $to;

    my %seen;
    if ($args{prev_file}) {
        open my $fh, "<", $args{prev_file} or return [500, "Can't open prev_file '$args{prev_file}': $!"];
        while (defined(my $line = <$fh>)) {
            chomp $line;
            $line = uc($line);
            $line =~ s/^($cfbase32_nondigit_re)+//g;
            next unless length $line;
            $seen{$line}++;
        }
        #use DD; dd \%seen;
    }

    my @res;
    my $i = 1;
    while ($i <= $args{num}) {
        my $enc;
        if ($gen) {
            $enc = $gen->();
        } else {
            my $num = $from + Math::Random::Secure::irand($to - $from + 1);
            #say "from=$from, to=$to, num=$num";
            $enc = Encode::Base32::Crockford::base32_encode($num);
        }
        if ($args{unique} && $seen{$enc}++) {
            next;
        }
        if (defined $args{fill_char_template}) {
            $enc = String::FillCharTemplate::fill_char_template($args{fill_char_template}, $enc);
        }
        push @res, $enc;
        $i++;
    }

    [200, "OK", \@res];
}

1;
# ABSTRACT: Utilities related to Crockford's Base 32 encoding

__END__

=pod

=encoding UTF-8

=head1 NAME

App::CrockfordBase32Utils - Utilities related to Crockford's Base 32 encoding

=head1 VERSION

This document describes version 0.005 of App::CrockfordBase32Utils (from Perl distribution App-CrockfordBase32Utils), released on 2026-01-28.

=head1 DESCRIPTION

This distribution contains the following CLIs:

=over

=item * L<cfbase32-decode>

=item * L<cfbase32-encode>

=item * L<cfbase32-rand>

=item * L<cfbase32-to-num>

=item * L<num-to-cfbase32>

=back

Keywords: base32, base 32, crockford's base 32

=head1 FUNCTIONS


=head2 cfbase32_decode

Usage:

 cfbase32_decode(%args) -> [$status_code, $reason, $payload, \%result_meta]

Decode Crockford's Base32-encoded string.

This function is not exported by default, but exportable.

Arguments ('*' denotes required arguments):

=over 4

=item * B<str> => I<str>

(No description)


=back

Returns an enveloped result (an array).

First element ($status_code) is an integer containing HTTP-like status code
(200 means OK, 4xx caller error, 5xx function error). Second element
($reason) is a string containing error message, or something like "OK" if status is
200. Third element ($payload) is the actual result, but usually not present when enveloped result is an error response ($status_code is not 2xx). Fourth
element (%result_meta) is called result metadata and is optional, a hash
that contains extra information, much like how HTTP response headers provide additional metadata.

Return value:  (any)



=head2 cfbase32_encode

Usage:

 cfbase32_encode(%args) -> [$status_code, $reason, $payload, \%result_meta]

Encode string to Crockford's Base32 encoding.

This function is not exported by default, but exportable.

Arguments ('*' denotes required arguments):

=over 4

=item * B<str> => I<str>

(No description)


=back

Returns an enveloped result (an array).

First element ($status_code) is an integer containing HTTP-like status code
(200 means OK, 4xx caller error, 5xx function error). Second element
($reason) is a string containing error message, or something like "OK" if status is
200. Third element ($payload) is the actual result, but usually not present when enveloped result is an error response ($status_code is not 2xx). Fourth
element (%result_meta) is called result metadata and is optional, a hash
that contains extra information, much like how HTTP response headers provide additional metadata.

Return value:  (any)



=head2 cfbase32_rand

Usage:

 cfbase32_rand(%args) -> [$status_code, $reason, $payload, \%result_meta]

Generate one or more Crockford Base 32 numbers.

Examples:

=over

=item * Generate 35 random numbers from 12 digits each, first digit(s) can be 0:

 cfbase32_rand(len => 12, num => 35);

Result:

 [
   200,
   "OK",
   [
     "6GWZVR17YXFP",
     "66GQZ1V967SP",
     "6BXPTRCWGD26",
     "CX57PE12V0C5",
     "37XKPHFWMX76",
     "N0076837F05J",
     "X801Y0PDYF7V",
     "32GA91BMG1KW",
     "S3F6NYS18HV7",
     "QJCG3SZME1A1",
     "HYF6HRE575QA",
     "KQ2QGJG8ZVV5",
     "49RGFFXA2247",
     "AS1A9QFK0GPR",
     "G6D2W6KPZPV3",
     "5A40WK7FTRS5",
     "V4XFWZ3QYHRN",
     "VSSKKHQ1VMGH",
     "DNBQV8A0PHYE",
     "CDDYH2XNC6ZZ",
     "52J63APZAVBP",
     "Y4MHANNGR3NX",
     "3TGYBFXMS7KD",
     "15AHNMMFVXZE",
     "AESWWKCF17J7",
     "PXEZZ3ZKTP60",
     "AH169RGQMTQW",
     "AMXWX8DPEPKD",
     "JSCVNX5QF6AH",
     "PQ6E6CNFRYAE",
     "9K9FSXQZTD3D",
     "51EDE2QMHCB3",
     "VK1YMTQSV788",
     "2WVZYW59XGZT",
     "N7VYPRF54F72",
   ],
   {},
 ]

=item * Generate 35 random numbers from 12 digits each, first digit(s) CANNOT be 0:

 cfbase32_rand(len => 12, num => 35, zero_prefix => 0);

Result:

 [
   200,
   "OK",
   [
     "4KMM7Y637F7R",
     "TDPQ4JA8FE23",
     "414K4NTED8G8",
     "838SDSPRRPNG",
     "ZPHFSRTV2PPG",
     "6NP6B4NNAJ12",
     "HKYFWJZS6HSC",
     "SEZAMQC370GD",
     "91SXVD7KWXTG",
     "194ZWXXRR3HV",
     "JV0N4X19MWCD",
     "AEJVWBG8KH8A",
     "PS79YVY3NNN9",
     "HRQXJ97N0MHR",
     "8BCRDBB4S5F6",
     "K685WE8SYAM8",
     "6G9ERYHC96VZ",
     "N1VE9S94BQ1V",
     "W8FNSSB187YZ",
     "RAKH5XGJMF5C",
     "R2MJKNBNZ8Y0",
     "TD6TZB4PKNJH",
     "14M3YZFS2JSA",
     "HXBK114F4C9D",
     "R8J0F737PK04",
     "TENQJ5F4A0Y8",
     "P0N96Q52KZ3B",
     "MJFCHDV6EM9P",
     "HW8RPT5DJ0A7",
     "MYSVA2QPJ9XA",
     "2GA5WXEK0MMK",
     "AW9F4DSTCHYF",
     "GFPX8S18AGA0",
     "P0TGFATCRV37",
     "1G1W3J3P9B38",
   ],
   {},
 ]

=item * Generate a formatted random code:

 cfbase32_rand(fill_char_template => "###-###-###", len => 9); # -> [200, "OK", ["3SQ-6SA-2Y6"], {}]

=back

This routine uses L<Math::Random::Secure> for cryptographically secure random
number generator.

This function is not exported by default, but exportable.

Arguments ('*' denotes required arguments):

=over 4

=item * B<fill_char_template> => I<str>

Provide a template for formatting number, e.g. "###-###-###".

See L<String::FillCharTemplate> for more details.

=item * B<len> => I<int>

Specify how many number of digits to generate for a number.

Note that the first digit can still be 0 unless zero_prefix is set to false.

=item * B<max_base32> => I<str>

(No description)

=item * B<max_int> => I<int>

(No description)

=item * B<max_len> => I<int>

Specify how many maximum number of digits to generate.

Note that the first digit can still be 0 unless zero_prefix is set to false.

=item * B<min_base32> => I<str>

(No description)

=item * B<min_int> => I<int>

(No description)

=item * B<min_len> => I<int>

Specify how many minimum number of digits to generate.

Note that the first digit can still be 0 unless zero_prefix is set to false.

=item * B<num> => I<uint> (default: 1)

Specify how many numbers to generate.

=item * B<prev_file> => I<filename>

Load list of previous numbers from the specified file.

The file will be read per-line. Empty lines and lines starting with "#" will be
skipped. Non-digits will be removed first. Lowercase will be converted to
uppercase. I L will be normalized to 1, O will be normalized to 0.

=item * B<unique> => I<bool>

Whether to avoid generating previously generated numbers.

=item * B<zero_prefix> => I<bool> (default: 1)

When generating random number of certain length range, whether the first digit is allowed to be zero.


=back

Returns an enveloped result (an array).

First element ($status_code) is an integer containing HTTP-like status code
(200 means OK, 4xx caller error, 5xx function error). Second element
($reason) is a string containing error message, or something like "OK" if status is
200. Third element ($payload) is the actual result, but usually not present when enveloped result is an error response ($status_code is not 2xx). Fourth
element (%result_meta) is called result metadata and is optional, a hash
that contains extra information, much like how HTTP response headers provide additional metadata.

Return value:  (any)



=head2 cfbase32_to_num

Usage:

 cfbase32_to_num(%args) -> [$status_code, $reason, $payload, \%result_meta]

Convert Crockford's Base 32 encoding to integer decimal number.

This function is not exported by default, but exportable.

Arguments ('*' denotes required arguments):

=over 4

=item * B<strs> => I<array[str]>

(No description)


=back

Returns an enveloped result (an array).

First element ($status_code) is an integer containing HTTP-like status code
(200 means OK, 4xx caller error, 5xx function error). Second element
($reason) is a string containing error message, or something like "OK" if status is
200. Third element ($payload) is the actual result, but usually not present when enveloped result is an error response ($status_code is not 2xx). Fourth
element (%result_meta) is called result metadata and is optional, a hash
that contains extra information, much like how HTTP response headers provide additional metadata.

Return value:  (any)



=head2 num_to_cfbase32

Usage:

 num_to_cfbase32(%args) -> [$status_code, $reason, $payload, \%result_meta]

Convert integer decimal number(s) to Crockford's Base 32 encoding.

This function is not exported by default, but exportable.

Arguments ('*' denotes required arguments):

=over 4

=item * B<nums> => I<array[int]>

(No description)


=back

Returns an enveloped result (an array).

First element ($status_code) is an integer containing HTTP-like status code
(200 means OK, 4xx caller error, 5xx function error). Second element
($reason) is a string containing error message, or something like "OK" if status is
200. Third element ($payload) is the actual result, but usually not present when enveloped result is an error response ($status_code is not 2xx). Fourth
element (%result_meta) is called result metadata and is optional, a hash
that contains extra information, much like how HTTP response headers provide additional metadata.

Return value:  (any)

=head1 HOMEPAGE

Please visit the project's homepage at L<https://metacpan.org/release/App-CrockfordBase32Utils>.

=head1 SOURCE

Source repository is at L<https://github.com/perlancar/perl-App-CrockfordBase32Utils>.

=head1 SEE ALSO

L<https://www.crockford.com/base32.html>

=head1 AUTHOR

perlancar <perlancar@cpan.org>

=head1 CONTRIBUTING


To contribute, you can send patches by email/via RT, or send pull requests on
GitHub.

Most of the time, you don't need to build the distribution yourself. You can
simply modify the code, then test via:

 % prove -l

If you want to build the distribution (e.g. to try to install it locally on your
system), you can install L<Dist::Zilla>,
L<Dist::Zilla::PluginBundle::Author::PERLANCAR>,
L<Pod::Weaver::PluginBundle::Author::PERLANCAR>, and sometimes one or two other
Dist::Zilla- and/or Pod::Weaver plugins. Any additional steps required beyond
that are considered a bug and can be reported to me.

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by perlancar <perlancar@cpan.org>.

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

=head1 BUGS

Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=App-CrockfordBase32Utils>

When submitting a bug or request, please include a test-file or a
patch to an existing test-file that illustrates the bug or desired
feature.

=cut
