use std::cmp::Ordering;
use std::ops::{Add, AddAssign, Sub, SubAssign};
use std::time::Duration;

#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Hash)]
pub struct Instant(Duration);

impl Ord for Instant {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.partial_cmp(other)
            .expect("an instant should never be NaN or Inf.")
    }
}
impl Eq for Instant {}

impl Instant {
    #[inline]
    pub fn now() -> Self {
        Instant(duration_from_f64(now()))
    }

    #[inline]
    pub fn duration_since(&self, earlier: Instant) -> Duration {
        assert!(
            earlier.0 <= self.0,
            "`earlier` cannot be later than `self`."
        );
        self.0 - earlier.0
    }

    #[inline]
    pub fn elapsed(&self) -> Duration {
        Self::now().duration_since(*self)
    }

    /// Returns `Some(t)` where `t` is the time `self + duration` if `t` can be represented as
    /// `Instant` (which means it's inside the bounds of the underlying data structure), `None`
    /// otherwise.
    #[inline]
    pub fn checked_add(&self, duration: Duration) -> Option<Instant> {
        self.0.checked_add(duration).map(Instant)
    }

    /// Returns `Some(t)` where `t` is the time `self - duration` if `t` can be represented as
    /// `Instant` (which means it's inside the bounds of the underlying data structure), `None`
    /// otherwise.
    #[inline]
    pub fn checked_sub(&self, duration: Duration) -> Option<Instant> {
        self.0.checked_sub(duration).map(Instant)
    }

    /// Returns the amount of time elapsed from another instant to this one, or None if that
    /// instant is later than this one.
    #[inline]
    pub fn checked_duration_since(&self, earlier: Instant) -> Option<Duration> {
        if earlier.0 > self.0 {
            None
        } else {
            Some(self.0 - earlier.0)
        }
    }

    /// Returns the amount of time elapsed from another instant to this one, or zero duration if
    /// that instant is later than this one.
    #[inline]
    pub fn saturating_duration_since(&self, earlier: Instant) -> Duration {
        self.checked_duration_since(earlier).unwrap_or_default()
    }
}

impl Add<Duration> for Instant {
    type Output = Self;

    #[inline]
    fn add(self, rhs: Duration) -> Self {
        Instant(self.0 + rhs)
    }
}

impl AddAssign<Duration> for Instant {
    #[inline]
    fn add_assign(&mut self, rhs: Duration) {
        self.0 += rhs
    }
}

impl Sub<Duration> for Instant {
    type Output = Self;

    #[inline]
    fn sub(self, rhs: Duration) -> Self {
        Instant(self.0 - rhs)
    }
}

impl Sub<Instant> for Instant {
    type Output = Duration;

    #[inline]
    fn sub(self, rhs: Instant) -> Duration {
        self.duration_since(rhs)
    }
}

impl SubAssign<Duration> for Instant {
    #[inline]
    fn sub_assign(&mut self, rhs: Duration) {
        self.0 -= rhs
    }
}

fn duration_from_f64(millis: f64) -> Duration {
    Duration::from_millis(millis.trunc() as u64)
        + Duration::from_nanos((millis.fract() * 1.0e6) as u64)
}

#[cfg(all(feature = "stdweb", not(feature = "wasm-bindgen")))]
#[allow(unused_results)] // Needed because the js macro triggers it.
pub fn now() -> f64 {
    use stdweb::unstable::TryInto;

    // https://developer.mozilla.org/en-US/docs/Web/API/Performance/now
    #[cfg(not(feature = "inaccurate"))]
    let v = js! { return performance.now(); };
    #[cfg(feature = "inaccurate")]
    let v = js! { return Date.now(); };
    v.try_into().unwrap()
}

#[cfg(feature = "wasm-bindgen")]
pub fn now() -> f64 {
    #[cfg(not(feature = "inaccurate"))]
    let now = {
        use wasm_bindgen_rs::prelude::*;
        use wasm_bindgen_rs::JsCast;
        js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("performance"))
            .expect("failed to get performance from global object")
            .unchecked_into::<web_sys::Performance>()
            .now()
    };
    #[cfg(feature = "inaccurate")]
    let now = js_sys::Date::now();
    now
}

// The JS now function is in a module so it won't have to be renamed
#[cfg(not(any(feature = "wasm-bindgen", feature = "stdweb")))]
mod js {
    extern "C" {
        #[cfg(not(target_os = "emscripten"))]
        pub fn now() -> f64;
        #[cfg(target_os = "emscripten")]
        pub fn _emscripten_get_now() -> f64;
    }
}
// Make the unsafe extern function "safe" so it can be called like the other 'now' functions
#[cfg(not(any(feature = "wasm-bindgen", feature = "stdweb")))]
pub fn now() -> f64 {
    #[cfg(not(target_os = "emscripten"))]
    return unsafe { js::now() };
    #[cfg(target_os = "emscripten")]
    return unsafe { js::_emscripten_get_now() };
}

/// Returns the number of millisecods elapsed since January 1, 1970 00:00:00 UTC.
#[cfg(any(feature = "wasm-bindgen", feature = "stdweb"))]
fn get_time() -> f64 {
    #[cfg(feature = "wasm-bindgen")]
    return js_sys::Date::now();
    #[cfg(all(feature = "stdweb", not(feature = "wasm-bindgen")))]
    {
        let v = js! { return Date.now(); };
        return v.try_into().unwrap();
    }
}

#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
pub struct SystemTime(f64);

impl SystemTime {
    pub const UNIX_EPOCH: SystemTime = SystemTime(0.0);

    pub fn now() -> SystemTime {
        cfg_if::cfg_if! {
            if #[cfg(any(feature = "wasm-bindgen", feature = "stdweb"))] {
                SystemTime(get_time())
            } else {
                SystemTime(now())
            }
        }
    }

    pub fn duration_since(&self, earlier: SystemTime) -> Result<Duration, ()> {
        let dur_ms = self.0 - earlier.0;
        if dur_ms < 0.0 {
            return Err(());
        }
        Ok(Duration::from_millis(dur_ms as u64))
    }

    pub fn elapsed(&self) -> Result<Duration, ()> {
        self.duration_since(SystemTime::now())
    }

    pub fn checked_add(&self, duration: Duration) -> Option<SystemTime> {
        Some(*self + duration)
    }

    pub fn checked_sub(&self, duration: Duration) -> Option<SystemTime> {
        Some(*self - duration)
    }
}

impl Add<Duration> for SystemTime {
    type Output = SystemTime;

    fn add(self, other: Duration) -> SystemTime {
        SystemTime(self.0 + other.as_millis() as f64)
    }
}

impl Sub<Duration> for SystemTime {
    type Output = SystemTime;

    fn sub(self, other: Duration) -> SystemTime {
        SystemTime(self.0 - other.as_millis() as f64)
    }
}

impl AddAssign<Duration> for SystemTime {
    fn add_assign(&mut self, rhs: Duration) {
        *self = *self + rhs;
    }
}

impl SubAssign<Duration> for SystemTime {
    fn sub_assign(&mut self, rhs: Duration) {
        *self = *self - rhs;
    }
}
