Documentation
use std::time::{
	Duration,
	SystemTime
};
use crate::message::PingOrPong;
use crate::util::*;
use super::{
	Address,
	status::{
		elapsed,
		remaining
	},
	data::{
		reping_delay,
		TIME_TO_PING,
		TIME_TO_DOUBLE_PING,
//		REPING_DELAY_MAX,
//		REACH_OUT_TIME,
//		INDIRECT_TIMEOUT,
		PINGS_UNTIL_SUSPICIOUS,
		PINGS_UNTIL_UNREACHABLE
	}
};
use serde::{Serialize, Deserialize};

use LinkState::*;

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Link {
	pub address: Address,
	pub state: LinkState,

	/// The last time we received a packet
	pub seen: Option<SystemTime>,

	/// When we last sent a ping
	pub pinged: Option<SystemTime>,
	/// This is technically the round-trip-time (RTT)
	pub latency: Option<Duration>
}

#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum LinkState {
	HasPinged,
	HasPonged,
	BeenPinged(u8),
	Suspicious(u8)
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LinkTickResult {
	Ok(Duration),
	PingDue(Duration),
	LinkSuspicious(Duration),
	LinkUnreachable
}

impl LinkState {

	pub fn been_pinged(&mut self) {
		//TODO: maybe check if unpongeds exceeds the threshold
		//No way to handle failure
		match self {
			HasPinged |
			HasPonged => *self = LinkState::BeenPinged(1),
			BeenPinged(unpongeds) => *unpongeds += 1,
			Suspicious(unpongeds) => *unpongeds += 1
		}
	}

}

impl Link {

	pub fn new(address: Address) -> Self {
		Self {
			address,
			state: LinkState::BeenPinged(0),
			seen: None,
			pinged: None,
			latency: None
		}
	}
/*
 *	TIMED UPDATES
 */

	#[tracing::instrument]
	#[must_use = "assumes due ping will be sent"]
	pub fn tick(&mut self) -> LinkTickResult {
		use LinkTickResult as Ltr;
		match self.state {
			// Our turn to ping
			HasPinged => match remaining(self.seen, TIME_TO_PING) {
				Some(t) => Ltr::Ok(t),
				None => {
					//TODO: is this fine?
					self.pinged.set_now();
					self.state = BeenPinged(1);
					Ltr::PingDue(reping_delay(0))
				}
			},
			// Their turn to ping, wait a little longer
			HasPonged => match remaining(self.seen, TIME_TO_DOUBLE_PING) {
				Some(t) => Ltr::Ok(t),
				None => {
					//TODO: is this fine?
					self.pinged.set_now();
					self.state = BeenPinged(1);
					Ltr::PingDue(reping_delay(0))
				}
			},
			//TODO: Link may have already become suspicious/failed
			// If pings keep being sent out before the delay runs out
			// Reping delay should only increase after the ping wasn't ponged
			BeenPinged(upp) => match remaining(self.pinged, reping_delay(upp)) {
				Some(t) => Ltr::Ok(t),
				None if upp >= PINGS_UNTIL_SUSPICIOUS => {
					//TODO: is this fine?
					self.pinged.set_now();
					self.state = Suspicious(upp + 1);
					Ltr::LinkSuspicious(reping_delay(upp + 1))
				},
				None => {
					//TODO: is this fine?
					self.pinged.set_now();
					self.state = BeenPinged(upp + 1);
					Ltr::PingDue(reping_delay(upp + 1))
				}
			},
			Suspicious(upp) => match remaining(self.pinged, reping_delay(upp)) {
				Some(t) => Ltr::Ok(t),
				None if upp >= PINGS_UNTIL_UNREACHABLE => Ltr::LinkUnreachable,
				None => {
					//TODO: is this fine?
					self.pinged.set_now();
					self.state = Suspicious(upp + 1);
					Ltr::PingDue(reping_delay(upp + 1))
				}
			}
		}
	}

/*
 *	HELPER
 */

	pub fn been_pinged(&mut self) {
		self.pinged.set_now();
		self.state.been_pinged();
	}

	pub fn has_pnged(&mut self, png: PingOrPong) {
		self.seen.set_now();
		match png {
			PingOrPong::Ping => {
				self.state = LinkState::HasPinged;
			},
			PingOrPong::Pong => {
				self.state = LinkState::HasPonged;
				self.latency = elapsed(self.pinged);
			}
		}
	}
}

impl std::fmt::Display for Link {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		write!(f, "{}", self.address)?;
		if self.state != BeenPinged(0) {
			write!(f, " ({:?})", self.state)?;
		}

		if let Some(seen) = self.seen {
			write!(f, ", seen ")?;
			fmt_time(f, seen)?;
		}
		if let Some(pinged) = self.pinged {
			write!(f, ", pinged ")?;
			fmt_time(f, pinged)?;
		}
		if let Some(latency) = self.latency {
			write!(f, ", latency {latency:.2?}")?;
		}
		Ok(())
	}
}