sourceafPillow::Pages.fan

using afIoc::Inject
using afIoc::Scope
using afIocEnv::IocEnv
using afIocConfig::Config
using afEfanXtra::EfanXtra
using afEfanXtra::EfanLibraries
using afEfanXtra::ComponentRenderer
using afEfanXtra::ComponentMeta
using afEfanXtra::InitRender
using afBedSheet::HttpRequest
using afBedSheet::HttpResponse
using afBedSheet::HttpStatus
using afBedSheet::BedSheetServer
using afBedSheet::ValueEncoders
using afBedSheet::ValueEncodingErr
using afBedSheet::Text

** (Service) - Methods for discovering Pillow pages and returning `PageMeta` instances.
const mixin Pages {

    ** Returns all Pillow page types.
    abstract Type[] pageTypes()

    ** Create 'PageMeta' for the given page type and context. 
    ** 
    ** (Note: 'pageContext' are the arguments to the '@InitRender' method, if any.) 
    abstract PageMeta pageMeta(Type pageType, Obj[]? pageContext := null)

    ** Create 'PageMeta' for the given page type and context.
    **  
    ** Convenience / alias for 'pageMeta(...)'.
    @Operator
    abstract PageMeta get(Type pageType, Obj[]? pageContext := null)

    ** Manually renders the given page using 'pageContext' as arguments to '@InitRender'. 
    ** 
    ** Note that 'pageContext' Strs are converted to their appropriate type via BedSheet's 'ValueEncoder' service.
    // Obj 'cos the method may be called manually (from ResponseProcessor)
    abstract Obj renderPage(Type pageType, Obj[]? pageContext := null)

    ** Manually renders the given 'pageMeta'. 
    ** 
    ** There should be no need to call this in normal Pillow usage.
    abstract Obj renderPageMeta(PageMeta pageMeta)

    ** Manually executes the page event in the given page context.
    ** 
    ** Note this may be used to call *any* method on a page, not just ones annotated with the '@PageEvent' facet.
    // Obj 'cos the method may be called manually (from ResponseProcessor)
    abstract Obj callPageEvent(Type pageType, Obj[]? pageContext, Method eventMethod, Obj[]? eventContext)

    // moar thought needs to go into how to get / set the data ctx so the page can retrieve thread local data
//  ** Returns the currently rendering page. Or 'null' if no page is being rendered.
//  abstract EfanComponent? getRenderingPage()
}

internal const class PagesImpl : Pages {
    
    @Inject private const ValueEncoders         valueEncoders
    @Inject private const EfanXtra              efanXtra
    @Inject private const EfanLibraries         efanLibs
    @Inject private const IocEnv                iocEnv
    @Inject private const BedSheetServer        bedServer           
    @Inject private const HttpResponse          httpRes
    @Inject private const HttpRequest           httpReq
    @Inject private const ComponentRenderer     componentRenderer
    @Inject private const ComponentMeta         componentMeta
    @Inject private const PageFinder            pageFinder
            private const Type:PageMetaState    pageCache 
            override const Type[]               pageTypes
    @Config
    @Inject private const Str                   cacheControl


    new make(Scope scope, |This| in) {
        in(this)
        metaFactory := (PageMetaStateFactory) scope.build(PageMetaStateFactory#)
        cache := Type:PageMetaState[:] { ordered = true }
        efanXtra.libraryNames.each |libName| {
            pod := efanLibs.pod(libName)
            pageFinder.findPageTypes(pod).each {
                cache[it] =  metaFactory.toPageMetaState(it)
            }
        }
        this.pageCache = cache
        this.pageTypes = pageCache.keys.sort
    }
    
    override PageMeta pageMeta(Type pageType, Obj[]? pageContext := null) {
        pageState := pageCache[pageType] ?: throw PageNotFoundErr(ErrMsgs.couldNotFindPage(pageType), pageCache.keys) 
        return PageMetaImpl(pageState, pageContext) {
            it.bedServer        = this.bedServer
            it.httpRequest      = this.httpReq
            it.valueEncoders    = this.valueEncoders
        }
    }

    override PageMeta get(Type pageType, Obj[]? pageContext := null) {
        pageMeta(pageType, pageContext)
    }
    
    override Obj renderPage(Type pageType, Obj[]? pageContext := null) {
        renderPageMeta(pageMeta(pageType, pageContext))
    }

    override Obj renderPageMeta(PageMeta pageMeta) {
        page     := efanXtra.component(pageMeta.pageType)
        initArgs := convertArgs(pageMeta.pageContext, pageMeta.initRender.paramTypes)
        pageMeta = pageMeta.withContext(initArgs)

        // todo - does this meta need to be stacked? -> see PageMetaCtx
        httpReq.stash["afPillow.pageMeta"]  = pageMeta
        try return PageMetaImpl.push(pageMeta) |->Obj| {
            retVal := null
            componentRenderer.runInCtx(page) |->| {  
                
                // call initRender() - process any non-null return value
                initValue := componentMeta.callMethod(InitRender#, page, initArgs)
                if (initValue != null) {
                    retVal = initValue
                    return

                } else {
                    // re-render the page without re-calling @InitRender so event changes get picked up 
                    if (!iocEnv.isProd)
                        httpRes.headers["X-afPillow-renderedPage"] = pageMeta.pageType.qname
    
                    if (iocEnv.isProd)
                        // set the default cache headers
                        httpRes.headers.cacheControl = cacheControl     
    
                    renderBuf := componentRenderer.doRenderLoop(page)
                    
                    retVal = Text.fromContentType(renderBuf.toStr, pageMeta.contentType)    
                }
            }
            return retVal
        }
        finally httpReq.stash.remove("afPillow.pageMeta")
    }

    override Obj callPageEvent(Type pageType, Obj[]? pageContext, Method eventMethod, Obj[]? eventContext) {
        if (!iocEnv.isProd) 
            httpRes.headers["X-afPillow-calledEvent"] = eventMethod.qname

        page        := efanXtra.component(pageType)
        pageMeta    := pageMeta(pageType, pageContext)
        initArgs    := convertArgs(pageContext  ?: Str#.emptyList, pageMeta.initRender.paramTypes)
        eventArgs   := convertArgs(eventContext ?: Str#.emptyList, eventMethod.params.map { it.type })
        
        // todo - does this meta need to be stacked? -> see PageMetaCtx
        httpReq.stash["afPillow.eventMeta"] = EventMeta {
            it.pageMeta     = pageMeta.withContext(initArgs)
            it.eventMethod  = eventMethod
            it.eventContext = eventArgs
        }
        
        try return PageMetaImpl.push(pageMeta) |->Obj| {
            retVal := null
            componentRenderer.runInCtx(page) |->| {  
                
                // call initRender() - process any non-null return value
                initValue := componentMeta.callMethod(InitRender#, page, initArgs)
                if (initValue != null) {
                    retVal = initValue
                    return

                } else {
                    // call event method - process any non-null return value
                    eventValue := eventMethod.callOn(page, eventArgs)
                    if (eventValue != null) {
                        retVal = eventValue
                        return
                    }
    
                    // re-render the page without re-calling @InitRender so event changes get picked up 
                    if (!iocEnv.isProd)
                        httpRes.headers["X-afPillow-renderedPage"] = pageMeta.pageType.qname
    
                    // set the default cache headers
                    if (iocEnv.isProd)
                        httpRes.headers.cacheControl = cacheControl     
    
                    renderBuf := componentRenderer.doRenderLoop(page)
                    
                    retVal = Text.fromContentType(renderBuf.toStr, pageMeta.contentType)    
                }
            }
            return retVal
        }
        finally httpReq.stash.remove("afPillow.eventMeta")
    }
    
//  override EfanComponent? getRenderingPage() {
//      Efan.renderingStack.eachrWhile |element| {
//          element.templateInstance is EfanComponent && element.templateMeta.type.hasFacet(Page#)
//              ? element.templateInstance
//              : null
//      }
//  }
    
    // ---- Private Methods --------------------------------------------------------------------------------------------
    
    ** Convert the Str from Routes into real arg objs
    private Obj[] convertArgs(Obj[] argsIn, Type[] convertTo) {
        try
            return argsIn.map |arg, i -> Obj?| {
                // guard against having more args than the method has params! 
                // Should never happen if the Routes do their job!
                paramType := convertTo.getSafe(i)
                if (paramType == null)
                    return arg
                return arg is Str ? valueEncoders.toValue(paramType, arg) : arg
            }
        // if the args can't be converted then clearly the URL doesn't exist!
        catch (ValueEncodingErr valEncErr) {
            throw HttpStatus.makeErr(404, valEncErr.msg, valEncErr)
        }
    }
}