//------------------------------------------------------------------------------
// emFileModel.h
//
// Copyright (C) 2005-2008,2014,2016,2018,2022 Oliver Hamann.
//
// Homepage: http://eaglemode.sourceforge.net/
//
// This program is free software: you can redistribute it and/or modify it under
// the terms of the GNU General Public License version 3 as published by the
// Free Software Foundation.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU General Public License version 3 for
// more details.
//
// You should have received a copy of the GNU General Public License version 3
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//------------------------------------------------------------------------------

#ifndef emFileModel_h
#define emFileModel_h

#ifndef emPriSchedAgent_h
#include <emCore/emPriSchedAgent.h>
#endif

#ifndef emSigModel_h
#include <emCore/emSigModel.h>
#endif

class emFileModelClient;


//==============================================================================
//================================ emFileModel =================================
//==============================================================================

class emFileModel : public emModel {

public:

        // Abstract base class for a shared file model. Such a file model can
        // represent a file of a certain file format in memory. The most
        // important features are:
        //
        //  - File models can have clients (=> class emFileModelClient). The
        //    clients tell how much memory the model may allocate at most. If
        //    the model would need more, the file is not loaded. Without any
        //    client, the file is never loaded. Thus, if you want a file to be
        //    loaded by a file model, you will have to create and manage at
        //    least one emFileModelClient.
        //
        //  - Loading and unloading is performed automatically, depending on the
        //    the state of the clients.
        //
        //  - Saving is not performed automatically, except a derived class
        //    implements such an automatism.
        //
        //  - Loading and saving are performed step by step in the Cycle method,
        //    so that it does not block the other engines of the program.
        //
        //  - Normally, only one file model is loading at a time (just to avoid
        //    heavy seeking of the hard drive). The order of loading multiple
        //    file models is determined by a priority which can be set at the
        //    clients.

        virtual const emString & GetFilePath() const;
                // Path name of the file. Returns the model name by default.

        const emSignal & GetFileStateSignal() const;
                // This signal is sent on any change in the results of
                // GetFileState(), GetMemoryNeed(), GetFileProgress() and
                // GetErrorText().

        enum FileState {
                // Possibilities for the state of the file model.

                FS_WAITING = 0,
                        // The file model wants to load the file, but it is
                        // waiting until there are no other file models in a
                        // loading state.

                FS_LOADING = 1,
                        // The file model is currently reading the file.

                FS_LOADED = 2,
                        // The file model is loaded.

                FS_UNSAVED = 3,
                        // The file model is loaded and there are unsaved
                        // changes in the model data. This state prevents from
                        // unloading and reloading.

                FS_SAVING = 4,
                        // The file model is currently writing the file.

                FS_TOO_COSTLY = 5,
                        // The file model is not loaded, because there is no
                        // file model client which accepts the memory need.

                FS_LOAD_ERROR = 6,
                        // The file model is not loaded, because there was an
                        // error in reading the file.

                FS_SAVE_ERROR = 7,
                        // Like FS_UNSAVED, but there was an error in writing
                        // the file.

                FS_MAX_VAL = 7
                        // Just the maximum possible integer value for the
                        // state.
        };

        FileState GetFileState() const;
                // Get current state of this file model.

        emUInt64 GetMemoryNeed() const;
                // Get best known number of memory bytes, which are allocated,
                // or which will be allocated, or which would be allocated, in
                // loaded state. In addition to the memory need of the file
                // model itself, this should also include the memory need of one
                // typical client (e.g. emFilePanel), if not negligible. The
                // result of GetMemoryNeed() is never zero. In unsaved or
                // errored state, it could be nonsense.

        double GetFileProgress() const;
                // Get progress of loading or saving in percent.

        const emString & GetErrorText() const;
                // Get the error description when in state FS_LOAD_ERROR or
                // FS_SAVE_ERROR.

        void Update();
                // Perform an update: If the state is FS_LOAD_ERROR, the error
                // text is cleared and the loading will be tried again. If the
                // state is FS_TOO_COSTLY with a last known memory need greater
                // than one, the loading will be tried again. If the state is
                // FS_LOADED, and if the model is out-of-date (checks file time
                // by default), the file is unloaded for loading it again.
                // Hints:
                //  - A good place to call this method is when creating a new
                //    emFileModelClient.
                //  - Do not call this too often, because some file models are
                //    always reloading.

        static emRef<emSigModel> AcquireUpdateSignalModel(
                emRootContext & rootContext
        );
                // This functions acquires a global signal model. When that
                // signal is signaled, all file models are updated (see method
                // Update()), except for those which have
                // GetIgnoreUpdateSignal()==true. The signal could be used as an
                // application-wide update signal even for reloading files which
                // are not interfaced through emFileModel.

        bool GetIgnoreUpdateSignal() const;
        void SetIgnoreUpdateSignal(bool ignore);
                // If true, this file model does not listen to the global update
                // signal. Should be set at most for private file models.

        void Load(bool immediately);
                // Normally, there is no need to call this method, because
                // loading is performed automatically. If the state is
                // FS_WAITING, Load(false) starts the loading right away,
                // ignoring the priority. If the state is FS_LOADING,
                // Load(false) performs one more step in loading the file.
                // Load(true) blocks until the state is not FS_WAITING and not
                // FS_LOADING. For other states, this method has no effect.

        void Save(bool immediately);
                // This is similar to Load, but remember that saving is not
                // started automatically. If the state is FS_UNSAVED,
                // Save(false) starts the saving (which continues
                // automatically). If the state is FS_SAVING, Save(false)
                // performs one more step in saving the file. Save(true) blocks
                // until the state is not FS_UNSAVED and not FS_SAVING. For
                // other states, this method has no effect.

        void ClearSaveError();
                // Resets the state to FS_UNSAVED, if it was FS_SAVE_ERROR.

        void HardResetFileState();
                // Unload the file and restart the loading logics. This works
                // even in unsaved or errored state, and it is the only way to
                // take back unsaved changes.

protected:

        emFileModel(emContext & context, const emString & name);
                // Constructor.
                // Arguments:
                //   context - Normally, this should be the root context.
                //   name - Normally, this is the path name of the file
                //          (otherwise GetFilePath has to be overloaded).

        virtual ~emFileModel();
                // Destructor.

        virtual bool Cycle();
                // See emEngine::Cycle. This one performs loading and saving.

        void SetUnsavedState();
                // This must be called before or immediately after modifying the
                // data (except through ResetData, TryStartLoading and
                // TryContinueLoading). Hereby, any loading or saving is
                // aborted, and the file state is set to FS_UNSAVED. The caller
                // should take care: if the state was FS_SAVING, the file will
                // be corrupted, and if the state was FS_LOADING, the data may
                // be corrupted.

        virtual void ResetData() = 0;
                // Called for unloading the file. The implementation may free
                // allocated memory or set the data to a default state.

        virtual void TryStartLoading() = 0;
        virtual bool TryContinueLoading() = 0;
        virtual void QuitLoading() = 0;
                // Called for loading the file. First, TryStartLoading is
                // called, and then TryContinueLoading is called again and again
                // until true is returned. For aborting by an error, an
                // exception with a user-readable error message can be thrown.
                // Returning true from TryContinueLoading means to have finished
                // with loading. No call should waist more than about 10
                // milliseconds (if possible somehow). There's no problem if
                // each call waists much fewer time, but if multiple calls
                // cannot really continue because of waiting for a child process
                // or so, they should do an emSleepMS(10) or something similar,
                // otherwise we would end up in busy waiting. Before allocating
                // a worth meaning amount of memory, a call should return and
                // the amount of memory should be reported through the result of
                // CalcMemoryNeed(), so that there is a chance to abort the
                // loading and to enter the state FS_TOO_COSTLY before the
                // memory would be allocated. Best is to determine the whole
                // memory need in TryStartLoading (e.g. through reading just a
                // file header), and to allocate that memory in
                // TryContinueLoading. It is guaranteed that ResetData is called
                // before TryStartLoading. And it is guaranteed that QuitLoading
                // is called at the end, either on success, or for aborting, or
                // after an error - but remember that it can never be called
                // through the destructor of emFileModel (=> prepare the
                // destructor of the derived class accordingly).

        virtual void TryStartSaving() = 0;
        virtual bool TryContinueSaving() = 0;
        virtual void QuitSaving() = 0;
                // This is just like above, but for saving. The memory need is
                // not relevant here.

        virtual emUInt64 CalcMemoryNeed() = 0;
                // While loading, this method is called again and again to
                // calculate the number of memory bytes, which will be allocated
                // by this file model and by one typical file model client in
                // loaded state. If it cannot be calculated, it should be a good
                // approximation (e.g. from file size).

        virtual double CalcFileProgress() = 0;
                // While loading or saving, this method is called to determine
                // the progress in percent. It's just an information for the
                // user.

        virtual void TryFetchDate();
                // Get and remember file information for IsOutOfDate(). The default
                // implementation is for the default implementation of IsOutOfDate().

        virtual bool IsOutOfDate();
                // Check whether the loaded file should be reloaded. The default
                // implementation checks by file times, size and inode.

private: friend class emFileModelClient;

        bool StepLoading();
        bool StepSaving();
        bool UpdateFileProgress();
        bool UpdateMemoryLimit();
        void UpdatePriority();
        void StartPSAgent();
        void EndPSAgent();

        class PSAgentClass : public emPriSchedAgent {
        public:
                PSAgentClass(emFileModel & fileModel);
        protected:
                virtual void GotAccess();
        private:
                emFileModel & FileModel;
        };

        friend class PSAgentClass;

        emSignal FileStateSignal;
        FileState State;
        unsigned MemoryLimitInvalid : 1;
        unsigned PriorityInvalid : 1;
        emUInt64 MemoryNeed;
        double FileProgress;
        emUInt64 FileProgressClock;
        emString ErrorText;
        emFileModelClient * ClientList;
        emUInt64 MemoryLimit;
        time_t LastMTime;
        time_t LastCTime;
        emUInt64 LastFSize;
        emUInt64 LastINode;
        PSAgentClass * PSAgent;
        emRef<emSigModel> UpdateSignalModel; // NULL if ignored
};

inline const emSignal & emFileModel::GetFileStateSignal() const
{
        return FileStateSignal;
}

inline emFileModel::FileState emFileModel::GetFileState() const
{
        return State;
}

inline emUInt64 emFileModel::GetMemoryNeed() const
{
        return MemoryNeed;
}

inline double emFileModel::GetFileProgress() const
{
        return FileProgress;
}

inline const emString & emFileModel::GetErrorText() const
{
        return ErrorText;
}

inline bool emFileModel::GetIgnoreUpdateSignal() const
{
        return UpdateSignalModel==NULL;
}


//==============================================================================
//============================= emFileModelClient ==============================
//==============================================================================

class emFileModelClient : public emUncopyable {

public:

        // Abstract base class for a client on an emFileModel. Multiple clients
        // can connect to the same file model. And:
        //
        // - Each client tells about the maximum memory which may be allocated
        //   by the file model and one typical client (e.g. emFilePanel). If no
        //   client accepts the memory need this way, the file is not loaded or
        //   it is unloaded.
        //
        // - Each client tells about the priority which is used for determining
        //   the order of loading file models. The maximum priority of all
        //   clients is taken.
        //
        // Hint: Even have a look at the class emFilePanel. It's a base class
        // for panels which want to be file model clients.

        emFileModelClient(emFileModel * model=NULL);
                // Constructor.
                // Arguments:
                //   model - See SetModel below.

        virtual ~emFileModelClient();
                // Destructor.

        emFileModel * GetModel() const;
        void SetModel(emFileModel * model=NULL);
                // The file model this client is connected to. NULL means to
                // have disconnected state.

        virtual emUInt64 GetMemoryLimit() const = 0;
                // Maximum memory need accepted for loading the file, from sight
                // of this client. Usually, this should be set from
                // emPanel::GetMemoryLimit().

        virtual double GetPriority() const = 0;
                // Priority in loading the file, from sight of this client.
                // Usually, this should be set from
                // emPanel::GetUpdatePriority().

        virtual bool IsReloadAnnoying() const = 0;
                // Whether a reload of the file model could currently be
                // annoying for the user.

        void InvalidateMemoryLimit();
                // Indicate a change in the results of GetMemoryLimit().

        void InvalidatePriority();
                // Indicate a change in the results of GetPriority().

        bool IsTheOnlyClient() const;
                // Whether the file model has no other client.

private: friend class emFileModel;

        emRef<emFileModel> Model;
        emFileModelClient * * ThisPtrInList;
        emFileModelClient * NextInList;
};

inline emFileModel * emFileModelClient::GetModel() const
{
        return Model;
}


//==============================================================================
//========================= emAbsoluteFileModelClient ==========================
//==============================================================================

class emAbsoluteFileModelClient : public emFileModelClient {

public:

        // Class for a file model client that demands loaded state in any case.

        emAbsoluteFileModelClient(emFileModel * model=NULL);
        virtual emUInt64 GetMemoryLimit() const;
        virtual double GetPriority() const;
        virtual bool IsReloadAnnoying() const;
};


#endif