// Copyright 2019-2022 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

use fvm_shared3::clock::{ChainEpoch, QuantSpec};
use serde::{Deserialize, Serialize};

/// Deadline calculations with respect to a current epoch.
/// "Deadline" refers to the window during which proofs may be submitted.
/// Windows are non-overlapping ranges [Open, Close), but the challenge epoch for a window occurs
/// before the window opens.
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq, Copy, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct DeadlineInfo {
    /// Epoch at which this info was calculated.
    pub current_epoch: ChainEpoch,
    /// First epoch of the proving period (`<= CurrentEpoch`).
    pub period_start: ChainEpoch,
    /// Current deadline index, in `[0..WPoStProvingPeriodDeadlines)`.
    pub index: u64,
    /// First epoch from which a proof may be submitted (`>= CurrentEpoch`).
    pub open: ChainEpoch,
    /// First epoch from which a proof may no longer be submitted (`>= Open`).
    pub close: ChainEpoch,
    /// Epoch at which to sample the chain for challenge (`< Open`).
    pub challenge: ChainEpoch,
    /// First epoch at which a fault declaration is rejected (`< Open`).
    pub fault_cutoff: ChainEpoch,

    // Protocol parameters (This is intentionally included in the JSON response for deadlines)
    #[serde(rename = "WPoStPeriodDeadlines")]
    pub w_post_period_deadlines: u64,
    #[serde(rename = "WPoStProvingPeriod")]
    pub w_post_proving_period: ChainEpoch,
    #[serde(rename = "WPoStChallengeWindow")]
    pub w_post_challenge_window: ChainEpoch,
    #[serde(rename = "WPoStChallengeLookback")]
    pub w_post_challenge_lookback: ChainEpoch,
    pub fault_declaration_cutoff: ChainEpoch,
}

impl DeadlineInfo {
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        period_start: ChainEpoch,
        deadline_idx: u64,
        current_epoch: ChainEpoch,
        w_post_period_deadlines: u64,
        w_post_proving_period: ChainEpoch,
        w_post_challenge_window: ChainEpoch,
        w_post_challenge_lookback: ChainEpoch,
        fault_declaration_cutoff: ChainEpoch,
    ) -> Self {
        if deadline_idx < w_post_period_deadlines {
            let deadline_open = period_start + (deadline_idx as i64 * w_post_challenge_window);
            Self {
                current_epoch,
                period_start,
                index: deadline_idx,
                open: deadline_open,
                close: deadline_open + w_post_challenge_window,
                challenge: deadline_open - w_post_challenge_lookback,
                fault_cutoff: deadline_open - fault_declaration_cutoff,
                w_post_period_deadlines,
                w_post_proving_period,
                w_post_challenge_window,
                w_post_challenge_lookback,
                fault_declaration_cutoff,
            }
        } else {
            let after_last_deadline = period_start + w_post_proving_period;
            Self {
                current_epoch,
                period_start,
                index: deadline_idx,
                open: after_last_deadline,
                close: after_last_deadline,
                challenge: after_last_deadline,
                fault_cutoff: 0,
                w_post_period_deadlines,
                w_post_proving_period,
                w_post_challenge_window,
                w_post_challenge_lookback,
                fault_declaration_cutoff,
            }
        }
    }

    /// Whether the proving period has begun.
    pub fn period_started(&self) -> bool {
        self.current_epoch >= self.period_start
    }

    /// Whether the proving period has elapsed.
    pub fn period_elapsed(&self) -> bool {
        self.current_epoch >= self.next_period_start()
    }

    /// The last epoch in the proving period.
    pub fn period_end(&self) -> ChainEpoch {
        self.period_start + self.w_post_proving_period - 1
    }

    /// The first epoch in the next proving period.
    pub fn next_period_start(&self) -> ChainEpoch {
        self.period_start + self.w_post_proving_period
    }

    /// Whether the current deadline is currently open.
    pub fn is_open(&self) -> bool {
        self.current_epoch >= self.open && self.current_epoch < self.close
    }

    /// Whether the current deadline has already closed.
    pub fn has_elapsed(&self) -> bool {
        self.current_epoch >= self.close
    }

    /// The last epoch during which a proof may be submitted.
    pub fn last(&self) -> ChainEpoch {
        self.close - 1
    }

    /// Epoch at which the subsequent deadline opens.
    pub fn next_open(&self) -> ChainEpoch {
        self.close
    }

    /// Whether the deadline's fault cutoff has passed.
    pub fn fault_cutoff_passed(&self) -> bool {
        self.current_epoch >= self.fault_cutoff
    }

    /// Returns the next instance of this deadline that has not yet elapsed.
    pub fn next_not_elapsed(self) -> Self {
        std::iter::successors(Some(self), |info| {
            Some(Self::new(
                info.next_period_start(),
                info.index,
                info.current_epoch,
                self.w_post_period_deadlines,
                self.w_post_proving_period,
                self.w_post_challenge_window,
                self.w_post_challenge_lookback,
                self.fault_declaration_cutoff,
            ))
        })
        .find(|info| !info.has_elapsed())
        .unwrap() // the iterator is infinite, so `find` won't ever return `None`
    }

    pub fn quant_spec(&self) -> QuantSpec {
        QuantSpec {
            unit: self.w_post_proving_period,
            offset: self.last(),
        }
    }
}
