// Copyright 2025 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at

//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/*
 * This example is an example code that will create a Matter Device which can be
 * commissioned and controlled from a Matter Environment APP.
 * Additionally the ESP32 will send debug messages indicating the Matter activity.
 * Turning DEBUG Level ON may be useful to following Matter Accessory and Controller messages.
 *
 * The example will create a Matter Occupancy Sensor Device.
 * The Occupancy Sensor will be simulated to toggle occupancy every 2 minutes.
 * When occupancy is detected, the sensor holds the "occupied" state for the HoldTime duration
 * (default: 30 seconds, configurable via Matter Controller). After HoldTime expires,
 * the sensor automatically switches to "unoccupied" state.
 *
 * The HoldTime attribute allows you to adjust how long the active status is retained
 * after the person leaves. The HoldTime can be changed by a Matter Controller, and the
 * onHoldTimeChange() callback is used to update the simulated sensor functionality.
 *
 * The HoldTime value is persisted to Preferences (NVS) and restored on reboot, so the
 * last configured HoldTime value is maintained across device restarts.
 *
 * The onboard button can be kept pressed for 5 seconds to decommission the Matter Node.
 * The example will also show the manual commissioning code and QR code to be used in the Matter environment.
 *
 */

// Matter Manager
#include <Arduino.h>
#include <Matter.h>
// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network
#if !CONFIG_ENABLE_CHIPOBLE
// if the device can be commissioned using BLE, WiFi is not used - save flash space
#include <WiFi.h>
// WiFi is manually set and started
const char *ssid = "your-ssid";          // Change this to your WiFi SSID
const char *password = "your-password";  // Change this to your WiFi password
#endif
#include <Preferences.h>

// HoldTime configuration constants
const uint16_t HOLD_TIME_MIN = 0;       // Minimum HoldTime in seconds
const uint16_t HOLD_TIME_MAX = 3600;    // Maximum HoldTime in seconds (1 hour)
const uint16_t HOLD_TIME_DEFAULT = 30;  // Default HoldTime in seconds

// List of Matter Endpoints for this Node
// Matter Occupancy Sensor Endpoint
MatterOccupancySensor OccupancySensor;

// Preferences to store HoldTime value across reboots
Preferences matterPref;
const char *holdTimePrefKey = "HoldTime";

// set your board USER BUTTON pin here - decommissioning only
const uint8_t buttonPin = BOOT_PIN;  // Set your pin here. Using BOOT Button.

// Button control
uint32_t button_time_stamp = 0;                // debouncing control
bool button_state = false;                     // false = released | true = pressed
const uint32_t decommissioningTimeout = 5000;  // keep the button pressed for 5s, or longer, to decommission

// Simulated Occupancy Sensor with HoldTime support
// When occupancy is detected, it holds the "occupied" state for HoldTime seconds
// After HoldTime expires, it automatically switches to "unoccupied"
//
// Behavior for different HoldTime vs detectionInterval relationships:
// - holdTime_ms < detectionInterval: State switches to unoccupied after HoldTime, then waits for next detection
// - holdTime_ms == detectionInterval: If detections keep coming, timer resets (continuous occupancy)
// - holdTime_ms > detectionInterval: If detections keep coming, timer resets (continuous occupancy)
//   If detections stop, HoldTime expires after the last detection
bool simulatedHWOccupancySensor() {
  static bool occupancyState = false;
  static uint32_t lastDetectionTime = 0;
  static uint32_t lastDetectionEvent = millis();
  const uint32_t detectionInterval = 120000;  // Simulate detection every 2 minutes

  // Get current HoldTime from the sensor (can be changed by Matter Controller)
  uint32_t holdTime_ms = OccupancySensor.getHoldTime() * 1000;  // Convert seconds to milliseconds

  // Check HoldTime expiration FIRST (before processing new detections)
  // This ensures HoldTime can expire even if a detection occurs in the same iteration
  if (occupancyState && (millis() - lastDetectionTime > holdTime_ms)) {
    occupancyState = false;
    // Reset detection interval counter so next detection can happen immediately
    // This makes the simulation more responsive after the room becomes unoccupied
    lastDetectionEvent = millis();
    Serial.println("HoldTime expired. Switching to unoccupied state.");
  }

  // Simulate periodic occupancy detection (e.g., motion detected)
  // Check this AFTER HoldTime expiration so new detections can immediately re-trigger occupancy
  if (millis() - lastDetectionEvent > detectionInterval) {
    // New detection event occurred
    lastDetectionEvent = millis();

    if (!occupancyState) {
      // Transition from unoccupied to occupied - start hold timer
      occupancyState = true;
      lastDetectionTime = millis();
      Serial.printf("Occupancy detected! Holding state for %u seconds (HoldTime)\n", OccupancySensor.getHoldTime());
    } else {
      // Already occupied - new detection extends the hold period by resetting the timer
      // This simulates continuous occupancy (person still present)
      lastDetectionTime = millis();
      Serial.printf("Occupancy still detected. Resetting hold timer to %u seconds (HoldTime)\n", OccupancySensor.getHoldTime());
    }
  }

  return occupancyState;
}

void setup() {
  // Initialize the USER BUTTON (Boot button) that will be used to decommission the Matter Node
  pinMode(buttonPin, INPUT_PULLUP);

  Serial.begin(115200);

// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network
#if !CONFIG_ENABLE_CHIPOBLE
  // Manually connect to WiFi
  WiFi.begin(ssid, password);
  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println();
#endif

  // Initialize Preferences and read stored HoldTime value
  matterPref.begin("MatterPrefs", false);
  uint16_t storedHoldTime = matterPref.getUShort(holdTimePrefKey, HOLD_TIME_DEFAULT);

  // Validate stored value is within limits
  if (storedHoldTime < HOLD_TIME_MIN || storedHoldTime > HOLD_TIME_MAX) {
    uint16_t invalidValue = storedHoldTime;
    storedHoldTime = HOLD_TIME_DEFAULT;
    Serial.printf("Invalid stored HoldTime (%u), using default: %u seconds\n", invalidValue, HOLD_TIME_DEFAULT);
  } else if (storedHoldTime != HOLD_TIME_DEFAULT) {
    Serial.printf("Restored HoldTime from Preferences: %u seconds\n", storedHoldTime);
  }

  // Register callback for HoldTime changes from Matter Controller
  OccupancySensor.onHoldTimeChange([](uint16_t holdTime_seconds) -> bool {
    Serial.printf("HoldTime changed to %u seconds by Matter Controller\n", holdTime_seconds);
    // Store the new HoldTime value to Preferences for persistence across reboots
    matterPref.putUShort(holdTimePrefKey, holdTime_seconds);
    // The callback can return false to reject the change, or true to accept it
    // In this case, we always accept the change and update the simulator
    return true;
  });

  // set initial occupancy sensor state as false and connected to a PIR sensor type (default)
  OccupancySensor.begin();

  // Matter beginning - Last step, after all EndPoints are initialized
  Matter.begin();

  // Set HoldTimeLimits after Matter.begin() (optional, but recommended for validation)
  if (!OccupancySensor.setHoldTimeLimits(HOLD_TIME_MIN, HOLD_TIME_MAX, HOLD_TIME_DEFAULT)) {
    Serial.println("Warning: Failed to set HoldTimeLimits");
  } else {
    Serial.printf("HoldTimeLimits set: Min=%u, Max=%u, Default=%u seconds\n", HOLD_TIME_MIN, HOLD_TIME_MAX, HOLD_TIME_DEFAULT);
  }

  // Set initial HoldTime (use stored value if valid, otherwise use default)
  // This must be done after Matter.begin() because setHoldTime() requires the Matter event loop
  if (!OccupancySensor.setHoldTime(storedHoldTime)) {
    Serial.printf("Warning: Failed to set HoldTime to %u seconds\n", storedHoldTime);
  } else {
    Serial.printf("HoldTime set to: %u seconds\n", storedHoldTime);
  }

  Serial.printf("Initial HoldTime: %u seconds\n", OccupancySensor.getHoldTime());

  // Check Matter Accessory Commissioning state, which may change during execution of loop()
  if (!Matter.isDeviceCommissioned()) {
    Serial.println("");
    Serial.println("Matter Node is not commissioned yet.");
    Serial.println("Initiate the device discovery in your Matter environment.");
    Serial.println("Commission it to your Matter hub with the manual pairing code or QR code");
    Serial.printf("Manual pairing code: %s\r\n", Matter.getManualPairingCode().c_str());
    Serial.printf("QR code URL: %s\r\n", Matter.getOnboardingQRCodeUrl().c_str());
    // waits for Matter Occupancy Sensor Commissioning.
    uint32_t timeCount = 0;
    while (!Matter.isDeviceCommissioned()) {
      delay(100);
      if ((timeCount++ % 50) == 0) {  // 50*100ms = 5 sec
        Serial.println("Matter Node not commissioned yet. Waiting for commissioning.");
      }
    }
    Serial.println("Matter Node is commissioned and connected to the network. Ready for use.");
  }
}

void loop() {
  // Check if the button has been pressed
  if (digitalRead(buttonPin) == LOW && !button_state) {
    // deals with button debouncing
    button_time_stamp = millis();  // record the time while the button is pressed.
    button_state = true;           // pressed.
  }

  if (button_state && digitalRead(buttonPin) == HIGH) {
    button_state = false;  // released
  }

  // Onboard User Button is kept pressed for longer than 5 seconds in order to decommission matter node
  uint32_t time_diff = millis() - button_time_stamp;
  if (button_state && time_diff > decommissioningTimeout) {
    Serial.println("Decommissioning Occupancy Sensor Matter Accessory. It shall be commissioned again.");
    Matter.decommission();
    button_time_stamp = millis();  // avoid running decommissining again, reboot takes a second or so
  }

  // Check Simulated Occupancy Sensor and set Matter Attribute
  OccupancySensor.setOccupancy(simulatedHWOccupancySensor());

  delay(50);
}
