/**
 *
 * @file src/statistics/Diagram.cpp
 *
 * @copyright 2008-2024 Bordeaux INP, CNRS (LaBRI UMR 5800), Inria,
 *                      Univ. Bordeaux. All rights reserved.
 *
 * @author Luca Bourroux
 * @author Camille Ordronneau
 * @author Johnny Jazeix
 * @author Mathieu Faverge
 *
 * @date 2024-07-17
 */
#include "Diagram.hpp"

#include <unordered_map>
#include <string>

#include <QtWidgets/qlabel.h>
#include <QtCharts/qbarseries.h>
#include <QtCharts/qchart.h>
#include <QtCharts/qbarset.h>
#include <QtCharts/qstackedbarseries.h>
#include <QtCharts/qbarcategoryaxis.h>
#include <QtCharts/qhorizontalbarseries.h>
#include <QtCharts/qhorizontalstackedbarseries.h>
#include <QtCharts/qvalueaxis.h>
#include <QtCharts/qlineseries.h>

/* -- */
#include "trace/values/Values.hpp"
#include "trace/EntityValue.hpp"
#include "trace/EntityTypes.hpp"
#include "trace/Entitys.hpp"
#include "trace/tree/Interval.hpp"
/* -- */
#include "statistics/Statistic.hpp"

// Helper function to help clean every information on the chart.
void clean_chart(QChart &chart) {
    chart.removeAllSeries();
    auto axes = chart.axes();
    for (auto &x: axes) {
        chart.removeAxis(x);
        delete x;
    }
}

// We must ensure that two distinct containers have distinct names.
// So if some of them are empty we need to construct a unique name we do that by walking the
// container tree and append the name like so: root -> child1 -> grandchild1 -> ... -> leaf
std::string construct_name(Container &container, size_t max_rec = 0) {
    auto parent = container.get_parent();
    std::string res;

    // >TODO(Tackwin) >Perf
    // back to front string construction :( for now it's good.
    while (parent && --max_rec > 0) {
        auto app = parent->get_name() + " -> ";
        res = app + res;
        parent = parent->get_parent();
    }

    return res;
}

// Helper function to check if a name is only whitespace.
bool name_is_whitespace(const std::string &name) {
    for (auto &c: name)
        if (!std::isspace(c))
            return false;
    return true;
}

// Helper function that handles the logic in common for stacked_/vertical/horizontal.
// fill the series and categories with appropriate data. (Time spent on different state type for
// each containers).
void feed_abstract_bar_series(
    const std::vector<Container *> &containers,
    const Interval &interval,
    QAbstractBarSeries *series,
    QStringList &categories) {
    // The Qt Api dictate that a bar set contains a value for each categories
    // But for us it would be more natural to says at once all the value for each categories
    // so we need to transform our data a little.

    // This struct will contains the data in a QtChart friendly way.
    struct Bar_Set_Data
    {
        std::vector<double> vec;
        QColor color;
    };

    std::unordered_map<std::string, Bar_Set_Data> data;

    // >Reverse
    // We go end to beginning to order the containers the same way as they are in the
    // principal view.
    for (size_t i = containers.size() - 1; i + 1 > 0; --i) { // for each container
        auto &c = containers[i];
        auto name = c->get_name();
        if (name_is_whitespace(name)) {
            name = construct_name(*c) + std::to_string(i);
        }

        categories << QString::fromStdString(name); // add a category

        Statistic stat_temp; // get the stats
        c->fill_stat(&stat_temp, interval);
        auto states_temp = stat_temp.get_states();

        for (auto &state: states_temp) { // for each states type
            auto h = state.second->_total_length * 100 / (interval._right - interval._left);
            auto c = state.first->get_used_color();

            // First it might be the first time that we encounter this state type so resize the
            // vector. We don't care if it's done multiple time since containers.size() is a
            // constant. Then add an entry in the corresponding set (meaning at index i)
            data[state.first->get_name()].vec.resize(containers.size());
            data[state.first->get_name()].vec[i] = (std::size_t)(h * 10) / 10.f;
            data[state.first->get_name()].color = {
                (int)(255 * c->get_red()),
                (int)(255 * c->get_green()),
                (int)(255 * c->get_blue())
            };
        }
    }

    // Then we have the data ordered by state type then by container so we can fill naturally the
    // qt objects.
    for (auto &it: data) {
        QBarSet *set = new QBarSet(QString::fromStdString(it.first));
        set->setColor(it.second.color);

        // >Reverse for the same reason as before.
        for (size_t i = it.second.vec.size() - 1; i + 1 > 0; --i) {
            *set << it.second.vec[i];
        }

        series->append(set);
        series->setLabelsVisible(true);
        series->setLabelsPosition(QAbstractBarSeries::LabelsPosition::LabelsOutsideEnd);
    }
}

void Diagram::vertical(
    QChart &chart, const std::vector<Container *> &containers, const Interval &interval) {
    clean_chart(chart);

    QStringList categories;
    auto series = new QBarSeries();
    feed_abstract_bar_series(containers, interval, series, categories);

    chart.addSeries(series);

    auto *axis_x = new QBarCategoryAxis();
    axis_x->append(categories);

    auto *axis_y = new QValueAxis();
    axis_y->setRange(0, 100);

    chart.addAxis(axis_y, Qt::AlignLeft);
    chart.addAxis(axis_x, Qt::AlignBottom);

    series->attachAxis(axis_x);
    series->attachAxis(axis_y);
}

void Diagram::stacked_vertical(
    QChart &chart, const std::vector<Container *> &containers, const Interval &interval) {
    clean_chart(chart);

    QStringList categories;
    auto series = new QStackedBarSeries();
    feed_abstract_bar_series(containers, interval, series, categories);

    chart.addSeries(series);

    auto *axis_x = new QBarCategoryAxis();
    axis_x->append(categories);

    auto *axis_y = new QValueAxis();
    axis_y->setRange(0, 100);

    chart.addAxis(axis_y, Qt::AlignLeft);
    chart.addAxis(axis_x, Qt::AlignBottom);

    series->attachAxis(axis_x);
    series->attachAxis(axis_y);
}

void Diagram::horizontal(
    QChart &chart, const std::vector<Container *> &containers, const Interval &interval) {
    clean_chart(chart);

    QStringList categories;
    auto series = new QHorizontalBarSeries();
    feed_abstract_bar_series(containers, interval, series, categories);

    chart.addSeries(series);

    auto *axis_y = new QBarCategoryAxis();
    axis_y->append(categories);

    auto *axis_x = new QValueAxis();
    axis_x->setRange(0, 100);

    chart.addAxis(axis_y, Qt::AlignLeft);
    chart.addAxis(axis_x, Qt::AlignBottom);

    series->attachAxis(axis_x);
    series->attachAxis(axis_y);
}

void Diagram::stacked_horizontal(
    QChart &chart, const std::vector<Container *> &containers, const Interval &interval) {
    clean_chart(chart);

    QStringList categories;
    auto series = new QHorizontalStackedBarSeries();
    feed_abstract_bar_series(containers, interval, series, categories);

    chart.addSeries(series);

    auto *axis_y = new QBarCategoryAxis();
    axis_y->append(categories);

    auto *axis_x = new QValueAxis();
    axis_x->setRange(0, 100);

    chart.addAxis(axis_y, Qt::AlignLeft);
    chart.addAxis(axis_x, Qt::AlignBottom);

    series->attachAxis(axis_x);
    series->attachAxis(axis_y);
}

void Diagram::counter(
    QChart &chart, const std::vector<Container *> &containers, const Interval &interval) {
    clean_chart(chart);

    // we have essentially the same logic than in feed_abstract_series.
    // For this line graph we want to have a "step" function since our counter are discretized but
    // a line graph is continuous.
    struct Line_Data
    {
        std::vector<double> xs;
        std::vector<double> ys;
    };

    std::unordered_map<std::string, Line_Data> data;

    QStringList categories;
    for (size_t i = 0; i < containers.size(); ++i) {
        auto &c = containers[i];
        auto name = c->get_Name().to_string();
        categories << QString::fromStdString(name);

        auto &var = *c->get_variables();
        for (auto &first_it: var) {
            auto &v = first_it.second;
            Line_Data d;

            // It's possible that we have more than one counter by containers so we need to
            // construct an unique name.
            auto name = c->get_name() + " : " + v->get_type()->get_name();

            for (auto &second_it: *v->get_values()) {
                auto &t = second_it.first;
                auto &y = second_it.second;
                data[name].xs.push_back(t.get_value());
                data[name].ys.push_back(y.get_value());
            }
        }
    }

    for (auto &it: data) {
        auto *series = new QLineSeries();
        series->setName(QString::fromStdString(it.first));

        // with this last_y and double append we construct the "step" function.
        double last_y = 0;
        for (size_t i = 0; i < it.second.xs.size(); ++i) {
            series->append(it.second.xs[i], last_y);
            last_y = it.second.ys[i];
            series->append(it.second.xs[i], it.second.ys[i]);
        }
        chart.addSeries(series);
    }

    chart.createDefaultAxes();
}
