//! Utility functions for calculating unlock schedules for a vesting account.

use crate::Vesting;

pub fn available_for_withdrawal(vesting: &Vesting, current_ts: i64) -> u64 {
    std::cmp::min(outstanding_vested(vesting, current_ts), balance(vesting))
}

// The amount of funds currently in the vault.
fn balance(vesting: &Vesting) -> u64 {
    vesting
        .outstanding
        .checked_sub(vesting.whitelist_owned)
        .unwrap()
}

// The amount of outstanding locked tokens vested. Note that these
// tokens might have been transferred to whitelisted programs.
fn outstanding_vested(vesting: &Vesting, current_ts: i64) -> u64 {
    total_vested(vesting, current_ts)
        .checked_sub(withdrawn_amount(vesting))
        .unwrap()
}

// Returns the amount withdrawn from this vesting account.
fn withdrawn_amount(vesting: &Vesting) -> u64 {
    vesting
        .start_balance
        .checked_sub(vesting.outstanding)
        .unwrap()
}

// Returns the total vested amount up to the given ts, assuming zero
// withdrawals and zero funds sent to other programs.
fn total_vested(vesting: &Vesting, current_ts: i64) -> u64 {
    if current_ts < vesting.start_ts {
        0
    } else if current_ts >= vesting.end_ts {
        vesting.start_balance
    } else {
        linear_unlock(vesting, current_ts).unwrap()
    }
}

fn linear_unlock(vesting: &Vesting, current_ts: i64) -> Option<u64> {
    // Signed division not supported.
    let current_ts = current_ts as u64;
    let start_ts = vesting.start_ts as u64;
    let end_ts = vesting.end_ts as u64;

    // If we can't perfectly partition the vesting window,
    // push the start of the window back so that we can.
    //
    // This has the effect of making the first vesting period shorter
    // than the rest.
    let shifted_start_ts =
        start_ts.checked_sub(end_ts.checked_sub(start_ts)? % vesting.period_count)?;

    // Similarly, if we can't perfectly divide up the vesting rewards
    // then make the first period act as a cliff, earning slightly more than
    // subsequent periods.
    let reward_overflow = vesting.start_balance % vesting.period_count;

    // Reward per period ignoring the overflow.
    let reward_per_period =
        (vesting.start_balance.checked_sub(reward_overflow)?).checked_div(vesting.period_count)?;

    // Number of vesting periods that have passed.
    let current_period = {
        let period_secs =
            (end_ts.checked_sub(shifted_start_ts)?).checked_div(vesting.period_count)?;
        let current_period_count =
            (current_ts.checked_sub(shifted_start_ts)?).checked_div(period_secs)?;
        std::cmp::min(current_period_count, vesting.period_count)
    };

    if current_period == 0 {
        return Some(0);
    }

    current_period
        .checked_mul(reward_per_period)?
        .checked_add(reward_overflow)
}
