// p7_coffre — P7 coffre unlocking actuator driver.
//
// All real implementation is inside #if CONFIG_ZACUS_P7_COFFRE_ENABLE so the
// object compiles to pure stubs when the flag is off (default).  No #ifdef
// leaks into callers — they include p7_coffre.h and call the three functions
// unconditionally.

#include "p7_coffre.h"

#include "sdkconfig.h"
#include "esp_log.h"
#include "esp_err.h"

static const char *TAG = "p7_coffre";

// ─── Servo duty helper ──────────────────────────────────────────────────────
//
// Maps an angle in degrees [0, 180] to a LEDC duty value.
//
// Timer parameters (compile-time constants):
//   Period       : 20 ms  (50 Hz)
//   Pulse range  : 500 µs (0°) … 2500 µs (180°)  — standard SG90 / MG90S
//   Resolution   : 13 bits → 8192 counts per 20 ms
//
//   counts_per_us = 8192 / 20000 = 0.4096
//   duty(0°)   = 500  * 0.4096 =  205
//   duty(90°)  = 1500 * 0.4096 =  614
//   duty(180°) = 2500 * 0.4096 = 1024

#if CONFIG_ZACUS_P7_COFFRE_ENABLE

#include "driver/ledc.h"
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

// LEDC resources — chosen to avoid conflicts with existing assignments:
//   TIMER_0 / CHANNEL_0  =  camera XCLK  (qr_puzzle)
//   TIMER_1 / CHANNEL_1  =  display backlight  (display_ui)
//   TIMER_2 / CHANNEL_2  =  P7 servo (this file)
#define P7_LEDC_TIMER    LEDC_TIMER_2
#define P7_LEDC_CHANNEL  LEDC_CHANNEL_2
#define P7_LEDC_MODE     LEDC_LOW_SPEED_MODE
#define P7_LEDC_RES      LEDC_TIMER_13_BIT   // 8192 counts per period
#define P7_LEDC_FREQ_HZ  50u                 // 20 ms period

// counts = pulse_us * (2^13 / 20000)
// Use integer arithmetic: counts = pulse_us * 8192 / 20000
#define P7_SERVO_COUNTS(pulse_us)  ((uint32_t)(pulse_us) * 8192u / 20000u)

// Pulse width at each extreme (SG90-compatible).
#define P7_SERVO_PULSE_MIN_US  500u    // 0°
#define P7_SERVO_PULSE_MAX_US  2500u   // 180°

// Convert angle [0, 180] to duty count.
static uint32_t angle_to_duty(int deg) {
    if (deg < 0)   deg = 0;
    if (deg > 180) deg = 180;
    uint32_t pulse_us = P7_SERVO_PULSE_MIN_US +
                        (uint32_t) deg *
                        (P7_SERVO_PULSE_MAX_US - P7_SERVO_PULSE_MIN_US) / 180u;
    return P7_SERVO_COUNTS(pulse_us);
}

static bool s_initialised = false;

// ─── Servo path ─────────────────────────────────────────────────────────────

#if CONFIG_ZACUS_P7_ACTUATOR_SERVO

static esp_err_t servo_set_angle(int deg) {
    uint32_t duty = angle_to_duty(deg);
    esp_err_t err = ledc_set_duty(P7_LEDC_MODE, P7_LEDC_CHANNEL, duty);
    if (err != ESP_OK) return err;
    return ledc_update_duty(P7_LEDC_MODE, P7_LEDC_CHANNEL);
}

esp_err_t p7_coffre_init(void) {
    if (s_initialised) return ESP_OK;

    // Configure the 50 Hz timer.
    ledc_timer_config_t timer_cfg = {
        .speed_mode      = P7_LEDC_MODE,
        .duty_resolution = P7_LEDC_RES,
        .timer_num       = P7_LEDC_TIMER,
        .freq_hz         = P7_LEDC_FREQ_HZ,
        .clk_cfg         = LEDC_AUTO_CLK,
    };
    esp_err_t err = ledc_timer_config(&timer_cfg);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "ledc_timer_config: %s", esp_err_to_name(err));
        return err;
    }

    // Bind the channel to the GPIO at the lock angle.
    uint32_t lock_duty = angle_to_duty(CONFIG_ZACUS_P7_SERVO_LOCK_DEG);
    ledc_channel_config_t ch_cfg = {
        .gpio_num   = CONFIG_ZACUS_P7_GPIO,
        .speed_mode = P7_LEDC_MODE,
        .channel    = P7_LEDC_CHANNEL,
        .intr_type  = LEDC_INTR_DISABLE,
        .timer_sel  = P7_LEDC_TIMER,
        .duty       = lock_duty,
        .hpoint     = 0,
    };
    err = ledc_channel_config(&ch_cfg);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "ledc_channel_config: %s", esp_err_to_name(err));
        return err;
    }

    s_initialised = true;
    ESP_LOGI(TAG, "servo init OK (GPIO %d, lock=%d°, unlock=%d°, "
                  "LEDC TIMER_%d/CH_%d 50 Hz 13-bit)",
             CONFIG_ZACUS_P7_GPIO,
             CONFIG_ZACUS_P7_SERVO_LOCK_DEG,
             CONFIG_ZACUS_P7_SERVO_UNLOCK_DEG,
             (int) P7_LEDC_TIMER, (int) P7_LEDC_CHANNEL);
    return ESP_OK;
}

void p7_coffre_unlock(void) {
    if (!s_initialised) {
        ESP_LOGW(TAG, "unlock called before init — ignored");
        return;
    }
    esp_err_t err = servo_set_angle(CONFIG_ZACUS_P7_SERVO_UNLOCK_DEG);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "servo_set_angle(%d): %s",
                 CONFIG_ZACUS_P7_SERVO_UNLOCK_DEG, esp_err_to_name(err));
    } else {
        ESP_LOGI(TAG, "coffre UNLOCKED (servo -> %d deg)",
                 CONFIG_ZACUS_P7_SERVO_UNLOCK_DEG);
    }
}

void p7_coffre_lock(void) {
    if (!s_initialised) return;
    esp_err_t err = servo_set_angle(CONFIG_ZACUS_P7_SERVO_LOCK_DEG);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "servo_set_angle(%d): %s",
                 CONFIG_ZACUS_P7_SERVO_LOCK_DEG, esp_err_to_name(err));
    } else {
        ESP_LOGI(TAG, "coffre re-LOCKED (servo -> %d deg)",
                 CONFIG_ZACUS_P7_SERVO_LOCK_DEG);
    }
}

#endif // CONFIG_ZACUS_P7_ACTUATOR_SERVO

// ─── Relay path ─────────────────────────────────────────────────────────────

#if CONFIG_ZACUS_P7_ACTUATOR_RELAY

// Helper: set the relay coil to active (energised) or rest.
// Handles active-high vs active-low inversion.
static void relay_set(bool active) {
    int level;
#if CONFIG_ZACUS_P7_RELAY_ACTIVE_HIGH
    level = active ? 1 : 0;
#else
    level = active ? 0 : 1;
#endif
    gpio_set_level(CONFIG_ZACUS_P7_GPIO, level);
}

esp_err_t p7_coffre_init(void) {
    if (s_initialised) return ESP_OK;

    gpio_config_t cfg = {
        .pin_bit_mask = (1ULL << CONFIG_ZACUS_P7_GPIO),
        .mode         = GPIO_MODE_OUTPUT,
        .pull_up_en   = GPIO_PULLUP_DISABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type    = GPIO_INTR_DISABLE,
    };
    esp_err_t err = gpio_config(&cfg);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "gpio_config(GPIO %d): %s",
                 CONFIG_ZACUS_P7_GPIO, esp_err_to_name(err));
        return err;
    }

    // Start in rest (de-energised) state.
    relay_set(false);
    s_initialised = true;
    ESP_LOGI(TAG, "relay init OK (GPIO %d, active-%s, pulse=%d ms)",
             CONFIG_ZACUS_P7_GPIO,
             CONFIG_ZACUS_P7_RELAY_ACTIVE_HIGH ? "HIGH" : "LOW",
             CONFIG_ZACUS_P7_RELAY_PULSE_MS);
    return ESP_OK;
}

void p7_coffre_unlock(void) {
    if (!s_initialised) {
        ESP_LOGW(TAG, "unlock called before init — ignored");
        return;
    }
    relay_set(true);
    ESP_LOGI(TAG, "coffre UNLOCKED (relay energised)");

#if CONFIG_ZACUS_P7_RELAY_PULSE_MS > 0
    vTaskDelay(pdMS_TO_TICKS(CONFIG_ZACUS_P7_RELAY_PULSE_MS));
    relay_set(false);
    ESP_LOGI(TAG, "coffre relay de-energised after %d ms pulse",
             CONFIG_ZACUS_P7_RELAY_PULSE_MS);
#endif
}

void p7_coffre_lock(void) {
    if (!s_initialised) return;
    relay_set(false);
    ESP_LOGI(TAG, "coffre relay de-energised (lock/re-arm)");
}

#endif // CONFIG_ZACUS_P7_ACTUATOR_RELAY

#else // CONFIG_ZACUS_P7_COFFRE_ENABLE not set — emit stubs only

esp_err_t p7_coffre_init(void) {
    return ESP_OK;
}

void p7_coffre_unlock(void) {
    /* stub — CONFIG_ZACUS_P7_COFFRE_ENABLE=n */
}

void p7_coffre_lock(void) {
    /* stub — CONFIG_ZACUS_P7_COFFRE_ENABLE=n */
}

#endif // CONFIG_ZACUS_P7_COFFRE_ENABLE
