package Crypto::NanoRPC;

use 5.018001;
use strict;
use warnings;
use HTTP::Request;
use LWP::UserAgent;
use JSON;

require Exporter;
our @ISA = qw(Exporter);
our %EXPORT_TAGS = ( 'all' => [] );
our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
our @EXPORT = ();

our $VERSION = '0.8.1';

my $rpc_actions = {
    # Node RPC's
    account_balance => [ 'account' ],
    account_block_count => [ 'account' ],
    account_get => [ 'key' ],
    account_history => [ 'account', 'count', [ 'raw', 'head', 'offset', 'reverse' ] ],
    account_info => [ 'account', [ 'representative', 'weight', 'pending' ] ],
    account_key => [ 'account' ],
    account_representative => [ 'account' ],
    account_weight => [ 'account' ],
    accounts_balances => [ 'accounts' ],    # accounts is reference to a list
    accounts_frontiers => [ 'accounts' ],
    accounts_pending => [ 'accounts', 'count' ],
    active_difficulty => [],
    available_supply => [],
    block_account => [ 'hash' ],
    block_count => [],
    block_count_type => [],
    block_confirm => [ 'hash' ],
    block_create => [ 'type', 'balance', 'representative', 'previous', [ 'account', 'wallet', 'source', 'destination', 'link', 'json_block', 'work' ] ],
    block_hash => [ 'block' ],
    block_info => [ 'hash', [ 'json_block' ] ],
    blocks => [ 'hashes' ],
    blocks_info => [ 'hashes', [ 'pending', 'source', 'balance', 'json_block' ] ],
    bootstrap => [ 'address', 'port' ],
    bootstrap_any => [],
    bootstrap_lazy => [ 'hash', [ 'force' ] ],
    bootstrap_status => [],
    chain => [ 'block', 'count', [ 'offset', 'reverse' ] ],
    confirmation_active => [ [ 'announcements' ] ],
    confirmation_height_currently_processing => [],
    confirmation_history => [ [ 'hash' ] ],
    confirmation_info => [ 'root', [ 'contents', 'representatives', 'json_block' ] ],
    confirmation_quorum => [ [ 'peer_details' ] ],
    database_txn_tracker => [ 'min_read_time', 'min_write_time' ],
    delegators => [ 'account' ],
    delegators_count => [ 'account' ],
    deterministic_key => [ 'seed', 'index' ],
    frontier_count => [],
    frontiers => [ 'account', 'count' ],
    keepalive => [ 'address', 'port' ],
    key_create => [],
    key_expand => [ 'key' ],
    ledger => [ 'account', 'count', [ 'representative', 'weight', 'pending', 'modified_since', 'sorting' ] ],
    node_id => [],
    node_id_delete => [],
    peers => [ [ 'peer_details' ] ],
    pending => [ 'account', [ 'count', 'threshold', 'source', 'include_active', 'sorting', 'include_only_confirmed' ] ],
    pending_exists => [ 'hash', [ 'include_active', 'include_only_confirmed' ] ],
    process => [ 'block', [ 'force', 'subtype', 'json_block' ] ],
    representatives => [ [ 'count', 'sorting' ] ],
    representatives_online => [ [ 'weight' ] ],
    republish => [ 'hash', [ 'sources', 'destinations' ] ],
    sign => [ 'block', [ 'key', 'wallet', 'account', 'json_block' ] ],
    stats => [ 'type' ],
    stats_clear => [],
    stop => [],
    successors => [ 'block', 'count', [ 'offset', 'reverse' ] ],
    validate_account_number => [ 'account' ],
    version => [],
    unchecked => [ 'count' ],
    unchecked_clear => [],
    unchecked_get => [ 'hash', [ 'json_block' ] ],
    unchecked_keys => [ 'key', 'count', [ 'json_block' ] ],
    unopened => [ [ 'account', 'count' ] ],
    uptime => [],
    work_cancel => [ 'hash' ],
    work_generate => [ 'hash', [ 'use_peers', 'difficulty' ] ],
    work_peer_add => [ 'address', 'port' ],
    work_peers => [],
    work_peers_clear => [],
    work_validate => [ 'work', 'hash', [ 'difficulty' ] ],

    # Wallet RPC's
    account_create => [ 'wallet', [ 'index', 'work' ] ],
    account_list => [ 'wallet' ],
    account_move => [ 'wallet', 'source', 'accounts' ],
    account_remove => [ 'wallet', 'account' ],
    account_representative_set => [ 'wallet', 'account', 'representative', [ 'work' ] ],
    accounts_create => [ 'wallet', 'count', [ 'work' ] ],
    password_change => [ 'wallet', 'password' ],
    password_enter => [ 'wallet', 'password' ],
    password_valid => [ 'wallet' ],
    receive => [ 'wallet', 'account', 'block', [ 'work' ] ],
    receive_minimum => [],
    receive_minimum_set => [ 'amount' ],
    search_pending => [ 'wallet' ],
    search_pending_all => [],
    send => [ 'wallet', 'source', 'destination', 'amount', [ 'work' ] ],
    wallet_add => [ 'wallet', 'key', [ 'work' ] ],
    wallet_add_watch => [ 'wallet', 'accounts' ],
    wallet_balances => [ 'wallet', [ 'threshold' ] ],
    wallet_change_seed => [ 'wallet', 'seed', [ 'count' ] ],
    wallet_contains => [ 'wallet', 'account' ],
    wallet_create => [ [ 'seed' ] ],
    wallet_destroy => [ 'wallet' ],
    wallet_export => [ 'wallet' ],
    wallet_frontiers => [ 'wallet' ],
    wallet_history => [ 'wallet', [ 'modified_since' ] ],
    wallet_info => [ 'wallet' ],
    wallet_ledger => [ 'wallet', [ 'representative', 'weight', 'pending', 'modified_since' ] ],
    wallet_lock => [ 'wallet' ],
    wallet_locked => [ 'wallet' ],
    wallet_pending => [ 'wallet', 'count', [ 'threshold', 'source', 'include_active', 'include_only_confirmed' ] ],
    wallet_representative => [ 'wallet' ],
    wallet_representative_set => [ 'wallet', 'representative', [ 'update_existing_accounts' ] ],
    wallet_republish => [ 'wallet', 'count' ],
    wallet_work_get => [ 'wallet' ],
    work_get => [ 'wallet', 'account' ],
    work_set => [ 'wallet', 'account', 'work' ],

    # Conversion RPC's  
    krai_from_raw => [ 'amount' ],
    krai_to_raw => [ 'amount' ],
    mrai_from_raw => [ 'amount' ],
    mrai_to_raw => [ 'amount' ],
    rai_from_raw => [ 'amount' ],
    rai_to_raw => [ 'amount' ],
};

sub new {
    my $class = shift;
    my $self = {
        url       => shift,
    };
    $self->{url} = 'http://[::1]:7076' unless defined $self->{url};
    $self->{request} = HTTP::Request->new( 'POST', $self->{url} );
    $self->{request}->content_type('application/json');
    $self->{ua} = LWP::UserAgent->new;
    bless $self, $class;
    return $self;
}

sub set_wallet {
    my $self = shift;
    $self->{wallet} = shift;
    return $self;
}

sub set_account {
    my $self = shift;
    $self->{account} = shift;
    return $self;
}

sub set_params {
    my ($self,$extra) = @_;
    if (ref $extra eq 'HASH') {
        $self->{$_} = $extra->{$_} for (keys %$extra);
    } else {
        # assume key/values were specified as array
        shift @_;
        while (@_) {
            my ($key,$value) = (shift,shift);
            $self->{$key} = $value;
        }
    }
    return $self;
}

sub AUTOLOAD {
    my ($self,$extra) = @_;
    if (ref $extra eq 'HASH') {
        $self->{$_} = $extra->{$_} for (keys %$extra);
    } else {
        # assume key/values were specified as array
        shift @_;
        while (@_) {
            my ($key,$value) = (shift,shift);
            $self->{$key} = $value;
        }
    }
    our $AUTOLOAD;
    my $action = $AUTOLOAD;
    $action =~ s/.*:://;
    if (defined $rpc_actions->{$action}) {
        my $options;
        my $json = '{"action": "'.$action.'"';
        foreach my $param (@{$rpc_actions->{$action}}) {
            return { error => "missing parameter $param" } unless defined $self->{$param};
            if (ref $param eq 'ARRAY') {
                $options = $param; next;
            }
            # assumes strings when params are not arrays
            $json .= ', "'.$param.'": "'.$self->{$param}.'"' if ref $self->{$param} ne 'ARRAY';
            $json .= ', "'.$param.'": '.$self->{$param} if ref $self->{$param} eq 'ARRAY';
        }
        if (ref $options eq 'ARRAY') {
            foreach my $option (@$options) {
                next unless defined $self->{$option};
                # assumes strings when options are not arrays
                $json .= ', "'.$option.'": "'.$self->{$option}.'"' if ref $self->{$option} ne 'ARRAY';
                $json .= ', "'.$option.'": '.$self->{$option} if ref $self->{$option} eq 'ARRAY';
            }
        }
        $json .= '}';
        return __do_rpc($self,$json);
    }
    return { error => "action $action not defined" };
}

sub rai_to_raw {
    my $rai = shift;
    return $rai * 1000000000000000000000000;
}

sub mrai_to_raw {
    my $rai = shift;
    return $rai * 1000000000000000000000000000000;
}

sub raw_to_rai {
    my $raw = shift;
    return $raw / 1000000000000000000000000;
}

sub raw_to_mrai {
    my $raw = shift;
    return $raw / 1000000000000000000000000000000;
}

sub __do_rpc {
    my ($self,$json) = @_;
    $self->{request}->content($json);
    my $response = $self->{ua}->request($self->{request});
    if ($response->is_success) {
        return decode_json($response->decoded_content);
    }
    return { error => "RPC call failed" };
}

1;
__END__

=head1 NAME

Crypto::NanoRPC - Perl module for interacting with Nano node

=head1 SYNOPSIS

  use Crypto::NanoRPC;
  $rpc = NanoRPC->new();
  $rpc = NanoRPC->new( 'http://[::1]:7076' );
 
  $rpc->set_params(
                        wallet => '000XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
                        account => 'nano_111111111111111111111111111111111111111111111111111111111111',
                  );
 
  $response = $rpc->account_balance();
  
  printf "Balance: %s\n", $response->{balance} unless defined $response->{error};


=head1 DESCRIPTION

=over 1

Object Oriented perl class for interacting with a Nano (rai) node

Implemented RPC calls are defined in the array rpc_actions in NanoRPC.pm. The required arguments can
be set using the set_params() method. The most common arguments, "wallet" and "account", have their own
set_ methods.

=back

=head1 METHODS

=over 1

account_balance

account_block_count

account_get

account_history

account_info

account_key

account_representative

account_weight

accounts_balances

accounts_frontiers

accounts_pending

active_difficulty

available_supply

block_account

block_count

block_count_type

block_confirm

block_create

block_hash

block_info

blocks

blocks_info

bootstrap

bootstrap_any

bootstrap_lazy

bootstrap_status

chain

confirmation_active

confirmation_height_currently_processing

confirmation_history

confirmation_info

confirmation_quorum

database_txn_tracker

delegators

delegators_count

deterministic_key

frontier_count

frontiers

keepalive

key_create

key_expand

ledger

node_id

node_id_delete

peers

pending

pending_exists

process

representatives

representatives_online

republish

sign

stats

stats_clear

stop

successors

validate_account_number

version

unchecked

unchecked_clear

unchecked_get

unchecked_keys

unopened

uptime

work_cancel

work_generate

work_peer_add

work_peers

work_peers_clear

work_validate

=back

=head1 DEPENDENCIES

These modules are required:

=over 1

=item HTTP::Request

=item LWP::UserAgent

=item JSON

=back

=head1 AUTHOR

Ruben de Groot, ruben at hacktor.com

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2020 by Ruben de Groot

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.16.1 or,
at your option, any later version of Perl 5 you may have available.


=cut
