sourceafIoc::Configuration.fan

using afBeanUtils::TypeCoercer

** Use to add create and override service configuration contributions. 
** 
** Every service may receive a list or ordered map of values; called its configuration.
** Any (external) module may contribute to this using in a configuration method.
** 
** The service defines the *type* of contribution by declaring a parameterised list or map in its 
** ctor or builder method. Contributions must be compatible with this type. 
** Example, the service 'SeaLions' may define a configuration map of '[Str:Penguin]' with the following ctor:
** 
** pre>
** class SeaLion {
**     new make(Str:Penguin foodGroups) {
**         ...
**     }
** }
** <pre
**
** Contribute to the 'SeaLion' service in the 'AppModule':
** 
** pre>
** const class AppModule {
**     Void defineServices(RegistryBuilder bob) {
**         bob.addService(SeaLion#)
**     }
** 
**     @Contribute { serviceType=SeaLion# }
**     Void contributeSeaLion(Configuration config) {
**         config["kevin"] = Penguin()
**     }
** } 
** <pre
**  
** Or use 'RegistryBuilder':
** 
** pre>
** bob := RegistryBuilder()
** bob.addService(
** bob.contributeToServiceType(SeaLion#) |Configuration config| {
**     config["kevin"] = Penguin()
** }
** <pre
** 
@Js
mixin Configuration {

    ** Convenience method for `Scope.build`; builds an instance of the given 'Type' injecting in all dependencies.  
    Obj build(Type type, Obj?[]? ctorArgs := null, [Field:Obj?]? fieldVals := null) {
        scope.build(type, ctorArgs, fieldVals)
    }

    ** Returns the active scope.
    abstract Scope scope()

    ** Fantom Bug: `http://fantom.org/sidewalk/topic/2163#c13978`
    @Operator 
    private Obj? get(Obj key) { null }

    ** Sets a key / value pair to the service configuration with optional ordering constraints.
    **
    ** If the end service configuration is a List, then the keys are discarded and only the values passed use. 
    ** In this case, typically 'Str' keys are used for ease of use when overriding / adding constraints.   
    **  
    ** Configuration contributions are ordered across modules. 
    ** 
    ** 'key' and 'value' are coerced to the service's contribution type.
    ** 
    **   syntax: fantom
    **   config.set("key", value)
    ** 
    **   config["key"] = value
    @Operator
    abstract Constraints set(Obj key, Obj? value)

    ** Adds a value to the service configuration with optional ordering constraints.
    ** 
    ** Because the keys of *added* values are unknown, they cannot be overridden. 
    ** For that reason it is advised to use 'set()' instead. 
    **  
    ** 'value' is coerced to the service's contribution type.
    ** 
    **   syntax: fantom
    **   config.add(value)
    abstract Constraints add(Obj value)

    ** Adds a placeholder. Placeholders are empty configurations used to aid the ordering of actual values:
    ** 
    ** pre>
    ** syntax: fantom
    ** config.placeholder("end")
    ** config.set("foo", val1).before("end")
    ** config.set("bar", val2).after("end")
    ** <pre
    ** 
    ** While not very useful in the same contribution method, they become very powerful when used across multiple modules and pods.
    ** 
    ** Placeholders do not appear in the the resulting configuration and are never seen by the end service. 
    abstract Constraints addPlaceholder(Str key)
    
    ** Overrides and replaces a contributed value. 
    ** The existing key must exist.
    ** 
    ** Note that when overriding, all existing ordering constraints are cleared and must be re-set.
    ** 
    ** 'existingKey' is the id / key of the value to be replaced. 
    ** It may have been initially provided by 'set()' or have be the 'newKey' of a previous override.
    ** 
    ** 'newKey' does not appear in the the resulting configuration and is never seen by the end service.
    ** It is only used as reference to this override, so this override itself may be overridden.
    ** 3rd party libraries, when overriding, should always supply a 'newKey'. 
    ** 'newKey' may be any 'Obj' instance but sane and intelligent people will *always* pass in a 'Str'.  
    ** 
    ** 'newValue' is coerced to the service's contribution type.
    abstract Constraints overrideValue(Obj existingKey, Obj? newValue, Obj? newKey := null)
    
    ** A special kind of override whereby, should this be the last override applied, the value is 
    ** removed from the configuration.
    ** 
    ** 'existingKey' is the id / key of the value to be replaced. 
    ** It may have been initially provided by 'set()' or have be the 'newKey' of a previous override.
    ** 
    ** 'newKey' does not appear in the the resulting configuration and is never seen by the end service.
    ** It is only used as reference to this override, so this override itself may be overridden.
    ** 3rd party libraries, when overriding, should always supply a 'newKey'. 
    ** 'newKey' may be any 'Obj' instance but sane and intelligent people will *always* pass in a 'Str'.  
    abstract Void remove(Obj existingKey, Obj? newKey := null)


    ** Defines a block where defined contributions are kept in the same order. 
    ** The block itself may be ordered *before* and / or *after* other contributions:
    ** 
    ** pre>
    ** syntax: fantom
    ** config.inOrder { 
    **     config["b-1"] = 1
    **     config["b-2"] = 1
    **     config.addPlaceholder("separator")
    **     config["b-3"] = 1
    ** }.before("c-1").after("a-1")
    ** <pre
    abstract Constraints inOrder(|This| f)
}

@Js
internal class ConfigurationImpl : Configuration {
    static const    TypeCoercer         typeCoercer := TypeCoercer()
    internal const  Type                contribType
    override        Scope               scope
    private const   Str                 serviceId
    private         Int                 impliedCount
    private         Str?                impliedConstraint
    private         Obj:Contrib         allConfig
    private         Obj:Contrib         modConfig
    private         Obj:Contrib         overrides
    private         Int                 overrideCount
    private         Contrib[]?          orderedContribs

    internal new make(Scope scope, Type contribType, Str serviceId) {
        if (contribType.name != "Map" && contribType.name != "List")
            throw Err("Contributions Type is NOT a Map or a List ???")
        if (contribType.isGeneric)
            throw IocErr(ErrMsgs.contributions_configTypeIsGeneric(contribType, serviceId)) 

        this.serviceId      = serviceId
        this.contribType    = contribType
        this.scope          = scope
        this.impliedCount   = 1
        this.allConfig      = makeMap(Obj#, Contrib#)
        this.modConfig      = makeMap(Obj#, Contrib#)
        this.overrides      = makeMap(Obj#, Contrib#)
        this.overrideCount  = 1
    }

    override Constraints set(Obj key, Obj? value) {
        key   = validateKey(key, false)
        value = validateVal(value)
        
        if (modConfig.containsKey(key))
            throw IocErr(ErrMsgs.contributions_configKeyAlreadyDefined(key.toStr, modConfig[key].val))
        if (allConfig.containsKey(key))
            throw IocErr(ErrMsgs.contributions_configKeyAlreadyDefined(key.toStr, allConfig[key].val))

        contrib := Contrib(key, value)
        modConfig[key] = contrib 
        
        if (orderedContribs != null) {
            last := orderedContribs.last
            if (last != null) {
                last.before(contrib.keyFudge)
                contrib.after(last.keyFudge)
            }
            orderedContribs.add(contrib)
        }
        
        return contrib
    }

    override Constraints add(Obj value) {
        if (!Str#.fits(keyType))
            throw IocErr(ErrMsgs.contributions_keyTypeNotKnown(keyType))

        key := "afIoc.unordered-" + impliedCount.toStr.padl(2)
        impliedCount++

        return set(key, value)
    }

    override Constraints addPlaceholder(Str key) {
        set(key, Orderer.PLACEHOLDER)
    }
    
    override Constraints overrideValue(Obj existingKey, Obj? newValue, Obj? newKey := null) {
        if (newKey == null)
            newKey = "afIoc.override-" + overrideCount.toStr.padl(2)
        overrideCount = overrideCount + 1

        newKey      = validateKey(newKey, true)
        existingKey = validateKey(existingKey, true)
        newValue    = validateVal(newValue)

        if (overrides.containsKey(existingKey))
            throw IocErr(ErrMsgs.contributions_configOverrideKeyAlreadyDefined(existingKey.toStr, overrides[existingKey].key.toStr))

        if (modConfig.containsKey(newKey) || allConfig.containsKey(newKey))
            throw IocErr(ErrMsgs.contributions_configOverrideKeyAlreadyExists(newKey.toStr))

        if (overrides.vals.map { it.key }.contains(newKey))
            throw IocErr(ErrMsgs.contributions_configOverrideKeyAlreadyExists(newKey.toStr))

        contrib := Contrib(newKey, newValue, existingKey)
        overrides[existingKey] = contrib

        if (orderedContribs != null) {
            last := orderedContribs.last
            if (last != null) {
                last.before(contrib.keyFudge)
                contrib.after(last.keyFudge)
            }
            orderedContribs.add(contrib)
        }

        return contrib
    }
    
    override Void remove(Obj existingKey, Obj? newKey := null) {
        overrideValue(existingKey, Orderer.DELETE, newKey)
    }

    override Constraints inOrder(|This| f) {
        orderedContribs = Contrib[,]
        
        f(this)
        
        contraints := GroupConstraints(orderedContribs)
        orderedContribs = null
        
        return contraints
    }

    
    // ---- Internal Methods ----------------------------------------------------------------------
    
    internal Void cleanupAfterMethod() {
        modConfig.each { it.finalise }
        modConfig.each { it.findImplied(modConfig) }
        modConfig.each |v, k| { allConfig[k] = v }
    }
    
    internal Int size() {
        allConfig.size
    }

    internal List toList() {
        contribs := orderedContributions
        config   := (Obj?[]) List.make(valueType, contribs.size)
        contribs.each { config.add(it.val) }
        return config
    }

    internal Map toMap() {
        mapType := Map#.parameterize(["K":keyType, "V":valueType])
        config  := (Obj:Obj?) Map.make(mapType) { it.ordered = true }
        
        orderedContributions.each {
            config[it.key] = it.val
        }
        return config
    }

    private Contrib[] orderedContributions() {
        keys := makeMap(keyType, keyType)
        allConfig.each |val, key| { keys[key] = key }
        
        // don't alter the class state so getConfig() may be called more than once
        config := (Obj:Contrib) this.allConfig.dup

        // normalise keys -> map all keys to orig key and apply overrides
        norm := (Obj:Contrib) this.overrides.dup 
        found := true
        while (!norm.isEmpty && found) {
            found = false
            norm = norm.exclude |val, existingKey| {
                overrideKey := val.key
                if (keys.containsKey(existingKey)) {
                    keys[overrideKey] = keys[existingKey]
                    found = true
                    
                    config[keys[existingKey]] = val
                    
                    // dispose of the override key
                    val.key = keys[existingKey]
                    return true
                } else {
                    return false
                }
            }
        }

        if (!norm.isEmpty) {
            overrideKeys := norm.vals.map { it.key.toStr }.join(", ")
            existingKeys := norm.keys.map { it.toStr }.join(", ")
            throw IocErr(ErrMsgs.contributions_overrideDoesNotExist(existingKeys, overrideKeys))
        }
        
        configKeys := config.keys
        orderer := Orderer()
        config.each |val, key| {
            value := (val.val === Orderer.DELETE || val.val === Orderer.PLACEHOLDER) ? val.val : val
            orderer.addOrdered(key, value, val.befores, val.afters)
        }

        return orderer.toOrderedList
    }   
    
    
    
    // ---- Helper Methods ------------------------------------------------------------------------

    private Obj validateKey(Obj key, Bool isOverrideKey) {
        // don't use ReflectUtils.fits() - let TypeCoercer do a proper job.
        if (key.typeof.fits(keyType))
            return key
        
        if (isOverrideKey)
            return key

        if (typeCoercer.canCoerce(key.typeof, keyType))
            return typeCoercer.coerce(key, keyType)

        throw IocErr(ErrMsgs.contributions_configTypeMismatch("key", key.typeof, keyType))
    }

    private Obj? validateVal(Obj? val) {
        if (val === Orderer.DELETE || val === Orderer.PLACEHOLDER)
            return val

        if (val == null) {
            if (!valueType.isNullable)
                throw IocErr(ErrMsgs.contributions_configTypeMismatch("value", null, valueType))
            return val
        }

        // don't use ReflectUtils.fits() - let TypeCoercer do a proper job.
        if (val.typeof.fits(valueType))
            return val

        // empty lists and maps can always be converted
        if (!isEmptyList(val) && !isEmptyMap(val))
            if (!typeCoercer.canCoerce(val.typeof, valueType))
                throw IocErr(ErrMsgs.contributions_configTypeMismatch("value", val.typeof, valueType))

        return typeCoercer.coerce(val, valueType)
    }
    
    private Bool isEmptyList(Obj val) {
        (val is List) && (((List) val).isEmpty)
    }
    
    private Bool isEmptyMap(Obj val) {
        (val is Map) && (((Map) val).isEmpty)
    }

    private once Type keyType() {
        contribType.name == "Map" ? contribType.params["K"] : Obj#
    }

    private once Type valueType() {
        contribType.params["V"]
    }

    private Obj:Obj? makeMap(Type keyType, Type valType) {
        mapType := Map#.parameterize(["K":keyType, "V":valType])
        return keyType.fits(Str#) ? Map.make(mapType) { caseInsensitive = true } : Map.make(mapType) { ordered = true }
    }

    @NoDoc
    override Str toStr() {
        "${contribType.name} configuration of ${contribType.signature} for '$serviceId'".replace("sys::", "")
    }   
}