role Perl6::Metamodel::MROBasedMethodDispatch {
    # If needed, a cached flattened method table accounting for all methods in
    # this class and its parents. This is only needed in the situation that a
    # megamorphic callsite involves the class, so calculated and cached on
    # demand.
    has $!cached_all_method_table;

    # Resolve a method. On MoarVM, with the generalized dispatch mechanism,
    # this is called to bootstrap callsites. On backends without that, it
    # is only called on a published cache miss.
    method find_method($obj, $name, :$no_fallback, *%adverbs) {
        # uncomment line below for verbose information about uncached method lookups
        #nqp::say( "looking for " ~ $name ~ " in " ~ $obj.HOW.name($obj) );

        my $obj_how := nqp::how_nd($obj);
        if nqp::can($obj_how, 'submethod_table') {
            my %submethods := nqp::hllize($obj_how.submethod_table($obj));
            my $found := nqp::atkey(%submethods, $name);
            return $found if nqp::isconcrete($found);
        }
        my %methods;
        my @mro := self.mro($obj);
        my int $i := 0;
        my int $n := nqp::elems(@mro);
        while $i < $n {
            my $class := nqp::atpos(@mro, $i);
            %methods := nqp::hllize($class.HOW.method_table($class));
            my $found := nqp::atkey(%methods, $name);
            return $found if nqp::isconcrete($found);
            $i++;
        }
        !$no_fallback && nqp::can(self, 'find_method_fallback')
            ?? self.find_method_fallback($obj, $name)
            !!nqp::null()
    }

    method find_method_qualified($obj, $qtype, $name) {
        if $qtype.HOW.archetypes.parametric && nqp::can(self, 'concretization') {
            # Resolve it via the concrete form of this parametric. Look deep for a candidate.
            my $conc := self.concretization($obj, $qtype, :local(0), :transitive(1), :relaxed(1));
            nqp::hllize($conc.HOW.method_table($conc)){$name}
                || nqp::hllize($conc.HOW.submethod_table($conc)){$name}
        }
        else {
            # Non-parametric, so just locate it from the already concrete
            # type (or fallback to this if no .concretization on ourself).
            $qtype.HOW.find_method($qtype, $name)
        }
    }

    # Maybe this belongs on a role. Also, may be worth memoizing.
    method can($obj, $name) {
        my @meths;
        my %smt := nqp::hllize(self.submethod_table($obj));
        if nqp::existskey(%smt, $name) {
            @meths.push(%smt{$name});
        }
        for self.mro($obj) {
            my %mt := nqp::hllize($_.HOW.method_table($_));
            if nqp::existskey(%mt, $name) {
                @meths.push(%mt{$name})
            }
        }
        @meths
    }

    method publish_method_cache($obj) {
#?if !moar
        # Walk MRO and add methods to cache, unless another method
        # lower in the class hierarchy "shadowed" it.
        my %cache;
        my @mro_reversed;
        my $authable := 1;
        for self.mro($obj) {
            @mro_reversed.unshift($_);
        }
        for @mro_reversed {
            for nqp::hllize($_.HOW.method_table($_)) {
                %cache{$_.key} := nqp::decont($_.value);
            }
            if nqp::can($_.HOW, 'is_composed') && !$_.HOW.is_composed($_) {
                $authable := 0;
            }
        }

        # Also add submethods.
        for nqp::hllize($obj.HOW.submethod_table($obj)) {
            %cache{$_.key} := nqp::decont($_.value);
        }

        nqp::setmethcache($obj, %cache);
        unless nqp::can(self, 'has_fallbacks') && self.has_fallbacks($obj) {
            nqp::setmethcacheauth($obj, $authable);
        }
#?endif
    }

    method all_method_table($obj) {
        my $table := $!cached_all_method_table;
        unless nqp::isconcrete($table) {
            $table := nqp::hash();
            my @mro := self.mro($obj);
            my int $i := nqp::elems(@mro);
            while $i-- {
                my $class := nqp::atpos(@mro, $i);
                for nqp::hllize($class.HOW.method_table($class)) {
                    $table{$_.key} := nqp::decont($_.value);
                }
            }
            for nqp::hllize($obj.HOW.submethod_table($obj)) {
                $table{$_.key} := nqp::decont($_.value);
            }
            nqp::scwbdisable();
            $!cached_all_method_table := $table;
            nqp::scwbenable();
        }
        $table
    }

    method invalidate_method_caches($obj) {
        nqp::scwbdisable();
        $!cached_all_method_table := nqp::null();
        nqp::scwbenable();
#?if !moar
        nqp::setmethcacheauth($obj, 0);
#?endif
    }
}

# vim: expandtab sw=4
