#pragma once

#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
#include "esphome/core/string_ref.h"
#include "climate_mode.h"
#include "climate_traits.h"

namespace esphome::climate {

#define LOG_CLIMATE(prefix, type, obj) \
  if ((obj) != nullptr) { \
    ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
  }

class Climate;

/** This class is used to encode all control actions on a climate device.
 *
 * It is supposed to be used by all code that wishes to control a climate device (mqtt, api, lambda etc).
 * Create an instance of this class by calling `id(climate_device).make_call();`. Then set all attributes
 * with the `set_x` methods. Finally, to apply the changes call `.perform();`.
 *
 * The integration that implements the climate device receives this instance with the `control` method.
 * It should check all the properties it implements and apply them as needed. It should do so by
 * getting all properties it controls with the getter methods in this class. If the optional value is
 * set (check with `.has_value()`) that means the user wants to control this property. Get the value
 * of the optional with the star operator (`*call.get_mode()`) and apply it.
 */
class ClimateCall {
 public:
  explicit ClimateCall(Climate *parent) : parent_(parent) {}
  friend struct ClimateDeviceRestoreState;

  /// Set the mode of the climate device.
  ClimateCall &set_mode(ClimateMode mode);
  /// Set the mode of the climate device.
  ClimateCall &set_mode(optional<ClimateMode> mode);
  /// Set the mode of the climate device based on a string.
  ClimateCall &set_mode(const std::string &mode);
  /// Set the target temperature of the climate device.
  ClimateCall &set_target_temperature(float target_temperature);
  /// Set the target temperature of the climate device.
  ClimateCall &set_target_temperature(optional<float> target_temperature);
  /** Set the low point target temperature of the climate device
   *
   * For climate devices with two point target temperature control
   */
  ClimateCall &set_target_temperature_low(float target_temperature_low);
  /** Set the low point target temperature of the climate device
   *
   * For climate devices with two point target temperature control
   */
  ClimateCall &set_target_temperature_low(optional<float> target_temperature_low);
  /** Set the high point target temperature of the climate device
   *
   * For climate devices with two point target temperature control
   */
  ClimateCall &set_target_temperature_high(float target_temperature_high);
  /** Set the high point target temperature of the climate device
   *
   * For climate devices with two point target temperature control
   */
  ClimateCall &set_target_temperature_high(optional<float> target_temperature_high);
  /// Set the target humidity of the climate device.
  ClimateCall &set_target_humidity(float target_humidity);
  /// Set the target humidity of the climate device.
  ClimateCall &set_target_humidity(optional<float> target_humidity);
  /// Set the fan mode of the climate device.
  ClimateCall &set_fan_mode(ClimateFanMode fan_mode);
  /// Set the fan mode of the climate device.
  ClimateCall &set_fan_mode(optional<ClimateFanMode> fan_mode);
  /// Set the fan mode of the climate device based on a string.
  ClimateCall &set_fan_mode(const std::string &fan_mode);
  /// Set the fan mode of the climate device based on a string.
  ClimateCall &set_fan_mode(optional<std::string> fan_mode);
  /// Set the custom fan mode of the climate device.
  ClimateCall &set_fan_mode(const char *custom_fan_mode);
  /// Set the custom fan mode of the climate device (zero-copy API path).
  ClimateCall &set_fan_mode(const char *custom_fan_mode, size_t len);
  /// Set the swing mode of the climate device.
  ClimateCall &set_swing_mode(ClimateSwingMode swing_mode);
  /// Set the swing mode of the climate device.
  ClimateCall &set_swing_mode(optional<ClimateSwingMode> swing_mode);
  /// Set the swing mode of the climate device based on a string.
  ClimateCall &set_swing_mode(const std::string &swing_mode);
  /// Set the preset of the climate device.
  ClimateCall &set_preset(ClimatePreset preset);
  /// Set the preset of the climate device.
  ClimateCall &set_preset(optional<ClimatePreset> preset);
  /// Set the preset of the climate device based on a string.
  ClimateCall &set_preset(const std::string &preset);
  /// Set the preset of the climate device based on a string.
  ClimateCall &set_preset(optional<std::string> preset);
  /// Set the custom preset of the climate device.
  ClimateCall &set_preset(const char *custom_preset);
  /// Set the custom preset of the climate device (zero-copy API path).
  ClimateCall &set_preset(const char *custom_preset, size_t len);

  void perform();

  const optional<float> &get_target_temperature() const;
  const optional<float> &get_target_temperature_low() const;
  const optional<float> &get_target_temperature_high() const;
  const optional<float> &get_target_humidity() const;

  const optional<ClimateMode> &get_mode() const;
  const optional<ClimateFanMode> &get_fan_mode() const;
  const optional<ClimateSwingMode> &get_swing_mode() const;
  const optional<ClimatePreset> &get_preset() const;
  StringRef get_custom_fan_mode() const { return StringRef::from_maybe_nullptr(this->custom_fan_mode_); }
  StringRef get_custom_preset() const { return StringRef::from_maybe_nullptr(this->custom_preset_); }
  bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; }
  bool has_custom_preset() const { return this->custom_preset_ != nullptr; }

 protected:
  void validate_();

  Climate *const parent_;
  optional<float> target_temperature_;
  optional<float> target_temperature_low_;
  optional<float> target_temperature_high_;
  optional<float> target_humidity_;
  optional<ClimateMode> mode_;
  optional<ClimateFanMode> fan_mode_;
  optional<ClimateSwingMode> swing_mode_;
  optional<ClimatePreset> preset_;

 private:
  const char *custom_fan_mode_{nullptr};
  const char *custom_preset_{nullptr};
};

/// Struct used to save the state of the climate device in restore memory.
/// Make sure to update RESTORE_STATE_VERSION when changing the struct entries.
struct ClimateDeviceRestoreState {
  ClimateMode mode;
  bool uses_custom_fan_mode{false};
  union {
    ClimateFanMode fan_mode;
    uint8_t custom_fan_mode;
  };
  bool uses_custom_preset{false};
  union {
    ClimatePreset preset;
    uint8_t custom_preset;
  };
  ClimateSwingMode swing_mode;
  union {
    float target_temperature;
    struct {
      float target_temperature_low;
      float target_temperature_high;
    };
  } __attribute__((packed));
  float target_humidity;

  /// Convert this struct to a climate call that can be performed.
  ClimateCall to_call(Climate *climate);
  /// Apply these settings to the climate device.
  void apply(Climate *climate);
} __attribute__((packed));

/**
 * ClimateDevice - This is the base class for all climate integrations. Each integration
 * needs to extend this class and implement two functions:
 *
 *  - get_traits() - return the static traits of the climate device
 *  - control(ClimateDeviceCall call) - Apply the given changes from call.
 *
 * To write data to the frontend, the integration must first set the properties using
 * this->property = value; (for example this->current_temperature = 42.0;); then the integration
 * must call this->publish_state(); to send the entire state to the frontend.
 *
 * The entire state of the climate device is encoded in public properties of the base class (current_temperature,
 * mode etc). These are read-only for the user and rw for integrations. The reason these are public
 * is for simple access to them from lambdas `if (id(my_climate).mode == climate::CLIMATE_MODE_HEAT_COOL) ...`
 */
class Climate : public EntityBase {
 public:
  Climate() {}

  /** Add a callback for the climate device state, each time the state of the climate device is updated
   * (using publish_state), this callback will be called.
   *
   * @param callback The callback to call.
   */
  void add_on_state_callback(std::function<void(Climate &)> &&callback);

  /**
   * Add a callback for the climate device configuration; each time the configuration parameters of a climate device
   * is updated (using perform() of a ClimateCall), this callback will be called, before any on_state callback.
   *
   * @param callback The callback to call.
   */
  void add_on_control_callback(std::function<void(ClimateCall &)> &&callback);

  /** Make a climate device control call, this is used to control the climate device, see the ClimateCall description
   * for more info.
   * @return A new ClimateCall instance targeting this climate device.
   */
  ClimateCall make_call();

  /** Publish the state of the climate device, to be called from integrations.
   *
   * This will schedule the climate device to publish its state to all listeners and save the current state
   * to recover memory.
   */
  void publish_state();

  /** Get the traits of this climate device with all overrides applied.
   *
   * Traits are static data that encode the capabilities and static data for a climate device such as supported
   * modes, temperature range etc.
   */
  ClimateTraits get_traits();

#ifdef USE_CLIMATE_VISUAL_OVERRIDES
  void set_visual_min_temperature_override(float visual_min_temperature_override);
  void set_visual_max_temperature_override(float visual_max_temperature_override);
  void set_visual_temperature_step_override(float target, float current);
  void set_visual_min_humidity_override(float visual_min_humidity_override);
  void set_visual_max_humidity_override(float visual_max_humidity_override);
#endif

  /// Check if a custom fan mode is currently active.
  bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; }

  /// Check if a custom preset is currently active.
  bool has_custom_preset() const { return this->custom_preset_ != nullptr; }

  /// The current temperature of the climate device, as reported from the integration.
  float current_temperature{NAN};

  /// The current humidity of the climate device, as reported from the integration.
  float current_humidity{NAN};

  union {
    /// The target temperature of the climate device.
    float target_temperature;
    struct {
      /// The minimum target temperature of the climate device, for climate devices with split target temperature.
      float target_temperature_low{NAN};
      /// The maximum target temperature of the climate device, for climate devices with split target temperature.
      float target_temperature_high{NAN};
    };
  };

  /// The target humidity of the climate device.
  float target_humidity;

  /// The active fan mode of the climate device.
  optional<ClimateFanMode> fan_mode;

  /// The active preset of the climate device.
  optional<ClimatePreset> preset;

  /// The active mode of the climate device.
  ClimateMode mode{CLIMATE_MODE_OFF};

  /// The active state of the climate device.
  ClimateAction action{CLIMATE_ACTION_OFF};

  /// The active swing mode of the climate device.
  ClimateSwingMode swing_mode{CLIMATE_SWING_OFF};

  /// Get the active custom fan mode (read-only access). Returns StringRef.
  StringRef get_custom_fan_mode() const { return StringRef::from_maybe_nullptr(this->custom_fan_mode_); }

  /// Get the active custom preset (read-only access). Returns StringRef.
  StringRef get_custom_preset() const { return StringRef::from_maybe_nullptr(this->custom_preset_); }

 protected:
  friend ClimateCall;
  friend struct ClimateDeviceRestoreState;

  /// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed.
  bool set_fan_mode_(ClimateFanMode mode);

  /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed.
  bool set_custom_fan_mode_(const char *mode) { return this->set_custom_fan_mode_(mode, strlen(mode)); }
  bool set_custom_fan_mode_(const char *mode, size_t len);
  bool set_custom_fan_mode_(StringRef mode) { return this->set_custom_fan_mode_(mode.c_str(), mode.size()); }
  /// Clear custom fan mode.
  void clear_custom_fan_mode_();

  /// Set preset. Reset custom preset. Return true if preset has been changed.
  bool set_preset_(ClimatePreset preset);

  /// Set custom preset. Reset primary preset. Return true if preset has been changed.
  bool set_custom_preset_(const char *preset) { return this->set_custom_preset_(preset, strlen(preset)); }
  bool set_custom_preset_(const char *preset, size_t len);
  bool set_custom_preset_(StringRef preset) { return this->set_custom_preset_(preset.c_str(), preset.size()); }
  /// Clear custom preset.
  void clear_custom_preset_();

  /// Find and return the matching custom fan mode pointer from traits, or nullptr if not found.
  const char *find_custom_fan_mode_(const char *custom_fan_mode);
  const char *find_custom_fan_mode_(const char *custom_fan_mode, size_t len);

  /// Find and return the matching custom preset pointer from traits, or nullptr if not found.
  const char *find_custom_preset_(const char *custom_preset);
  const char *find_custom_preset_(const char *custom_preset, size_t len);

  /** Get the default traits of this climate device.
   *
   * Traits are static data that encode the capabilities and static data for a climate device such as supported
   * modes, temperature range etc. Each integration must implement this method and the return value must
   * be constant during all of execution time.
   */
  virtual ClimateTraits traits() = 0;

  /** Control the climate device, this is a virtual method that each climate integration must implement.
   *
   * See more info in ClimateCall. The integration should check all of its values in this method and
   * set them accordingly. At the end of the call, the integration must call `publish_state()` to
   * notify the frontend of a changed state.
   *
   * @param call The ClimateCall instance encoding all attribute changes.
   */
  virtual void control(const ClimateCall &call) = 0;
  /// Restore the state of the climate device, call this from your setup() method.
  optional<ClimateDeviceRestoreState> restore_state_();
  /** Internal method to save the state of the climate device to recover memory. This is automatically
   * called from publish_state()
   */
  void save_state_();

  void dump_traits_(const char *tag);

  LazyCallbackManager<void(Climate &)> state_callback_{};
  LazyCallbackManager<void(ClimateCall &)> control_callback_{};
  ESPPreferenceObject rtc_;
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
  float visual_min_temperature_override_{NAN};
  float visual_max_temperature_override_{NAN};
  float visual_target_temperature_step_override_{NAN};
  float visual_current_temperature_step_override_{NAN};
  float visual_min_humidity_override_{NAN};
  float visual_max_humidity_override_{NAN};
#endif

 private:
  /** The active custom fan mode (private - enforces use of safe setters).
   *
   * Points to an entry in traits.supported_custom_fan_modes_ or nullptr.
   * Use get_custom_fan_mode() to read, set_custom_fan_mode_() to modify.
   */
  const char *custom_fan_mode_{nullptr};

  /** The active custom preset (private - enforces use of safe setters).
   *
   * Points to an entry in traits.supported_custom_presets_ or nullptr.
   * Use get_custom_preset() to read, set_custom_preset_() to modify.
   */
  const char *custom_preset_{nullptr};
};

}  // namespace esphome::climate
