/*
 * Copyright (C) 2007-2023 Xagasoft, All rights reserved.
 *
 * This file is part of the libbu++ library and is released under the
 * terms of the license contained in the file LICENSE.
 */

#ifndef BU_CACHE_BASE_H
#define BU_CACHE_BASE_H

#include "bu/string.h"
#include "bu/archive.h"
#include "bu/hash.h"
#include "bu/readwritemutex.h"
#include "bu/mutexlocker.h"
#include "bu/cacheobject.h"

#include "bu/sio.h"

namespace Bu
{
    template<typename keytype, typename obtype> class CacheBase;
    template<typename keytype, typename obtype> class CacheEntry;
        
    template<typename keytype, typename obtype>
    class CacheEntry
    {
    friend class CacheBase<keytype, obtype>;
    private:
        CacheEntry( obtype *pObject ) :
            iRefCount( 0 ),
            bDeleted( false ),
            pObject( pObject )
        {
            //Bu::println("CacheEntry::CacheEntry: registering pObject (0x%1)").
            //    arg( reinterpret_cast<ptrdiff_t>(pObject), Bu::Fmt::hex() );
        }

        virtual ~CacheEntry()
        {
            mEntry.lock();
            //Bu::println("CacheEntry::~CacheEntry: deleting pObject (0x%1)").
            //    arg( reinterpret_cast<ptrdiff_t>(pObject), Bu::Fmt::hex() );
            delete pObject;
            mEntry.unlock();
        }

    public:
        int getRefCount() const
        {
            mEntry.lock();
            int ret = iRefCount;
            mEntry.unlock();
            return ret;
        }

        obtype *getPtr() const
        {
            return pObject;
        }

        void lock() const
        {
            mObject.lock();
        }

        void unlock() const
        {
            mObject.unlock();
        }

        Bu::Mutex &getMutex()
        {
            return mObject;
        }

        void incRef()
        {
            mEntry.lock();
            iRefCount++;
            mEntry.unlock();
        }

        bool decRef()
        {
            mEntry.lock();
            iRefCount--;
            bool bRet = iRefCount > 0;
            mEntry.unlock();
            return bRet;
        }

        bool isDeleted()
        {
            mEntry.lock();
            bool bRet = bDeleted;
            mEntry.unlock();
            return bRet;
        }

        // Indicates that the entry was both deleted
        // and has a refcount of zero
        bool isReadyForCleanup()
        {
            mEntry.lock();
            bool bRet = bDeleted && iRefCount == 0;
            mEntry.unlock();
            return bRet;
        }

    private:
        mutable Bu::Mutex mEntry;
        mutable Bu::Mutex mObject;
        int iRefCount;
        bool bDeleted;
        obtype *pObject;
    };

    template<typename keytype, typename basetype>
    class CachePtrInterface
    {
    protected:
        CachePtrInterface()
        {
        }

        virtual ~CachePtrInterface()
        {
        }
        
        template<typename obtype>
        void checkRef( CacheBase<keytype, basetype> *pCache,
                const keytype &kId,
                CacheEntry<keytype, basetype> * &rpEnt,
                obtype * &rpData ) const
        {
            if( pCache == NULL )
                throw Bu::ExceptionBase("Invalid cache pointer");

            if( !rpData )
            {
                rpEnt = pCache->getRef( kId );
                rpEnt->incRef();
                rpData = dynamic_cast<obtype *>(rpEnt->getPtr());
                if( rpData == NULL )
                {
                    rpEnt->decRef();
                    rpEnt = NULL;
                    throw std::bad_cast();
                }
            }
        }

        template<typename obtype>
        void releaseRef( CacheBase<keytype, basetype> *pCache,
                CacheEntry<keytype, basetype> * &rpEnt,
                obtype * &rpData ) const
        {
            if( pCache == NULL )
                return;

            if( rpData == NULL )
                return;

            rpData = NULL;
            rpEnt->decRef();
            pCache->releaseRef( rpEnt );
            rpEnt = NULL;
        }
    };

    template<typename keytype, typename obtype, typename basetype=obtype>
    class CachePtr : protected CachePtrInterface<keytype, basetype>
    {
    friend class CacheBase<keytype, basetype>;
    private:
        typedef CachePtr<keytype, obtype, basetype> MyType;

        CachePtr( CacheBase<keytype, basetype> *pCache,
                CacheEntry<keytype, basetype> *pEnt,
                const keytype &kId ) :
            pCache( pCache ),
            kId( kId ),
            pEnt( pEnt ),
            pData( NULL )
        {
            pEnt->incRef();
            pData = dynamic_cast<obtype *>( pEnt->getPtr() );
            if( pData == NULL )
            {
                pEnt->decRef();
                throw std::bad_cast();
            }
        }

        CachePtr( CacheBase<keytype, basetype> *pCache, const keytype &kId ) :
            pCache( pCache ),
            kId( kId ),
            pEnt( NULL ),
            pData( NULL )
        {
        }

    public:
        typedef keytype Key;
        typedef obtype Value;

        CachePtr() :
            pCache( NULL ),
            pEnt( NULL ),
            pData( NULL )
        {
        }

        CachePtr( const MyType &rhs ) :
            pCache( rhs.pCache ),
            kId( rhs.kId ),
            pEnt( rhs.pEnt ),
            pData( rhs.pData )
        {
            if( pEnt )
                pEnt->incRef();
        }

        virtual ~CachePtr()
        {
            unbind();
        }

        const keytype &getKey() const
        {
            return kId;
        }

        obtype &operator*()
        {
            bind();
            return *pData;
        }
        
        const obtype &operator*() const
        {
            bind();
            return *pData;
        }

        obtype *operator->()
        {
            bind();
            return pData;
        }
        
        const obtype *operator->() const
        {
            bind();
            return pData;
        }

        MyType operator=( const MyType &rhs )
        {
            unbind();
            pCache = rhs.pCache;
            kId = rhs.kId;
            pEnt = rhs.pEnt;
            pData = rhs.pData;
            if( pEnt )
                pEnt->incRef();

            return *this;
        }
        
        template<typename castto>
        CachePtr<keytype, castto, basetype> cast()
        {
            return pCache->template cast<obtype, castto>( *this );
        }
        
        template<typename castto>
        CachePtr<keytype, castto, basetype> cast() const
        {
            return pCache->template cast<obtype, castto>( *this );
        }
        
        bool operator==( const MyType &rhs ) const
        {
            return pCache == rhs.pCache &&
                kId == rhs.kId;
        }
        
        bool operator!=( const MyType &rhs ) const
        {
            return pCache != rhs.pCache ||
                kId != rhs.kId;
        }
        
        void bind()
        {
            CachePtrInterface<keytype, basetype>::checkRef( pCache, kId, pEnt, pData );
        }

        void unbind()
        {
            CachePtrInterface<keytype, basetype>::releaseRef( pCache, pEnt, pData );
        }
        
        void bind() const
        {
            CachePtrInterface<keytype, basetype>::checkRef( pCache, kId, pEnt, pData );
        }

        void unbind() const
        {
            CachePtrInterface<keytype, basetype>::releaseRef( pCache, pEnt, pData );
        }

        bool isSet() const
        {
            if( pCache )
                return true;
            return false;
        }

        void clear()
        {
            unbind();
            pCache = NULL;
            pEnt = NULL;
            pData = NULL;
        }

        void lock()
        {
            bind();
            if( pEnt )
                pEnt->lock();
        }

        void unlock()
        {
            bind();
            if( pEnt )
                pEnt->unlock();
        }

        class Locker
        {
        public:
            Locker( MyType &rPtr ) :
                rPtr( rPtr ),
                bLocked( true )
            {
                rPtr.lock();
            }

            ~Locker()
            {
                unlock();
            }

            void unlock()
            {
                if( !bLocked )
                    return;
                rPtr.unlock();
                bLocked = false;
            }

            void lock()
            {
                if( bLocked )
                    return;
                rPtr.lock();
                bLocked = true;
            }

        private:
            MyType &rPtr;
            bool bLocked;
        };

    private:
        CacheBase<keytype, basetype> *pCache;
        mutable keytype kId;
        mutable CacheEntry<keytype, basetype> *pEnt;
        mutable obtype *pData;
    };

    template<typename keytype, typename obtype>
    class CacheBase
    {
    friend class CachePtrInterface<keytype, obtype>;
    friend class CacheObject<keytype, obtype>;
    public:
        CacheBase()
        {
        }

        virtual ~CacheBase()
        {
            for( typename CacheEntryHash::iterator i = hCacheEntry.begin(); i; i++ )
            {
                delete i.getValue();
            }
        }

        typedef CacheEntry<keytype, obtype> Entry;
        typedef Bu::List<keytype> KeyList;
        typedef CacheObject<keytype, obtype> ObjectType;

        CachePtr<keytype, obtype> insert( obtype *pObject )
        {
            Entry *pEnt = addEntry( pObject );

            return CachePtr<keytype, obtype>(
                this, pEnt, pObject->getKey()
                );
        }

        template<typename supertype>
        CachePtr<keytype, supertype, obtype> insert( supertype *pObject )
        {
            obtype *pCast = dynamic_cast<obtype *>( pObject );
            if( pCast == NULL )
                throw std::bad_cast();

            Entry *pEnt = addEntry( pCast );

            return CachePtr<keytype, supertype, obtype>(
                this, pEnt, pObject->getKey()
                );
        }

        bool has( const keytype &key )
        {
            Bu::ReadWriteMutex::ReadLocker rl( mCacheEntry );

            if( hCacheEntry.has( key ) )
                return true;

            return _has( key );
        }

        CachePtr<keytype, obtype> get( const keytype &key )
        {
            Entry *pEnt = getEntry( key );
            return CachePtr<keytype, obtype>( this, pEnt, key );
        }

        template<typename supertype>
        CachePtr<keytype, supertype, obtype> get( const keytype &key )
        {
            Entry *pEnt = getEntry( key );
            return CachePtr<keytype, supertype, obtype>( this, pEnt, key );
        }

        CachePtr<keytype, obtype> getLazy( const keytype &key )
        {
            return CachePtr<keytype, obtype>( this, key );
        }

        template<typename supertype>
        CachePtr<keytype, supertype, obtype> getLazy( const keytype &key )
        {
            return CachePtr<keytype, supertype, obtype>( this, key );
        }

        template<typename supertype, typename castto>
        CachePtr<keytype, castto, obtype> cast( CachePtr<keytype, supertype, obtype> &ptr )
        {
            if( ptr.pEnt )
                return CachePtr<keytype, castto, obtype>( this, ptr.pEnt, ptr.kId );
            else
                return CachePtr<keytype, castto, obtype>( this, ptr.kId );
        }
        
        template<typename supertype, typename castto>
        CachePtr<keytype, castto, obtype> cast( const CachePtr<keytype, supertype, obtype> &ptr )
        {
            if( ptr.pEnt )
                return CachePtr<keytype, castto, obtype>( this, ptr.pEnt, ptr.kId );
            else
                return CachePtr<keytype, castto, obtype>( this, ptr.kId );
        }

        /**
         * Removes an item from the cache.
         * The object in question can have references to it, and those
         * references will still work after the erase is called.  When erase
         * is called the object is immediately removed from the backing store
         * as well as from the main index.  At that point existing references
         * can be used as normal, but when the last reference is released
         * the memory will be automatically cleaned up.
         */
        void erase( const keytype &key )
        {
            Bu::ReadWriteMutex::WriteLocker wl( mCacheEntry );
            if( hCacheEntry.has( key ) )
            {
                Entry *pEnt = hCacheEntry.get( key );
                pEnt->mEntry.lock();
                if( pEnt->iRefCount == 0 )
                {
                    pEnt->mEntry.unlock();
                    //delete pEnt->pObject;
                    delete pEnt;
                }
                else
                {
                    pEnt->bDeleted = true;
//                  Bu::println("Deleted item, ref count = %1").arg( pEnt->iRefCount );
                    pEnt->mEntry.unlock();
                }
                hCacheEntry.erase( key );
            }
            _erase( key );
        }

        /**
         * Removes an item from the cache and frees memory immediately.
         * The object in question cannot have any active references in order
         * for this to succeed, otherwise it will throw an exception.
         */
        void eraseNow( const keytype &key )
        {
            Bu::ReadWriteMutex::WriteLocker wl( mCacheEntry );
            if( hCacheEntry.has( key ) )
            {
                Entry *pEnt = hCacheEntry.get( key );
                pEnt->mEntry.lock();
                if( pEnt->iRefCount > 0 )
                {
                    int iCount = pEnt->iRefCount;
                    pEnt->mEntry.unlock();
                    throw Bu::ExceptionBase( Bu::String("Cache entry %1 cannot be erased, there are %2 active references.").arg( key ).arg( iCount ).end().getStr() );
                }
                //delete pEnt->pObject;
                delete pEnt;
                hCacheEntry.erase( key );
            }
            _erase( key );
        }

        virtual KeyList getKeys() const=0;
        virtual int getSize() const=0;

        void sync()
        {
            {
                Bu::ReadWriteMutex::WriteLocker wl( mCacheEntry );
                _sync();
            }
            syncChanges();
        }

    protected:
        Entry *getRef( const keytype &k )
        {
            Entry *pEnt = getEntry( k );
            return pEnt;
        }

        void releaseRef( Entry *pEnt )
        {
            if( pEnt->isReadyForCleanup() )
            {
                //delete pEnt->pObject;
                delete pEnt;
            }
        }

        void objectChanged( const keytype &k )
        {
            Bu::ReadWriteMutex::WriteLocker wl( mCacheEntry );
            hChanged.insert( k, true );
        }

    protected:
        virtual bool _has( const keytype &key )=0;
        virtual void _create( const obtype *o )=0;
        virtual void _erase( const keytype &k )=0;

        virtual obtype *_load(
            typename Bu::CacheObject<keytype, obtype>::Initializer &initObj,
            const keytype &k
            )=0;
        virtual void _save( const obtype *o )=0;

        virtual void _sync()=0;

    private:
        Entry *addEntry( obtype *pObject )
        {
            Entry *pEnt = new Entry( pObject );
            mCacheEntry.lockWrite();
            hCacheEntry.insert( pObject->getKey(), pEnt );
            if( pObject->hasChanged() )
                hChanged.insert( pObject->getKey(), true );
            mCacheEntry.unlockWrite();
            _create( pObject );
            // I'm not sold on the ordering here...or the fact we're not
            // using th initilizer
            pObject->setCache( this );
            pObject->setCacheEntry( pEnt );
            
            return pEnt;
        }

        Entry *getEntry( const keytype &k )
        {
            Entry *pEnt = NULL;
            try
            {
                Bu::ReadWriteMutex::ReadLocker rl( mCacheEntry );
                pEnt = hCacheEntry.get( k );
            }
            catch(...)
            {
                // try to load the object from the backing store
                typename ObjectType::Initializer initObj(
                    this
                    );
                obtype *pObject = _load( initObj, k );
                pEnt = new Entry( pObject );
                pObject->setCacheEntry( pEnt );
                Bu::ReadWriteMutex::WriteLocker wl( mCacheEntry );
                hCacheEntry.insert( k, pEnt );
            }
            return pEnt;
        }

        void syncChanges()
        {
            Bu::List<keytype> lKeys;
            mCacheEntry.lockWrite();
            for( typename CacheKeySet::iterator i = hChanged.begin(); i; i++ )
            {
                lKeys.append( i.getKey() );
            }
            hChanged.clear();
            mCacheEntry.unlockWrite();

            for( typename List<keytype>::iterator i = lKeys.begin(); i; i++ )
            {
                Entry *pEnt = NULL;
                try
                {
                    Bu::ReadWriteMutex::ReadLocker rl( mCacheEntry );
                    pEnt = hCacheEntry.get( *i );
                }
                catch( Bu::HashException & )
                {
                    // The entry wasn't there, most likely because it
                    // was deleted.  That's ok.
                }
                // This isn't inside of the above because we want to be
                // able to handle these exceptions differently.
                if( pEnt )
                {
                    Bu::MutexLocker ml( pEnt->getMutex() );
                    _save( pEnt->getPtr() );
                    pEnt->getPtr()->changed( false );
                }
            }
        }

    private:
        typedef Bu::Hash<keytype, Entry *> CacheEntryHash;
        typedef Bu::Hash<keytype, bool> CacheKeySet;
        CacheEntryHash hCacheEntry;
        CacheKeySet hChanged;
        Bu::ReadWriteMutex mCacheEntry;
    };

    template<typename obtype>
    void _cacheObjectSave( Bu::Stream &s, obtype *pObject )
    {
        Bu::Archive ar( s, Bu::Archive::save );
        ar << *pObject;
    }

    template<typename keytype, typename obtype>
    obtype *_cacheObjectLoad(
        typename Bu::CacheObject<keytype, obtype>::Initializer &initObject,
        const keytype & /*rKey*/,
        Bu::Stream &s
        )
    {
        Bu::Archive ar( s, Bu::Archive::load );
        obtype *ret = initObject(new obtype());
        ar >> *ret;
        return ret;
    }
}

#endif
