/**
 * 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/>.
 **/


#include "PrecompiledHeaders.h"
#include "MetricsRegistry.h"

#include "ChunkedBuffer.h"
#include "Compatibility.h"
#include "CompatibilityMath.h"
#include "Logging.h"
#include "OrthancException.h"

#include <boost/date_time/posix_time/posix_time.hpp>


namespace Orthanc
{
  static const boost::posix_time::ptime GetNow()
  {
    return boost::posix_time::microsec_clock::universal_time();
  }

  namespace MetricsRegistryInternals
  {
    template <typename T>
    class TimestampedValue : public boost::noncopyable
    {
    private:
      boost::posix_time::ptime  time_;
      bool                      hasValue_;
      T                         value_;
      bool                      hasNextValue_;  // for min and max values over period, we need to store the next value
      T                         nextValue_;

      void SetValue(const T& value,
                    const boost::posix_time::ptime& now)
      {
        hasValue_ = true;
        value_ = value;
        time_ = now;
      }

      bool IsLargerOverPeriod(const T& value,
                              int duration,
                              const boost::posix_time::ptime& now) const
      {
        if (hasValue_)
        {
          return (value > value_ ||
                  (now - time_).total_seconds() > duration /* old value has expired */);
        }
        else
        {
          return true;  // No value yet
        }
      }

      bool IsSmallerOverPeriod(const T& value,
                               int duration,
                               const boost::posix_time::ptime& now) const
      {
        if (hasValue_)
        {
          return (value < value_ ||
                  (now - time_).total_seconds() > duration /* old value has expired */);
        }
        else
        {
          return true;  // No value yet
        }
      }

    public:
      explicit TimestampedValue() :
        hasValue_(false),
        value_(0),
        hasNextValue_(false),
        nextValue_(0)
      {
      }

      static int GetPeriodDuration(const MetricsUpdatePolicy& policy)
      {
        switch (policy)
        {
          case MetricsUpdatePolicy_MaxOver10Seconds:
          case MetricsUpdatePolicy_MinOver10Seconds:
            return 10;
          case MetricsUpdatePolicy_MaxOver1Minute:
          case MetricsUpdatePolicy_MinOver1Minute:
            return 60;
          default:
            THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_InternalError);
        }
      }

      void Update(const T& value,
                  const MetricsUpdatePolicy& policy)
      {
        const boost::posix_time::ptime now = GetNow();

        switch (policy)
        {
          case MetricsUpdatePolicy_Directly:
            SetValue(value, now);
            break;
          
          case MetricsUpdatePolicy_MaxOver10Seconds:
          case MetricsUpdatePolicy_MaxOver1Minute:
            if (IsLargerOverPeriod(value, GetPeriodDuration(policy), now))
            {
              SetValue(value, now);
            }
            else
            {
              hasNextValue_ = true;
              nextValue_ = value;
            }
            break;

          case MetricsUpdatePolicy_MinOver10Seconds:
          case MetricsUpdatePolicy_MinOver1Minute:
            if (IsSmallerOverPeriod(value, GetPeriodDuration(policy), now))
            {
              SetValue(value, now);
            }
            else
            {
              hasNextValue_ = true;
              nextValue_ = value;
            }
            break;
          default:
            throw OrthancException(ErrorCode_NotImplemented, "Unsupported metrics policy");
        }
      }

      void Refresh(const MetricsUpdatePolicy& policy)
      {
        const boost::posix_time::ptime now = GetNow();

        switch (policy)
        {
          case MetricsUpdatePolicy_Directly:
            // nothing to do
            break;
          
          case MetricsUpdatePolicy_MaxOver10Seconds:
          case MetricsUpdatePolicy_MinOver10Seconds:
          case MetricsUpdatePolicy_MaxOver1Minute:
          case MetricsUpdatePolicy_MinOver1Minute:
            // if the min/max value is older than the period, get the latest value
            if ((now - time_).total_seconds() > GetPeriodDuration(policy) /* old value has expired */ && hasNextValue_)
            {
              SetValue(nextValue_, now);
              hasNextValue_ = false;
            }
            break;

          default:
            throw OrthancException(ErrorCode_NotImplemented, "Unsupported metrics policy");
        }
      }

      void Increment(const T& delta)
      {
        time_ = GetNow();

        if (hasValue_)
        {
          value_ += delta;
        }
        else
        {
          value_ = delta;
          hasValue_ = true;
        }
      }

      bool HasValue() const
      {
        return hasValue_;
      }

      const boost::posix_time::ptime& GetTime() const
      {
        if (hasValue_)
        {
          return time_;
        }
        else
        {
          throw OrthancException(ErrorCode_BadSequenceOfCalls);
        }
      }

      const T& GetValue() const
      {
        if (hasValue_)
        {
          return value_;
        }
        else
        {
          throw OrthancException(ErrorCode_BadSequenceOfCalls);
        }
      }
    };
  }


  class MetricsRegistry::Item : public boost::noncopyable
  {
  private:
    MetricsUpdatePolicy   policy_;
    
  public:
    explicit Item(MetricsUpdatePolicy policy) :
      policy_(policy)
    {
    }
    
    virtual ~Item()
    {
    }

    MetricsUpdatePolicy GetPolicy() const
    {
      return policy_;
    }

    virtual void UpdateFloat(float value) = 0;

    virtual void UpdateInteger(int64_t value) = 0;

    virtual void IncrementInteger(int64_t delta) = 0;

    virtual MetricsDataType GetDataType() const = 0;

    virtual bool HasValue() const = 0;

    virtual const boost::posix_time::ptime& GetTime() const = 0;
    
    virtual std::string FormatValue() const = 0;

    virtual void Refresh() = 0;

    virtual void SetInitialValue(int64_t value) = 0;
  };

  
  class MetricsRegistry::FloatItem : public Item
  {
  private:
    MetricsRegistryInternals::TimestampedValue<float>  value_;

  public:
    explicit FloatItem(MetricsUpdatePolicy policy) :
      Item(policy)
    {
    }
    
    virtual void UpdateFloat(float value) ORTHANC_OVERRIDE
    {
      value_.Update(value, GetPolicy());
    }

    virtual void UpdateInteger(int64_t value) ORTHANC_OVERRIDE
    {
      value_.Update(static_cast<float>(value), GetPolicy());
    }

    virtual void IncrementInteger(int64_t delta) ORTHANC_OVERRIDE
    {
      value_.Increment(static_cast<float>(delta));
    }

    virtual MetricsDataType GetDataType() const ORTHANC_OVERRIDE
    {
      return MetricsDataType_Float;
    }

    virtual bool HasValue() const ORTHANC_OVERRIDE
    {
      return value_.HasValue();
    }

    virtual const boost::posix_time::ptime& GetTime() const ORTHANC_OVERRIDE
    {
      return value_.GetTime();
    }
    
    virtual std::string FormatValue() const ORTHANC_OVERRIDE
    {
      return boost::lexical_cast<std::string>(value_.GetValue());
    }

    virtual void Refresh() ORTHANC_OVERRIDE
    {
      value_.Refresh(GetPolicy());
    }

    virtual void SetInitialValue(int64_t value) ORTHANC_OVERRIDE
    {
      value_.Update(static_cast<float>(value), MetricsUpdatePolicy_Directly);
    }
  };

  
  class MetricsRegistry::IntegerItem : public Item
  {
  private:
    MetricsRegistryInternals::TimestampedValue<int64_t>  value_;

  public:
    explicit IntegerItem(MetricsUpdatePolicy policy) :
      Item(policy)
    {
    }
    
    virtual void UpdateFloat(float value) ORTHANC_OVERRIDE
    {
      value_.Update(Orthanc::Math::llround(value), GetPolicy());
    }

    virtual void UpdateInteger(int64_t value) ORTHANC_OVERRIDE
    {
      value_.Update(value, GetPolicy());
    }

    virtual void IncrementInteger(int64_t delta) ORTHANC_OVERRIDE
    {
      value_.Increment(delta);
    }

    virtual MetricsDataType GetDataType() const ORTHANC_OVERRIDE
    {
      return MetricsDataType_Integer;
    }

    virtual bool HasValue() const ORTHANC_OVERRIDE
    {
      return value_.HasValue();
    }

    virtual const boost::posix_time::ptime& GetTime() const ORTHANC_OVERRIDE
    {
      return value_.GetTime();
    }
    
    virtual std::string FormatValue() const ORTHANC_OVERRIDE
    {
      return boost::lexical_cast<std::string>(value_.GetValue());
    }

    virtual void Refresh() ORTHANC_OVERRIDE
    {
      value_.Refresh(GetPolicy());
    }

    virtual void SetInitialValue(int64_t value) ORTHANC_OVERRIDE
    {
      value_.Update(value, MetricsUpdatePolicy_Directly);
    }
  };


  MetricsRegistry::~MetricsRegistry()
  {
    for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
    {
      assert(it->second != NULL);
      delete it->second;
    }
  }

  bool MetricsRegistry::IsEnabled() const
  {
    return enabled_;
  }


  void MetricsRegistry::SetEnabled(bool enabled)
  {
    Mutex::ScopedLock lock(mutex_);
    enabled_ = enabled;
  }


  void MetricsRegistry::Register(const std::string& name,
                                 MetricsUpdatePolicy policy,
                                 MetricsDataType type)
  {
    Mutex::ScopedLock lock(mutex_);

    if (content_.find(name) != content_.end())
    {
      throw OrthancException(ErrorCode_BadSequenceOfCalls, "Cannot register twice the same metrics: " + name);
    }
    else
    {
      GetItemInternal(name, policy, type);
    }
  }


  MetricsRegistry::Item& MetricsRegistry::GetItemInternal(const std::string& name,
                                                          MetricsUpdatePolicy policy,
                                                          MetricsDataType type)
  {
    Content::iterator found = content_.find(name);

    if (found == content_.end())
    {
      Item* item = NULL;
      
      switch (type)
      {
        case MetricsDataType_Float:
          item = new FloatItem(policy);
          break;

        case MetricsDataType_Integer:
          item = new IntegerItem(policy);
          break;

        default:
          throw OrthancException(ErrorCode_ParameterOutOfRange);
      }

      content_[name] = item;
      return *item;
    }
    else
    {
      assert(found->second != NULL);
      return *found->second;
    }
  }

  MetricsRegistry::MetricsRegistry() :
    enabled_(true)
  {
  }


  void MetricsRegistry::SetFloatValue(const std::string& name,
                                      float value,
                                      MetricsUpdatePolicy policy)
  {
    // Inlining to avoid loosing time if metrics are disabled
    if (enabled_)
    {
      Mutex::ScopedLock lock(mutex_);
      GetItemInternal(name, policy, MetricsDataType_Float).UpdateFloat(value);
    }
  }
  

  void MetricsRegistry::SetIntegerValue(const std::string &name,
                                        int64_t value,
                                        MetricsUpdatePolicy policy)
  {
    // Inlining to avoid loosing time if metrics are disabled
    if (enabled_)
    {
      Mutex::ScopedLock lock(mutex_);
      GetItemInternal(name, policy, MetricsDataType_Integer).UpdateInteger(value);
    }
  }


  void MetricsRegistry::IncrementIntegerValue(const std::string &name,
                                              int64_t delta)
  {
    // Inlining to avoid loosing time if metrics are disabled
    if (enabled_)
    {
      Mutex::ScopedLock lock(mutex_);
      GetItemInternal(name, MetricsUpdatePolicy_Directly, MetricsDataType_Integer).IncrementInteger(delta);
    }
  }

  void MetricsRegistry::SetInitialValue(const std::string& name,
                                        int64_t value)
  {
    if (enabled_)
    {
      Mutex::ScopedLock lock(mutex_);
      GetItemInternal(name, MetricsUpdatePolicy_Directly, MetricsDataType_Integer).SetInitialValue(value);
    }
  }


  MetricsUpdatePolicy MetricsRegistry::GetUpdatePolicy(const std::string& metrics)
  {
    Mutex::ScopedLock lock(mutex_);

    Content::const_iterator found = content_.find(metrics);

    if (found == content_.end())
    {
      throw OrthancException(ErrorCode_InexistentItem);
    }
    else
    {
      assert(found->second != NULL);
      return found->second->GetPolicy();
    }
  }


  MetricsDataType MetricsRegistry::GetDataType(const std::string& metrics)
  {
    Mutex::ScopedLock lock(mutex_);

    Content::const_iterator found = content_.find(metrics);

    if (found == content_.end())
    {
      throw OrthancException(ErrorCode_InexistentItem);
    }
    else
    {
      assert(found->second != NULL);
      return found->second->GetDataType();
    }
  }


  void MetricsRegistry::ExportPrometheusText(std::string& s)
  {
    // https://www.boost.org/doc/libs/1_69_0/doc/html/date_time/examples.html#date_time.examples.seconds_since_epoch
    static const boost::posix_time::ptime EPOCH(boost::gregorian::date(1970, 1, 1));

    Mutex::ScopedLock lock(mutex_);

    s.clear();

    if (!enabled_)
    {
      return;
    }

    ChunkedBuffer buffer;

    for (Content::const_iterator it = content_.begin();
         it != content_.end(); ++it)
    {
      assert(it->second != NULL);

      if (it->second->HasValue())
      {
        boost::posix_time::time_duration diff = it->second->GetTime() - EPOCH;

        it->second->Refresh();

        std::string line = (it->first + " " +
                            it->second->FormatValue() + " " + 
                            boost::lexical_cast<std::string>(diff.total_milliseconds()) + "\n");

        buffer.AddChunk(line);
      }
    }

    buffer.Flatten(s);
  }


  MetricsRegistry::SharedMetrics::SharedMetrics(MetricsRegistry &registry,
                                                const std::string &name,
                                                MetricsUpdatePolicy policy) :
    registry_(registry),
    name_(name),
    value_(0)
  {
    registry_.Register(name, policy, MetricsDataType_Integer);
  }

  void MetricsRegistry::SharedMetrics::Add(int64_t delta)
  {
    Mutex::ScopedLock lock(mutex_);
    value_ += delta;
    registry_.SetIntegerValue(name_, value_);
  }

  void MetricsRegistry::SharedMetrics::SetInitialValue(int64_t value)
  {
    Mutex::ScopedLock lock(mutex_);
    value_ = value;
    registry_.SetInitialValue(name_, value_);
  }

  MetricsRegistry::ActiveCounter::ActiveCounter(MetricsRegistry::SharedMetrics &metrics) :
    metrics_(metrics)
  {
    metrics_.Add(1);
  }

  MetricsRegistry::ActiveCounter::~ActiveCounter()
  {
    try
    {
      metrics_.Add(-1);
    }
    catch (OrthancException& e)
    {
      // Don't throw exceptions in destructors
      LOG(ERROR) << "Exception in destructor: " << e.What();
    }
  }


  MetricsRegistry::AvailableResourcesDecounter::AvailableResourcesDecounter(MetricsRegistry::SharedMetrics &metrics) :
    metrics_(metrics)
  {
    metrics_.Add(-1);
  }

  MetricsRegistry::AvailableResourcesDecounter::~AvailableResourcesDecounter()
  {
    try
    {
      metrics_.Add(1);
    }
    catch (OrthancException& e)
    {
      // Don't throw exceptions in destructors
      LOG(ERROR) << "Exception in destructor: " << e.What();
    }
  }


  struct MetricsRegistry::Timer::PImpl
  {
    boost::posix_time::ptime  start_;
  };


  void MetricsRegistry::Timer::Start()
  {
    if (registry_.IsEnabled())
    {
      active_ = true;
      pimpl_->start_ = GetNow();
    }
    else
    {
      active_ = false;
    }
  }


  MetricsRegistry::Timer::Timer(MetricsRegistry &registry,
                                const std::string &name) :
    pimpl_(new PImpl),
    registry_(registry),
    name_(name),
    policy_(MetricsUpdatePolicy_MaxOver10Seconds)
  {
    Start();
  }


  MetricsRegistry::Timer::Timer(MetricsRegistry &registry,
                                const std::string &name,
                                MetricsUpdatePolicy policy) :
    pimpl_(new PImpl),
    registry_(registry),
    name_(name),
    policy_(policy)
  {
    Start();
  }


  MetricsRegistry::Timer::~Timer()
  {
    if (active_)
    {
      boost::posix_time::time_duration diff = GetNow() - pimpl_->start_;

      try
      {
        registry_.SetIntegerValue(name_, static_cast<int64_t>(diff.total_milliseconds()), policy_);
      }
      catch (OrthancException& e)
      {
        // Don't throw exceptions in destructors
        LOG(ERROR) << "Exception in destructor: " << e.What();
      }
    }

    assert(pimpl_ != NULL);
    delete pimpl_;
  }
}
