package Data::COW;

use 5.006001;
use strict;
no warnings;

use Exporter;
use Scalar::Util qw<reftype blessed>;
use overload ();  # we're not overloading anything, but we'd like to
                  # check if they're already implementing a value type

use base 'Exporter';

our @EXPORT = qw<make_cow_ref>;

our $VERSION = '0.02';

sub tied_any {
    my ($ref) = @_;
    if (ref $ref) {
        if (reftype($ref) eq 'SCALAR') {
            tied $$ref;
        }
        elsif (reftype($ref) eq 'ARRAY') {
            tied @$ref;
        }
        elsif (reftype($ref) eq 'HASH') {
            tied %$ref;
        }
    }

}

sub cow_object {
    my ($ref) = @_;
    my $tied = tied_any $ref;
    $tied && $tied->isa('Data::COW') && $tied;
}

sub make_cow_ref {
    my ($ref, $parent, $key) = @_;

    make_cow_ref_nocheck($ref, $parent, $key);
}

sub make_temp_cow_ref {
    my ($ref, $parent, $key) = @_;

    if (my $obj = cow_object $ref) {
        if ($obj->{parent} == $parent) {
            $ref;
        }
        else {
            make_cow_ref_nocheck($ref, $parent, $key);
        }
    }
    else {
        make_cow_ref_nocheck($ref, $parent, $key);
    }
}

sub make_cow_ref_nocheck {
    my ($ref, $parent, $key) = @_;

    if (ref $ref && 
        # check if they already think they're a value type
        !(overload::Overloaded($ref) && overload::Method($ref, '=')))
    {
        my $ret;
        if (reftype($ref) eq 'SCALAR') {
            tie my $it => 'Data::COW::Scalar', $ref, $parent, $key;
            $ret = \$it;
        }
        elsif (reftype($ref) eq 'ARRAY') {
            tie my @it => 'Data::COW::Array', $ref, $parent, $key;
            $ret = \@it;
        }
        elsif (reftype($ref) eq 'HASH') {
            tie my %it => 'Data::COW::Hash', $ref, $parent, $key;
            $ret = \%it;
        }
        else {
            # code and glob are not aggregates that we can take control
            # of, so punt and just return them like anything else
            return $ref;
        }
        
        if (blessed($ref)) {
            bless $ret => blessed($ref);
        }
        
        return $ret;
    }
    else {
        return $ref;
    }
}

sub clone_using {
    my ($self, $copier) = @_;

    return unless $self->{const};
    my $old = $self->{ref};
    my $new = $copier->($old);

    if (blessed $old) {
        bless $new => blessed $old;
    }
    if ($self->{parent}) {
        my $cnew = make_cow_ref $new, $self->{parent}, undef;
        tied_any($cnew)->{const} = 0;
        $self->{parent}->clone($self->{key} => $cnew);
    }
    $self->{ref} = $new;
    $self->{const} = 0;
}

package Data::COW::Scalar;

use Tie::Scalar;
use base 'Tie::Scalar';
use base 'Data::COW';

sub TIESCALAR {
    my ($class, $ref, $parent, $key) = @_;
    bless {
        ref => $ref,
        parent => $parent,
        key => $key,
        const => 1,
    } => ref $class || $class;
}

sub clone {
    my ($self, $key, $value) = @_;

    $self->clone_using(sub { my $v = ${$_[0]}; \$v });
    ${$self->{ref}} = $value if defined $key;
}

sub FETCH {
    my ($self) = @_;
    Data::COW::make_temp_cow_ref(${$self->{ref}}, $self, 1);
}

sub STORE {
    my ($self, $value) = @_;
    $self->clone(1 => $value);
    $value;
}

package Data::COW::Array;

use Tie::Array;
use base 'Tie::Array';
use base 'Data::COW';

sub TIEARRAY {
    my ($class, $ref, $parent, $key) = @_;
    bless {
        ref => $ref,
        parent => $parent,
        key => $key,
        const => 1,
    } => ref $class || $class;
}

sub clone {
    my ($self, $key, $value) = @_;
    $self->clone_using(sub { [ @{$_[0]} ] });
    $self->{ref}[$key] = $value if defined $key;
}

sub FETCH {
    my ($self, $key) = @_;
    Data::COW::make_temp_cow_ref($self->{ref}[$key], $self, $key);
}

sub STORE {
    my ($self, $key, $value) = @_;
    $self->clone($key => $value);
    $value;
}

sub FETCHSIZE {
    my ($self) = @_;
    scalar @{$self->{ref}};
}

sub STORESIZE {
    my ($self, $size) = @_;
    $self->clone;
    $#{$self->{ref}} = $size-1;
}

sub DELETE {
    my ($self, $key) = @_;
    $self->clone;
    delete $self->{ref}[$key];
}

sub EXISTS {
    my ($self, $key) = @_;
    exists $self->{ref}[$key];
}

package Data::COW::Hash;

use Tie::Hash;
use base 'Tie::Hash';
use base 'Data::COW';

sub TIEHASH {
    my ($class, $ref, $parent, $key) = @_;
    bless {
        ref => $ref,
        parent => $parent,
        key => $key,
        const => 1,
    } => ref $class || $class;
}

sub clone {
    my ($self, $key, $value) = @_;
    $self->clone_using(sub { 
        my $ret = { %{$_[0]} };
        $ret;
    });
    $self->{ref}{$key} = $value if defined $key;
}

sub FETCH {
    my ($self, $key) = @_;
    Data::COW::make_temp_cow_ref($self->{ref}{$key}, $self, $key);
}

sub STORE {
    my ($self, $key, $value) = @_;
    $self->clone($key => $value);
    $value;
}

sub EXISTS {
    my ($self, $key) = @_;
    exists $self->{ref}{$key};
}

sub DELETE {
    my ($self, $key) = @_;
    $self->clone;
    delete $self->{ref}{$key};
}

sub CLEAR {
    my ($self) = @_;
    $self->clone_using(sub { {} });
    ();
}

sub FIRSTKEY {
    my ($self) = @_;
    my $a = keys %{$self->{ref}};  # reset iterator
    each %{$self->{ref}};
}

sub NEXTKEY {
    my ($self) = @_;
    each %{$self->{ref}};
}

sub SCALAR {
    my ($self) = @_;
    scalar %{$self->{ref}};
}

1;

=head1 NAME

Data::COW - clone deep data structures copy-on-write

=head1 SYNOPSIS

    use Data::COW;

    my $array = [ 0, 1, 2 ];
    my $copy = make_cow_ref $array;

    push @$array, 3;
    # $copy->[3] is 3
    push @$copy, 4;
    # $array->[4] is not defined (and doesn't even exist)
    # $copy is a real copy now
    push @$array, 5;
    # $copy is unaffected

=head1 DESCRIPTION

Data::COW makes copies of data structures copy-on-write, or "lazily".
So if you have a data structure that takes up ten megs of memory, it
doesn't take ten megs to copy it.  Even if you change part of it,
Data::COW only copies the parts that need to be copied in order to
reflect the change.

Data::COW exports one function: C<make_cow_ref>.  This takes a reference
and returns a copy-on-write reference to it.  If you don't want this
in your namespace, and you want to use it as C<Data::COW::make_cow_ref>,
use the module like this:

    use Data::COW ();

Data::COW won't be able to copy filehandles or glob references.  But how
do you change those anyway?  It's also probably a bad idea to give it
objects that refer to XS internal state without providing a value type
interface.  Also, don't use stringified references from this data
structure: they're different each time you access them!

=head1 SEE ALSO

L<Clone>

=head1 AUTHOR

Luke Palmer <luke@luqui.org>

=head1 COPYRIGHT

    Copyright (C) 2005 by Luke Palmer

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