/**
 * Orthanc - A Lightweight, RESTful DICOM Store
 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 * Department, University Hospital of Liege, Belgium
 * Copyright (C) 2017-2023 Osimis S.A., Belgium
 * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium
 * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 *
 * This program 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 3 of
 * the License, or (at your option) any later version.
 *
 * 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program. If not, see
 * <http://www.gnu.org/licenses/>.
 **/


#if defined(__GNUC__) || defined(__clang__)
#  pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif

#include "../PrecompiledHeaders.h"
#include "MemoryObjectCache.h"

#include "../Compatibility.h"
#include "../Logging.h"

namespace Orthanc
{
  class MemoryObjectCache::Item : public boost::noncopyable
  {
  private:
    ICacheable*               value_;
    boost::posix_time::ptime  time_;

  public:
    explicit Item(ICacheable* value) :   // Takes ownership
    value_(value),
    time_(boost::posix_time::second_clock::local_time())
    {
      if (value == NULL)
      {
        throw OrthancException(ErrorCode_NullPointer);
      }
    }

    ~Item()
    {
      assert(value_ != NULL);
      delete value_;
    }

    ICacheable& GetValue() const
    {
      assert(value_ != NULL);
      return *value_;
    }

    const boost::posix_time::ptime& GetTime() const
    {
      return time_;
    }
  };


  void MemoryObjectCache::Recycle(size_t targetSize)
  {
    // WARNING: "cacheMutex_" must be locked
    while (currentSize_ > targetSize)
    {
      assert(!content_.IsEmpty());
        
      Item* item = NULL;
      content_.RemoveOldest(item);

      assert(item != NULL);
      const size_t size = item->GetValue().GetMemoryUsage();
      delete item;

      assert(currentSize_ >= size);
      currentSize_ -= size;
    }

    // Post-condition: "currentSize_ <= targetSize"
  }
    

  MemoryObjectCache::MemoryObjectCache() :
    currentSize_(0),
    maxSize_(static_cast<size_t>(100) * 1024 * 1024)  // 100 MB
  {
  }


  MemoryObjectCache::~MemoryObjectCache()
  {
    try
    {
      Recycle(0);
    }
    catch (OrthancException& e)
    {
      // Don't throw exceptions in destructors
      LOG(ERROR) << "Exception in destructor: " << e.What();
    }

    assert(content_.IsEmpty());
  }


  size_t MemoryObjectCache::GetNumberOfItems()
  {
#if ORTHANC_ENABLE_THREADS == 1
    boost::mutex::scoped_lock lock(cacheMutex_);
#endif

    return content_.GetSize();
  }
  

  size_t MemoryObjectCache::GetCurrentSize()
  {
#if ORTHANC_ENABLE_THREADS == 1
    boost::mutex::scoped_lock lock(cacheMutex_);
#endif

    return currentSize_;
  }


  size_t MemoryObjectCache::GetMaximumSize()
  {
#if ORTHANC_ENABLE_THREADS == 1
    boost::mutex::scoped_lock lock(cacheMutex_);
#endif

    return maxSize_;
  }


  void MemoryObjectCache::SetMaximumSize(size_t size)
  {
    if (size == 0)
    {
      throw OrthancException(ErrorCode_ParameterOutOfRange);
    }
      
#if ORTHANC_ENABLE_THREADS == 1
    // Make sure no accessor is currently open (as its data may be
    // removed if recycling is needed)
    WriterLock contentLock(contentMutex_);

    // Lock the global structure of the cache
    boost::mutex::scoped_lock cacheLock(cacheMutex_);
#endif

    Recycle(size);
    maxSize_ = size;
  }


  void MemoryObjectCache::Acquire(const std::string& key,
                                  ICacheable* value)
  {
    std::unique_ptr<Item> item(new Item(value));

    if (value == NULL)
    {
      throw OrthancException(ErrorCode_NullPointer);
    }
    else
    {
#if ORTHANC_ENABLE_THREADS == 1
      // Make sure no accessor is currently open (as its data may be
      // removed if recycling is needed)
      WriterLock contentLock(contentMutex_);

      // Lock the global structure of the cache
      boost::mutex::scoped_lock cacheLock(cacheMutex_);
#endif

      const size_t size = item->GetValue().GetMemoryUsage();

      if (size > maxSize_)
      {
        // This object is too large to be stored in the cache, discard it
      }
      else if (content_.Contains(key))
      {
        // Value already stored, don't overwrite the old value
        content_.MakeMostRecent(key);
      }
      else
      {
        Recycle(maxSize_ - size);   // Post-condition: currentSize_ <= maxSize_ - size
        assert(currentSize_ + size <= maxSize_);

        content_.Add(key, item.release());
        currentSize_ += size;
      }
    }
  }


  void MemoryObjectCache::Invalidate(const std::string& key)
  {
#if ORTHANC_ENABLE_THREADS == 1
    // Make sure no accessor is currently open (as it may correspond
    // to the key to remove)
    WriterLock contentLock(contentMutex_);

    // Lock the global structure of the cache
    boost::mutex::scoped_lock cacheLock(cacheMutex_);
#endif

    Item* item = NULL;
    if (content_.Contains(key, item))
    {
      assert(item != NULL);
      const size_t size = item->GetValue().GetMemoryUsage();
      delete item;

      content_.Invalidate(key);
          
      assert(currentSize_ >= size);
      currentSize_ -= size;
    }
  }


  MemoryObjectCache::Accessor::Accessor(MemoryObjectCache& cache,
                                        const std::string& key,
                                        bool unique) :
    item_(NULL)
  {
#if ORTHANC_ENABLE_THREADS == 1
    if (unique)
    {
      writerLock_ = WriterLock(cache.contentMutex_);
    }
    else
    {
      readerLock_ = ReaderLock(cache.contentMutex_);
    }

    // Lock the global structure of the cache, must be *after* the
    // reader/writer lock
    cacheLock_ = boost::mutex::scoped_lock(cache.cacheMutex_);
#endif

    if (cache.content_.Contains(key, item_))
    {
      cache.content_.MakeMostRecent(key);
    }
    
#if ORTHANC_ENABLE_THREADS == 1
    cacheLock_.unlock();

    if (item_ == NULL)
    {
      // This item does not exist in the cache, we can release the
      // reader/writer lock
      if (unique)
      {
        writerLock_.unlock();
      }
      else
      {
        readerLock_.unlock();
      }
    }
#endif
  }


  ICacheable& MemoryObjectCache::Accessor::GetValue() const
  {
    if (IsValid())
    {
      return item_->GetValue();
    }
    else
    {
      throw OrthancException(ErrorCode_BadSequenceOfCalls);
    }
  }


  const boost::posix_time::ptime& MemoryObjectCache::Accessor::GetTime() const
  {
    if (IsValid())
    {
      return item_->GetTime();
    }
    else
    {
      throw OrthancException(ErrorCode_BadSequenceOfCalls);
    }        
  }
}
