package Object::InsideOut; {

use strict;
use warnings;
no warnings 'redefine';

my $GBL = {};

sub generate_CHAINED :Sub(Private)
{
    ($GBL) = @_;
    my $g_ch = $$GBL{'sub'}{'chain'};
    my $chain_td = $$g_ch{'new'}{'td'} || [];
    my $chain_bu = $$g_ch{'new'}{'bu'} || [];
    delete($$g_ch{'new'});
    if (! exists($$g_ch{'td'})) {
        $$GBL{'sub'}{'chain'} = {
            td => {},       # 'Top down'
            bu => {},       # 'Bottom up'
            restrict => {}, # :Restricted
        };
        $g_ch = $$GBL{'sub'}{'chain'};
    }
    my $ch_td    = $$g_ch{'td'};
    my $ch_bu    = $$g_ch{'bu'};
    my $ch_restr = $$g_ch{'restrict'};

    # Get names for :CHAINED methods
    my (%chain_loc);
    while (my $info = shift(@{$chain_td})) {
        $$info{'name'} ||= sub_name($$info{'code'}, ':CHAINED', $$info{'loc'});
        my $package = $$info{'pkg'};
        my $name    = $$info{'name'};

        $chain_loc{$name}{$package} = $$info{'loc'};

        $$ch_td{$name}{$package} = $$info{'wrap'};
        if (exists($$info{'exempt'})) {
            push(@{$$ch_restr{$package}{$name}},
                    sort grep {$_} split(/[,'\s]+/, $$info{'exempt'} || ''));
        }
    }

    # Get names for :CHAINED(BOTTOM UP) methods
    while (my $info = shift(@{$chain_bu})) {
        $$info{'name'} ||= sub_name($$info{'code'}, ':CHAINED(BOTTOM UP)', $$info{'loc'});
        my $package = $$info{'pkg'};
        my $name    = $$info{'name'};

        # Check for conflicting definitions of 'name'
        if ($$ch_td{$name}) {
            foreach my $other_package (keys(%{$$ch_td{$name}})) {
                if ($$GBL{'isa'}->($other_package, $package) ||
                    $$GBL{'isa'}->($package, $other_package))
                {
                    my ($pkg,  $file,  $line)  = @{$chain_loc{$name}{$other_package}};
                    my ($pkg2, $file2, $line2) = @{$$info{'loc'}};
                    OIO::Attribute->die(
                        'location' => $$info{'loc'},
                        'message'  => "Conflicting definitions for chained method '$name'",
                        'Info'     => "Declared as :CHAINED in class '$pkg' (file '$file', line $line), but declared as :CHAINED(BOTTOM UP) in class '$pkg2' (file '$file2' line $line2)");
                }
            }
        }

        $$ch_bu{$name}{$package} = $$info{'wrap'};
        if (exists($$info{'exempt'})) {
            push(@{$$ch_restr{$package}{$name}},
                    sort grep {$_} split(/[,'\s]+/, $$info{'exempt'} || ''));
        }
    }

    # Propagate restrictions
    my $reapply = 1;
    my $trees = $$GBL{'tree'}{'td'};
    while ($reapply) {
        $reapply = 0;
        foreach my $pkg (keys(%{$ch_restr})) {
            foreach my $class (keys(%{$trees})) {
                next if (! grep { $_ eq $pkg } @{$$trees{$class}});
                foreach my $p (@{$$trees{$class}}) {
                    foreach my $n (keys(%{$$ch_restr{$pkg}})) {
                        if (exists($$ch_restr{$p}{$n})) {
                            next if ($$ch_restr{$p}{$n} == $$ch_restr{$pkg}{$n});
                            my $equal = (@{$$ch_restr{$p}{$n}} == @{$$ch_restr{$pkg}{$n}});
                            if ($equal) {
                                for (1..@{$$ch_restr{$p}{$n}}) {
                                    if ($$ch_restr{$pkg}{$n}[$_-1] ne $$ch_restr{$p}{$n}[$_-1]) {
                                        $equal = 0;
                                        last;
                                    }
                                }
                            }
                            if (! $equal) {
                                my %restr = map { $_ => 1 } @{$$ch_restr{$p}{$n}}, @{$$ch_restr{$pkg}{$n}};
                                $$ch_restr{$pkg}{$n} = [ sort(keys(%restr)) ];
                                $reapply = 1;
                            }
                        } else {
                            $reapply = 1;
                        }
                        $$ch_restr{$p}{$n} = $$ch_restr{$pkg}{$n};
                    }
                }
            }
        }
    }

    no warnings 'redefine';
    no strict 'refs';

    # Implement :CHAINED methods
    foreach my $name (keys(%{$ch_td})) {
        my $code = create_CHAINED($name, $trees, $$ch_td{$name});
        foreach my $package (keys(%{$$ch_td{$name}})) {
            *{$package.'::'.$name} = $code;
            add_meta($package, $name, 'kind', 'chained');
            if (exists($$ch_restr{$package}{$name})) {
                add_meta($package, $name, 'restricted', 1);
            }
        }
    }

    # Implement :CHAINED(BOTTOM UP) methods
    foreach my $name (keys(%{$ch_bu})) {
        my $code = create_CHAINED($name, $$GBL{'tree'}{'bu'}, $$ch_bu{$name});
        foreach my $package (keys(%{$$ch_bu{$name}})) {
            *{$package.'::'.$name} = $code;
            add_meta($package, $name, 'kind', 'chained (bottom up)');
            if (exists($$ch_restr{$package}{$name})) {
                add_meta($package, $name, 'restricted', 1);
            }
        }
    }
}


# Returns a closure back to initialize() that is used to setup CHAINED
# and CHAINED(BOTTOM UP) methods for a particular method name.
sub create_CHAINED :Sub(Private)
{
    # $name      - method name
    # $tree      - either $GBL{'tree'}{'td'} or $GBL{'tree'}{'bu'}
    # $code_refs - hash ref by package of code refs for a particular method name
    my ($name, $tree, $code_refs) = @_;

    return sub {
        my $thing = shift;
        my $class = ref($thing) || $thing;
        if (! $class) {
            OIO::Method->die('message' => "Must call '$name' as a method");
        }
        my @args = @_;

        # Caller must be in class hierarchy
        my $restr = $$GBL{'sub'}{'chain'}{'restrict'};
        if ($restr && exists($$restr{$class}{$name})) {
            my $caller = caller();
            if (! ((grep { $_ eq $caller } @{$$restr{$class}{$name}}) ||
                   $$GBL{'isa'}->($caller, $class) ||
                   $$GBL{'isa'}->($class, $caller)))
            {
                OIO::Method->die('message' => "Can't call restricted method '$class->$name' from class '$caller'");
            }
        }

        # Chain results together
        foreach my $pkg (@{$$tree{$class}}) {
            if (my $code = $$code_refs{$pkg}) {
                local $SIG{'__DIE__'} = 'OIO::trap';
                @args = $thing->$code(@args);
            }
        }

        # Return results
        return (@args);
    };
}

}  # End of package's lexical scope


# Ensure correct versioning
my $VERSION = 3.09;
($Object::InsideOut::VERSION == 3.09) or die("Version mismatch\n");
