sourceafQuickFlux::Datastore.fan

using afConcurrent
using afIoc

const abstract class Datastore {
    private static const Log        log         := Utils.getLog(Datastore#)
    private const SynchronizedState conState    

    const Type  entityType
    private const Str   stateFileName
    
    @Inject private const ActorPools actorPools
    
    new make(Type entityType, |This| in) {
        in(this)
        this.entityType     = entityType
        this.stateFileName  = "${entityType.name}State.fog"
        this.conState       = SynchronizedState(actorPools["afQuickFlux.system"]) |->Obj| { loadFromFile(DatastoreState#) }
    }
    
    virtual Entity save(Entity entity) {
        ent := entity
        getState |state| {
            state.entities[ent.id] = ent
            saveToFile(state)
            return null
        }
        return entity
    }

    virtual Entity delete(Entity entity) {
        withState |state| {
            state.entities.remove(entity.id)
            saveToFile(state)
        }
        return entity       
    }
    
    virtual Entity[] findAll() {
        getState {
            // injecting deps into entities takes *forever*! So don't do it!
            it.entities.vals.toImmutable
        }       
    }
    
    virtual Entity? findById(Int entityId, Bool checked := true) {
        getState {
            it.entities.get(entityId)
        } ?: (checked ? throw Err("Entity not found for ID $entityId") : null)
    }   
    
    protected Int getNextId() {
        getState |state| {
            nextId := state.nextId
            state.nextId = state.nextId + 1
            return nextId
        }       
    }
    
    internal Void withState(|DatastoreState| state) {
        conState.withState(state)
    }

    internal Obj? getState(|DatastoreState -> Obj?| state) {
        conState.getState(state)
    }   

    private static File? toFile(Type prefsType, Str name := "${prefsType.name}.fog") {
        pathUri := `etc/${QuickFluxModule.appPod.name}/${name}`
        envFile := Env.cur.findFile(pathUri, false) ?: Env.cur.workDir + pathUri
        file    := envFile.normalize    // normalize gives the full absolute path
        return file
    }

    private Obj loadFromFile(Type stateType) {
        file    := toFile(stateType, stateFileName)
        state   := readFromFile(file)
        if (state == null)
            state = stateType.make([entityType])
        return state
    }

    private Void saveToFile(Obj prefs) {
        file := toFile(prefs.typeof, stateFileName)
        file.writeObj(prefs, ["indent":2, "skipDefaults":true])
    }
    
    private Obj? readFromFile(File? file) {
        Obj? value := null
        try {
            if (file != null && file.exists) {
                log.info("Loading state: $file")
                value = file.in.readObj(["makeArgs":[entityType]])
            }
        } catch (Err e) {
            log.err("Cannot load state: $file", e)
        }
        return value
    }
}

@Serializable
internal class DatastoreState {
    
    Int     nextId      := 1
    Int:Obj entities    := [:]
    
    new make(Type entityType := Obj#, |This|? in := null) {
        in?.call(this)
        entities = Utils.makeMap(Int#, entityType).addAll(entities)
    }
}