// Copyright 2019 TiKV Project Authors. Licensed under Apache-2.0.
// Modifications Copyright 2022 PingCAP, Inc.

use fail::fail_point;
use kvproto::errorpb;

use super::{
    metrics::{ServerIsBusyType, new_server_is_busy},
    time::{Duration, Instant},
};

#[derive(Debug, Copy, Clone)]
pub struct DeadlineError;

impl std::error::Error for DeadlineError {
    fn description(&self) -> &str {
        "deadline has elapsed"
    }
}

impl std::fmt::Display for DeadlineError {
    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(fmt, "deadline has elapsed")
    }
}

/// A handy structure for checking deadline.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct Deadline {
    deadline: Instant,
}

impl Deadline {
    /// Creates a new `Deadline` by the given `deadline`.
    pub fn new(deadline: Instant) -> Self {
        Self { deadline }
    }

    /// Creates a new `Deadline` that will reach after specified amount of time
    /// in future.
    pub fn from_now(after_duration: Duration) -> Self {
        let deadline = Instant::now_coarse() + after_duration;
        Self { deadline }
    }

    pub fn inner(&self) -> Instant {
        self.deadline
    }

    /// Returns error if the deadline is exceeded.
    pub fn check(&self) -> std::result::Result<Duration, DeadlineError> {
        fail_point!("deadline_check_fail", |_| Err(DeadlineError));

        let now = Instant::now_coarse();
        if self.deadline <= now {
            return Err(DeadlineError);
        }
        Ok(self.deadline - now)
    }

    // Returns the deadline instant of the std library.
    pub fn to_std_instant(&self) -> std::time::Instant {
        std::time::Instant::now() + self.deadline.duration_since(Instant::now_coarse())
    }

    #[inline]
    pub fn to_tokio_instant(&self) -> tokio::time::Instant {
        tokio::time::Instant::from_std(self.to_std_instant())
    }
}

const DEADLINE_EXCEEDED: &str = "deadline is exceeded";

pub fn set_deadline_exceeded_busy_error(e: &mut errorpb::Error) {
    e.set_server_is_busy(new_server_is_busy(
        ServerIsBusyType::Deadline,
        DEADLINE_EXCEEDED,
    ));
}
