package Bitcoin::Crypto::Util;

our $VERSION = "1.004";

use v5.10;
use strict;
use warnings;
use Exporter qw(import);
use List::Util qw(first);
use Crypt::PK::ECC;
use Unicode::Normalize;
use Crypt::KeyDerivation qw(pbkdf2);
use Encode qw(encode);

use Bitcoin::Crypto::Config;
use Bitcoin::Crypto::Base58 qw(decode_base58check);

our @EXPORT_OK = qw(
	validate_wif
	get_key_type
	mnemonic_to_seed
	get_path_info
);

our %EXPORT_TAGS = (all => [@EXPORT_OK]);

sub validate_wif
{
	my ($wif) = @_;
	my $byte_wif = decode_base58check($wif);
	my $last_byte = substr $byte_wif, -1;
	if (length $byte_wif == Bitcoin::Crypto::Config::key_max_length + 2) {
		return $last_byte eq Bitcoin::Crypto::Config::wif_compressed_byte;
	}
	else {
		return length $byte_wif == Bitcoin::Crypto::Config::key_max_length + 1;
	}
}

sub get_key_type
{
	my ($entropy) = @_;

	my $curve_size = Bitcoin::Crypto::Config::key_max_length;
	my $octet = substr $entropy, 0, 1;

	my $has_unc_oc = $octet eq "\x04" || $octet eq "\x06" || $octet eq "\x07";
	my $is_unc = $has_unc_oc && length $entropy == 2 * $curve_size + 1;

	my $has_com_oc = $octet eq "\x02" || $octet eq "\x03";
	my $is_com = $has_com_oc && length $entropy == $curve_size + 1;

	return 0
		if $is_com || $is_unc;
	return 1
		if length $entropy <= $curve_size;
	return;
}

sub mnemonic_to_seed
{
	my ($mnemonic, $password) = @_;

	$mnemonic = encode("UTF-8", NFKD($mnemonic));
	$password = encode("UTF-8", NFKD("mnemonic" . ($password // "")));

	return pbkdf2($mnemonic, $password, 2048, "SHA512", 64);
}

sub get_path_info
{
	my ($path) = @_;
	if ($path =~ m#^([mM])((?:/\d+'?)*)$#) {
		my %info;
		$info{private} = $1 eq "m";
		if (defined $2 && length $2 > 0) {
			$info{path} =
				[map { s#(\d+)'#$1 + Bitcoin::Crypto::Config::max_child_keys#e; $_ } split "/", substr $2, 1];
		}
		else {
			$info{path} = [];
		}
		return undef if first { $_ >= Bitcoin::Crypto::Config::max_child_keys * 2 }
			@{$info{path}};
		return \%info;
	}
	else {
		return undef;
	}
}

1;

__END__
=head1 NAME

Bitcoin::Crypto::Util - Basic utilities for working with bitcoin

=head1 SYNOPSIS

	use Bitcoin::Crypto::Util qw(
		validate_wif
		get_key_type
		get_path_info
	);

=head1 DESCRIPTION

These are basic utilities for working with bitcoin, used by other packages.

=head1 FUNCTIONS

=head2 validate_wif

	$bool = validate_wif($str);

Ensures Base58 encoded string looks like encoded private key in WIF format.
Throws an exception if C<$str> is not valid base58.

=head2 get_key_type

	$is_private = get_key_type($bytestr);

Checks if the C<$bytestr> looks like a valid ASN X9.62 format (compressed / uncompressed / hybrid public key or private key entropy up to curve size bits).
Returns boolean which can be used to determine if the key is private.
Returns undef if C<$bytestr> does not look like a valid key entropy.

=head2 mnemonic_to_seed

	$seed = mnemonic_to_seed($mnemonic, $password);

Transforms the given BIP39 C<$mnemonic> and C<$password> into a valid BIP32 C<$seed>, which can be fed into L<Bitcoin::Crypto::Key::ExtPrivate/from_seed>.

C<$seed> is a 512 bit bytestring (64 characters). C<$mnemonic> should be a BIP39 mnemonic, but will not be checked against a dictionary.

This function is only useful if you need a seed instead of mnemonic (for example, you use a wallet implementation which does not implement BIP39). If you only want to create a private key from mnemonic, you should consider using L<Bitcoin::Crypto::Key::ExtPrivate/from_mnemonic> instead.

B<Important note about unicode:> this function only accepts UTF8-decoded strings (both C<$mnemonic> and C<$password>), but can't detect whether it got it or not. This will only become a problem if you use non-ascii mnemonic and/or password. If there's a possibility of non-ascii, always use utf8 and set binmodes to get decoded (wide) characters to avoid problems recovering your wallet.

=head2 get_path_info

	$path_data = get_path_info($path);

Tries to get derivation path data from C<$path>.
Returns undef if C<$path> is not a valid path.
Otherwise returns the structure:

	{
		private => bool, # is path derivation private (lowercase m)
		path => [
			# derivation path with 2^31 added to every hardened child number
			int, int, ..
		],
	}

Example:

	my $path = "m/1/3'";
	my $path_data = get_path_info($path);

=head1 SEE ALSO

=over 2

=item L<Bitcoin::Crypto::Key::ExtPrivate>

=back

=cut