sourceafReflux::Reflux.fan

using afIoc
using gfx
using fwt

** (Service) - The main API for managing a Reflux application.
mixin Reflux {
    
    // TODO: Fandoc this class
    abstract Registry registry()
    abstract Void callLater(Duration delay, |->| f)

    abstract RefluxPrefs preferences()
    
    abstract Void refresh()
    abstract Window window()
    abstract Void exit()
    
    ** Resolves the given URI into a 'Resource'.
    abstract Resource resolve(Str uri)

    abstract Void load(Str uri, LoadCtx? ctx := null)
    abstract Void loadResource(Resource resource, LoadCtx? ctx := null)
    abstract View? activeView()
    abstract Bool closeView(View view, Bool force)      // currently, only activeView is available, need views()
    abstract Void replaceView(View view, Type viewType) // currently, only activeView is available, need views()
    
    abstract Panel? getPanel(Type panelType, Bool checked := true)
    abstract Panel showPanel(Type panelType)
    abstract Panel hidePanel(Type panelType)
    
    abstract Void copyToClipboard(Str text)


    ** Use to launch a Reflux application. Example:
    **
    **   Reflux.start("Example App", [AppModule#]) |Reflux reflux, Window window| {
    **       reflux.showPanel(MyPanel#)
    **       ...
    **   }
    static Void start(Str appName, Type[] modules, |Reflux, Window|? onOpen := null) {
        bob := RegistryBuilder()

        // try to dig out the project name
        projName := modules.first?.pod?.meta?.get("proj.name")
        version  := modules.first?.pod?.version
        if (projName != null && version != null)
            bob["afIoc.bannerText"] = "$projName v$version"
        
        registry := bob
            .addModule(RefluxModule#)
            .addModules(modules)
            .set("afReflux.appName", appName)
            .build.startup
        reflux   := (Reflux) registry.serviceById(Reflux#.qname)
        frame    := (Frame)  reflux.window
        
        // onActive -> onFocus -> onOpen
        frame.onOpen.add {
            // Give the widgets a chance to display themselves and set defaults
            Desktop.callLater(50ms) |->| {
                // load the session before we start loading URIs and opening tabs
                session := (Session) registry.serviceById(Session#.qname)
                session.load
                
                // once we've all started up and settled down, load URIs from the command line
                onOpen?.call(reflux, frame)
            }
        }

        frame.open
        registry.shutdown
    }
}

internal class RefluxImpl : Reflux, RefluxEvents {
    @Inject private UriResolvers    uriResolvers
    @Inject private RefluxEvents    refluxEvents
    @Inject private Preferences     prefsCache
    @Inject private Errors          errors
    @Inject private Panels          panels
    @Inject private History         history
    @Inject private Session         session
    @Inject override Registry       registry
            override View?          activeView
//  @Autobuild { implType=Frame# }
            override Window         window

    new make(EventHub eventHub, |This| in) { in(this)
        eventHub.register(this)
        // FIXME: IoC Err - autobuild builds twice
        window = registry.autobuild(Frame#, [this])
    }

    override RefluxPrefs preferences() {
        prefsCache.loadPrefs(RefluxPrefs#)
    }

    override Void callLater(Duration delay, |->| f) {
        Desktop.callLater(delay) |->| {
            try f()
            catch (Err err) {
                errors.add(err)
            }
        }
    }

    override Resource resolve(Str uri) {
        uriResolvers.resolve(uri)
    }
    
    override Void load(Str uri, LoadCtx? ctx := null) {
        ctx = ctx ?: LoadCtx()

        try {
            u := uri.toUri
            if (u.query.containsKey("view")) {
                ctx.viewType = Type.find(u.query["view"])
                uri = removeQuery(uri, "view", u.query["view"])
            }
    
            u = uri.toUri
            if (u.query.containsKey("newTab")) {
                ctx.newTab = u.query["newTab"].toBool(false) ?: false
                uri = removeQuery(uri, "newTab", u.query["newTab"])
            }
            
        } catch { /* meh */ }

        try {
            resource := uriResolvers.resolve(uri)
            loadResource(resource, ctx)
        } catch (Err err) {
            errors.add(err)
        }
    }

    override Void loadResource(Resource resource, LoadCtx? ctx := null) {
        history.load(resource, ctx ?: LoadCtx())
        view := frame.load(resource, ctx ?: LoadCtx())
        loadIntoView(view, resource)
    }

    override Void refresh() {
        activeView?.refresh

        panels.panels.each {
            if (it.isShowing)
                it.refresh
        }
    }

    override Void replaceView(View view, Type viewType) {
        resource := view.resource

        if (view.isDirty)
            view.save
        
        newView := frame.replaceView(view, viewType)
        loadIntoView(newView, resource)
    }
    
    override Bool closeView(View view, Bool force) {
        frame.closeView(view, true)
    }

    override Panel? getPanel(Type panelType, Bool checked := true) {
        panels.get(panelType, checked)
    }
    
    override Panel showPanel(Type panelType) {
        panel := getPanel(panelType)
        
        if (panel.isShowing)
            return panel
        
        frame.showPanel(panel, prefAlign(panel))

        // initialise panel with data
        if (panel is RefluxEvents && activeView?.resource != null)
            Desktop.callLater(50ms) |->| {
                ((RefluxEvents) panel)->onLoad(activeView.resource, LoadCtx())
            }

        return panel
    }

    override Panel hidePanel(Type panelType) {
        panel := getPanel(panelType)
        
        if (!panel.isShowing)
            return panel

        frame.hidePanel(panel, prefAlign(panel))        

        return panel
    }
    
    override Void exit() {
        // save the open panels before we close them!
        session.save

        // actually, what is the need to close down each view and panel?
        // we've already saved the session!
//      while (activeView != null && closeView(activeView, true)) { }
//      if    (activeView != null) return
//      panels.panels.each { hidePanel(it.typeof) }
        
        frame.close
    }
    
    override Void copyToClipboard(Str text) {
        Desktop.clipboard.setText(text)
    }
    
    override Void onViewActivated(View view) {
        activeView = view
    }

    override Void onViewDeactivated(View view) {
        activeView = null
    }
    
    override Void onLoadSession(Str:Obj? session) {
        frame.size = session["afReflux.frameSize"] ?: frame.size

        // a tiny fudge to show the Folder Panel by defauly
        panels := (Str[]) session.get("afReflux.openPanels", ["afExplorer::FoldersPanel"])
        panels.each {
            panelType := Type.find(it, false)
            if (panelType != null && getPanel(panelType, false) != null)
                showPanel(panelType)
        }
    }

    override Void onSaveSession(Str:Obj? session) {
        session["afReflux.frameSize" ] = frame.size
        session["afReflux.openPanels"] = panels.panels.findAll { it.isShowing }.map { it.typeof.qname }
    }

    private Void loadIntoView(View? view, Resource resource) {
        try view?.load(resource)
        catch (Err err)
            errors.add(err)
        refluxEvents.onLoad(resource)       
    }

    private Obj prefAlign(Panel panel) {
        preferences.panelPrefAligns[panel.typeof] ?: panel.prefAlign
    }

    private Frame frame() {
        window
    }
    
    private static Str removeQuery(Str str, Str key, Str val) {
        str = str.replace("${key}=${val}", "")
        if (str.endsWith("?"))
            str = str[0..-2]
        return str
    }
}

** Contextual data for loading 'Resources'. 
class LoadCtx {
    ** If 'true' then the resource is opened in a new View tab.
    Bool    newTab
    
    ** The 'View' the resource should be opened in. 
    Type?   viewType

    @NoDoc
    Bool    addToHistory    := true
    
    override Str toStr() {
        str := "LoadCtx { "
        str += "newTab=${newTab} "
        if (viewType != null)
            str += "viewType=${viewType.qname} "
        str += "}"
        return str
    }
}