#pragma once

#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include <cstring>

namespace esphome::globals {

template<typename T> class GlobalsComponent : public Component {
 public:
  using value_type = T;
  explicit GlobalsComponent() = default;
  explicit GlobalsComponent(T initial_value) : value_(initial_value) {}
  explicit GlobalsComponent(std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value) {
    memcpy(this->value_, initial_value.data(), sizeof(T));
  }

  T &value() { return this->value_; }
  void setup() override {}

 protected:
  T value_{};
};

template<typename T> class RestoringGlobalsComponent : public PollingComponent {
 public:
  using value_type = T;
  explicit RestoringGlobalsComponent() : PollingComponent(1000) {}
  explicit RestoringGlobalsComponent(T initial_value) : PollingComponent(1000), value_(initial_value) {}
  explicit RestoringGlobalsComponent(
      std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value)
      : PollingComponent(1000) {
    memcpy(this->value_, initial_value.data(), sizeof(T));
  }

  T &value() { return this->value_; }

  void setup() override {
    this->rtc_ = global_preferences->make_preference<T>(1944399030U ^ this->name_hash_);
    this->rtc_.load(&this->value_);
    memcpy(&this->last_checked_value_, &this->value_, sizeof(T));
  }

  float get_setup_priority() const override { return setup_priority::HARDWARE; }

  void update() override { store_value_(); }

  void on_shutdown() override { store_value_(); }

  void set_name_hash(uint32_t name_hash) { this->name_hash_ = name_hash; }

 protected:
  void store_value_() {
    int diff = memcmp(&this->value_, &this->last_checked_value_, sizeof(T));
    if (diff != 0) {
      this->rtc_.save(&this->value_);
      memcpy(&this->last_checked_value_, &this->value_, sizeof(T));
    }
  }

  T value_{};
  T last_checked_value_{};
  uint32_t name_hash_{};
  ESPPreferenceObject rtc_;
};

// Use with string or subclasses of strings
template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public PollingComponent {
 public:
  using value_type = T;
  explicit RestoringGlobalStringComponent() : PollingComponent(1000) {}
  explicit RestoringGlobalStringComponent(T initial_value) : PollingComponent(1000) { this->value_ = initial_value; }
  explicit RestoringGlobalStringComponent(
      std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value)
      : PollingComponent(1000) {
    memcpy(this->value_, initial_value.data(), sizeof(T));
  }

  T &value() { return this->value_; }

  void setup() override {
    char temp[SZ];
    this->rtc_ = global_preferences->make_preference<uint8_t[SZ]>(1944399030U ^ this->name_hash_);
    bool hasdata = this->rtc_.load(&temp);
    if (hasdata) {
      this->value_.assign(temp + 1, static_cast<uint8_t>(temp[0]));
    }
    this->last_checked_value_.assign(this->value_);
  }

  float get_setup_priority() const override { return setup_priority::HARDWARE; }

  void update() override { store_value_(); }

  void on_shutdown() override { store_value_(); }

  void set_name_hash(uint32_t name_hash) { this->name_hash_ = name_hash; }

 protected:
  void store_value_() {
    int diff = this->value_.compare(this->last_checked_value_);
    if (diff != 0) {
      // Make it into a length prefixed thing
      unsigned char temp[SZ];

      // If string is bigger than the allocation, do not save it.
      int size = this->value_.size();
      // Less than, not less than or equal, SZ includes the length byte.
      if (size < SZ) {
        memcpy(temp + 1, this->value_.c_str(), size);
        // SZ should be pre checked at the schema level, it can't go past the char range.
        temp[0] = ((unsigned char) size);
        this->rtc_.save(&temp);
      }
      // Always update last_checked_value_ to match current value, even for oversized strings.
      // This prevents redundant size checks on every loop iteration when a string remains oversized.
      // Without this, the diff != 0 check would pass repeatedly for the same oversized string,
      // wasting CPU cycles on size comparisons.
      this->last_checked_value_.assign(this->value_);
    }
  }

  T value_{};
  T last_checked_value_{};
  uint32_t name_hash_{};
  ESPPreferenceObject rtc_;
};

template<class C, typename... Ts> class GlobalVarSetAction : public Action<Ts...> {
 public:
  explicit GlobalVarSetAction(C *parent) : parent_(parent) {}

  using T = typename C::value_type;

  TEMPLATABLE_VALUE(T, value);

  void play(const Ts &...x) override { this->parent_->value() = this->value_.value(x...); }

 protected:
  C *parent_;
};

template<typename T> T &id(GlobalsComponent<T> *value) { return value->value(); }
template<typename T> T &id(RestoringGlobalsComponent<T> *value) { return value->value(); }
template<typename T, uint8_t SZ> T &id(RestoringGlobalStringComponent<T, SZ> *value) { return value->value(); }

}  // namespace esphome::globals
