#include "GPSUpdateScheduling.h"

#include "Default.h"
#include "UptimeClock.h"

// Sampled from the original `2750 * seconds^1.22` curve. Interpolation tracks it within 0.6% for
// inputs >=10s and 1.7% below that; the 1s/2s/3s points keep the convex first segment from
// overshooting (a 0s-to-5s chord reads 42% high at 1s).
static constexpr uint32_t kThresholdCurveSecs[] = {0, 1, 2, 3, 5, 10, 15, 20, 30, 45, 60, 90, 120, 180, 240, 300, 450, 600, 900};
static constexpr uint32_t kThresholdCurveMs[] = {0,       2750,    6406,    10506,   19592,   45639,  74845,
                                                 106314,  174350,  285925,  406141,  666053,  946093, 1551548,
                                                 2203893, 2893481, 4745172, 6740269, 11053722};
static constexpr size_t kThresholdCurvePoints = sizeof(kThresholdCurveSecs) / sizeof(kThresholdCurveSecs[0]);

// How long does gps_update_interval need to be, for GPS_HARDSLEEP to become more efficient than
// GPS_SOFTSLEEP? Avoids pow() so this heuristic doesn't pull double-precision libm into the image.
uint32_t gpsHardsleepThresholdMs(uint32_t predictedSearchSecs)
{
    if (predictedSearchSecs >= kThresholdCurveSecs[kThresholdCurvePoints - 1])
        return kThresholdCurveMs[kThresholdCurvePoints - 1];

    size_t i = 1;
    while (kThresholdCurveSecs[i] < predictedSearchSecs)
        i++;

    uint32_t x0 = kThresholdCurveSecs[i - 1], x1 = kThresholdCurveSecs[i];
    uint32_t y0 = kThresholdCurveMs[i - 1], y1 = kThresholdCurveMs[i];
    return y0 + (uint32_t)((uint64_t)(y1 - y0) * (predictedSearchSecs - x0) / (x1 - x0));
}

// Mark the time when searching for GPS position begins
void GPSUpdateScheduling::informSearching()
{
    searching = true;
    searchStartedMs = Time::getMillis();
}

// Mark the time when searching for GPS is complete,
// then update the predicted lock-time
void GPSUpdateScheduling::informGotLock()
{
    searching = false;
    searchEndedMs = Time::getMillis();
    LOG_DEBUG("Took %us to get lock", (searchEndedMs - searchStartedMs) / 1000);
    updateLockTimePrediction();
    consecutiveFailures = 0; // Drop back to fast cadence as soon as we acquire any fix
}

// Search finished without obtaining a fix. We still need to mark the end time so
// the next sleep is timed correctly, but we must not feed the timeout duration
// into predictedMsToGetLock - doing so poisons msUntilNextSearch() and causes
// down() to fall into GPS_IDLE, leaving the chip awake on subsequent indoor cycles.
void GPSUpdateScheduling::informSearchFailed()
{
    searching = false;
    searchEndedMs = Time::getMillis();
    consecutiveFailures++;
    LOG_DEBUG("GPS search ended without fix after %us (consecutive failures: %u)", (searchEndedMs - searchStartedMs) / 1000,
              consecutiveFailures);
}

// Clear old lock-time prediction data.
// When re-enabling GPS with user button.
void GPSUpdateScheduling::reset()
{
    searching = false;
    searchStartedMs = 0;
    searchEndedMs = 0;
    searchCount = 0;
    predictedMsToGetLock = 0;
    consecutiveFailures = 0;
}

// How many milliseconds before we should next search for GPS position
// Used by GPS hardware directly, to enter timed hardware sleep
uint32_t GPSUpdateScheduling::msUntilNextSearch()
{
    uint32_t now = Time::getMillis();

    // Target interval (seconds), between GPS updates
    uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval, default_gps_update_interval);

    // After a failed search, back off: indoors / no-sky environments will keep failing,
    // so wake at most once per broadcast interval rather than once per gps_update_interval.
    // Capped at 1 hour so a user-configured very-long broadcast interval still retries
    // periodically (in case conditions change). Reset on any successful lock.
    if (consecutiveFailures > 0) {
        constexpr uint32_t failureRetryCapMs = 60UL * 60UL * 1000UL; // 1 hour cap
        uint32_t failureSleepMs =
            Default::getConfiguredOrDefaultMs(config.position.position_broadcast_secs, default_broadcast_interval_secs);
        if (failureSleepMs > failureRetryCapMs)
            failureSleepMs = failureRetryCapMs;
        if (updateInterval < failureSleepMs)
            updateInterval = failureSleepMs;
    }

    // Check how long until we should start searching, to hopefully hit our target interval
    uint32_t dueAtMs = searchEndedMs + updateInterval;
    uint32_t compensatedStart = dueAtMs - predictedMsToGetLock;
    int32_t remainingMs = compensatedStart - now;

    // If we should have already started (negative value), start ASAP
    if (remainingMs < 0)
        remainingMs = 0;

    return (uint32_t)remainingMs;
}

// How long have we already been searching?
// Used to abort a search in progress, if it runs unacceptably long
uint32_t GPSUpdateScheduling::elapsedSearchMs()
{
    // Recorded, not inferred from searchStartedMs > searchEndedMs: ordering two stamps inverts
    // across the 32-bit wrap, and the inform*() calls already know which state we are in.
    if (!searching)
        return 0; // Not searching. We shouldn't really consume this value

    return Time::getMillis() - searchStartedMs;
}

// Is it now time to begin searching for a GPS position?
bool GPSUpdateScheduling::isUpdateDue()
{
    return (msUntilNextSearch() == 0);
}

// Have we been searching for a GPS position for too long?
bool GPSUpdateScheduling::searchedTooLong()
{
    constexpr uint32_t oneMinuteMs = 60UL * 1000UL;
    constexpr uint32_t maxSearchClampMs = 15UL * oneMinuteMs;   // Hard cap: 15 minutes is always too long
    constexpr uint32_t postFailureSearchMs = 5UL * oneMinuteMs; // Tighter dwell once we know the environment is hostile
    uint32_t elapsed = elapsedSearchMs();

    // Anything over 15 minutes is too long, regardless of the broadcast interval.
    if (elapsed > maxSearchClampMs)
        return true;

    // After a prior failed search, shorten the dwell
    if (consecutiveFailures > 0 && elapsed > postFailureSearchMs)
        return true;

    uint32_t minimumOrConfiguredSecs =
        Default::getConfiguredOrMinimumValue(config.position.position_broadcast_secs, default_broadcast_interval_secs);
    uint32_t maxSearchMs = Default::getConfiguredOrDefaultMs(minimumOrConfiguredSecs, default_broadcast_interval_secs);

    // If we've been searching longer than our position broadcast interval: that's too long
    if (elapsed > maxSearchMs)
        return true;

    // Otherwise, not too long yet!
    return false;
}

// Updates the predicted time-to-get-lock, by exponentially smoothing the latest observation
void GPSUpdateScheduling::updateLockTimePrediction()
{

    // How long did it take to get GPS lock this time?
    // Duration between down() calls
    int32_t lockTime = searchEndedMs - searchStartedMs;
    if (lockTime < 0)
        lockTime = 0;

    // Ignore the first lock-time: likely to be long, will skew data

    // Second locktime: likely stable. Use to initialize the smoothing filter
    if (searchCount == 1)
        predictedMsToGetLock = lockTime;

    // Third locktime and after: predict using exponential smoothing. Respond slowly to changes
    else if (searchCount > 1)
        predictedMsToGetLock = (lockTime * weighting) + (predictedMsToGetLock * (1 - weighting));

    searchCount++; // Only tracked so we can disregard initial lock-times

    LOG_DEBUG("Predict %us to get next lock", predictedMsToGetLock / 1000);
}

// How long do we expect to spend searching for a lock?
uint32_t GPSUpdateScheduling::predictedSearchDurationMs()
{
    return GPSUpdateScheduling::predictedMsToGetLock;
}
