package Bitcoin::Crypto::Network;

use Modern::Perl "2010";
use Exporter qw(import);
use Storable qw(dclone);
use Carp qw(croak);

our @EXPORT_OK = qw(
    set_default_network
    add_network
    find_network
    get_network
    get_default_network
    get_available_networks
    validate_network
);

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

my %networks = (
    mainnet => {
        name => "Bitcoin Mainnet",
        p2pkh_byte => 0x00,
        wif_byte => 0x80
    },
    testnet => {
        name => "Bitcoin Testnet",
        p2pkh_byte => 0x6f,
        wif_byte => 0xef
    },
);

my $default_network = "mainnet";

my @network_keys = qw(name p2pkh_byte wif_byte);

my %network_maps;

sub set_default_network
{
    my ($name) = @_;
    croak "Trying to set unknown network: $name"
            unless defined $networks{$name};
    $default_network = $name;
}

sub add_network
{
    my ($name, $args) = @_;
    validate_network($args);
    $networks{$name} = $args;
    _map_networks();
}

sub validate_network
{
    my ($args) = @_;
    for my $el (@network_keys) {
        croak "Incomplete network configuration: missing key $el"
            unless defined $args->{$el};
    }
}

sub find_network
{
    my ($by, $value) = @_;
    croak "Network key does not exist: $by"
        unless defined $network_maps{$by};
    return grep { $value eq $network_maps{$by}{$_} } keys %{$network_maps{$by}};
}

sub get_network
{
    my ($name) = @_;
    $name //= $default_network;
    croak "Network key does not exist: $name"
        unless defined $networks{$name};
    return dclone($networks{$name});
}

sub get_default_network
{
    return get_network();
}

sub get_available_networks
{
    return keys %networks;
}

sub _map_networks
{
    %network_maps = ();
    for my $el (@network_keys) {
        my %el_map;
        $network_maps{$el} = \%el_map;
        for my $network (keys %networks) {
            $el_map{$network} = $networks{$network}{$el};
        }
    }
}

_map_networks();

1;

__END__
=head1 NAME

Bitcoin::Crypto::Network - Management tool for cryptocurrency networks

=head1 SYNOPSIS

  use Bitcoin::Crypto::Network qw(:all);

  # by default network is set to bitcoin

  get_default_network()->{name}; # Bitcoin Mainnet

  # by default there are two networks specified
  # these are identified with keys which you can get with

  get_available_networks(); # (mainnet, testnet)

  # you can get other network configuration

  get_network("testnet")->{name}; # Bitcoin Testnet

  # search for the network and get array of keys in return
  # there will be multiple results if your search can be matched
  # by multiple networks

  find_network(name => "Bitcoin Mainnet"); # (mainnet)
  find_network(p2pkh_byte => 0x6f); # (testnet)

  # if you're working with different cryptocurrency you need to add a new network

  # network configuration is important for importing WIF private keys (network
  # recognition) and generating addresses. Don't use addresses without validating
  # your configuration first!

  # keys below are required. You can add extra keys and it'll work, but you won't
  # be able to use find_network function with them

  my $litecoin = {
      name => "Litecoin Mainnet",
      p2pkh_byte => 0x30,
      wif_byte => 0xb0
  };
  add_network(litecoin_mainnet => $litecoin);

  # after you've added your network you can set it as default. This means that
  # all private keys generated by other means than WIF importing will use that
  # configuration.

  set_default_network("litecoin_mainnet");


=head1 DESCRIPTION

This package allows you to manage non-bitcoin cryptocurrencies.
Before you start producing keys and addresses for your favorite crypto
you have to configure it's network first. Right now networks only use three
keys, but they may grow in the future (for p2sh, bech32 etc.)

  my $network = (
      name => "human-readable network name",
      p2pkh_byte => "p2pkh address prefix byte, eg. 0x00",
      wif_byte => "WIF private key prefix byte, eg. 0x80"
  );

After you add_network your program will be able to import WIF keys for that
network but all keys created from other sources will be treated as bitcoin.
You need to set_default_network to make all new keys use it. If you use many
networks it might be better to set a network with key's setNetwork method:

  $priv->setNetwork("your_network");

Some things to consider:

=over 2

=item * it is entirely possible to add a network that already exists. Because of
this, if you don't need bitcoin in your program you can replace existing
networks with custom ones.

=item * get_network functions make clones of network configuration at the time
of creation, so  changing configuration after you've created your keys
may not bring the results you're expecting. You probably shouldn't be doing
this anyway, but if for some reason you need to update your configuration
then you need to either re-create all private and public keys or use setNetwork
method on them all.

=back

=head1 FUNCTIONS

=head2 set_default_network($str)

Sets the network with $name as default one. All newly created private and public
keys will be bound to this network.
Croaks if network doesn't exist

=head2 get_default_network()

Returns deep clone of currently active network's configuration.

=head2 add_network(name => $hashref)

Adds network "name" with configuration from $hashref.
Performs $hashref validation.

=head2 validate_network($hashref)

Validates network configuration under $hashref.
Croaks if configuration is invalid.

=head2 find_network(key => $value)

Searches for all networks that have configuration "key" set to $value.
Returns list.
Croaks if key doesn't exist.

=head2 get_network($name)

Returns network $name configuration. If $name is omitted behaves like
get_default_network().
Croaks if network $name doesn't exist.

=head2 get_available_networks()

Returns all available network names.

=head1 SEE ALSO

=over 2

=item L<Bitcoin::Crypto::PrivateKey>

=item L<Bitcoin::Crypto::PublicKey>

=back

=cut
