#include "stm32g0xx.h"

#include "vacuum_board.h"
#include "vacuum_telemetry.h"
#include "vacuum_output_lock.h"

#include <stddef.h>

#define FAN_POWER_EN_PIN 12u
#define FAN_POWER_REMAPPED_PIN 10u
#define FAN_PWM_PIN 8u
#define FAN_TACH_PIN 1u
#define KEY_IN_PIN 2u
#define USER_ADC_CHANNEL 0u
#define VBUS_ADC_CHANNEL 3u
#define CURRENT_ADC_CHANNEL 4u
#define VREFINT_ADC_CHANNEL 13u
#define VREFINT_CAL_ADDRESS 0x1FFF75AAUL
#define FAN_PWM_PROBE_HZ 25000u
#define ADC_POLL_LIMIT 100000u

volatile vacuum_board_sample_t g_vacuum_diagnostics;

static volatile uint32_t g_tach_edges;
static uint32_t g_last_tach_edge_count;
static bool g_adc_ready;

static void gpio_input(GPIO_TypeDef *port, uint32_t pin, uint32_t pull)
{
    const uint32_t shift = pin * 2u;
    port->MODER &= ~(3u << shift);
    port->PUPDR = (port->PUPDR & ~(3u << shift)) | (pull << shift);
}

static void gpio_analog(GPIO_TypeDef *port, uint32_t pin)
{
    const uint32_t shift = pin * 2u;
    port->MODER = (port->MODER & ~(3u << shift)) | (3u << shift);
    port->PUPDR &= ~(3u << shift);
}

static void gpio_output_low(GPIO_TypeDef *port, uint32_t pin)
{
    const uint32_t shift = pin * 2u;
    port->BSRR = 1u << (pin + 16u);
    port->OTYPER &= ~(1u << pin);
    port->OSPEEDR &= ~(3u << shift);
    port->PUPDR = (port->PUPDR & ~(3u << shift)) | (2u << shift);
    port->MODER = (port->MODER & ~(3u << shift)) | (1u << shift);
}

/* U5 TSSOP20 pin17 is PA12 unless SYSCFG remaps PA10 onto that pad.
 * Stage both outputs low before selecting PA12, so a retained remap cannot
 * leave the physical fan-power pad enabled during the mapping transition. */
static void force_power_pad_low(void)
{
    RCC->IOPENR |= RCC_IOPENR_GPIOAEN;
    RCC->APBENR2 |= RCC_APBENR2_SYSCFGEN;
    (void)RCC->IOPENR;
    (void)RCC->APBENR2;
    gpio_output_low(GPIOA, FAN_POWER_REMAPPED_PIN);
    gpio_output_low(GPIOA, FAN_POWER_EN_PIN);
    SYSCFG->CFGR1 &= ~SYSCFG_CFGR1_PA12_RMP;
    __DSB();
}

void vacuum_board_force_off(void)
{
    force_power_pad_low();
    GPIOA->BSRR = (1u << (FAN_POWER_EN_PIN + 16u)) |
                  (1u << (FAN_PWM_PIN + 16u));
    __DSB();
    gpio_input(GPIOA, FAN_PWM_PIN, 2u);
}

void vacuum_board_force_fan_disabled(void)
{
    force_power_pad_low();
    if ((RCC->APBENR2 & RCC_APBENR2_TIM1EN) != 0u) {
        TIM1->CCR1 = 0u;
    }
    __DSB();
}

int vacuum_board_set_fan_power(void *context, bool enabled)
{
    (void)context;
    if (!vacuum_output_lock_allows_power(enabled)) {
        vacuum_board_force_fan_disabled();
        return -1;
    }
    vacuum_board_force_fan_disabled();
    return 0;
}

int vacuum_board_set_fan_duty(void *context, uint16_t duty_permille)
{
    (void)context;
    if (!vacuum_output_lock_allows_duty(duty_permille)) {
        vacuum_board_force_fan_disabled();
        return -1;
    }
    if ((RCC->APBENR2 & RCC_APBENR2_TIM1EN) != 0u) {
        TIM1->CCR1 = 0u;
    }
    vacuum_board_force_fan_disabled();
    return 0;
}

static bool init_pwm_timer(void)
{
    uint16_t auto_reload;
    const uint32_t period = FAN_PWM_PROBE_HZ;
    const uint32_t pin_shift = FAN_PWM_PIN * 2u;
    const uint32_t af_shift = (FAN_PWM_PIN - 8u) * 4u;

    if (!vacuum_timer_autoreload(SystemCoreClock, period, &auto_reload)) {
        return false;
    }

    /* PA8 is TIM1_CH1 AF2 on STM32G030F6P6. The Q2 N-FET stage is held
     * released by CCR1=0 and by the independent FAN_PWR_EN gate. */
    GPIOA->BSRR = 1u << (FAN_PWM_PIN + 16u);
    GPIOA->OTYPER &= ~(1u << FAN_PWM_PIN);
    GPIOA->OSPEEDR &= ~(3u << pin_shift);
    GPIOA->PUPDR = (GPIOA->PUPDR & ~(3u << pin_shift)) | (2u << pin_shift);
    GPIOA->AFR[1] = (GPIOA->AFR[1] & ~(0xFu << af_shift)) | (2u << af_shift);
    GPIOA->MODER = (GPIOA->MODER & ~(3u << pin_shift)) | (2u << pin_shift);

    RCC->APBENR2 |= RCC_APBENR2_TIM1EN;
    (void)RCC->APBENR2;
    TIM1->CR1 = 0u;
    TIM1->PSC = 0u;
    TIM1->ARR = auto_reload;
    TIM1->CCR1 = 0u;
    TIM1->CCMR1 = TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1PE;
    TIM1->CCER = TIM_CCER_CC1E;
    TIM1->BDTR = TIM_BDTR_MOE;
    TIM1->EGR = TIM_EGR_UG;
    TIM1->CR1 = TIM_CR1_ARPE | TIM_CR1_CEN;
    return true;
}

static bool init_adc(void)
{
    uint32_t timeout;

    RCC->APBENR2 |= RCC_APBENR2_ADCEN;
    (void)RCC->APBENR2;
    ADC1->CFGR2 = (ADC1->CFGR2 & ~ADC_CFGR2_CKMODE_Msk) | ADC_CFGR2_CKMODE_0;
    ADC1->CR |= ADC_CR_ADVREGEN;
    for (volatile uint32_t settle = 0u; settle < 1200u; ++settle) {
        __NOP();
    }

    ADC1->CR |= ADC_CR_ADCAL;
    timeout = ADC_POLL_LIMIT;
    while (((ADC1->CR & ADC_CR_ADCAL) != 0u) && timeout != 0u) {
        --timeout;
    }
    if (timeout == 0u) {
        return false;
    }

    /* Use the longest SMP1 sample time for every channel. The previous setup
     * selected SMP2 on every channel while leaving SMP2 at its shortest value. */
    ADC1->CFGR1 = 0u;
    ADC1->SMPR = ADC_SMPR_SMP1;
    ADC1_COMMON->CCR |= ADC_CCR_VREFEN;
    ADC1->ISR = ADC_ISR_ADRDY | ADC_ISR_EOC | ADC_ISR_EOS | ADC_ISR_OVR;
    ADC1->CR |= ADC_CR_ADEN;
    timeout = ADC_POLL_LIMIT;
    while (((ADC1->ISR & ADC_ISR_ADRDY) == 0u) && timeout != 0u) {
        --timeout;
    }
    if (timeout == 0u) {
        return false;
    }
    g_adc_ready = true;
    return true;
}

static bool read_adc(uint32_t channel, uint16_t *raw)
{
    uint32_t timeout;

    if (!g_adc_ready || raw == NULL || channel > 18u ||
        (ADC1->CR & ADC_CR_ADSTART) != 0u) {
        return false;
    }

    ADC1->ISR = ADC_ISR_EOC | ADC_ISR_EOS | ADC_ISR_OVR | ADC_ISR_CCRDY;
    ADC1->CHSELR = 1u << channel;
    /* RM0454: ADSTART is ignored until the new channel configuration is
       applied. Never convert with an unacknowledged channel selection. */
    timeout = ADC_POLL_LIMIT;
    while (((ADC1->ISR & ADC_ISR_CCRDY) == 0u) && timeout != 0u) {
        --timeout;
    }
    if (timeout == 0u) {
        g_adc_ready = false; /* Do not reuse an uncertain ADC state. */
        return false;
    }
    ADC1->CR |= ADC_CR_ADSTART;
    timeout = ADC_POLL_LIMIT;
    while (((ADC1->ISR & ADC_ISR_EOC) == 0u) && timeout != 0u) {
        --timeout;
    }
    if (timeout == 0u || (ADC1->ISR & ADC_ISR_OVR) != 0u) {
        g_adc_ready = false;
        if ((ADC1->CR & ADC_CR_ADSTART) != 0u) {
            ADC1->CR |= ADC_CR_ADSTP;
        }
        return false;
    }

    *raw = (uint16_t)(ADC1->DR & 0x0FFFu);
    return true;
}

static void init_tach_capture(void)
{
    const uint32_t line = EXTI_RTSR1_RT1;
    RCC->APBENR2 |= RCC_APBENR2_SYSCFGEN;
    (void)RCC->APBENR2;

    gpio_input(GPIOA, FAN_TACH_PIN, 0u); /* External 10 kΩ pull-up on the board. */
    EXTI->EXTICR[0] &= ~(0xFu << 4u); /* EXTI1 source = Port A. */
    EXTI->RTSR1 = (EXTI->RTSR1 & ~line) | line;
    EXTI->FTSR1 &= ~line;
    EXTI->RPR1 = EXTI_RPR1_RPIF1;
    EXTI->IMR1 |= EXTI_IMR1_IM1;
    NVIC_ClearPendingIRQ(EXTI0_1_IRQn);
    NVIC_EnableIRQ(EXTI0_1_IRQn);
}

bool vacuum_board_init(void)
{
    g_adc_ready = false;
    g_tach_edges = 0u;
    g_last_tach_edge_count = 0u;
    vacuum_board_force_off();

    gpio_analog(GPIOA, 0u); /* USER_ADC / ADC_IN0 */
    gpio_analog(GPIOA, 3u); /* ADC_VBUS / ADC_IN3 */
    gpio_analog(GPIOA, 4u); /* ADC_IIN / ADC_IN4 */
    gpio_input(GPIOA, KEY_IN_PIN, 0u); /* External 10 kΩ pull-up. */
    init_tach_capture();

    if (!init_pwm_timer() || !init_adc()) {
        vacuum_board_force_off();
        return false;
    }
    vacuum_board_force_fan_disabled();
    return true;
}

void vacuum_board_note_tach_edge(void)
{
    ++g_tach_edges;
}

bool vacuum_board_sample(vacuum_board_sample_t *sample, uint32_t now_ms)
{
    uint32_t edge_count;
    const uint16_t vrefint_cal_raw =
        *(const volatile uint16_t *)(uintptr_t)VREFINT_CAL_ADDRESS;

    if (sample == NULL || !read_adc(USER_ADC_CHANNEL, &sample->user_adc_raw) ||
        !read_adc(VBUS_ADC_CHANNEL, &sample->vbus_adc_raw) ||
        !read_adc(CURRENT_ADC_CHANNEL, &sample->current_adc_raw) ||
        !read_adc(VREFINT_ADC_CHANNEL, &sample->vrefint_adc_raw) ||
        !vacuum_vdda_from_vrefint(sample->vrefint_adc_raw, vrefint_cal_raw,
                                  &sample->estimated_vdda_mv) ||
        !vacuum_vbus_count_to_mv(sample->vbus_adc_raw,
                                 (uint16_t)sample->estimated_vdda_mv,
                                 &sample->estimated_vbus_mv) ||
        !vacuum_current_count_to_ma(sample->current_adc_raw,
                                    (uint16_t)sample->estimated_vdda_mv,
                                    &sample->estimated_current_ma)) {
        return false;
    }

    sample->key_pressed = (GPIOA->IDR & (1u << KEY_IN_PIN)) == 0u;
    edge_count = g_tach_edges;
    sample->tach_edges_100ms = edge_count - g_last_tach_edge_count;
    g_last_tach_edge_count = edge_count;
    sample->sampled_at_ms = now_ms;
    sample->adc_valid = true;
    return true;
}

void vacuum_board_publish_diagnostics(const vacuum_board_sample_t *sample)
{
    if (sample != NULL) {
        g_vacuum_diagnostics = *sample;
    }
}
