package UR::Context::AutoUnloadPool;

use strict;
use warnings;

require UR;
our $VERSION = "0.46"; # UR $VERSION

use Scalar::Util qw();

# These are plain Perl objects that get garbage collected in the normal way,
# not UR::Objects

our @CARP_NOT = qw( UR::Context );

my $pool_count = 0;
sub _pool_count { $pool_count }

sub create {
    my $class = shift;
    my $self = bless { pool => {} }, $class;
    $self->_attach_observer();
    $pool_count++;
    UR::Context::manage_objects_may_go_out_of_scope();
    return $self;
}

sub delete {
    my $self = shift;
    delete $self->{pool};
    $self->_detach_observer();
    $pool_count--;
    UR::Context::manage_objects_may_go_out_of_scope();
    return 1;
}

sub _attach_observer {
    my $self = shift;
    Scalar::Util::weaken($self);
    my $o = UR::Object->add_observer(
                aspect => 'load',
                callback => sub {
                    my $loaded = shift;

                    return if ! $loaded->is_prunable();
                    $self->_object_was_loaded($loaded);
                }
            );
    $self->{observer} = $o;
}

sub _detach_observer {
    my $self = shift;
    delete($self->{observer})->delete();
}

sub _is_printing_debug {
    $ENV{UR_DEBUG_OBJECT_PRUNING} || $ENV{'UR_DEBUG_OBJECT_RELEASE'};
}

sub _object_was_loaded {
    my($self, $o) = @_;
    if (_is_printing_debug()) {
        my($class, $id) = ($o->class, $o->id);
        print STDERR Carp::shortmess("MEM AUTORELEASE $class id $id loaded in pool $self\n");
    }
    $self->{pool}->{$o->class}->{$o->id} = undef;
}

sub _unload_objects {
    my $self = shift;
    return unless $self->{pool};

    print STDERR Carp::shortmess("MEM AUTORELEASE pool $self draining\n") if _is_printing_debug();

    foreach my $class_name ( keys %{$self->{pool}} ) {
        if (_is_printing_debug()) {
            printf STDERR "MEM AUTORELEASE class $class_name: %s\n",
                            join(', ', values %{ $self->{pool}->{$class_name}} );
        }
        my $objs_for_class = $UR::Context::all_objects_loaded->{$class_name};
        next unless $objs_for_class;
        my @objs_to_release = grep { ! $_->__changes__ }
                              @$objs_for_class{ keys %{$self->{pool}->{$class_name}}};
        UR::Context->current->_weaken_references_for_objects(\@objs_to_release);
    }
    delete $self->{pool};
}

sub DESTROY {
    local $@;

    my $self = shift;
    return unless ($self->{pool});
    $self->_detach_observer();
    $self->_unload_objects();
    $pool_count--;
    UR::Context::manage_objects_may_go_out_of_scope();
}

1;

=pod

=head1 NAME

UR::Context::AutoUnloadPool - Automatically unload objects when scope ends

=head1 SYNOPSIS

  my $not_unloaded = Some::Class->get(...);
  do {
    my $guard = UR::Context::AutoUnloadPool->create();
    my $object = Some::Class->get(...);  # load an object from the database
    ...                                  # load more things
  };  # $guard goes out of scope - unloads objects

=head1 DESCRIPTION

UR Objects retrieved from the database normally live in the object cache for
the life of the program.  When a UR::Context::AutoUnloadPool is instantiated,
it tracks every object loaded during its life.  The Pool's destructor calls
unload() on those objects.

Changed objects and objects loaded before before the Pool is created will not
get unloaded.

=head1 METHODS

=over 4

=item create

  my $guard = UR::Context::AutoUnloadPool->create();

Creates a Pool object.  All UR Objects loaded from the database during this
object's lifetime will get unloaded when the Pool goes out of scope.

=item delete

  $guard->delete();

Invalidates the Pool object.  No objects are unloaded.  When the Pool later
goes out of scope, no objects will be unloaded.

=back

=head1 SEE ALSO

UR::Object, UR::Context

=cut
