#include "FxCyanF.hpp"

// C++ system level
#include <string>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <functional>

// ESP32 specific
#include "esp_err.h"
#include "esp_log.h"
#include "driver/ledc.h"
#include "tcpip_adapter.h"

// project specific
#include "FxVSync.hpp"
#include "CyanBus.hpp"
#include "Metrics.hpp"

// qthing stuff
#include <qthing>
#include <qthing/mqtt_common.hpp>

// misc
#include <nlohmann/json.hpp>


using namespace qthing;
using json = nlohmann::json;



namespace SiliconTorch {

  namespace FxCyanF {

    const char* TAG = "FxCyanF";
    const char* nvsNameSpace = TAG;

    const std::string HEADER("fxCyanF");


    const uint8_t MAX_CHANNELS = 8;  // Maybe 16…?

    const std::string delimiter = ":";


    static float bytes2float(const uint8_t* bytes) {
      float f;

      memcpy(&f, bytes, sizeof(f));

      return f;
    }


    FxCyanF::FxCyanF(uint32_t baseChannel) : metrics(Metrics::Metrics("fxCyan")), baseChannel(baseChannel) {

      this->metrics.registerMetric("frames", "frameCounter");
      this->metrics.registerMetric("errors", "errorCounter");

      this->frameCntInc = this->metrics.generateMetricIncrementer("frames");
      this->errorCntInc = this->metrics.generateMetricIncrementer("errors");


      // TODO: make vector!
      this->channels = new Impl::PWMChannel*[MAX_CHANNELS];

      this->timer_cfg.duty_resolution = (ledc_timer_bit_t)this->resolution;
      this->timer_cfg.freq_hz         = this->frequency;
      this->timer_cfg.speed_mode      = LEDC_HIGH_SPEED_MODE;
      this->timer_cfg.timer_num       = LEDC_TIMER_0;
      this->timer_cfg.clk_cfg         = LEDC_AUTO_CLK;

      ledc_timer_config(&this->timer_cfg);

      ledc_fade_func_install(0);


      add_binary_message_callback(this->genDeviceTopic("pwm/$all"), [&](qthing::multipart_message_t message) {
        if (message.offset == 0) this->handleUnicast((const uint8_t*)message.payload, message.length);
        else ESP_LOGE(TAG, "Invalid message format: Fragmentation is unsupported");
      });


      std::string header("fxCyanF");
      uint8_t headerLength = header.length();

      qthing::addUDPPacketCallback(header, [&, headerLength](udpPacket packet) {
        this->handleUnicast((const uint8_t*)packet.payload + headerLength, packet.length - headerLength);
      });


      std::function<void(const std::string&)> setCh = [&](const std::string& message) {
        long int ch = strtol(message.c_str(), NULL, 0);
        this->setBaseChannel(ch);
      };

      std::function<void(const std::string&)> setFrq = [&](const std::string& message) {
        long int frq = strtol(message.c_str(), NULL, 0);
        this->setFrequency(frq);
      };

      std::function<void(const std::string&)> setRes = [&](const std::string& message) {
        long int res = strtol(message.c_str(), NULL, 0);
        this->setResolution(res);
      };

      std::function<void(const std::string&)> setFrqRes = [&](const std::string& message) {
        std::string::size_type found = message.find(delimiter);

        if (found == std::string::npos) {
          ESP_LOGE(TAG, "Invalid message format[ %s ]", message.c_str());
          return;
        }

        std::string _frq = message.substr(0, found);
        std::string _res = message.substr(found + delimiter.length());

        long int frq = strtol(_frq.c_str(), NULL, 0);
        long int res = strtol(_res.c_str(), NULL, 0);

        if (frq == 0 || res == 0) {
          ESP_LOGE(TAG, "Invalid message format[ '%s' ]", message.c_str());
          return;
        }

        this->setFrqRes(frq, res);
      };


      std::function<void(const std::string&)> getBCh = [&](const std::string& ignored) {
        this->publishBaseChannel();
      };

      std::function<void(const std::string&)> getChs = [&](const std::string& ignored) {
        this->publishChannelCount();
      };

      std::function<void(const std::string&)> getFrq = [&](const std::string& ignored) {
        this->publishFrequency();
      };

      std::function<void(const std::string&)> getRes = [&](const std::string& ignored) {
        this->publishResolution();
      };

      std::function<void(const std::string&)> getFrqRes = [&](const std::string& ignored) {
        this->publishFrqRes();
      };

      std::function<void(const std::string&)> getListener = [&](const std::string& ignored) {
        this->publishListenerInfo();
      };


      // device-local setters
      add_message_callback(this->genDeviceTopic("frqres/set"), setFrqRes);
      add_message_callback(this->genDeviceTopic("channel/set"), setCh);
      add_message_callback(this->genDeviceTopic("frequency/set"), setFrq);
      add_message_callback(this->genDeviceTopic("resolution/set"), setRes);

      // device-lokal getters
      add_message_callback(this->genDeviceTopic("frequency/get"), getFrq);
      add_message_callback(this->genDeviceTopic("resolution/get"), getRes);
      add_message_callback(this->genDeviceTopic("frqres/get"), getFrqRes);
      add_message_callback(this->genDeviceTopic("channel/get"), getBCh);
      add_message_callback(this->genDeviceTopic("channelCnt/get"), getChs);

      // global setters
      add_message_callback(this->genServiceTopic("frqres/set"), setFrqRes);
      add_message_callback(this->genServiceTopic("channel/set"), setCh);
      add_message_callback(this->genServiceTopic("frequency/set"), setFrq);
      add_message_callback(this->genServiceTopic("resolution/set"), setRes);

      // global getters
      add_message_callback(this->genServiceTopic("frequency/get"), getFrq);
      add_message_callback(this->genServiceTopic("resolution/get"), getRes);
      add_message_callback(this->genServiceTopic("frqres/get"), getFrqRes);
      add_message_callback(this->genServiceTopic("channel/get"), getBCh);
      add_message_callback(this->genServiceTopic("channelCnt/get"), getChs);

      // Listener info getters and auto-publish
      add_message_callback(this->genDeviceTopic("listener/get"), getListener);
      add_message_callback(this->genServiceTopic("listener/get"), getListener);
      add_mqtt_connected_callback(std::bind(&FxCyanF::publishListenerInfo, this));
    }


    bool FxCyanF::addChannel(uint8_t gpio) {
      if (this->channelsConfigured >= MAX_CHANNELS) {
        ESP_LOGE(TAG, "Cannot create channel#[ %i ]! ESP32 hardware limit is #[ %i ]", this->channelsConfigured, MAX_CHANNELS);
        return false;
      }

      uint8_t channel = this->channelsConfigured;

      // TODO: catch channel creation errors from IDF (e.g. on input-only GPIO)
      this->channels[channel] = new Impl::PWMChannel(channel, gpio);

      char topic[32];
      snprintf(topic, sizeof(topic), "pwm/$%i", channel);

      add_message_callback(this->genDeviceTopic(topic), [&, channel](const std::string& message) {
        this->setPWM(channel, strtof(message.c_str(), NULL));
        this->callPacketCallback();
      });

      this->channelsConfigured++;
      return true;
    }


    bool FxCyanF::handleUnicast(const uint8_t* data, std::size_t length) {
      std::size_t size = this->getChannelCount() * sizeof(float);

      int32_t diff = length - size;
      if (diff < 0) {
        ESP_LOGE(TAG, "Invalid |data|[ %d ]: Received ΔB[ %d ] bytes too few", length, -diff);

        this->errorCntInc();

        return false;
      }

      for (uint8_t ch = 0; ch < this->getChannelCount(); ch++) {
        float f = bytes2float(data + ch * sizeof(float));
        this->setPWM(ch, f);
      }

      this->frameCntInc();
      this->callPacketCallback();

      return true;
    }

    bool FxCyanF::handleBroadcast(const uint8_t* data, std::size_t length) {
      std::size_t size = this->getChannelCount() * sizeof(float);
      std::size_t offset = this->getBaseChannel() * sizeof(float);

      int32_t diff = length - offset - size;
      if (diff < 0) {  // TODO: test thoroughly!
        ESP_LOGE(TAG, "Invalid data length[ %i ]: Received ΔB = %i bytes too few", length, -diff);

        this->errorCntInc();

        return false;
      }

      for (uint8_t ch = 0; ch < this->getChannelCount(); ch++) {
        float f = bytes2float(data + offset + ch * sizeof(float));
        this->setPWM(ch, f);
      }

      this->frameCntInc();
      this->callPacketCallback();

      return true;
    }


    void FxCyanF::setPWM(uint8_t channel, float value) {

      if (channel >= this->channelsConfigured) {
        ESP_LOGW(TAG, "ChannelID out of range: channel[ %i ]", channel);
        return;
      }

      value = this->gammaCorrector(value);

      if (value < 0.0f) value = 0.0f;
      if (value > 1.0f) value = 1.0f;

      uint32_t maxPWM = 1 << (this->resolution - 1);
      uint32_t pwm = (uint32_t)( value * (maxPWM - 1) );

      this->channels[channel]->setPWM(pwm);
    }


    void FxCyanF::setGammaCorrector(GammaCorrector gammaCorrector) {
      this->gammaCorrector = gammaCorrector;
    }


    bool FxCyanF::setFrqRes(uint32_t frq_hz, uint8_t res_bits) {

      this->timer_cfg.duty_resolution = (ledc_timer_bit_t)res_bits;
      this->timer_cfg.freq_hz = frq_hz;

      if (ledc_timer_config(&this->timer_cfg) == ESP_OK) {
        this->frequency = frq_hz;
        this->resolution = res_bits;

        ESP_LOGI(TAG, "frequency[ %i Hz ]  resolution[ %i bit ]", frq_hz, res_bits);

        return true;

      } else {

        ESP_LOGW(TAG, "Invalid frequency[ %i Hz ] and resolution[ %i bit ] setting!", frq_hz, res_bits);
        return false;
      }
    }

    void FxCyanF::registerAtCyanBus(CyanBus::CyanBus& cyanBus) {
    /*
      uint8_t headerLength = HEADER.length();

      cyanBus.packetCallback.add(HEADER, [&, headerLength](const CyanBus::PacketData& packet) {
        this->handlePacket((uint8_t*)(packet.payload + headerLength), packet.length - headerLength);
      });

      FxVSync::registerAtCyanBus(cyanBus, [&]() {
        //requestBufferSwap();
      });
    */
    }

    void FxCyanF::setBaseChannel(uint16_t baseChannel) {
      this->baseChannel = baseChannel;
    }

    uint16_t FxCyanF::getBaseChannel() {
      return this->baseChannel;
    }

    uint8_t FxCyanF::getChannelCount() {
      return this->channelsConfigured;
    }

    bool FxCyanF::setFrequency(uint32_t frq_hz) {
      return this->setFrqRes(frq_hz, this->resolution);
    }

    uint32_t FxCyanF::getFrequency() {
      return this->frequency;
    }

    bool FxCyanF::setResolution(uint8_t res_bits) {
      return this->setFrqRes(this->frequency, res_bits);
    }

    uint8_t FxCyanF::getResolution() {
      return this->resolution;
    }

    void FxCyanF::setPacketHandledCallback(PacketHandledCallback callback) {
      this->packetCallback = callback;
    }


    std::string FxCyanF::genDeviceTopic(const char *suffix) {
      return std::string(DEVICE_NAMESPACE + "fxCyan/") + std::string(suffix);
    }

    std::string FxCyanF::genServiceTopic(const char *suffix) {
      return std::string("service/fxCyan/") + std::string(suffix);
    }


    void FxCyanF::callPacketCallback() {
      this->packetCallback();
    }


    void FxCyanF::publishBaseChannel() {
      char tmp[16];
      snprintf(tmp, sizeof(tmp), "%i", this->getBaseChannel());
      publish_message(this->genDeviceTopic("channel"), tmp);
    }

    void FxCyanF::publishChannelCount() {
      char tmp[16];
      snprintf(tmp, sizeof(tmp), "%i", this->getChannelCount());
      publish_message(this->genDeviceTopic("channelCnt"), tmp);
    }

    void FxCyanF::publishFrequency() {
      char tmp[16];
      snprintf(tmp, sizeof(tmp), "%i", this->getFrequency());
      publish_message(this->genDeviceTopic("frequency"), tmp);
    }

    void FxCyanF::publishResolution() {
      char tmp[16];
      snprintf(tmp, sizeof(tmp), "%i", this->getResolution());
      publish_message(this->genDeviceTopic("resolution"), tmp);
    }

    void FxCyanF::publishFrqRes() {
      char tmp[32];
      snprintf(tmp, sizeof(tmp), "%i%s%i", this->getFrequency(), delimiter.c_str(), this->getResolution());
      publish_message(this->genDeviceTopic("frqres"), tmp);
    }

    void FxCyanF::publishListenerInfo() {
      if (qthing::is_mqtt_connected()) {

        // TODO: get IP Address of ethernet adapter

        tcpip_adapter_ip_info_t ipInfo;
        esp_err_t err;
        err = tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ipInfo);

        if (err == ESP_OK) {
          char ip[16];
          snprintf(ip, 16, "%d.%d.%d.%d", ipInfo.ip.addr & 0xFF, (ipInfo.ip.addr >> 8) & 0xFF, (ipInfo.ip.addr >> 16) & 0xFF, ipInfo.ip.addr >> 24);

          json j;
          j["IPv4"] = ip;
          j["port"] = "4213";  // TODO: get from qthing (currently not possible)

          publish_message(this->genDeviceTopic("listener"), j.dump().c_str());
        } else {
          ESP_LOGW(TAG, "Can't determine IP");
        }
      }
    }
  }
}