/***************************************************************************
**
**  This file is part of QGpCoreTools.
**
**  This library is free software; you can redistribute it and/or
**  modify it under the terms of the GNU Lesser General Public
**  License as published by the Free Software Foundation; either
**  version 2.1 of the License, or (at your option) any later version.
**
**  This file 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 Lesser General Public
**  License for more details.
**
**  You should have received a copy of the GNU Lesser General Public
**  License along with this library; if not, write to the Free Software
**  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
**
**  See http://www.geopsy.org for more information.
**
**  Created: 2006-07-06
**  Copyright: 2006-2019
**    Marc Wathelet
**    Marc Wathelet (LGIT, Grenoble, France)
**
***************************************************************************/

#include <time.h>

#include "Cache.h"
#include "CacheItem.h"
#include "CoreApplication.h"
#include "MemoryChecker.h"

namespace QGpCoreTools {

/*!
  \class Cache Cache.h
  \brief Cache space with "inteligent" swap mechanism

  CacheItem can allocate data vectors into a Cache. Cache controls that the total memory does not exceed
  a maximum number of bytes. If so, already allocated blocks are automatically deallocated if they are not locked.

  Contrary to most of the system swaps, this swap mechanism knows what the item usage will be in the next operations
  avoiding unefficient deallocation of blocks. This "intelligent" deallocation is performed thanks to CacheProcess. They
  can be created for any process that use a sequence of items. The order of signal usage is stored in advance and it is
  used to predict the time of next usage. Items that will be used in a far future are preference for deallocation.
  Items already processed or not included in a CacheProcess have a predicted time set to almost infinite, hence they
  are deallocated first.
*/

/*!
  Initializes a cache that manages allocation and deallocation of memory.

  The two parameters of a cache are its size in byte and the path of a temporary directory
  used for swapping if the requested memory for data exceed its size.
*/
Cache::Cache() : _mutex(Mutex::Recursive)
{
  TRACE;
  _loadedBytes=0;
  _maxLoadedBytes=0;
}

/*!
  \fn Cache::~Cache()
  It does nothing because the removal of data is the responsability of the CacheItem
*/

/*!
  \fn Cache::setSize(double nMegaBytes)
  Set the maximum number of mega bytes that can be allocated simultaneously. The default is 0.
*/

/*!
  Set the path where to swap. The default is the home directory.
*/
void Cache::setSwapDir(const QDir& d)
{
  TRACE;
  _swapDir=d;
  if(_swapDir.exists()) { // clean file that might be left after a crash
    QStringList filters;
    filters << "cache_data_*";
    QStringList cacheFiles=_swapDir.entryList(filters);
    for(QStringList::iterator it=cacheFiles.begin();it!=cacheFiles.end();it++) {
      _swapDir.remove(*it);
    }
  } else {
    _swapDir.mkpath(_swapDir.absolutePath ());
  }
}

/*!
  Clean the cache of all unlocked items
*/
void Cache::free()
{
  TRACE;
  free(_maxLoadedBytes);
}

/*!
  Free items with priority lest than \a priority in order to obtain at least \a nBytes.
*/
bool Cache::free(double nBytes)
{
  TRACE;
  double obtainedBytes=_maxLoadedBytes-_loadedBytes;
  do {
    const CacheItem * item=bestToFree();
    if(item) {
      //printf("Saving item %s\n",item->debugName().toLatin1().data());
      item->save(_swapDir);
      item->_saved=true;
      item->free();
      double dataSize=(double) item->dataSize();
      obtainedBytes += dataSize;
      _loadedBytes -= dataSize;
      _allocatedList.removeOne(item);
      item->_dataLock.unlock();
      //printf("Freeing %lf Kb from item %s\t\t",dataSize/1024,
      //       item->debugName().toLatin1().data());
      //printf("Free space=%lf Kb\n",freeBytes()/1024);
    } else return false;
  } while(obtainedBytes<nBytes);
  return true;
}

/*!
  Free data used by item. If \a saveTemp is false and if data is swaped, the corresponding file is removed.
  It \a saveTemp is true and if data is allocated, data is saved to file.
*/
void Cache::free(CacheItem * item, bool saveTemp)
{
  TRACE;
  if(!saveTemp && item->_saved) {
    _swapDir.remove(item->swapFileName());
    item->_saved=false;
  }
  if(item->isAllocated()) {
    if(saveTemp) {
      //printf("Saving item %s\n",item->debugName().toLatin1().data());
      item->save(_swapDir);
      item->_saved=true;
    }
    item->free();
    _mutex.MUTEX_LOCK;
    _loadedBytes -= (double) item->dataSize();
    _allocatedList.removeOne(item);
    _mutex.unlock();
    //printf("Freeing item, %i are still allocated\n",_allocatedList.count());
    //printf("Freeing %lf Kb from item %s\t\t",(double)item->dataSize()/1024, item->debugName().toLatin1().data());
    //printf("Free space=%lf Kb\n",freeBytes()/1024);
  }
}

/*!
  In all cases the data must not be allocated. This is not double checked.
  In a multi-threaded context, this function must be used by only one thread at a time and lock() of item
  must be called with this object locked.
*/
bool Cache::makeAvailable(const CacheItem * item)
{
  TRACE;
  // Process low level allocation of data
  double neededSize=static_cast<double>(item->dataSize());
  QMutexLocker ml(&_mutex);
  if(neededSize>freeBytes()) {
    // Not enough space in cache
    if(!free(neededSize)) {
      QString msg=tr("Impossible to allocate new data\n"
                     "   current used space: %1 Kb\n"
                     "   current free space: %2 Kb\n"
                     "   current buffer size: %3 Kb\n"
                     "   required space: %4 Kb\n"
                     "Increase buffer size, a good compromize is 80% of you physical memory\n"
                     "This buffer is allocated dynamically upon requests, so it will not overload\n"
                     "your memory usage.\n\n")
                        .arg(_loadedBytes/1024.0).arg(freeBytes()/1024.0)
                        .arg(_maxLoadedBytes/1024.0).arg(neededSize/1024.0)+humanInfo();
      App::log(msg);
      //CoreApplication::instance()->reportBug(Message::Warning, msg.toLatin1().data());
      humanInfo();
      return false; // really not lucky, no more space
    }
    // Perfect got memory free
  }
  if(item->allocate()) {
    _allocatedList.append(item);
    _loadedBytes+=neededSize;
    ml.unlock();
    if(item->_saved) {
      //printf("Loading item %s\n",item->debugName().toLatin1().data());
      item->load(_swapDir);
      _swapDir.remove(item->swapFileName());
      item->_saved=false;
    }
    //printf("Allocating %lf Kbytes for item %s\t\t",neededSize/1024, item->debugName().toLatin1().data());
    //printf("Free space=%lf Kb\n",freeBytes()/1024);
    return true;
  }
  // Impossible to allocate new memory, problem with your computer?
  QString msg=tr("Impossible to allocate new data of size %1: current used space: %2 Kb.\n\n")
                   .arg(neededSize)
                   .arg(_loadedBytes * sizeof(double)/1024.0)+humanInfo();
  App::log(msg);
  CoreApplication::instance()->reportBug(Message::Warning, msg.toLatin1().data());
  return false;
}

/*!
  In all cases the data must be still allocated with its original size. This is not double checked.
*/
bool Cache::enlarge(CacheItem * item, double finalSize)
{
  TRACE;
  // Process low level allocation of data
  double neededSize=finalSize - (double) item->dataSize();
  QMutexLocker ml(&_mutex);
  if(neededSize>freeBytes()) {
    // Not enough space in cache
    if(!free(neededSize)) {
      QString msg=tr( "Impossible to allocate new data\n"
                        "   current used space: %1 Kb\n"
                        "   current free space: %2 Kb\n"
                        "   current buffer size: %3 Kb\n"
                        "   required space: %4 Kb\n"
                        "Increase buffer size, a good compromize is 80% of you physical memory\n"
                        "This buffer is allocated dynamically upon requests, so it will not overload\n"
                        "your memory usage.\n\n")
                        .arg(_loadedBytes/1024.0).arg(freeBytes()/1024.0)
                        .arg(_maxLoadedBytes/1024.0).arg(neededSize/1024.0)+humanInfo();
      App::log(msg);
      CoreApplication::instance()->reportBug(Message::Warning, msg.toLatin1().data());
      return false; // really not lucky, no more space
    }
    // Perfect got memory free
  }
  _loadedBytes+=neededSize;
  return true;
}

/*!
  Returns the best item to free. It is either according to current CacheProcess
  or to last time items were touched in case of equality for the first criterium.
*/
const CacheItem * Cache::bestToFree()
{
  TRACE;
  const CacheItem * bestItem=0;
  quint64 bestPredicted=0;
  quint64 bestAccess=0;
  quint64 t;
  for(QList<const CacheItem *>::iterator it=_allocatedList.begin();it!=_allocatedList.end();++it) {
    const CacheItem * item=*it;
    item->_adminLock.MUTEX_LOCK;
    if(item->_dataLock.tryLockForWrite()) {
      if(item->_lockCount<2) {  // Only the one of this try, hence not locked elsewhere
        t=item->nextPredictedTime();
        if(t>bestPredicted) {
          if(bestItem) {
            bestItem->_dataLock.unlock();
          }
          bestItem=item;
          bestPredicted=t;
          bestAccess=item->_lastAccess;
        } else if(t==bestPredicted) {
          t=item->_lastAccess;
          if(t<bestAccess) {
            if(bestItem) {
              bestItem->_dataLock.unlock();
            }
            bestItem=item;
            bestAccess=t;
          } else {
            item->_dataLock.unlock();
          }
        } else {
          item->_dataLock.unlock();
        }
      } else {
        item->_dataLock.unlock();
      }
    }
    item->_adminLock.unlock();
  }
  return bestItem;
}

/*!
  Print information about allocated blocks (may be useful for checking memory usage of processes)
*/
QString Cache::humanInfo() const
{
  TRACE;
  int sizeBlocks=0, sizeLockedBlocks=0, countLockedBlocks=0;
  QMutexLocker ml(&_mutex);
  for(QList<const CacheItem *>::const_iterator it=_allocatedList.begin();it!=_allocatedList.end();++it) {
    const CacheItem& item=**it;
    sizeBlocks+=item.dataSize();
    item._adminLock.MUTEX_LOCK;
    if(item._dataLock.tryLockForWrite()) {
      if(item._lockCount>1) { // Not only the one of this try, hence locked elsewhere
        sizeLockedBlocks+=item.dataSize();
        countLockedBlocks++;
      }
      item._dataLock.unlock();
    }
    item._adminLock.unlock();
  }
  return tr("%1 allocated blocks (%2 Mb), %3 locked blocks (%4 Mb)\n")
         .arg(_allocatedList.count()).arg(sizeBlocks >> 20).arg(countLockedBlocks).arg(sizeLockedBlocks >> 20);
}

/*!
  Print information about allocated blocks
*/
void Cache::debugStatus(const QString& tag) const
{
  TRACE;
  QMutexLocker ml(&_mutex);
  App::log(tr("#### Cache debug status %1\n").arg(tag));
  for(QList<const CacheItem *>::const_iterator it=_allocatedList.begin();it!=_allocatedList.end();++it) {
    const CacheItem& item=**it;
    item._adminLock.MUTEX_LOCK;
    if(item._dataLock.tryLockForWrite()) {
      if(item._lockCount>1) { // Not only the one of this try, hence locked elsewhere
        App::log(tr("#### %1 %2 kb locked %3 times (trylock success)\n")
                              .arg(item.debugName())
                              .arg(item.dataSize() >> 10)
                              .arg(item._lockCount-1));
      } else {
        App::log(tr("#### %1 %2 kb unlocked\n")
                              .arg(item.debugName())
                              .arg(item.dataSize() >> 10));
      }
      item._dataLock.unlock();
    } else {
      if(item._lockCount>0) {
        App::log(tr("#### %1 %2 kb locked %3 times (trylock failed)\n")
                              .arg(item.debugName())
                              .arg(item.dataSize() >> 10)
                              .arg(item._lockCount));
      } else {
        App::log(tr("#### %1 %2 kb unlocked\n")
                              .arg(item.debugName())
                              .arg(item.dataSize() >> 10));
      }
    }
    item._adminLock.unlock();
  }
  App::log(tr("#### ---------------\n"));
}

} // namespace QGpCoreTools
