#pragma once

class Sensors {
protected:
  static uint8_t maxSensors;

public:
  enum class Type : uint8_t {
    OT_OUTDOOR_TEMP         = 0,
    OT_HEATING_TEMP         = 1,
    OT_HEATING_RETURN_TEMP  = 2,
    OT_DHW_TEMP             = 3,
    OT_DHW_TEMP2            = 4,
    OT_DHW_FLOW_RATE        = 5,
    OT_CH2_TEMP             = 6,
    OT_EXHAUST_TEMP         = 7,
    OT_HEAT_EXCHANGER_TEMP  = 8,
    OT_PRESSURE             = 9,
    OT_MODULATION_LEVEL     = 10,
    OT_CURRENT_POWER        = 11,
    OT_EXHAUST_CO2          = 12,
    OT_EXHAUST_FAN_SPEED    = 13,
    OT_SUPPLY_FAN_SPEED     = 14,
    OT_SOLAR_STORAGE_TEMP   = 15,
    OT_SOLAR_COLLECTOR_TEMP = 16,
    OT_FAN_SPEED_SETPOINT   = 17,
    OT_FAN_SPEED_CURRENT    = 18,

    OT_BURNER_STARTS        = 19,
    OT_DHW_BURNER_STARTS    = 20,
    OT_HEATING_PUMP_STARTS  = 21,
    OT_DHW_PUMP_STARTS      = 22,
    OT_BURNER_HOURS         = 23,
    OT_DHW_BURNER_HOURS     = 24,
    OT_HEATING_PUMP_HOURS   = 25,
    OT_DHW_PUMP_HOURS       = 26,
    OT_COOLING_HOURS        = 27,

    NTC_10K_TEMP            = 50,
    DALLAS_TEMP             = 51,
    BLUETOOTH               = 52,
    DHT11                   = 53,
    DHT22                   = 54,

    HEATING_SETPOINT_TEMP   = 253,
    MANUAL                  = 254,
    NOT_CONFIGURED          = 255
  };

  enum class Purpose : uint8_t {
    OUTDOOR_TEMP          = 0,
    INDOOR_TEMP           = 1,
    HEATING_TEMP          = 2,
    HEATING_RETURN_TEMP   = 3,
    DHW_TEMP              = 4,
    DHW_RETURN_TEMP       = 5,
    DHW_FLOW_RATE         = 6,
    EXHAUST_TEMP          = 7,
    MODULATION_LEVEL      = 8,

    NUMBER                = 247,
    POWER_FACTOR          = 248,
    POWER                 = 249,
    FAN_SPEED             = 250,
    CO2                   = 251,
    PRESSURE              = 252,
    HUMIDITY              = 253,
    TEMPERATURE           = 254,
    NOT_CONFIGURED        = 255
  };

  enum class ValueType : uint8_t {
    PRIMARY     = 0,
    TEMPERATURE = 0,
    HUMIDITY    = 1,
    BATTERY     = 2,
    RSSI        = 3
  };

  enum class AverageType : uint8_t {
    MEAN    = 0,
    MINIMUM = 1,
    MAXIMUM = 2
  };

  typedef struct {
    bool enabled = false;
    char name[33];
    Purpose purpose = Purpose::NOT_CONFIGURED;
    Type type = Type::NOT_CONFIGURED;
    uint8_t gpio = GPIO_IS_NOT_CONFIGURED;
    uint8_t address[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
    float offset = 0.0f;
    float factor = 1.0f;
    bool filtering = false;
    float filteringFactor = 0.15f;
  } Settings;

  typedef struct {
    bool connected = false;
    unsigned long activityTime = 0;
    uint8_t signalQuality = 100;
    //float raw[4] = {0.0f, 0.0f, 0.0f, 0.0f};
    float values[4] = {0.0f, 0.0f, 0.0f, 0.0f};
  } Result;


  static Settings* settings;
  static Result* results;

  static inline void setMaxSensors(uint8_t value) {
    maxSensors = value;
  }

  static inline uint8_t getMaxSensors() {
    return maxSensors;
  }

  static uint8_t getMaxSensorId() {
    uint8_t maxSensors = getMaxSensors();
    return maxSensors > 1 ? (maxSensors - 1) : 0;
  }

  static inline bool isValidSensorId(const uint8_t id) {
    return id >= 0 && id <= getMaxSensorId();
  }

  static inline bool isValidValueId(const uint8_t id) {
    return id >= (uint8_t) ValueType::TEMPERATURE && id <= (uint8_t) ValueType::RSSI;
  }

  static bool hasEnabledAndValid(const uint8_t id) {
    if (!isValidSensorId(id) || !settings[id].enabled) {
      return false;
    }

    if (settings[id].type == Type::NOT_CONFIGURED || settings[id].purpose == Purpose::NOT_CONFIGURED) {
      return false;
    }

    return true;
  }

  static uint8_t getAmountByType(Type type, bool onlyEnabled = false) {
    if (settings == nullptr) {
      return 0;
    }

    uint8_t amount = 0;
    for (uint8_t id = 0; id <= getMaxSensorId(); id++) {
      if (settings[id].type == type && (!onlyEnabled || settings[id].enabled)) {
        amount++;
      }
    }

    return amount;
  }

  static int16_t getIdByName(const char* name) {
    if (settings == nullptr) {
      return -1;
    }

    for (uint8_t id = 0; id <= getMaxSensorId(); id++) {
      if (strcmp(settings[id].name, name) == 0) {
        return id;
      }
    }

    return -1;
  }

  static int16_t getIdByObjectId(const char* objectId) {
    if (settings == nullptr) {
      return -1;
    }

    String refObjectId;
    for (uint8_t id = 0; id <= getMaxSensorId(); id++) {
      Sensors::makeObjectId(refObjectId, settings[id].name);
      if (refObjectId.equals(objectId)) {
        return id;
      }
    }

    return -1;
  }

  static bool setValueById(const uint8_t sensorId, float value, const ValueType valueType, const bool updateActivityTime = false, const bool markConnected = false) {
    if (settings == nullptr || results == nullptr) {
      return false;
    }

    uint8_t valueId = (uint8_t) valueType;
    if (!isValidSensorId(sensorId) || !isValidValueId(valueId)) {
      return false;
    }

    auto& sSensor = settings[sensorId];
    auto& rSensor = results[sensorId];

    float compensatedValue = value;
    if (sSensor.type == Type::HEATING_SETPOINT_TEMP || sSensor.type == Type::MANUAL) {
      rSensor.values[valueId] = compensatedValue;

    } else {
      if (valueType == ValueType::PRIMARY) {
        if (fabsf(sSensor.factor) > 0.001f) {
          compensatedValue *= sSensor.factor;
        }

        if (fabsf(sSensor.offset) > 0.001f) {
          compensatedValue += sSensor.offset;
        }

      } else if (valueType == ValueType::RSSI) {
        if (sSensor.type == Type::BLUETOOTH) {
          rSensor.signalQuality = Sensors::bluetoothRssiToQuality(value);
        }
      }

      if (sSensor.filtering && fabsf(rSensor.values[valueId]) >= 0.1f) {
        rSensor.values[valueId] += (compensatedValue - rSensor.values[valueId]) * sSensor.filteringFactor;

      } else {
        rSensor.values[valueId] = compensatedValue;
      }
    }

    if (updateActivityTime) {
      rSensor.activityTime = millis();
    }

    if (markConnected && !rSensor.connected) {
      rSensor.connected = true;

      Log.snoticeln(
        FPSTR(L_SENSORS), F("#%hhu '%s' new status: CONNECTED"),
        sensorId, sSensor.name
      );
    }

    Log.snoticeln(
      FPSTR(L_SENSORS), F("#%hhu '%s' new value %hhu: %.2f, compensated: %.2f, raw: %.2f"),
      sensorId, sSensor.name, valueId, rSensor.values[valueId], compensatedValue, value
    );

    return true;
  }

  static uint8_t setValueByType(Type type, float value, const ValueType valueType, const bool updateActivityTime = false, const bool markConnected = false) {
    if (settings == nullptr) {
      return 0;
    }

    uint8_t updated = 0;

    // read sensors data for current instance
    for (uint8_t sensorId = 0; sensorId <= getMaxSensorId(); sensorId++) {
      auto& sSensor = settings[sensorId];

      // only target & valid sensors
      if (!sSensor.enabled || sSensor.type != type) {
        continue;
      }

      if (setValueById(sensorId, value, valueType, updateActivityTime, markConnected)) {
        updated++;
      }
    }

    return updated;
  }

  static bool getConnectionStatusById(const uint8_t sensorId) {
    if (settings == nullptr || results == nullptr) {
      return false;
    }

    if (!isValidSensorId(sensorId)) {
      return false;
    }

    return results[sensorId].connected;
  }

  static bool setConnectionStatusById(const uint8_t sensorId, const bool status, const bool updateActivityTime = true) {
    if (settings == nullptr || results == nullptr) {
      return false;
    }

    if (!isValidSensorId(sensorId)) {
      return false;
    }

    auto& sSensor = settings[sensorId];
    auto& rSensor = results[sensorId];

    if (rSensor.connected != status) {
      Log.snoticeln(
        FPSTR(L_SENSORS), F("#%hhu '%s' new status: %s"),
        sensorId, sSensor.name, status ? F("CONNECTED") : F("DISCONNECTED")
      );

      rSensor.connected = status;
    }

    if (updateActivityTime) {
      rSensor.activityTime = millis();
    }

    return true;
  }

  static uint8_t setConnectionStatusByType(Type type, const bool status, const bool updateActivityTime = true) {
    if (settings == nullptr) {
      return 0;
    }

    uint8_t updated = 0;

    // read sensors data for current instance
    for (uint8_t sensorId = 0; sensorId <= getMaxSensorId(); sensorId++) {
      auto& sSensor = settings[sensorId];

      // only target & valid sensors
      if (!sSensor.enabled || sSensor.type != type) {
        continue;
      }

      if (setConnectionStatusById(sensorId, status, updateActivityTime)) {
        updated++;
      }
    }

    return updated;
  }

  static float getMeanValueByPurpose(Purpose purpose, const ValueType valueType, const AverageType avgType = AverageType::MEAN, const bool onlyConnected = true, const float defaultValue = NAN) {
    if (settings == nullptr || results == nullptr) {
      return defaultValue;
    }

    uint8_t valueId = (uint8_t) valueType;
    if (!isValidValueId(valueId)) {
      return defaultValue;
    }

    float value = 0.0f;
    uint8_t amount = 0;

    if (avgType == AverageType::MEAN) {
      float sum = 0.0f;
      for (uint8_t id = 0; id <= getMaxSensorId(); id++) {
        auto& sSensor = settings[id];
        auto& rSensor = results[id];

        if (sSensor.purpose == purpose && (!onlyConnected || rSensor.connected)) {
          sum += rSensor.values[valueId];
          amount++;
        }
      }

      value = amount == 1 ? sum : (sum / amount);

    } else if (avgType == AverageType::MINIMUM) {
      for (uint8_t id = 0; id <= getMaxSensorId(); id++) {
        auto& sSensor = settings[id];
        auto& rSensor = results[id];

        if (sSensor.purpose == purpose && (!onlyConnected || rSensor.connected)) {
          if (amount == 0 || rSensor.values[valueId] < value) {
            value = rSensor.values[valueId];
            amount++;
          }
        }
      }

    } else if (avgType == AverageType::MAXIMUM) {
      for (uint8_t id = 0; id <= getMaxSensorId(); id++) {
        auto& sSensor = settings[id];
        auto& rSensor = results[id];

        if (sSensor.purpose == purpose && (!onlyConnected || rSensor.connected)) {
          if (amount == 0 || rSensor.values[valueId] > value) {
            value = rSensor.values[valueId];
            amount++;
          }
        }
      }
    }

    return amount > 0 ? value : defaultValue;
  }

  static bool existsConnectedSensorsByPurpose(Purpose purpose) {
    if (settings == nullptr || results == nullptr) {
      return false;
    }

    for (uint8_t id = 0; id <= getMaxSensorId(); id++) {
      if (settings[id].purpose == purpose && results[id].connected) {
        return true;
      }
    }

    return false;
  }

  static String& cleanName(String& value, char space = ' ') {
    // only valid symbols
    for (uint8_t pos = 0; pos < value.length(); pos++) {
      char symbol = value.charAt(pos);

      // 0..9
      if (symbol >= 48 && symbol <= 57) {
        continue;
      }

      // A..Z
      if (symbol >= 65 && symbol <= 90) {
        continue;
      }

      // a..z
      if (symbol >= 97 && symbol <= 122) {
        continue;
      }

      // _-
      if (symbol == 95 || symbol == 45 || symbol == space) {
        continue;
      }

      value.setCharAt(pos, space);
    }

    value.trim();

    return value;
  }

  template <class T>
  static String cleanName(T value, char space = ' ') {
    String res = value;
    return cleanName(res, space);
  }

  template <class T>
  static String& makeObjectId(String& res, T value, char separator = '_') {
    res = value;
    cleanName(res);
    res.toLowerCase();
    res.replace(' ', separator);

    return res;
  }

  template <class T>
  static String makeObjectId(T value, char separator = '_') {
    String res;
    makeObjectId(res, value, separator);

    return res;
  }

  template <class TV, class TS>
  static String& makeObjectIdWithSuffix(String& res, TV value, TS suffix, char separator = '_') {
    res.clear();
    makeObjectId(res, value, separator);
    res += separator;
    res += suffix;

    return res;
  }

  template <class TV, class TS>
  static String makeObjectIdWithSuffix(TV value, TS suffix, char separator = '_') {
    String res;
    makeObjectIdWithSuffix(res, value, suffix, separator);
    return res;
  }

  template <class TV, class TP>
  static String& makeObjectIdWithPrefix(String& res, TV value, TP prefix, char separator = '_') {
    res = prefix;
    res += separator;
    res += makeObjectId(value, separator).c_str();

    return res;
  }

  template <class TV, class TP>
  static String makeObjectIdWithPrefix(TV value, TP prefix, char separator = '_') {
    String res;
    return makeObjectIdWithPrefix(res, value, prefix, separator);
  }

  static uint8_t bluetoothRssiToQuality(int rssi) {
    return constrain(map(rssi, -110, -50, 0, 100), 0, 100);;
  }
};

uint8_t Sensors::maxSensors = 0;
Sensors::Settings* Sensors::settings = nullptr;
Sensors::Result* Sensors::results = nullptr;
