/*
 * Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

#include <stdint.h>
#include <stdio.h>
#include <inttypes.h>

#include "pico.h"

#include "hardware/gpio.h"
#include "hardware/powman.h"

#ifndef PICO_POWMAN_DEBUG
#define PICO_POWMAN_DEBUG 0
#endif

#if PICO_POWMAN_DEBUG
bool powman_debug_printf = false;
void powman_enable_debug_printf(void) {
    powman_debug_printf = true;
}
#define powman_debug(format, args...) if (powman_debug_printf) printf(format, ## args)
#else
#define powman_debug(...)
#endif

static inline void powman_write(volatile uint32_t *reg, uint32_t value) {
    // Write needs a password in top 16 bits
    invalid_params_if(HARDWARE_POWMAN, value >> 16);
    *reg = POWMAN_PASSWORD_BITS | value;
}

void powman_timer_set_ms(uint64_t time_ms) {
    bool was_running = powman_timer_is_running();
    if (was_running) powman_timer_stop();
    powman_write(&powman_hw->set_time_15to0, time_ms & 0xffff);
    powman_write(&powman_hw->set_time_31to16, (time_ms >> 16) & 0xffff);
    powman_write(&powman_hw->set_time_47to32, (time_ms >> 32) & 0xffff);
    powman_write(&powman_hw->set_time_63to48, (time_ms >> 48) & 0xffff);
    if (was_running) powman_timer_start();
}

uint64_t powman_timer_get_ms(void) {
    // Need to make sure that the upper 32 bits of the timer
    // don't change, so read that first
    uint32_t hi = powman_hw->read_time_upper;
    uint32_t lo;
    do {
        // Read the lower 32 bits
        lo = powman_hw->read_time_lower;
        // Now read the upper 32 bits again and
        // check that it hasn't incremented. If it has loop around
        // and read the lower 32 bits again to get an accurate value
        uint32_t next_hi = powman_hw->read_time_upper;
        if (hi == next_hi) break;
        hi = next_hi;
    } while (true);
    return ((uint64_t) hi << 32u) | lo;
}

void powman_timer_set_1khz_tick_source_lposc(void) {
    powman_timer_set_1khz_tick_source_lposc_with_hz(32768);
}

void powman_timer_set_1khz_tick_source_lposc_with_hz(uint32_t lposc_freq_hz) {
    bool was_running = powman_timer_is_running();
    if (was_running) powman_timer_stop();
    uint32_t lposc_freq_khz = lposc_freq_hz / 1000;
    uint32_t lposc_freq_khz_frac16 = (lposc_freq_hz % 1000) * 65536 / 1000;
    powman_write(&powman_hw->lposc_freq_khz_int, lposc_freq_khz);
    powman_write(&powman_hw->lposc_freq_khz_frac, lposc_freq_khz_frac16);
    powman_set_bits(&powman_hw->timer, POWMAN_TIMER_USE_LPOSC_BITS);
    if (was_running) {
        powman_timer_start();
        while(!(powman_hw->timer & POWMAN_TIMER_USING_LPOSC_BITS));
    }
}

void powman_timer_set_1khz_tick_source_xosc(void) {
    powman_timer_set_1khz_tick_source_xosc_with_hz(XOSC_HZ);
}

void powman_timer_set_1khz_tick_source_xosc_with_hz(uint32_t xosc_freq_hz) {
    bool was_running = powman_timer_is_running();
    if (was_running) powman_timer_stop();
    uint32_t xosc_freq_khz = xosc_freq_hz / 1000;
    uint32_t xosc_freq_khz_frac16 = (xosc_freq_hz % 1000) * 65536 / 1000;
    powman_write(&powman_hw->xosc_freq_khz_int, xosc_freq_khz);
    powman_write(&powman_hw->xosc_freq_khz_frac, xosc_freq_khz_frac16);
    powman_set_bits(&powman_hw->timer, POWMAN_TIMER_USE_XOSC_BITS);
    if (was_running) {
        powman_timer_start();
        while(!(powman_hw->timer & POWMAN_TIMER_USING_XOSC_BITS));
    }
}

#ifndef GPIO_TO_POWMAN_EXT_TIME_REF_SOURCE
#define GPIO_TO_POWMAN_EXT_TIME_REF_SOURCE(gpio, default_ext_time_ref_source) \
    ((gpio) == 12 ? 0 :                                 \
        ((gpio) == 20 ? 1 :                             \
            ((gpio) == 14 ? 2 :                         \
                ((gpio) == 22 ? 3 :                     \
                    (default_ext_time_ref_source)))))
#endif

static inline uint32_t gpio_to_powman_ext_time_ref_source(uint gpio, uint32_t default_ext_time_ref_source) {
    return GPIO_TO_POWMAN_EXT_TIME_REF_SOURCE(gpio, ({invalid_params_if(HARDWARE_POWMAN, true); default_ext_time_ref_source;}));
}

static void powman_timer_use_gpio(uint32_t gpio, uint32_t use, uint32_t using) {
    bool was_running = powman_timer_is_running();
    if (was_running) powman_timer_stop();
    uint32_t source = gpio_to_powman_ext_time_ref_source(gpio, 0);
    gpio_set_input_enabled(gpio, true);
    powman_write(&powman_hw->ext_time_ref, source);
    powman_set_bits(&powman_hw->timer, use);
    if (was_running) {
        powman_timer_start();
        while(!(powman_hw->timer & using));
    }
}

void powman_timer_set_1khz_tick_source_gpio(uint32_t gpio) {
    // todo check if we're using the GPIO setup already?
    powman_timer_use_gpio(gpio, POWMAN_TIMER_USE_GPIO_1KHZ_BITS, POWMAN_TIMER_USING_GPIO_1KHZ_BITS);
}

void powman_timer_enable_gpio_1hz_sync(uint32_t gpio) {
    // todo check if we're using the GPIO setup already?
    powman_timer_use_gpio(gpio, POWMAN_TIMER_USE_GPIO_1HZ_BITS, POWMAN_TIMER_USING_GPIO_1HZ_BITS);
}

void powman_timer_disable_gpio_1hz_sync(void) {
    powman_clear_bits(&powman_hw->timer, POWMAN_TIMER_USE_GPIO_1HZ_BITS);
}

powman_power_state powman_get_power_state(void) {
    uint32_t state_reg = ~powman_hw->state & POWMAN_STATE_CURRENT_BITS;
    // todo we should have hardware/regs/powman.h values for these
    static_assert(POWMAN_POWER_DOMAIN_SRAM_BANK1 == 0, "");
    static_assert(POWMAN_POWER_DOMAIN_SRAM_BANK0 == 1, "");
    static_assert(POWMAN_POWER_DOMAIN_XIP_CACHE == 2, "");
    static_assert(POWMAN_POWER_DOMAIN_SWITCHED_CORE == 3, "");
    static_assert(POWMAN_STATE_CURRENT_BITS == 0xf, "");
    return (powman_power_state) state_reg;
}

// TODO: Should this fail to go to sleep if there is no wakeup alarm
int powman_set_power_state(powman_power_state state) {
    // Clear req ignored in case it has been set
    powman_clear_bits(&powman_hw->state, POWMAN_STATE_REQ_IGNORED_BITS);
    powman_debug("powman: Requesting state %x\n", state);
    powman_write(&powman_hw->state, (~state << POWMAN_STATE_REQ_LSB) & POWMAN_STATE_REQ_BITS);

    // Has it been ignored?
    if (powman_hw->state & POWMAN_STATE_REQ_IGNORED_BITS) {
        powman_debug("State req ignored because of a pending pwrup req: %"PRIx32"\n", powman_hw->current_pwrup_req);
        return PICO_ERROR_PRECONDITION_NOT_MET;
    }

    bool state_valid = (powman_hw->state & POWMAN_STATE_BAD_SW_REQ_BITS) == 0;
    if (!state_valid) {
        powman_debug("powman: Requested state invalid\n");
        return PICO_ERROR_INVALID_ARG;
    } else {
        powman_debug("powman: Requested state valid\n");
    }
    if (!powman_power_state_is_domain_on(state, POWMAN_POWER_DOMAIN_SWITCHED_CORE)) {
        // If we are turning off switched core then POWMAN_STATE_WAITING_BITS will be
        // set because we are waiting for proc to go to sleep, so return ok and then the proc
        // can go to sleep

        // Note if the powerdown is being blocked by a pending pwrup request we will break out of this and return a failure

        // Clk pow is slow so can take a few clk_pow cycles for waiting to turn up
        for (int i = 0; i < 100; i++) {
            if (powman_hw->state & POWMAN_STATE_WAITING_BITS) {
                return PICO_OK;
            }
        }

        // If it hasn't turned up then false
        powman_debug("powman: STATE_WAITING hasn't turned up\n");
        return PICO_ERROR_TIMEOUT;
    }
    // Wait while the state is changing then return true as we will be in the new state
    powman_debug("powman: waiting for state change\n");
    while(powman_hw->state & POWMAN_STATE_CHANGING_BITS) tight_loop_contents();
    powman_debug("powman: state changed to %x\n", state);
    return PICO_OK;
}

bool powman_configure_wakeup_state(powman_power_state sleep_state, powman_power_state wakeup_state) {
    // Must be entering a P1 low-power state (SWCORE OFF).
    bool valid = !powman_power_state_is_domain_on(sleep_state, POWMAN_POWER_DOMAIN_SWITCHED_CORE);
    // Must be waking up in a P0 state (SWCORE ON).
    valid &= powman_power_state_is_domain_on(wakeup_state, POWMAN_POWER_DOMAIN_SWITCHED_CORE);
    powman_power_state current_state = powman_get_power_state();
    bool current_sram0 = powman_power_state_is_domain_on(current_state, POWMAN_POWER_DOMAIN_SRAM_BANK0);
    bool current_sram1 = powman_power_state_is_domain_on(current_state, POWMAN_POWER_DOMAIN_SRAM_BANK1);
    bool sleep_sram0 = powman_power_state_is_domain_on(sleep_state, POWMAN_POWER_DOMAIN_SRAM_BANK0);
    bool sleep_sram1 = powman_power_state_is_domain_on(sleep_state, POWMAN_POWER_DOMAIN_SRAM_BANK1);
    bool wakeup_sram0 = powman_power_state_is_domain_on(wakeup_state, POWMAN_POWER_DOMAIN_SRAM_BANK0);
    bool wakeup_sram1 = powman_power_state_is_domain_on(wakeup_state, POWMAN_POWER_DOMAIN_SRAM_BANK1);
    // Sleep state cannot turn ON SRAM0 or SRAM1 if it is OFF in the current state.
    if (!current_sram0) valid &= !sleep_sram0;
    if (!current_sram1) valid &= !sleep_sram1;
    // Wakeup state cannot turn OFF SRAM0 or SRAM1 if it is ON in the sleep state.
    if (sleep_sram0) valid &= wakeup_sram0;
    if (sleep_sram1) valid &= wakeup_sram1;
    if (valid) {
        // Prepare power sequencer to power SRAM domains on wakeup.
        powman_clear_bits(&powman_hw->seq_cfg, POWMAN_SEQ_CFG_HW_PWRUP_SRAM0_BITS | POWMAN_SEQ_CFG_HW_PWRUP_SRAM1_BITS);
        uint32_t seq_cfg_set = 0;
        // Sequencer SRAM power transitions from sleep to wakeup are: OFF->ON (0), ON->ON (1) and OFF->OFF (1)
        if (sleep_sram0 == wakeup_sram0) seq_cfg_set |= POWMAN_SEQ_CFG_HW_PWRUP_SRAM0_BITS;
        if (sleep_sram1 == wakeup_sram1) seq_cfg_set |= POWMAN_SEQ_CFG_HW_PWRUP_SRAM1_BITS;
        powman_set_bits(&powman_hw->seq_cfg, seq_cfg_set);
    }
    return valid;
}

void powman_timer_enable_alarm_at_ms(uint64_t alarm_time_ms) {
    powman_set_bits(&powman_hw->inte, POWMAN_INTE_TIMER_BITS);
    powman_clear_bits(&powman_hw->timer, POWMAN_TIMER_ALARM_ENAB_BITS);
    // Alarm must be disabled to set the alarm time
    powman_write(&powman_hw->alarm_time_15to0, alarm_time_ms & 0xffff);
    powman_write(&powman_hw->alarm_time_31to16, (alarm_time_ms >> 16) & 0xffff);
    powman_write(&powman_hw->alarm_time_47to32, (alarm_time_ms >> 32) & 0xffff);
    powman_write(&powman_hw->alarm_time_63to48, (alarm_time_ms >> 48) & 0xffff);
    powman_clear_alarm();
    // TODO: Assuming pwrup on alarm has no bad side effects if already powered up
    powman_set_bits(&powman_hw->timer, POWMAN_TIMER_ALARM_ENAB_BITS);
}

void powman_timer_disable_alarm(void) {
    powman_clear_bits(&powman_hw->inte, POWMAN_INTE_TIMER_BITS);
    powman_clear_bits(&powman_hw->timer, POWMAN_TIMER_ALARM_ENAB_BITS);
}

void powman_enable_alarm_wakeup_at_ms(uint64_t alarm_time_ms) {
    powman_timer_enable_alarm_at_ms(alarm_time_ms);
    powman_set_bits(&powman_hw->timer, POWMAN_TIMER_PWRUP_ON_ALARM_BITS);
}

void powman_disable_alarm_wakeup(void) {
    powman_timer_disable_alarm();
    powman_clear_bits(&powman_hw->timer, POWMAN_TIMER_PWRUP_ON_ALARM_BITS);
}

void powman_enable_gpio_wakeup(uint gpio_wakeup_num, uint32_t gpio, bool edge, bool high) {
    invalid_params_if(HARDWARE_POWMAN, gpio_wakeup_num >= count_of(powman_hw->pwrup));

    // Need to make sure pad is input enabled
    gpio_set_input_enabled(gpio, true);

    // Set up gpio hardware for what we want
    uint32_t pwrup = (edge ? POWMAN_PWRUP0_MODE_VALUE_EDGE : POWMAN_PWRUP0_MODE_VALUE_LEVEL) << POWMAN_PWRUP0_MODE_LSB;
    pwrup |= (high ? POWMAN_PWRUP0_DIRECTION_BITS : 0);
    pwrup |= gpio << POWMAN_PWRUP0_SOURCE_LSB;
    powman_write(&powman_hw->pwrup[gpio_wakeup_num], pwrup);

    // Clear the status bit in case an edge is already latched
    powman_clear_bits(&powman_hw->pwrup[gpio_wakeup_num], POWMAN_PWRUP0_STATUS_BITS);

    // Important to enable it separately to allow the gpio to change
    powman_set_bits(&powman_hw->pwrup[gpio_wakeup_num], POWMAN_PWRUP0_ENABLE_BITS);
}

void powman_disable_gpio_wakeup(uint gpio_wakeup_num) {
    invalid_params_if(HARDWARE_POWMAN, gpio_wakeup_num >= count_of(powman_hw->pwrup));
    powman_clear_bits(&powman_hw->pwrup[gpio_wakeup_num], POWMAN_PWRUP0_ENABLE_BITS);
}

void powman_disable_all_wakeups(void) {
    for (uint i = 0; i < count_of(powman_hw->pwrup); i++) {
        powman_disable_gpio_wakeup(i);
    }
    powman_disable_alarm_wakeup();
}
