extern "C" {
# include <liblocky.h>
}

#include <algorithm>
#include <cinttypes>
#include <cmath>
#include <numeric>
#include <string>
#include <vector>

#include <imgui.h>
#include <implot.h>

#include "app.hh"
#include "input.hh"


namespace pg {
namespace {

class Sensor final : public App {
 public:
  static inline TypeInfo kType = TypeInfo::Create<Sensor>("Sensor");

  Sensor() noexcept {
  }

  void Update() noexcept override {
    const auto em = ImGui::GetFontSize();
    const auto id = "Sensor | "+
        std::to_string(reinterpret_cast<uintptr_t>(this));

    ImGui::SetNextWindowSize({64*em, 24*em}, ImGuiCond_Once);
    if (ImGui::Begin(id.c_str())) {
      bool mod = false;
      ImGui::BeginGroup();
      {
        ImGui::PushItemWidth(6*em);
        mod |= ImGui::DragInt("input slot", &src_, 1, 0, 1024);
        mod |= ImGui::DragInt("start", &start_, 1, 0, 1024);
        mod |= ImGui::DragInt("dur", &dur_, 1, 1, BLKY_SENSOR_MAX_DUR);

        ImGui::Spacing();
        ImGui::DragFloat2("pos", pos_, .001f);
        ImGui::DragInt("offset", &offset_, 1, 0, 3);
        if (ImGui::Button("add")) {
          mod = true;
          data_.emplace_back(pos_[0], pos_[1], offset_);
        }
        if (ImGui::BeginListBox("##points")) {
          for (size_t i = 0; i < data_.size(); ++i) {
            const auto& data = data_[i];
            const auto  name =
                std::to_string(i)+". "+
                std::to_string(data.pos[0])+","+
                std::to_string(data.pos[1]);
            ImGui::Selectable(name.c_str());
            if (ImGui::BeginPopupContextItem()) {
              if (ImGui::MenuItem("clear all")) {
                mod = true;
                data_.clear();
              }
              ImGui::EndPopup();
            }
            if (ImGui::IsItemHovered()) {
              ImGui::SetTooltip(
                  "pos   : %f,%f + %" PRIiMAX "\n"
                  "avg   : %f\n"
                  "var   : %f\n"
                  "correl: %f",
                  data.pos[0], data.pos[1], data.offset,
                  data.avg, data.var, data.correl);
            }
          }
          ImGui::EndListBox();
        }

        double correl_abs_avg = 0;
        for (auto& data : data_) {
          correl_abs_avg += fabs(data.correl);
        }
        correl_abs_avg /= static_cast<float>(data_.size());
        ImGui::Text("avg(abs(r)) = %f", correl_abs_avg);

        double correl_abs_var = 0;
        for (auto& data : data_) {
          const auto diff = fabs(data.correl)-correl_abs_avg;
          correl_abs_var += diff*diff;
        }
        correl_abs_var /= static_cast<float>(data_.size());
        ImGui::Text("var(abs(r)) = %f", correl_abs_var);

        ImGui::PopItemWidth();
        if (mod) {
          try {
            Calc();
            msg_ = "";
          } catch (const char* msg) {
            msg_ = msg;
          }
        }
      }
      ImGui::EndGroup();
      ImGui::SameLine();
      ImGui::BeginGroup();
      auto avail = ImGui::GetContentRegionAvail();
      avail.x -= em;
      avail.y -= em;

      const ImVec2 size = {avail.x/2, avail.y};

      if (ImPlot::BeginPlot("input value", size)) {
        ImPlot::SetupAxis(ImAxis_X1, nullptr, ImPlotAxisFlags_AutoFit);
        ImPlot::SetupAxisLimits(ImAxis_Y1, -0.1, 1.1);
        for (size_t i = 0; i < data_.size(); ++i) {
          const auto& d = data_[i];
          ImPlot::PlotLine(
              std::to_string(i).c_str(),
              d.values.data(), static_cast<int>(d.values.size()), 1, start_);
        }
        ImPlot::EndPlot();
      }
      ImGui::SameLine();
      if (ImPlot::BeginPlot("input histogram", size)) {
        ImPlot::SetupAxisLimits(ImAxis_X1, 0, 1);
        ImPlot::SetupAxis(ImAxis_Y1, nullptr, ImPlotAxisFlags_AutoFit);
        for (size_t i = 0; i < data_.size(); ++i) {
          const auto& d = data_[i];
          ImPlot::PlotHistogram(
              std::to_string(i).c_str(),
              d.values.data(), static_cast<int>(d.values.size()));
        }
        ImPlot::EndPlot();
      }
      ImGui::EndGroup();
      ImGui::TextUnformatted(msg_.c_str());
    }
    ImGui::End();
  }

 private:
  int src_ = 0, start_ = 0, dur_ = 30;
  float pos_[2];
  int offset_;

  struct Data final {
   public:
    float pos[2];
    size_t offset;
    std::vector<float> values;

    double avg;
    double var;
    double cov;
    double correl;

    Data(float x, float y, int off) noexcept :
        pos{x, y}, offset(static_cast<size_t>(off)) {
    }
  };
  std::vector<Data> data_;
  std::string msg_;

  void Calc();
};

void Sensor::Calc() {
  auto in = Input::instance().slots(static_cast<size_t>(src_));
  if (!in) throw "missing slot";

  if (dur_ == 0) throw "invalid time range";

  for (auto& data : data_) {
    const auto xf = std::clamp(data.pos[0], 0.f, 1.f);
    const auto yf = std::clamp(data.pos[1], 0.f, 1.f);

    const auto dur = static_cast<size_t>(dur_);
    const auto st  = static_cast<size_t>(start_);
    data.values = in->FetchSamples(st, dur, xf, yf, data.offset);

    blky_sensor_t sensor = {};
    blky_sensor_feed(&sensor, data.values.data(), data.values.size());
    data.avg    = sensor.avg;
    data.var    = sensor.var;
    data.cov    = sensor.cov;
    data.correl = sensor.correl;
  }
}

}
}  // namespace pg