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(Resource? resource := null)
    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 Void saveAll()
    
    abstract Panel? getPanel(Type panelType, Bool checked := true)
    abstract Panel showPanel(Type panelType)
    abstract Panel hidePanel(Type panelType)

    abstract Void copyToClipboard(Str text)
}

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
            override Window         window

    new make(EventHub eventHub, |This| in) { 
        in(this)
        eventHub.register(this)
        window = registry.autobuild(Frame#, [this])
    }

    override RefluxPrefs preferences() {
        prefsCache.loadPrefs(RefluxPrefs#, "afReflux.fog")
    }

    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(Resource? resource := null) {
        frame.refreshViews(resource)
        panels.refreshPanels(resource)
        refluxEvents.onRefresh(resource)
    }

    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 saveAll() {
        frame.dirtyViews.each |view| {
            view.save
        }       
    }
    
    override Void exit() {
        // only active and close what we have to - we don't want refresh flashes causing an epi-fit!
        frame.dirtyViews.each |view| {
            frame.activateView(view)
            closeView(view, true)
        }

        session.save
        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
    }
}