// Copyright 2015-2019 Parity Technologies (UK) Ltd.
// This file is part of Parity Ethereum.

// Parity Ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// Parity Ethereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with Parity Ethereum.  If not, see <http://www.gnu.org/licenses/>.

#![warn(missing_docs)]

//! Parse ethereum client ID strings and provide querying functionality

use semver::Version;
use std::fmt;

/// Parity client string prefix
const LEGACY_CLIENT_ID_PREFIX: &str = "Parity";
const PARITY_CLIENT_ID_PREFIX: &str = "Parity-Ethereum";

lazy_static! {
/// Parity versions starting from this will accept block bodies requests
/// of 256 bodies
	static ref PARITY_CLIENT_LARGE_REQUESTS_VERSION: Version = Version::parse("2.4.0").unwrap();
}

/// Description of the software version running in a peer
/// according to https://github.com/ethereum/wiki/wiki/Client-Version-Strings
/// This structure as it is represents the format used by Parity clients. Other
/// vendors may provide additional fields.
#[derive(Clone,Debug,PartialEq,Eq,Serialize)]
pub struct ParityClientData {
	name: String,
	identity: Option<String>,
	semver: Version,
	os: String,
	compiler: String,

	// Capability flags, should be calculated in constructor
	can_handle_large_requests: bool,
}

/// Accessor methods for ParityClientData. This will probably
/// need to be abstracted away into a trait.
impl ParityClientData {
	fn new(
		name: String,
		identity: Option<String>,
		semver: Version,
		os: String,
		compiler: String,
	) -> Self {
		// Flags logic
		let can_handle_large_requests = &semver >= &PARITY_CLIENT_LARGE_REQUESTS_VERSION;

		// Instantiate and return
		ParityClientData {
			name: name,
			identity: identity,
			semver: semver,
			os: os,
			compiler: compiler,

			can_handle_large_requests: can_handle_large_requests,
		}
	}

	fn name(&self) -> &str {
		self.name.as_str()
	}

	fn identity(&self) -> Option<&str> {
		self.identity.as_ref().map(String::as_str)
	}

	fn semver(&self) -> &Version {
		&self.semver
	}

	fn os(&self) -> &str {
		self.os.as_str()
	}

	fn compiler(&self) -> &str {
		self.compiler.as_str()
	}

	fn can_handle_large_requests(&self) -> bool {
		self.can_handle_large_requests
	}
}

/// Enum describing the version of the software running on a peer.
#[derive(Clone,Debug,Eq,PartialEq,Serialize)]
pub enum ClientVersion {
	/// The peer runs software from parity and the string format is known
	ParityClient(
		/// The actual information fields: name, version, os, ...
		ParityClientData
	),
	/// The string ID is recognized as Parity but the overall format
	/// could not be parsed
	ParityUnknownFormat(String),
	/// Other software vendors than Parity
	Other(String),
}

impl Default for ClientVersion {
	fn default() -> Self {
		ClientVersion::Other("".to_owned())
	}
}

/// Provide information about what a particular version of a
/// peer software can do
pub trait ClientCapabilities {
	/// Parity versions before PARITY_CLIENT_LARGE_REQUESTS_VERSION would not
	/// check the accumulated size of a packet when building a response to a
	/// GET_BLOCK_BODIES request. If the packet was larger than a given limit,
	/// instead of sending fewer blocks no packet would get sent at all. Query
	/// if this version can handle requests for a large number of block bodies.
	fn can_handle_large_requests(&self) -> bool;

	/// Service transactions are specific to parity. Query if this version
	/// accepts them.
	fn accepts_service_transaction(&self) -> bool;
}

impl ClientCapabilities for ClientVersion {
	fn can_handle_large_requests(&self) -> bool {
		match self {
			ClientVersion::ParityClient(data) => data.can_handle_large_requests(),
			ClientVersion::ParityUnknownFormat(_) => false, // Play it safe
			ClientVersion::Other(_) => true // As far as we know
		}
	}

	fn accepts_service_transaction(&self) -> bool {
		match self {
			ClientVersion::ParityClient(_) => true,
			ClientVersion::ParityUnknownFormat(_) => true,
			ClientVersion::Other(_) => false
		}
	}

}

fn is_parity(client_id: &str) -> bool {
	client_id.starts_with(LEGACY_CLIENT_ID_PREFIX) || client_id.starts_with(PARITY_CLIENT_ID_PREFIX)
}

/// Parse known parity formats. Recognizes either a short format with four fields
/// or a long format which includes the same fields and an identity one.
fn parse_parity_format(client_version: &str) -> Result<ParityClientData, ()> {
	const PARITY_ID_STRING_MINIMUM_TOKENS: usize = 4;

	let tokens: Vec<&str> = client_version.split("/").collect();

	if tokens.len() < PARITY_ID_STRING_MINIMUM_TOKENS {
		return Err(())
	}

	let name = tokens[0];

	let identity = if tokens.len() - 3 > 1 {
		Some(tokens[1..(tokens.len() - 3)].join("/"))
	} else {
		None
	};

	let compiler = tokens[tokens.len() - 1];
	let os = tokens[tokens.len() - 2];

	// If version is in the right position and valid format return a valid
	// result. Otherwise return an error.
	get_number_from_version(tokens[tokens.len() - 3])
		.and_then(|v| Version::parse(v).ok())
		.map(|semver| ParityClientData::new(
			name.to_owned(),
			identity,
			semver,
			os.to_owned(),
			compiler.to_owned(),
		))
		.ok_or(())
}

/// Parse a version string and return the corresponding
/// ClientVersion. Only Parity clients are destructured right now, other
/// strings will just get wrapped in a variant so that the information is
/// not lost.
/// The parsing for parity may still fail, in which case return a ParityUnknownFormat with
/// the original version string. TryFrom would be a better trait to implement.
impl<T> From<T> for ClientVersion
where T: AsRef<str> {
	fn from(client_version: T) -> Self {
		let client_version_str: &str = client_version.as_ref();

		if !is_parity(client_version_str) {
			return ClientVersion::Other(client_version_str.to_owned());
		}

		if let Ok(data) = parse_parity_format(client_version_str) {
			ClientVersion::ParityClient(data)
		} else {
			ClientVersion::ParityUnknownFormat(client_version_str.to_owned())
		}
	}
}

fn format_parity_version_string(client_version: &ParityClientData, f: &mut fmt::Formatter) -> std::fmt::Result {
	let name = client_version.name();
	let semver = client_version.semver();
	let os = client_version.os();
	let compiler = client_version.compiler();

	match client_version.identity() {
		None => write!(f, "{}/v{}/{}/{}", name, semver, os, compiler),
		Some(identity) => write!(f, "{}/{}/v{}/{}/{}", name, identity, semver, os, compiler),
	}
}

impl fmt::Display for ClientVersion {
	fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result {
		match self {
			ClientVersion::ParityClient(data) => format_parity_version_string(data, f),
			ClientVersion::ParityUnknownFormat(id) => write!(f, "{}", id),
			ClientVersion::Other(id) => write!(f, "{}", id)
		}
	}
}

fn get_number_from_version(version: &str) -> Option<&str> {
	if version.starts_with("v") {
		return version.get(1..);
	}

	None
}

#[cfg(test)]
pub mod tests {
	use super::*;

	const PARITY_CLIENT_SEMVER: &str = "2.4.0";
	const PARITY_CLIENT_OLD_SEMVER: &str = "2.2.0";
	const PARITY_CLIENT_OS: &str = "linux";
	const PARITY_CLIENT_COMPILER: &str = "rustc";
	const PARITY_CLIENT_IDENTITY: &str = "ExpanseSOLO";
	const PARITY_CLIENT_MULTITOKEN_IDENTITY: &str = "ExpanseSOLO/abc/v1.2.3";


	fn make_default_version_string() -> String {
		format!(
			"{}/v{}/{}/{}",
			PARITY_CLIENT_ID_PREFIX,
			PARITY_CLIENT_SEMVER,
			PARITY_CLIENT_OS,
			PARITY_CLIENT_COMPILER
		)
	}

	fn make_default_long_version_string() -> String {
		format!(
			"{}/{}/v{}/{}/{}",
			PARITY_CLIENT_ID_PREFIX,
			PARITY_CLIENT_IDENTITY,
			PARITY_CLIENT_SEMVER,
			PARITY_CLIENT_OS,
			PARITY_CLIENT_COMPILER
		)
	}

	fn make_multitoken_identity_long_version_string() -> String {
		format!(
			"{}/{}/v{}/{}/{}",
			PARITY_CLIENT_ID_PREFIX,
			PARITY_CLIENT_MULTITOKEN_IDENTITY,
			PARITY_CLIENT_SEMVER,
			PARITY_CLIENT_OS,
			PARITY_CLIENT_COMPILER
		)
	}

	fn make_old_semver_version_string() -> String {
		format!(
			"{}/v{}/{}/{}",
			PARITY_CLIENT_ID_PREFIX,
			PARITY_CLIENT_OLD_SEMVER,
			PARITY_CLIENT_OS,
			PARITY_CLIENT_COMPILER
		)
	}

	#[test]
	pub fn client_version_when_from_empty_string_then_default() {
		let default = ClientVersion::default();

		assert_eq!(ClientVersion::from(""), default);
	}

	#[test]
	pub fn get_number_from_version_when_valid_then_number() {
		let version_string = format!("v{}", PARITY_CLIENT_SEMVER);

		assert_eq!(get_number_from_version(&version_string).unwrap(), PARITY_CLIENT_SEMVER);
	}

	#[test]
	pub fn client_version_when_str_parity_format_and_valid_then_all_fields_match() {
		let client_version_string = make_default_version_string();

		if let ClientVersion::ParityClient(client_version) = ClientVersion::from(client_version_string.as_str()) {
			assert_eq!(client_version.name(), PARITY_CLIENT_ID_PREFIX);
			assert_eq!(*client_version.semver(), Version::parse(PARITY_CLIENT_SEMVER).unwrap());
			assert_eq!(client_version.os(), PARITY_CLIENT_OS);
			assert_eq!(client_version.compiler(), PARITY_CLIENT_COMPILER);
		} else {
			panic!("shouldn't be here");
		}
	}

	#[test]
	pub fn client_version_when_str_parity_long_format_and_valid_then_all_fields_match() {
		let client_version_string = make_default_long_version_string();

		if let ClientVersion::ParityClient(client_version) = ClientVersion::from(client_version_string.as_str()) {
			assert_eq!(client_version.name(), PARITY_CLIENT_ID_PREFIX);
			assert_eq!(client_version.identity().unwrap(), PARITY_CLIENT_IDENTITY);
			assert_eq!(*client_version.semver(), Version::parse(PARITY_CLIENT_SEMVER).unwrap());
			assert_eq!(client_version.os(), PARITY_CLIENT_OS);
			assert_eq!(client_version.compiler(), PARITY_CLIENT_COMPILER);
		} else {
			panic!("shouldnt be here");
		}
	}

	#[test]
	pub fn client_version_when_str_parity_long_format_and_valid_and_identity_multiple_tokens_then_all_fields_match() {
		let client_version_string = make_multitoken_identity_long_version_string();

		if let ClientVersion::ParityClient(client_version) = ClientVersion::from(client_version_string.as_str()) {
			assert_eq!(client_version.name(), PARITY_CLIENT_ID_PREFIX);
			assert_eq!(client_version.identity().unwrap(), PARITY_CLIENT_MULTITOKEN_IDENTITY);
			assert_eq!(*client_version.semver(), Version::parse(PARITY_CLIENT_SEMVER).unwrap());
			assert_eq!(client_version.os(), PARITY_CLIENT_OS);
			assert_eq!(client_version.compiler(), PARITY_CLIENT_COMPILER);
		} else {
			panic!("shouldnt be here");
		}
	}

	#[test]
	pub fn client_version_when_string_parity_format_and_valid_then_all_fields_match() {
		let client_version_string: String = make_default_version_string();

		if let ClientVersion::ParityClient(client_version) = ClientVersion::from(client_version_string.as_str()) {
			assert_eq!(client_version.name(), PARITY_CLIENT_ID_PREFIX);
			assert_eq!(*client_version.semver(), Version::parse(PARITY_CLIENT_SEMVER).unwrap());
			assert_eq!(client_version.os(), PARITY_CLIENT_OS);
			assert_eq!(client_version.compiler(), PARITY_CLIENT_COMPILER);
		} else {
			panic!("shouldn't be here");
		}
	}

	#[test]
	pub fn client_version_when_parity_format_and_invalid_then_equals_parity_unknown_client_version_string() {
		// This is invalid because version has no leading 'v'
		let client_version_string = format!(
			"{}/{}/{}/{}",
			PARITY_CLIENT_ID_PREFIX,
			PARITY_CLIENT_SEMVER,
			PARITY_CLIENT_OS,
			PARITY_CLIENT_COMPILER);

		let client_version = ClientVersion::from(client_version_string.as_str());

		let parity_unknown = ClientVersion::ParityUnknownFormat(client_version_string.to_string());

		assert_eq!(client_version, parity_unknown);
	}

	#[test]
	pub fn client_version_when_parity_format_without_identity_and_missing_compiler_field_then_equals_parity_unknown_client_version_string() {
		let client_version_string = format!(
			"{}/v{}/{}",
			PARITY_CLIENT_ID_PREFIX,
			PARITY_CLIENT_SEMVER,
			PARITY_CLIENT_OS,
			);

		let client_version = ClientVersion::from(client_version_string.as_str());

		let parity_unknown = ClientVersion::ParityUnknownFormat(client_version_string.to_string());

		assert_eq!(client_version, parity_unknown);
	}

	#[test]
	pub fn client_version_when_parity_format_with_identity_and_missing_compiler_field_then_equals_parity_unknown_client_version_string() {
		let client_version_string = format!(
			"{}/{}/v{}/{}",
			PARITY_CLIENT_ID_PREFIX,
			PARITY_CLIENT_IDENTITY,
			PARITY_CLIENT_SEMVER,
			PARITY_CLIENT_OS,
			);

		let client_version = ClientVersion::from(client_version_string.as_str());

		let parity_unknown = ClientVersion::ParityUnknownFormat(client_version_string.to_string());

		assert_eq!(client_version, parity_unknown);
	}

	#[test]
	pub fn client_version_when_not_parity_format_and_valid_then_other_with_client_version_string() {
		let client_version_string = "Geth/main.jnode.network/v1.8.21-stable-9dc5d1a9/linux";

		let client_version = ClientVersion::from(client_version_string);

		assert_eq!(client_version, ClientVersion::Other(client_version_string.to_string()));
	}

	#[test]
	pub fn client_version_when_parity_format_and_valid_then_to_string_equal() {
		let client_version_string: String = make_default_version_string();

		let client_version = ClientVersion::from(client_version_string.as_str());

		assert_eq!(client_version.to_string(), client_version_string);
	}

	#[test]
	pub fn client_version_when_other_then_to_string_equal_input_string() {
		let client_version_string: String = "Other".to_string();

		let client_version = ClientVersion::from("Other");

		assert_eq!(client_version.to_string(), client_version_string);
	}

	#[test]
	pub fn client_capabilities_when_parity_old_version_then_handles_large_requests_false() {
		let client_version_string: String = make_old_semver_version_string();

		let client_version = ClientVersion::from(client_version_string.as_str());

		assert!(!client_version.can_handle_large_requests());
	}

	#[test]
	pub fn client_capabilities_when_parity_beta_version_then_not_handles_large_requests_true() {
		let client_version_string: String = format!(
			"{}/v{}/{}/{}",
			"Parity-Ethereum",
			"2.4.0-beta",
			"x86_64-linux-gnu",
			"rustc1.31.1")
			.to_string();

		let client_version = ClientVersion::from(client_version_string.as_str());

		assert!(!client_version.can_handle_large_requests());
	}

	#[test]
	pub fn client_version_when_to_owned_then_both_objects_equal() {
		let client_version_string: String = make_old_semver_version_string();

		let origin = ClientVersion::from(client_version_string.as_str());

		let borrowed = &origin;

		let owned = origin.to_owned();

		assert_eq!(*borrowed, owned);
	}

	#[test]
	fn client_version_accepts_service_transaction_for_different_versions() {
		assert!(!ClientVersion::from("Geth").accepts_service_transaction());
		assert!(ClientVersion::from("Parity-Ethereum/v2.6.0/linux/rustc").accepts_service_transaction());
		assert!(ClientVersion::from("Parity-Ethereum/ABCDEFGH/v2.7.3/linux/rustc").accepts_service_transaction());
	}

	#[test]
	fn is_parity_when_parity_then_true() {
		let client_id = format!("{}/", PARITY_CLIENT_ID_PREFIX);

		assert!(is_parity(&client_id));
	}

	#[test]
	fn is_parity_when_empty_then_false() {
		let client_id = "";

		assert!(!is_parity(&client_id));
	}

	#[test]
	fn is_parity_when_other_then_false() {
		let client_id = "other";

		assert!(!is_parity(&client_id));
	}
}
