package RevBank::Amount;

use v5.32;
use warnings;
use experimental 'isa';         # stable since v5.36
use experimental 'signatures';  # stable since v5.36

use Carp qw(carp croak);
use Scalar::Util;
use POSIX qw(lround);

our $C = __PACKAGE__;

sub _coerce {
    for (@_) {
        unless ($_ isa $C) {
            croak "Unsupported operation on $C with " . ref if ref;
            croak "Unsupported operation on $C with undef" if not defined;

            my $old = $_;

            $_ = RevBank::Amount->parse_string("$_");
            croak "Unsupported operation on $C with invalid amount '$old'"
                if not defined;
        }
    }
}

use overload (
    '""'   => sub ($self, @) { $self->string },
    "bool" => sub ($self, @) { $self->cents },
    "0+"   => sub ($self, @) { $self->_float_warn },
    "+" => sub ($a, $b, $swap) {
        $b //= 0;
        _coerce($a, $b);
        return $C->new($$a + $$b)
    },
    "-" => sub ($a, $b, $swap) {
        _coerce($a, $b);
        return $C->new(($swap?-1:1) * ($$a - $$b))
    },
    "*" => sub ($a, $b, $swap) {
        $b = $b->_float_warn if ref $b;
        $C->new($$a * $b);
    },
    "/" => sub ($a, $b, $swap) {
        carp "Using floating-point arithmetic for $a/$b (use \$amount->float to suppress warning)";
        $b = $b->float if ref $b;
        $C->new($$a / $b);
    },
    "<=>" => sub ($a, $b, $swap) {
        _coerce($a, $b);
        return $swap ? $$b<=>$$a : $$a<=>$$b;
    },
    "cmp" => sub ($a, $b, $swap) {
        _coerce($a, $b);
        return $swap ? $$b<=>$$a : $$a<=>$$b;
    },
);

sub new($class, $cents) {
    my $int = int sprintf "%d", $cents;
    #carp "$cents rounded to $int cents" if $int != $cents;
    return bless \$int, $class;
}

sub new_from_float($class, $num) {
    return $class->new(lround 100 * sprintf "%.02f", $num);

    # First, round the float with sprintf for bankers rounding, then
    # multiply to get number of cents. However, 4.56 as a float is
    # 4.55999... which with int would get truncated to 455, so lround is needed
    # to get 456.
    # Note: _l_round, because normal round gives the int *AS A FLOAT*; sigh.
}

sub parse_string($class, $str) {
    defined $str and $str =~ /\S/ or return undef;

    my ($neg, $int, $cents)
        = $str =~ /^\s*(?:\+|(-)?)([0-9]+)?(?:[,.]([0-9]{1,2}))?\s*$/
        or return undef;

    defined $int or defined $cents or return undef;

    $int //= 0;
    $cents //= 0;
    $cents *= 10 if length($cents) == 1;  # 4.2 -> 4.20

    return $class->new(
        ($neg ? -1 : 1) * ($int * 100 + $cents)
    );
}

sub cents($self) {
    return $$self;
}

sub float($self) {
    return $$self / 100;
}

sub _float_warn($self) {
    carp "Using $C $self as floating-point number (use %s in sprintf instead of %.2f, or \$amount->float to suppress warning)";
    return $self->float;
}

sub string($self, $plus = "") {
    return sprintf(
        "%s%d.%02d",
        $$self < 0 ? "-" : $plus,
        abs($$self) / 100,
        abs($$self) % 100,
    );
}

sub string_flipped($self, $sep = " ") {
    return sprintf(
        "%s%s%d.%02d",
        $$self > 0 ? "+" : "",
        $$self > 0 ? $sep : "",
        abs($$self) / 100,
        abs($$self) % 100,
    );
}

sub abs($self) {
    return $C->new(abs $$self)
}

1;