sourceafIoc::StrategyRegistry.fan

using afConcurrent

** A helper class that looks up Objs via Type inheritance search.
const class StrategyRegistry {  
    private const AtomicMap     parentCache     := AtomicMap()
    private const AtomicMap     childrenCache   := AtomicMap()
    private const Type:Obj?     values
    
    ** Creates an StrategyRegistry with the given list. All types are coerced to non-nullable types.
    ** An 'Err' is thrown if a duplicate is found in the process. 
    new make(Type:Obj? values) {
        nonDups := Utils.makeMap(Type#, Obj?#)
        values.each |val, type| {
            nonNullable := type.toNonNullable
            if (nonDups.containsKey(nonNullable)) 
                throw Err("Type $nonNullable is already mapped to value ${nonDups[nonNullable]}")
            nonDups[nonNullable] = val
        }
        this.values = nonDups.toImmutable
    }

    ** Standard Map behaviour - looks up an Obj via the type. 
    Obj? findExactMatch(Type exact, Bool checked := true) {
        nonNullable := exact.toNonNullable
        return values.get(nonNullable)
            ?: check(nonNullable, checked)
    }

    ** Returns the value of the closest parent of the given type.
    ** Example:
    ** pre>
    **   strategy := StrategyRegistry( [Obj#:1, Num#:2, Int#:3] )
    **   strategy.findClosestParent(Obj#)   // --> 1
    **   strategy.findClosestParent(Num#)   // --> 2
    **   strategy.findClosestParent(Float#) // --> 2
    ** <pre
    Obj? findClosestParent(Type type, Bool checked := true) {
        nonNullable := type.toNonNullable
        // chill, I got tests for all this!
        return parentCache.getOrAdd(nonNullable) |->Obj?| {
            deltas := values
                .findAll |val, t2| { nonNullable.fits(t2) }
                .map |val, t2->Int?| {
                    nonNullable.inheritance.eachWhile |sup, i| {
                        (sup == t2 || sup.mixins.contains(t2)) ? i : null
                    }
                }
            
            if (deltas.isEmpty)
                return null
            
            minDelta := deltas.vals.min
            match    := deltas.eachWhile |delta, t2| { (delta == minDelta) ? t2 : null }
            return values[match]
        } ?: check(nonNullable, checked)
    }
    
    ** Returns the values of the children of the given type.
    ** Example:
    ** pre>
    **   strategy := StrategyRegistry( [Obj#:1, Num#:2, Int#:3] )
    **   strategy.findChildrenOf(Obj#)   // --> [1, 2, 3]
    **   strategy.findChildrenOf(Num#)   // --> [2, 3]
    **   strategy.findChildrenOf(Float#) // --> [,]
    ** <pre
    Obj?[] findAllChildren(Type type) {
        nonNullable := type.toNonNullable
        return childrenCache.getOrAdd(nonNullable) |->Obj?[]| {
            values.findAll |val, key| { key.fits(type) }.vals
        }
    }
    
    @NoDoc @Deprecated { msg="Use findClosestParent() instead" }  
    Obj? findBestFit(Type bestFit, Bool checked := true) {
        findClosestParent(bestFit, checked)
    }

    ** Clears the lookup caches
    Void clearCache() {
        parentCache.clear
        parentCache.clear
    }
    
    private Obj? check(Type nonNullable, Bool checked) {
        checked ? throw NotFoundErr("Could not find match for Type ${nonNullable}.", values.keys) : null
    }
    
    @NoDoc
    override Str toStr() {
        values.keys.toStr
    }
}