#pragma once

#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"

namespace esphome {
namespace tsl2591 {

/** Enum listing all conversion/integration time settings for the TSL2591.
 *
 * Specific values of the enum constants are register values taken from the TSL2591 datasheet.
 * Longer times mean more accurate results, but will take more energy/more time.
 */
enum TSL2591IntegrationTime {
  TSL2591_INTEGRATION_TIME_100MS = 0b000,
  TSL2591_INTEGRATION_TIME_200MS = 0b001,
  TSL2591_INTEGRATION_TIME_300MS = 0b010,
  TSL2591_INTEGRATION_TIME_400MS = 0b011,
  TSL2591_INTEGRATION_TIME_500MS = 0b100,
  TSL2591_INTEGRATION_TIME_600MS = 0b101,
};

/** Enum listing all gain settings for the TSL2591.
 *
 * Specific values of the enum constants are register values taken from the TSL2591 datasheet.
 * Higher values are better for low light situations, but can increase noise.
 */
enum TSL2591Gain {
  TSL2591_GAIN_LOW = 0b00 << 4,   // 1x
  TSL2591_GAIN_MED = 0b01 << 4,   // 25x
  TSL2591_GAIN_HIGH = 0b10 << 4,  // 400x
  TSL2591_GAIN_MAX = 0b11 << 4,   // 9500x
};

/** Enum listing sensor channels.
 *
 * They identify the type of light to report.
 */
enum TSL2591SensorChannel {
  TSL2591_SENSOR_CHANNEL_VISIBLE,
  TSL2591_SENSOR_CHANNEL_INFRARED,
  TSL2591_SENSOR_CHANNEL_FULL_SPECTRUM,
};

/// This class includes support for the TSL2591 i2c ambient light
/// sensor.  The device has two distinct sensors. One is for visible
/// light plus infrared light, and the other is for infrared
/// light. They are reported as separate sensors, and the difference
/// between the values is reported as a third sensor as a convenience
/// for visible light only.
class TSL2591Component : public PollingComponent, public i2c::I2CDevice {
 public:
  /** Set device integration time and gain.
   *
   * These are set as a single I2C transaction, so you must supply values
   * for both.
   *
   * Longer integration times provides more accurate values, but also
   * means more power consumption. Higher gain values are useful for
   * lower light intensities but are also subject to more noise. The
   * device might use a slightly different gain multiplier than those
   * indicated; see the datasheet for details.
   *
   * Possible values for integration_time (from enum
   * TSL2591IntegrationTime) are:
   *
   *  - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_100MS`
   *  - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_200MS`
   *  - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_300MS`
   *  - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_400MS`
   *  - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_500MS`
   *  - `esphome::tsl2591::TSL2591_INTEGRATION_TIME_600MS`
   *
   * Possible values for gain (from enum TSL2591Gain) are:
   *
   *  - `esphome::tsl2591::TSL2591_GAIN_LOW`  (1x)
   *  - `esphome::tsl2591::TSL2591_GAIN_MED`  (25x)
   *  - `esphome::tsl2591::TSL2591_GAIN_HIGH` (400x)
   *  - `esphome::tsl2591::TSL2591_GAIN_MAX`  (9500x)
   *
   * @param integration_time The new integration time.
   * @param gain The new gain.
   */
  void set_integration_time_and_gain(TSL2591IntegrationTime integration_time, TSL2591Gain gain);

  /** Should the device be powered down between readings?
   *
   * The disadvantage of powering down the device between readings
   * is that you have to wait for the ADC to go through an
   * integration cycle before a reliable reading is available.
   * This happens during ESPHome's update loop, so waiting slows
   * down the entire ESP device. You should only enable this if
   * you need to minimize power consumption and you can tolerate
   * that delay. Otherwise, keep the default of disabling
   * power save mode.
   *
   * @param enable Enable or disable power save mode.
   */
  void set_power_save_mode(bool enable);

  /** Sets the name for this instance of the device.
   *
   * @param name The user-friendly name.
   */
  void set_name(const char *name);

  /** Sets the device and glass attenuation factors.
   *
   * The lux equation, used to calculate the lux from the ADC readings,
   * involves a scaling coefficient that is the product of a device
   * factor (specific to the type of device being used) and a glass
   * attenuation factor (specific to whatever glass or plastic cover
   * is installed in front of the light sensors.
   *
   * AMS does not publish the device factor for the TSL2591. In the
   * datasheet for the earlier TSL2571 and in application notes, they
   * use the value 53, so we use that as the default.
   *
   * The glass attenuation factor depends on factors external to the
   * TSL2591 and is best obtained through experimental measurements.
   * The Adafruit TSL2591 library use a value of ~7.7, which we use as
   * a default. Waveshare uses a value of ~14.4. Presumably, those
   * factors are appropriate to the breakout boards from those vendors,
   * but we have not verified that.
   *
   * @param device_factor The device factor.
   * @param glass_attenuation_factor The glass attenuation factor.
   */
  void set_device_and_glass_attenuation_factors(float device_factor, float glass_attenuation_factor);

  /** Calculates and returns a lux value based on the ADC readings.
   *
   * @param full_spectrum The ADC reading for the full spectrum sensor.
   * @param infrared The ADC reading for the infrared sensor.
   */
  float get_calculated_lux(uint16_t full_spectrum, uint16_t infrared);

  /** Get the combined illuminance value.
   *
   * This is encoded into a 32 bit value. The high 16 bits are the value of the
   * infrared sensor. The low 16 bits are the sum of the combined sensor values.
   *
   * If power saving mode is enabled, there can be a delay (up to the value of the integration
   * time) while waiting for the device ADCs to signal that values are valid.
   */
  uint32_t get_combined_illuminance();

  /** Get an individual sensor channel reading.
   *
   * This gets an individual light sensor reading. Since it goes through
   * the entire component read cycle to get one value, it's not optimal if
   * you want to get all possible channel values. If you want that, first
   * call `get_combined_illuminance()` and pass that value to the companion
   * method with a different signature.
   *
   * If power saving mode is enabled, there can be a delay (up to the value of the integration
   * time) while waiting for the device ADCs to signal that values are valid.
   *
   * @param channel The sensor channel of interest.
   */
  uint16_t get_illuminance(TSL2591SensorChannel channel);

  /** Get an individual sensor channel reading from combined illuminance.
   *
   * This gets an individual light sensor reading from a combined illuminance
   * value, which you would obtain from calling `getCombinedIlluminance()`.
   * This method does not communicate with the sensor at all. It's strictly
   * local calculations, so it is efficient if you call it multiple times.
   *
   * @param channel The sensor channel of interest.
   * @param combined_illuminance The previously obtained combined illuminance value.
   */
  uint16_t get_illuminance(TSL2591SensorChannel channel, uint32_t combined_illuminance);

  /** Are the device ADC values valid?
   *
   * Useful for scripting. This should be checked before calling update().
   * It asks the TSL2591 if the ADC has completed an integration cycle
   * and has reliable values in the device registers. If you call update()
   * before the ADC values are valid, you may cause a general delay in
   * the ESPHome update loop.
   *
   * It should take no more than the configured integration time for
   * the ADC values to become valid after the TSL2591 device is enabled.
   */
  bool is_adc_valid();

  /** Powers on the TSL2591 device and enables its sensors.
   *
   * You only need to call this if you have disabled the device.
   * The device starts enabled in ESPHome unless power save mode is enabled.
   */
  void enable();
  /** Powers off the TSL2591 device.
   *
   * You can call this from an ESPHome script if you are explicitly
   * controlling TSL2591 power consumption.
   * The device starts enabled in ESPHome unless power save mode is enabled.
   */
  void disable();

  // ========== INTERNAL METHODS ==========
  // (In most use cases you won't need these. They're for ESPHome integration use.)
  /** Used by ESPHome framework. */
  void set_full_spectrum_sensor(sensor::Sensor *full_spectrum_sensor);
  /** Used by ESPHome framework. */
  void set_infrared_sensor(sensor::Sensor *infrared_sensor);
  /** Used by ESPHome framework. */
  void set_visible_sensor(sensor::Sensor *visible_sensor);
  /** Used by ESPHome framework. */
  void set_calculated_lux_sensor(sensor::Sensor *calculated_lux_sensor);
  /** Used by ESPHome framework. Does NOT actually set the value on the device. */
  void set_integration_time(TSL2591IntegrationTime integration_time);
  /** Used by ESPHome framework. Does NOT actually set the value on the device. */
  void set_gain(TSL2591Gain gain);
  /** Used by ESPHome framework. */
  void setup() override;
  /** Used by ESPHome framework. */
  void dump_config() override;
  /** Used by ESPHome framework. */
  void update() override;
  /** Used by ESPHome framework. */
  float get_setup_priority() const override;

 protected:
  const char *name_;  // TODO: extend esphome::Nameable
  sensor::Sensor *full_spectrum_sensor_;
  sensor::Sensor *infrared_sensor_;
  sensor::Sensor *visible_sensor_;
  sensor::Sensor *calculated_lux_sensor_;
  TSL2591IntegrationTime integration_time_;
  TSL2591Gain gain_;
  bool power_save_mode_enabled_;
  float device_factor_;
  float glass_attenuation_factor_;
  uint64_t interval_start_;
  uint64_t interval_timeout_;
  void disable_if_power_saving_();
  void process_update_();
  void interval_function_for_update_();
};

}  // namespace tsl2591
}  // namespace esphome
