#pragma once

#include "caffe2/core/blob_serialization.h"
#include "caffe2/core/context.h"
#include "caffe2/core/operator.h"
#include "caffe2/core/timer.h"

#include <mutex>

namespace caffe2 {
struct QPSMetricState {
  Timer lifetimeTimer;
  Timer windowTimer;
  int64_t windowExamples{0};
  int64_t lifetimeExamples{0};

  std::mutex mutex;
};

CAFFE_KNOWN_TYPE(std::unique_ptr<QPSMetricState>);

class QPSMetricStateSerializer : public BlobSerializerBase {
 public:
  /**
   * Serializes a std::unique_ptr<QPSMetricState>. Note that this blob has to
   * contain std::unique_ptr<QPSMetricState>, otherwise this function produces a
   * fatal error.
   */
  void Serialize(
      const Blob& blob,
      const string& name,
      BlobSerializerBase::SerializationAcceptor acceptor) override;
};

class QPSMetricStateDeserializer : public BlobDeserializerBase {
 public:
  void Deserialize(const BlobProto& proto, Blob* blob) override;
};

// TODO(amalevich): Consider making all the code below templated, so it'll be
// easier to share it across different metrics.
class CreateQPSMetricOp final : public Operator<CPUContext> {
 public:
  using Operator<CPUContext>::Operator;

  bool RunOnDevice() override {
    *OperatorBase::Output<std::unique_ptr<QPSMetricState>>(0) =
        caffe2::make_unique<QPSMetricState>();
    return true;
  }
};

class QPSMetricOp final : public Operator<CPUContext> {
 public:
  using Operator<CPUContext>::Operator;

  bool RunOnDevice() override {
    auto& metricsBlob =
        *OperatorBase::Input<std::unique_ptr<QPSMetricState>>(0);
    auto examples = Input(1).dim(0);
    // All changes to metrics should happen under critical section.
    {
      std::lock_guard<std::mutex> guard(metricsBlob.mutex);
      metricsBlob.windowExamples += examples;
      metricsBlob.lifetimeExamples += examples;
    }
    return true;
  }
};

class QPSMetricReportOp final : public Operator<CPUContext> {
 public:
  using Operator<CPUContext>::Operator;

  bool RunOnDevice() override {
    auto& metricsBlob =
        *OperatorBase::Input<std::unique_ptr<QPSMetricState>>(0);
    // All changes to metrics should happen under critical section.
    float windowSeconds = -1;
    int64_t windowExamples = 0;
    float lifetimeSeconds = -1;
    int64_t lifetimeExamples = 0;
    {
      std::lock_guard<std::mutex> guard(metricsBlob.mutex);
      windowSeconds = metricsBlob.windowTimer.Seconds();
      lifetimeSeconds = metricsBlob.lifetimeTimer.Seconds();
      windowExamples = metricsBlob.windowExamples;
      lifetimeExamples = metricsBlob.lifetimeExamples;

      metricsBlob.windowTimer.Start();
      metricsBlob.windowExamples = 0;
    }
    // TODO(amalevich): Add output blobs, so it would be relatively easy to
    // access this metrics from the outside
    LOG(INFO) << "Lifetime Examples = " << lifetimeExamples
              << ", Lifetime QPS = "
              << (static_cast<double>(lifetimeExamples) / lifetimeSeconds)
              << ", Window Examples = " << windowExamples << ", Window QPS = "
              << (static_cast<double>(windowExamples) / windowSeconds);
    return true;
  }
};
}
