sourceafBedSheet::BedSheetBuilder.fan

using afIoc
using afIocEnv

class BedSheetBuilder {
    private const static Log log := Utils.getLog(BedSheetBuilder#)

    ** The application name. Taken from the app pod's 'proj.name' meta, or the pod name if the meta doesn't exist.
    ** Read only.
    Str appName {
        get { options[BsConstants.meta_appName] }
        private set { throw Err("Read only") }
    }
    
    ** The HTTP port to run the app on. Defaults to '8069'
    Int port {
        get { registryBuilder.options[BsConstants.meta_appPort] }
        set { registryBuilder.options[BsConstants.meta_appPort] = it }
    }

    ** Returns the options from the IoC 'RegistryBuilder'.
    ** Read only.
    Str:Obj? options {
        get { registryBuilder.options }
        private set { throw Err("Read only") }
    }

    ** The underlying IoC 'RegistryBuilder'.
    ** Read only.
    RegistryBuilder registryBuilder { private set }

    private new makeFromBob(RegistryBuilder bob) {
        this.registryBuilder = bob
        initBanner(registryBuilder)
    }
    
    ** Creates a 'BedSheetBuilder'. If 
    new make(Str appName, Bool addPodDependencies := true) {
        this.registryBuilder = RegistryBuilder()
        this.port = 0
        initModules(registryBuilder, appName, addPodDependencies)
        initBanner(registryBuilder)
    }

    ** Adds a module to the registry. 
    ** Any modules defined with the '@SubModule' facet are also added.
    ** 
    ** Convenience for 'registryBuilder.addModule()'
    This addModule(Type moduleType) {
        registryBuilder.addModule(moduleType)
        return this
    }
    
    ** Adds many modules to the registry
    ** 
    ** Convenience for 'registryBuilder.addModules()'
    This addModules(Type[] moduleTypes) {
        registryBuilder.addModules(moduleTypes)
        return this
    }
    
    ** Inspects the [pod's meta-data]`docLang::Pods#meta` for the key 'afIoc.module'. This is then 
    ** treated as a CSV list of (qualified) module type names to load.
    ** 
    ** If 'addDependencies' is 'true' then the pod's dependencies are also inspected for IoC 
    ** modules.
    **  
    ** Convenience for 'registryBuilder.addModulesFromPod()'
    This addModulesFromPod(Str podName, Bool addDependencies := true) {
        registryBuilder.addModulesFromPod(podName, addDependencies)
        return this     
    }
    
    ** Build the IoC 'Registry'. Note the registry will still need to be started.
    Registry buildRegistry() {
        registryBuilder.removeModule(IocEnvModule#)
        return registryBuilder.build
    }

    ** Convenience method to start a Wisp server running BedSheet.
    Int startWisp(Int port := 8069, Bool proxy := false, Str? env := null) {
        this.port = port
        options["afBedSheet.env"] = env
        mod := proxy ? ProxyMod(this, port) : BedSheetWebMod(this)
        return WebModRunner().run(mod, port)
    }

    @NoDoc // for serialisation
    Str toStringy() {
        bob := registryBuilder.dup
        bob.options.remove("afIoc.bannerText")
        
        // Pod's aren't serializable
        appPod := (Pod) bob.options[BsConstants.meta_appPod]
        bob.options[BsConstants.meta_appPodName] = appPod.name
        bob.options.remove(BsConstants.meta_appPod)
        
        // we code into a Str so it's all on one line
        return Buf().writeObj(bob).flip.readAllStr.toCode
    }

    @NoDoc // for serialisation
    static BedSheetBuilder fromStringy(Str str) {
        nwl := (Str) str.toBuf.readObj
        bob := (RegistryBuilder) nwl.toBuf.readObj
        
        // re-instate appPod
        appPodName  := (Str) bob.options[BsConstants.meta_appPodName]
        bob.options[BsConstants.meta_appPod] = Pod.find(appPodName, true)
        bob.options.remove(BsConstants.meta_appPodName)
        
        return BedSheetBuilder(bob)
    }
    
    private static Void initBanner(RegistryBuilder bob) {
        bannerText := easterEgg("Alien-Factory BedSheet v${BedSheetWebMod#.pod.version}, IoC v${Registry#.pod.version}")
        bob.options["afIoc.bannerText"] = bannerText        
    }

    private static Void initModules(RegistryBuilder bob, Str moduleName, Bool transDeps) {
        Pod?  pod
        Type? mod
        Type[] mods := Type#.emptyList
        
        // Pod name given...
        // lots of start up checks looking for pods and modules... 
        // see https://bitbucket.org/SlimerDude/afbedsheet/issue/1/add-a-warning-when-no-appmodule-is-passed
        if (!moduleName.contains("::")) {
            pod = Pod.find(moduleName, true)
            log.info(BsLogMsgs.bedSheetWebMod_foundPod(pod))
            mods = findModFromPod(pod)
            mod = mods.first
        }

        // AppModule name given...
        if (moduleName.contains("::")) {
            mod = Type.find(moduleName, true)
            log.info(BsLogMsgs.bedSheetWebMod_foundType(mod))
            pod = mod.pod
        }

        // we're screwed! No module = no web app!
        if (mod == null)
            log.warn(BsLogMsgs.bedSheetWebMod_noModuleFound)
        
        if (pod != null) {
            if (!transDeps)
                log.info("Suppressing transitive dependencies...")
            bob.addModulesFromPod(pod.name, transDeps)
        }
        if (mod != null) {
            if (!bob.moduleTypes.contains(mod))
                bob.addModule(mod)
        }
        bob.addModules(mods)
        
        // A simple thing - ensure the BedSheet module is added! 
        // (transitive dependencies are added explicitly via @SubModule)
        if (!bob.moduleTypes.contains(BedSheetModule#))
             bob.addModule(BedSheetModule#)

        regOpts := bob.options
        regOpts[BsConstants.meta_appName]   = (pod?.meta?.get("proj.name") ?: pod?.name) ?: "Unknown"
        regOpts[BsConstants.meta_appPod]    = pod
        regOpts[BsConstants.meta_appModule] = mod
    }

    ** Looks for an 'AppModule' in the given pod. 
    private static Type[] findModFromPod(Pod pod) {
        mods := Type#.emptyList
        modNames := pod.meta["afIoc.module"]
        if (modNames != null) {
            mods = modNames.split.map { Type.find(it, true) }
            log.info(BsLogMsgs.bedSheetWebMod_foundType(mods.first))
        } else {
            // we have a pod with no module meta... so lets guess the name 'AppModule'
            mod := pod.type("AppModule", false)
            if (mod != null) {
                mods = [mod]
                log.info(BsLogMsgs.bedSheetWebMod_foundType(mod))
                log.warn(BsLogMsgs.bedSheetWebMod_addModuleToPodMeta(pod, mod))
            }
        }
        return mods
    }

    private static Str easterEgg(Str title) {
        quotes := loadQuotes
        if (quotes.isEmpty || (Int.random(0..8) != 2))
            return title
        return quotes[Int.random(0..<quotes.size)]
    }

    private static Str[] loadQuotes() {
        BedSheetWebMod#.pod.file(`/res/misc/quotes.txt`).readAllLines.exclude { it.isEmpty || it.startsWith("#")}
    }
}