#include "metric.hpp"

#include "metrics/collection_data.hpp"
#include "types/report_types.hpp"
#include "types/sensor_types.hpp"
#include "utils/labeled_tuple.hpp"
#include "utils/transform.hpp"

#include <sdbusplus/exception.hpp>

#include <algorithm>

Metric::Metric(Sensors sensorsIn, OperationType operationTypeIn,
               CollectionTimeScope timeScopeIn,
               CollectionDuration collectionDurationIn,
               std::unique_ptr<interfaces::Clock> clockIn) :
    sensors(std::move(sensorsIn)), operationType(operationTypeIn),
    collectionTimeScope(timeScopeIn), collectionDuration(collectionDurationIn),
    collectionAlgorithms(
        metrics::makeCollectionData(sensors.size(), operationType,
                                    collectionTimeScope, collectionDuration)),
    clock(std::move(clockIn))
{}

void Metric::registerForUpdates(interfaces::MetricListener& listener)
{
    listeners.emplace_back(listener);
}

void Metric::unregisterFromUpdates(interfaces::MetricListener& listener)
{
    listeners.erase(
        std::remove_if(listeners.begin(), listeners.end(),
                       [&listener](const interfaces::MetricListener& item) {
                           return &item == &listener;
                       }),
        listeners.end());
}

void Metric::initialize()
{
    for (const auto& sensor : sensors)
    {
        sensor->registerForUpdates(weak_from_this());
    }
}

void Metric::deinitialize()
{
    for (const auto& sensor : sensors)
    {
        sensor->unregisterFromUpdates(weak_from_this());
    }
}

const std::vector<MetricValue>& Metric::getUpdatedReadings()
{
    const auto steadyTimestamp = clock->steadyTimestamp();
    const auto systemTimestamp =
        std::chrono::duration_cast<Milliseconds>(clock->systemTimestamp())
            .count();

    for (size_t i = 0; i < collectionAlgorithms.size(); ++i)
    {
        if (const auto value = collectionAlgorithms[i]->update(steadyTimestamp))
        {
            if (i < readings.size())
            {
                readings[i].timestamp = systemTimestamp;
                readings[i].value = *value;
            }
            else
            {
                if (i > readings.size())
                {
                    const auto idx = readings.size();
                    std::swap(collectionAlgorithms[i],
                              collectionAlgorithms[idx]);
                    std::swap(sensors[i], sensors[idx]);
                    i = idx;
                }

                readings.emplace_back(sensors[i]->metadata(), *value,
                                      systemTimestamp);
            }
        }
    }

    return readings;
}

void Metric::sensorUpdated(interfaces::Sensor& notifier, Milliseconds timestamp,
                           double value)
{
    auto& data = findAssociatedData(notifier);
    double newValue = data.update(timestamp, value);

    if (data.updateLastValue(newValue))
    {
        for (interfaces::MetricListener& listener : listeners)
        {
            listener.metricUpdated();
        }
    }
}

metrics::CollectionData& Metric::findAssociatedData(
    const interfaces::Sensor& notifier)
{
    auto it = std::find_if(
        sensors.begin(), sensors.end(),
        [&notifier](const auto& sensor) { return sensor.get() == &notifier; });
    auto index = std::distance(sensors.begin(), it);
    return *collectionAlgorithms.at(index);
}

LabeledMetricParameters Metric::dumpConfiguration() const
{
    auto sensorPath = utils::transform(sensors, [this](const auto& sensor) {
        return LabeledSensorInfo(sensor->id().service, sensor->id().path,
                                 sensor->metadata());
    });

    return LabeledMetricParameters(std::move(sensorPath), operationType,
                                   collectionTimeScope, collectionDuration);
}

uint64_t Metric::metricCount() const
{
    return sensors.size();
}

void Metric::updateReadings(Milliseconds timestamp)
{
    for (auto& data : collectionAlgorithms)
    {
        if (std::optional<double> newValue = data->update(timestamp))
        {
            if (data->updateLastValue(*newValue))
            {
                for (interfaces::MetricListener& listener : listeners)
                {
                    listener.metricUpdated();
                }
                return;
            }
        }
    }
}

bool Metric::isTimerRequired() const
{
    if (collectionTimeScope == CollectionTimeScope::point)
    {
        return false;
    }

    if (collectionTimeScope == CollectionTimeScope::startup &&
        (operationType == OperationType::min ||
         operationType == OperationType::max))
    {
        return false;
    }

    return true;
}