# $File: //depot/libOurNet/BBS/lib/OurNet/BBS/Base.pm $ $Author: autrijus $
# $Revision: #8 $ $Change: 3850 $ $DateTime: 2003/01/25 20:03:29 $

package OurNet::BBS::Base;
use 5.006;

use strict;
no warnings 'deprecated';

use constant EGO    => 0; use constant FLAG  => 1;
use constant HASH   => 1; use constant ARRAY => 2;
use constant CODE   => 3; use constant GLOB  => 4;
use constant TYPES  => [qw/_ego _hash _array _code _glob/];
use constant SIGILS => [qw/$ % @ & */];

require PerlIO if $] >= 5.008;

# These magical hashes below holds all cached initvar constants:
# = subrountines   as $RegSub{$glob}
# = module imports as $RegMod{$glob}
# = variables      as $RegVar{$class}{$sym}

my (%RegVar, %RegSub, %RegMod);

my %Packlists; # $packlist cache for contains()

## Class Methods ######################################################
# These methods expects a package name as their first argument.

# constructor method; turn into an pseudo hash if _phash exists

use constant CONSTRUCTOR => << '.';
sub __PKG__::new {
    my __PACKAGE__ $self = bless([\%{__PKG__::FIELDS}], '__PACKAGE__');

#    eval {
    if (ref($_[1])) {
        # Passed in a single hashref -- assign it!
	%{$self} = %{$_[1]};
    }
    else {
        # Automagically fill in the fields.
	$self->{$_} = $_[$self->[0]{$_}] foreach ((__KEYS__)[0 .. $#_-1]);
    }
#    };

#    require Carp and Carp::confess($@) if $@;
    
__TIE__
    return $self->{_ego} = bless (\[$self, __OBJ__], '__PKG__');
}

1;
.

# import does following things:
# 1. set up @ISA.
# 2. export type constants.
# 3. set overload bits.
# 4. install accessor methods.
# 5. handle variable propagation.
# 6. install the new() handler.

require overload; # no import, please

sub import {
    my $class = shift;
    my $pkg   = caller(0);

    no strict 'refs';
    no warnings 'once';

    # in non-direct usage, only ournet client gets symbols and sigils.
    my $is_client = ($pkg eq 'OurNet::BBS::Client' or $pkg eq 'OurNet::BBS::OurNet::BBS');
    return unless $class eq __PACKAGE__ or $is_client;

    *{"$pkg\::$_"} = \&{$_} foreach qw/EGO FLAG HASH ARRAY CODE GLOB/;
    return *{"$pkg\::SIGILS"} = \&{SIGILS} if $is_client;

    *{"$pkg\::ego"} = sub { ${$_[0]}->[0] };

    push @{"$pkg\::ISA"}, $class;

    my (@overload, $tie_eval, $obj_eval);
    my $fields = \%{"$pkg\::FIELDS"};

    foreach my $type (HASH .. GLOB) {
	if (exists($fields->{TYPES->[$type]})) { # checks for _hash .. _glob
	    my $sigil = SIGILS->[$type];

	    push @overload, "$sigil\{}" => sub { 
		# use Carp; eval { ${$_[0]}->[$type] } || Carp::confess($@) 
		${$_[0]}->[$type]
	    };

	    if ($type == HASH or $type == ARRAY) {
		$tie_eval = "tie my ${sigil}obj => '$pkg', ".
		            "[\$self, $type];\n" . $tie_eval;
		$obj_eval .= ", \\${sigil}obj";
	    }
	    elsif ($type == CODE) {
		$tie_eval .= 'my $code = sub { $self->refresh(undef, CODE);'.
			     '$self->{_code}(@_) };';
		$obj_eval .= ', $code';
	    }
	    elsif ($type == GLOB) {
		$tie_eval = 'my $glob = \$self->{_glob};' . $tie_eval;
		$obj_eval .= ', $glob';
	    }
	}
	else {
	    $obj_eval .= ', undef';
	    
	}
    }

    $obj_eval =~ s/(?:, undef)+$//;

    my $sub_new = CONSTRUCTOR;
    my $keys = join(' ', sort {
	$fields->{$a} <=> $fields->{$b} 
    } grep {
	/^[^_]/ 
    } keys(%{$fields}));

    $sub_new =~ s/__TIE__/$tie_eval/g;
    $sub_new =~ s/__OBJ__/$obj_eval/g;
    $sub_new =~ s/__PKG__/$pkg/g;
    $sub_new =~ s/__KEYS__/qw{$keys}/g;
    $sub_new =~ s/__PACKAGE__/OurNet::BBS::Base/g;

    unless (eval $sub_new) {
	require Carp;
	Carp::confess "$sub_new\n\n$@";
    }

    $pkg->overload::OVERLOAD(
	@overload,
	'""'   => sub { overload::AddrRef($_[0]) },
	'0+'   => sub { 0 },
	'bool' => sub { 1 },
	'cmp' => sub { "$_[0]" cmp "$_[1]" },
	'<=>' => sub { "$_[0]" cmp "$_[1]" }, # for completeness' sake
    );

    # install accessor methods
    unless (UNIVERSAL::can($pkg, '__accessor')) {
        foreach my $property (keys(%{"$pkg\::FIELDS"}), '__accessor') {
            *{"$pkg\::$property"} = sub {
                my $self = ${$_[0]}->[EGO];
		$self->refresh_meta;
                $self->{$property} = $_[1] if $#_;
                return $self->{$property};
            };
        }
    }

    # my $backend = $1 if $pkg =~ m|^OurNet::BBS::([^:]+)|;
    my $backend = substr($pkg, 13, index($pkg, ':', 14) - 13); # fast

    my @defer; # delayed aliasing until variables are processed
    foreach my $parent (@{"$pkg\::ISA"}) {
        next if $parent eq __PACKAGE__; # Base won't use mutable variables

        while (my ($sym, $ref) = each(%{"$parent\::"})) {
	    push @defer, ($pkg, $sym, $ref);
        }

	unshift @_, @{$RegMod{$parent}} if ($RegMod{$parent});
    }

    while (my ($mod, $symref) = splice(@_, 0, 2)) {
        if ($mod =~ m/^\w/) { # getvar from other modules
	    push @{$RegMod{$pkg}}, $mod, $symref;

            require "OurNet/BBS/$backend/$mod.pm";
            $mod = "OurNet::BBS::$backend\::$mod";

            foreach my $symref (@{$symref}) {
                my ($ch, $sym) = CORE::unpack('a1a*', $symref);
		die "can't import: $mod\::$sym" unless *{"$mod\::$sym"};

		++$RegVar{$pkg}{$sym};

                *{"$pkg\::$sym"} = (
                    $ch eq '$' ? \${"$mod\::$sym"} :
                    $ch eq '@' ? \@{"$mod\::$sym"} :
                    $ch eq '%' ? \%{"$mod\::$sym"} :
                    $ch eq '*' ? \*{"$mod\::$sym"} :
                    $ch eq '&' ? \&{"$mod\::$sym"} : undef
                );
            }
        }
        else { # this module's own setvar
            my ($ch, $sym) = CORE::unpack('a1a*', $mod);

	    *{"$pkg\::$sym"} = ($ch eq '$') ? \$symref : $symref;
	    ++$RegVar{$pkg}{$sym};
        }
    }

    my @defer_sub; # further deferred subroutines that needs localizing
    while (my ($pkg, $sym, $ref) = splice(@defer, 0, 3)) {
	next if exists $RegVar{$pkg}{$sym} # already imported
	     or *{"$pkg\::$sym"}{CODE}; # defined by use subs

	if (defined(&{$ref})) { 
	    push @defer_sub, ($pkg, $sym, $ref);
	    next; 
	}

	next unless ($ref =~ /^\*(.+)::(.+)/)
	        and exists $RegVar{$1}{$2};

	*{"$pkg\::$sym"} = $ref;
	++$RegVar{$pkg}{$sym};
    } 

    # install per-package wrapper handlers for mutable variables
    while (my ($pkg, $sym, $ref) = splice(@defer_sub, 0, 3)) {
	my $ref = ($RegSub{$ref} || $ref);
	next unless ($ref =~ /^\*(.+)::([^:]+)$/);
	next if defined(&{"$pkg\::$sym"});

	if (%{$RegVar{$pkg}}) {
	    eval qq(
		sub $pkg\::$sym {
	    ) . join('', 
		map { qq(
		    local *$1\::$_ = *$pkg\::$_;
		)} (keys(%{$RegVar{$pkg}}))
	    ) . qq(
		    &{$ref}(\@_);
		};
	    );
	}
	else {
	    *{"$pkg\::$sym"} = $ref;
	};

	$RegSub{"*$pkg\::$sym"} = $ref;
    }

    return unless $OurNet::BBS::Encoding;
    *{"$pkg\::unpack"} = \&_unpack;
    *{"$pkg\::pack"} = \&_pack;
}

sub _unpack {
    require Encode;
    return map Encode::decode($OurNet::BBS::Encoding => $_), CORE::unpack($_[0], $_[1]);
}

sub _pack {
    require Encode;
    return CORE::pack($_[0], map Encode::encode($OurNet::BBS::Encoding => $_), @_[1..$#_]);
}

## Instance Methods ###################################################
# These methods expects a tied object as their first argument.

# unties through an object to get back the true $self
sub ego { $_[0] }

# the all-important cache refresh instance method
sub refresh {
    my $self = shift;
    my $ego;

    ($self, $ego) = (ref($self) eq __PACKAGE__)
	? ($self->{_ego}, $self)
	: ($self, ${$self}->[EGO]);

    no strict 'refs';

    my $prefix = ref($self)."::refresh_";
    my $method = $_[0] && defined(&{"$prefix$_[0]"}) 
	? "$prefix$_[0]" : $prefix.'meta';

    return $method->($ego, @_);
}

# opens access to connections via OurNet protocol
sub daemonize {
    require OurNet::BBS::Server;
    OurNet::BBS::Server->daemonize(@_);
}

=begin comment

# The following code doesn't work, because they always override.

# permission checking; fall-back for undefined packages
sub writeok {
    my ($self, $user, $op, $argref) = @_;

    print "warning: permission model for ".ref($self)." unimplemented.\n".
          "         access forbidden for user ".$user->id().".\n"
	if $OurNet::BBS::DEBUG;

    return;
}

# ditto
sub readok {
    my ($self, $user, $op, $argref) = @_;

    print "warning: permission model for ".ref($self)." unimplemented.\n".
          "         access forbidden for user ".$user->id().".\n"
	if $OurNet::BBS::DEBUG;

    return;
}

=end comment
=cut

# clears internal memory; uses CLEAR instead
sub purge {
    $_[0]->ego->{_ego}->CLEAR;
}

# returns the BBS backend for the object
sub backend {
    my $backend = ref($_[0]);

    $backend = ref($_[0]{_ego}) if $backend eq __PACKAGE__;
    $backend = substr($backend, 13, index($backend, ':', 14) - 13); # fast
    # $backend = $1 if $backend =~ m|^OurNet::BBS::(\w+)|;

    return $backend;
}

# developer-friendly way to check files' timestamp for mtime fields
sub filestamp {
    my ($self, $file, $field, $check_only) = @_;
    my $time = (stat($file))[9];

    no warnings 'uninitialized';

    return 1 if $self->{$field ||= 'mtime'} == $time;
    $self->{$field} = $time unless $check_only;

    return 0; # something changed
}

# developer-friendly way to check timestamp for mtime fields
sub timestamp {
    my ($self, $time, $field, $check_only) = @_;

    no warnings 'uninitialized';

    return 1 if $self->{$field ||= 'mtime'} == $time;
    $self->{$field} = $time unless $check_only;

    return 0; # something changed
}

# check if something's in packlist; packages don't contain undef
sub contains {
    my ($self, $key) = @_;
    $self = $self->{_ego} if ref($self) eq __PACKAGE__;

    no strict 'refs';
    no warnings 'uninitialized';
    # print "checking $key against $self: @{ref($self).'::packlist'}\n";

    return (length($key) and index(
        $Packlists{ref($self)} ||= " @{ref($self).'::packlist'} ",
        " $key ",
    ) > -1);
}

# loads a module: ($self, $backend, $module).
sub fillmod {
    my $self = $_[0];
    $self =~ s|::|/|g;
    
    require "$self/$_[1]/$_[2].pm";
    return "$_[0]::$_[1]::$_[2]";
}

# create a new module and fills in arguments in the expected order
sub fillin {
    my ($self, $key, $class) = splice(@_, 0, 3);
    return if defined($self->{_hash}{$key});

    $self->{_hash}{$key} = OurNet::BBS->fillmod(
	$self->{backend}, $class
    )->new(@_);

    return 1;
}

# returns the module in the same backend, or $val's package if supplied
sub module {
    my ($self, $mod, $val) = @_;

    if ($val and UNIVERSAL::isa($val, 'UNIVERSAL')) {
	my $pkg = ref($val);

	if (UNIVERSAL::isa($val, 'HASH')) {
	    # special case: somebody blessed a hash to put into STORE.
	    bless $val, 'main'; # you want black magic?
	    $_[2] = \%{$val};   # curse (unbless) it!
	}

	return $pkg;
    }

    my $backend = $self->backend;
    require "OurNet/BBS/$backend/$mod.pm";
    return "OurNet::BBS::$backend\::$mod";
}

# object serialization for OurNet::Server calls; does nothing otherwise
sub SPAWN { return $_[0] }
sub REF { return ref($_[0]) }
sub KEYS { return keys(%{$_[0]}) }

# XXX: Object injection
sub INJECT {
    my ($self, $code, @param) = @_;

    if (UNIVERSAL::isa($code, 'CODE')) {
	require B::Deparse;

	my $deparse = B::Deparse->new(qw/-p -sT/);
	$code = $deparse->coderef2text($code);
	$code =~ s/^\s+use (?:strict|warnings)[^;\n]*;\n//m;
    }

    require Safe;
    my $safe = Safe->new;
    $safe->permit_only(qw{
	:base_core padsv padav padhv padany rv2gv refgen srefgen ref gvsv gv gelem
    });

    my $result = $safe->reval("sub $code");
    warn $@ if $@;

    return sub { $result->($self, @_) };
}

## Tiescalar Accessors ################################################
# XXX: Experimental: Globs only.

sub TIESCALAR {
    return bless(\$_[1], $_[0]);
}

## Tiearray Accessors #################################################
# These methods expects a raw (untied) object as their first argument.

# merged hasharray!
sub TIEARRAY {
    return bless(\$_[1], $_[0]);
}

sub FETCHSIZE {
    my ($self, $key) = @_;
    my ($ego, $flag) = @{${$self}};

    $self->refresh(undef, ARRAY);

    return scalar @{$ego->{_array} ||= []};
}

sub PUSH {
    my $self = shift;
    my $size = $self->FETCHSIZE;

    foreach my $item (@_) {
        $self->STORE($size++, $item);
    }
}

## Tiehash Accessors ##################################################
# These methods expects a raw (untied) object as their first argument.

# the Tied Hash constructor method
sub TIEHASH {
    return bless(\$_[1], $_[0]);
}

# fetch accessesor
sub FETCH {
    my ($self, $key) = @_;
    my ($ego, $flag) = @{${$self}};

    $self->refresh($key, $flag);

    return ($flag == HASH) ? $ego->{_hash}{$key} : $ego->{_array}[$key];
}

# fallback implementation to STORE
sub STORE {
    die "@_: STORE unimplemented";
}

# delete an element; calls its remove() subroutine to handle actual removal
sub DELETE {
    my ($self, $key) = @_;
    my ($ego, $flag) = @{${$self}};

    $self->refresh($key, $flag);

    if ($flag == HASH) {
	return unless exists $ego->{_hash}{$key};
	$ego->{_hash}{$key}->ego->remove
	    if UNIVERSAL::can($ego->{_hash}{$key}, 'ego');
	return delete($ego->{_hash}{$key});
    }
    else {
	return unless exists $ego->{_array}[$key];
	$ego->{_array}[$key]->ego->remove
	    if UNIVERSAL::can($ego->{_array}[$key], 'ego');
	return delete($ego->{_array}[$key]);
    }
}

# check for existence of a key.
sub EXISTS {
    my ($self, $key) = @_;
    my ($ego, $flag) = @{${$self}};

    $self->refresh($key, $flag);

    return ($flag == HASH) ? exists $ego->{_hash}{$key} 
                           : exists $ego->{_array}[$key];
}

# iterator; this one merely uses 'scalar keys()'
sub FIRSTKEY {
    my $self = $_[0];
    my $ego = ${$self}->[EGO];

    $ego->refresh_meta(undef, HASH);

    scalar keys (%{$ego->{_hash}});
    return $self->NEXTKEY;
}

# ditto
sub NEXTKEY {
    my $self = $_[0];

    return each %{${$self}->[EGO]->{_hash}};
}

# empties the cache, do not DELETE the objects themselves
sub CLEAR {
    my $self = ${$_[0]}->[EGO];

    %{$self->{_hash}}  = () if (exists $self->{_hash});
    @{$self->{_array}} = () if (exists $self->{_array});
}

# could care less
sub DESTROY () {};
sub UNTIE   () {};

our $AUTOLOAD;

sub AUTOLOAD {
    my $action = substr($AUTOLOAD, (
        (rindex($AUTOLOAD, ':') - 1) || return
    ));

    no strict 'refs';

    *{$AUTOLOAD} = sub {
	use Carp; confess ref($_[0]->{_ego}).$action
	    unless defined &{ref($_[0]->{_ego}).$action};
	goto &{ref($_[0]->{_ego}).$action}
    };

    goto &{$AUTOLOAD};
}

1;