package Promise::ES6;

#----------------------------------------------------------------------
# This module iS NOT a defined interface. Nothing to see here …
#----------------------------------------------------------------------

use strict;
use warnings;

use Carp ();

use constant {

    # These aren’t actually defined.
    _RESOLUTION_CLASS => 'Promise::ES6::_RESOLUTION',
    _REJECTION_CLASS  => 'Promise::ES6::_REJECTION',
    _PENDING_CLASS    => 'Promise::ES6::_PENDING',

    _DEBUG => 0,
};

use constant {
    _PROMISE_ID_IDX  => 0,
    _PID_IDX         => _DEBUG + 0,
    _CHILDREN_IDX    => _DEBUG + 1,
    _VALUE_SR_IDX    => _DEBUG + 2,
    _DETECT_LEAK_IDX => _DEBUG + 3,
    _ON_RESOLVE_IDX  => _DEBUG + 4,
    _ON_REJECT_IDX   => _DEBUG + 5,
    _IS_FINALLY_IDX  => _DEBUG + 6,

    # For async/await:
    _ON_READY_IMMEDIATE_IDX => _DEBUG + 7,
    _SELF_REF_IDX => _DEBUG + 8,
};

# "$value_sr" => $value_sr
our %_UNHANDLED_REJECTIONS;

my $_debug_promise_id = 0;
sub _create_promise_id { return $_debug_promise_id++ . "-$_[0]" }

sub new {
    my ( $class, $cr ) = @_;

    die 'Need callback!' if !$cr;

    my $value;
    my $value_sr = bless \$value, _PENDING_CLASS();

    my @children;

    my $self = bless [
        ( _DEBUG ? undef : () ),
        $$,
        \@children,
        $value_sr,
        $Promise::ES6::DETECT_MEMORY_LEAKS,
    ], $class;

    $self->[_PROMISE_ID_IDX] = _create_promise_id($self) if _DEBUG;

    # NB: These MUST NOT refer to $self, or else we can get memory leaks
    # depending on how $resolver and $rejector are used.
    my $resolver = sub {
        $$value_sr = $_[0];

        # NB: UNIVERSAL::can() is used in order to avoid an eval {}.
        # It is acknowledged that many Perl experts strongly discourage
        # use of this technique.
        if ( UNIVERSAL::can( $$value_sr, 'then' ) ) {
            return _repromise( $value_sr, \@children, $value_sr );
        }

        bless $value_sr, _RESOLUTION_CLASS();

        $self->[_ON_READY_IMMEDIATE_IDX]->() if $self->[_ON_READY_IMMEDIATE_IDX];

        undef $self->[_SELF_REF_IDX];

        if (@children) {
            $_->_settle($value_sr) for splice @children;
        }
    };

    my $rejecter = sub {
        if (!defined $_[0]) {
            my $msg;

            if (@_) {
                $msg = "$class: Uninitialized rejection value given";
            }
            else {
                $msg = "$class: No rejection value given";
            }

            Carp::carp($msg);
        }

        $$value_sr = $_[0];
        bless $value_sr, _REJECTION_CLASS();

        $_UNHANDLED_REJECTIONS{$value_sr} = $value_sr;

        $self->[_ON_READY_IMMEDIATE_IDX]->() if $self->[_ON_READY_IMMEDIATE_IDX];

        undef $self->[_SELF_REF_IDX];

        # We do not repromise rejections. Whatever is in $$value_sr
        # is literally what rejection callbacks receive.
        if (@children) {
            $_->_settle($value_sr) for splice @children;
        }
    };

    local $@;
    if ( !eval { $cr->( $resolver, $rejecter ); 1 } ) {
        $$value_sr = $@;
        bless $value_sr, _REJECTION_CLASS();

        $_UNHANDLED_REJECTIONS{$value_sr} = $value_sr;
    }

    return $self;
}

sub then {
    return $_[0]->_then_or_finally(@_[1, 2]);
}

sub finally {

    # There’s no reason to call finally() without a callback
    # since it would just be a no-op.
    die 'finally() requires a callback!' if !$_[1];

    return $_[0]->_then_or_finally($_[1], undef, 1);
}

sub _then_or_finally {
    my ($self, $on_resolve_or_finish, $on_reject, $is_finally) = @_;

    my $value_sr = bless( \do { my $v }, _PENDING_CLASS() );

    my $new = bless [
        ( _DEBUG ? undef : () ),
        $$,
        [],
        $value_sr,
        $Promise::ES6::DETECT_MEMORY_LEAKS,
        $on_resolve_or_finish,
        $on_reject,
        $is_finally,
      ],
      ref($self);

    $new->[_PROMISE_ID_IDX] = _create_promise_id($new) if _DEBUG;

    if ( _PENDING_CLASS eq ref $self->[_VALUE_SR_IDX] ) {
        push @{ $self->[_CHILDREN_IDX] }, $new;
    }
    else {

        # $self might already be settled, in which case we immediately
        # settle the $new promise as well.

        $new->_settle( $self->[_VALUE_SR_IDX] );
    }

    return $new;
}

sub _repromise {
    my ( $value_sr, $children_ar, $repromise_value_sr, $orig_finally_sr ) = @_;
    $$repromise_value_sr->then(
        sub {
            if (ref $orig_finally_sr) {
                $$value_sr = $$orig_finally_sr;
            }
            else {
                $$value_sr = $_[0];
            }

            bless $value_sr, _RESOLUTION_CLASS;
            $_->_settle($value_sr) for splice @$children_ar;
        },
        sub {
            $$value_sr = $_[0];
            bless $value_sr, _REJECTION_CLASS;
            $_UNHANDLED_REJECTIONS{$value_sr} = $value_sr;
            $_->_settle($value_sr) for splice @$children_ar;
        },
    );
    return;

}

# It’s gainfully faster to inline this:
#sub _is_completed {
#    return (_PENDING_CLASS ne ref $_[0][ _VALUE_SR_IDX ]);
#}

# This method *only* runs to “settle” a promise.
sub _settle {
    my ( $self, $final_value_sr ) = @_;

    die "$self already settled!" if _PENDING_CLASS ne ref $self->[_VALUE_SR_IDX];

    my $settle_is_rejection = _REJECTION_CLASS eq ref $final_value_sr;

    # This has to happen up-front or else we can get spurious
    # unhandled-rejection warnings in asynchronous mode.
    delete $_UNHANDLED_REJECTIONS{$final_value_sr} if $settle_is_rejection;

    if ($Promise::ES6::_EVENT) {
        _postpone( sub {
            $self->_settle_now($final_value_sr, $settle_is_rejection);
        } );
    }
    else {
        $self->_settle_now($final_value_sr, $settle_is_rejection);
    }
}

sub _settle_now {
    my ( $self, $final_value_sr, $settle_is_rejection ) = @_;

    my $self_is_finally = $self->[_IS_FINALLY_IDX];

    # A promise that new() created won’t have on-settle callbacks,
    # but a promise that came from then/catch/finally will.
    # It’s a good idea to delete the callbacks in order to trigger garbage
    # collection as soon and as reliably as possible. It’s safe to do so
    # because _settle() is only called once.
    my $callback = $self->[ ($settle_is_rejection && !$self_is_finally) ? _ON_REJECT_IDX : _ON_RESOLVE_IDX ];

    @{$self}[ _ON_RESOLVE_IDX, _ON_REJECT_IDX ] = ();

    # In some contexts this function runs quite a lot,
    # so caching the is-promise lookup is useful.
    my $value_sr_contents_is_promise = 1;

    if ($callback) {

        # This is the block that runs for promises that were created by a
        # call to then() that assigned a handler for the state that
        # $final_value_sr indicates (i.e., resolved or rejected).

        my ($new_value, $callback_failed);

        local $@;

        if ( eval { $new_value = $callback->($self_is_finally ? () : $$final_value_sr); 1 } ) {

            # The callback succeeded. If $new_value is not itself a promise,
            # then $self is now resolved. (Yay!) Note that this is true
            # even if $final_value_sr indicates a rejection: in this case, we’ve
            # just run a successful “catch” block, so resolution is correct.

            # If $new_value IS a promise, though, then we have to wait.
            if ( !UNIVERSAL::can( $new_value, 'then' ) ) {
                $value_sr_contents_is_promise = 0;

                if ($self_is_finally) {

                    # finally() is a bit weird. Assuming its callback succeeds,
                    # it takes its parent’s resolution state. It’s important
                    # that we make a *new* reference to the resolution value,
                    # though, rather than merely using $final_value_sr itself,
                    # because we need $self to have its own entry in
                    # %_UNHANDLED_REJECTIONS.
                    ${ $self->[_VALUE_SR_IDX] } = $$final_value_sr;
                    bless $self->[_VALUE_SR_IDX], ref $final_value_sr;

                    $_UNHANDLED_REJECTIONS{ $self->[_VALUE_SR_IDX] } = $self->[_VALUE_SR_IDX] if $settle_is_rejection;
                }
                else {
                    bless $self->[_VALUE_SR_IDX], _RESOLUTION_CLASS;
                }
            }
        }
        else {
            $callback_failed = 1;

            # The callback errored, which means $self is now rejected.

            $new_value                    = $@;
            $value_sr_contents_is_promise = 0;

            bless $self->[_VALUE_SR_IDX], _REJECTION_CLASS();
            $_UNHANDLED_REJECTIONS{ $self->[_VALUE_SR_IDX] } = $self->[_VALUE_SR_IDX];
        }

        if (!$self_is_finally || $value_sr_contents_is_promise || ($self_is_finally && $callback_failed)) {
            ${ $self->[_VALUE_SR_IDX] } = $new_value;
        }
    }
    else {

        # There was no handler from then(), so whatever state $final_value_sr
        # indicates # (i.e., resolution or rejection) is now $self’s state
        # as well.

        # NB: We should NEVER be here if the promise is from finally().

        bless $self->[_VALUE_SR_IDX], ref($final_value_sr);
        ${ $self->[_VALUE_SR_IDX] } = $$final_value_sr;
        $value_sr_contents_is_promise = UNIVERSAL::can( $$final_value_sr, 'then' );

        if ($settle_is_rejection) {
            $_UNHANDLED_REJECTIONS{ $self->[_VALUE_SR_IDX] } = $self->[_VALUE_SR_IDX];
        }
    }

    if ($value_sr_contents_is_promise) {

        # Stash the given concrete value. If the $value_sr promise
        # rejects, then we’ll accept that, but if it resolves, then
        # we’ll look at this to know to discard that resolution.
        if ($self_is_finally) {
            $self->[_IS_FINALLY_IDX] = $final_value_sr;
        }

        return _repromise( @{$self}[ _VALUE_SR_IDX, _CHILDREN_IDX, _VALUE_SR_IDX, _IS_FINALLY_IDX ] );
    }
    elsif ( @{ $self->[_CHILDREN_IDX] } ) {
        $_->_settle( $self->[_VALUE_SR_IDX] ) for splice @{ $self->[_CHILDREN_IDX] };
    }

    $self->[_ON_READY_IMMEDIATE_IDX]->() if $self->[_ON_READY_IMMEDIATE_IDX];

    undef $self->[_SELF_REF_IDX];

    return;
}

sub DESTROY {

    # The PID should always be there, but this accommodates mocks.
    return unless $_[0][_PID_IDX] && $$ == $_[0][_PID_IDX];

    if ( $_[0][_DETECT_LEAK_IDX] && ${^GLOBAL_PHASE} && ${^GLOBAL_PHASE} eq 'DESTRUCT' ) {
        warn( ( '=' x 70 ) . "\n" . 'XXXXXX - ' . ref( $_[0] ) . " survived until global destruction; memory leak likely!\n" . ( "=" x 70 ) . "\n" );
    }

    if ( defined $_[0][_VALUE_SR_IDX] ) {
        my $promise_value_sr = $_[0][_VALUE_SR_IDX];
        if ( my $value_sr = delete $_UNHANDLED_REJECTIONS{$promise_value_sr} ) {
            warn "$_[0]: Unhandled rejection: $$value_sr";
        }
    }
}

#----------------------------------------------------------------------

# Future::AsyncAwait::Awaitable interface:

# Future::AsyncAwait doesn’t retain a strong reference to its created
# promises, as a result of which we need to create a self-reference
# inside the promise. We’ll clear that self-reference once the promise
# is finished, which avoids memory leaks.
#
sub _immortalize {
    my $method = $_[0];

    my $new = $_[1]->$method(@_[2 .. $#_]);

    $new->[_SELF_REF_IDX] = $new;
}

sub AWAIT_NEW_DONE {
    _immortalize('resolve', (ref($_[0]) || $_[0]), $_[1]);
}

sub AWAIT_NEW_FAIL {
    _immortalize('reject', (ref($_[0]) || $_[0]), $_[1]);
}

sub AWAIT_CLONE {
    _immortalize('new', ref($_[0]), \&_noop);
}

sub AWAIT_DONE {
    my $copy = $_[1];

    $_[0]->_settle_now(bless \$copy, _RESOLUTION_CLASS);
}

sub AWAIT_FAIL {
    my $copy = $_[1];

    $_[0]->_settle_now(bless(\$copy, _REJECTION_CLASS), 1);
}

sub AWAIT_IS_READY {
    !UNIVERSAL::isa( $_[0]->[_VALUE_SR_IDX], _PENDING_CLASS );
}

use constant AWAIT_IS_CANCELLED => 0;

sub AWAIT_GET {
    delete $_UNHANDLED_REJECTIONS{$_[0]->[_VALUE_SR_IDX]};

    return ${ $_[0]->[_VALUE_SR_IDX] } if UNIVERSAL::isa( $_[0]->[_VALUE_SR_IDX], _RESOLUTION_CLASS );

    my $err = ${ $_[0]->[_VALUE_SR_IDX] };
    die $err if substr($err, -1) eq "\n";
    Carp::croak $err;
}

use constant _noop => ();

sub AWAIT_ON_READY {
    $_[0][_ON_READY_IMMEDIATE_IDX] = $_[1];
}

*AWAIT_CHAIN_CANCEL = *_noop;
*AWAIT_ON_CANCEL = *_noop;

1;