// Copyright 2018 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Grin server implementation, glues the different parts of the system (mostly
//! the peer-to-peer server, the blockchain and the transaction pool) and acts
//! as a facade.

use std::net::SocketAddr;
use std::sync::{Arc, RwLock};
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time;

use common::adapters::*;
use api;
use chain;
use core::{consensus, genesis, global};
use core::core::target::Difficulty;
use core::core::hash::Hashed;
use grin::dandelion_monitor;
use mining::miner;
use mining::stratumserver;
use p2p;
use pool;
use grin::seed;
use grin::sync;
use common::types::*;
use common::stats::*;
use pow;
use util::LOGGER;

/// Grin server holding internal structures.
pub struct Server {
	/// server config
	pub config: ServerConfig,
	/// handle to our network server
	pub p2p: Arc<p2p::Server>,
	/// data store access
	pub chain: Arc<chain::Chain>,
	/// in-memory transaction pool
	tx_pool: Arc<RwLock<pool::TransactionPool<PoolToChainAdapter>>>,
	/// Whether we're currently syncing
	currently_syncing: Arc<AtomicBool>,
	/// To be passed around to collect stats and info
	state_info: ServerStateInfo,
	/// Stop flag
	stop: Arc<AtomicBool>,
}

impl Server {
	/// Instantiates and starts a new server. Optionally takes a callback
	/// for the server to send an ARC copy of itself, to allow another process
	/// to poll info about the server status
	pub fn start<F>(config: ServerConfig, mut info_callback: F) -> Result<(), Error>
	where
		F: FnMut(Arc<Server>),
	{
		let mut mining_config = config.mining_config.clone();
		let serv = Arc::new(Server::new(config)?);

		if mining_config.as_mut().unwrap().enable_mining {
			{
				let mut mining_stats = serv.state_info.mining_stats.write().unwrap();
				mining_stats.is_enabled = true;
			}
			serv.start_miner(mining_config.clone().unwrap());
		}
		let enable_stratum_server = mining_config.as_mut().unwrap().enable_stratum_server;
		if let Some(s) = enable_stratum_server {
			if s {
				{
					let mut stratum_stats = serv.state_info.stratum_stats.write().unwrap();
					stratum_stats.is_enabled = true;
				}
				serv.start_stratum_server(mining_config.clone().unwrap());
			}
		}

		info_callback(serv.clone());
		loop {
			thread::sleep(time::Duration::from_secs(1));
			if serv.stop.load(Ordering::Relaxed) {
				return Ok(());
			}
		}
	}

	/// Instantiates a new server associated with the provided future reactor.
	pub fn new(mut config: ServerConfig) -> Result<Server, Error> {
		// Defaults to None (optional) in config file.
		// This translates to false here.
		let archive_mode = match config.archive_mode {
			None => false,
			Some(b) => b,
		};

		// If archive mode is enabled then the flags should contains the FULL_HIST flag
		if archive_mode && !config.capabilities.contains(p2p::Capabilities::FULL_HIST) {
			config.capabilities.insert(p2p::Capabilities::FULL_HIST);
		}

		let stop = Arc::new(AtomicBool::new(false));

		let pool_adapter = Arc::new(PoolToChainAdapter::new());
		let pool_net_adapter = Arc::new(PoolToNetAdapter::new());
		let tx_pool = Arc::new(RwLock::new(pool::TransactionPool::new(
			config.pool_config.clone(),
			pool_adapter.clone(),
			pool_net_adapter.clone(),
		)));

		let chain_adapter = Arc::new(ChainToPoolAndNetAdapter::new(tx_pool.clone()));

		let genesis = match config.chain_type {
			global::ChainTypes::Testnet1 => genesis::genesis_testnet1(),
			global::ChainTypes::Testnet2 => genesis::genesis_testnet2(),
			global::ChainTypes::AutomatedTesting => genesis::genesis_dev(),
			_ => pow::mine_genesis_block(config.mining_config.clone())?,
		};

		info!(LOGGER, "Starting server, genesis block: {}", genesis.hash());

		let shared_chain = Arc::new(chain::Chain::init(
			config.db_root.clone(),
			chain_adapter.clone(),
			genesis.clone(),
			pow::verify_size,
		)?);

		pool_adapter.set_chain(Arc::downgrade(&shared_chain));

		let currently_syncing = Arc::new(AtomicBool::new(true));
		let awaiting_peers = Arc::new(AtomicBool::new(false));

		let net_adapter = Arc::new(NetToChainAdapter::new(
			currently_syncing.clone(),
			Arc::downgrade(&shared_chain),
			tx_pool.clone(),
			config.clone(),
		));

		let block_1_hash = match shared_chain.get_header_by_height(1) {
			Ok(header) => Some(header.hash()),
			Err(_) => None,
		};

		let p2p_config = config.p2p_config.clone();
		let p2p_server = Arc::new(p2p::Server::new(
			config.db_root.clone(),
			config.capabilities,
			p2p_config,
			net_adapter.clone(),
			genesis.hash(),
			stop.clone(),
			archive_mode,
			block_1_hash,
		)?);
		chain_adapter.init(Arc::downgrade(&p2p_server.peers));
		pool_net_adapter.init(Arc::downgrade(&p2p_server.peers));
		net_adapter.init(Arc::downgrade(&p2p_server.peers));

		if config.seeding_type.clone() != Seeding::Programmatic {
			let seeder = match config.seeding_type.clone() {
				Seeding::None => {
					warn!(
						LOGGER,
						"No seed configured, will stay solo until connected to"
					);
					seed::predefined_seeds(vec![])
				}
				Seeding::List => seed::predefined_seeds(config.seeds.as_mut().unwrap().clone()),
				Seeding::DNSSeed => seed::dns_seeds(),
				Seeding::WebStatic => seed::web_seeds(),
				_ => unreachable!(),
			};
			seed::connect_and_monitor(
				p2p_server.clone(),
				config.capabilities,
				seeder,
				stop.clone(),
			);
		}

		// Defaults to None (optional) in config file.
		// This translates to false here so we do not skip by default.
		let skip_sync_wait = match config.skip_sync_wait {
			None => false,
			Some(b) => b,
		};

		sync::run_sync(
			currently_syncing.clone(),
			awaiting_peers.clone(),
			p2p_server.peers.clone(),
			shared_chain.clone(),
			skip_sync_wait,
			archive_mode,
			stop.clone(),
		);

		let p2p_inner = p2p_server.clone();
		let _ = thread::Builder::new()
			.name("p2p-server".to_string())
			.spawn(move || p2p_inner.listen());

		info!(LOGGER, "Starting rest apis at: {}", &config.api_http_addr);

		api::start_rest_apis(
			config.api_http_addr.clone(),
			Arc::downgrade(&shared_chain),
			Arc::downgrade(&tx_pool),
			Arc::downgrade(&p2p_server.peers),
		);

		info!(
			LOGGER,
			"Starting dandelion monitor: {}", &config.api_http_addr
		);
		dandelion_monitor::monitor_transactions(
			config.pool_config.clone(),
			tx_pool.clone(),
			stop.clone(),
		);

		warn!(LOGGER, "Grin server started.");
		Ok(Server {
			config: config,
			p2p: p2p_server,
			chain: shared_chain,
			tx_pool: tx_pool,
			currently_syncing: currently_syncing,
			state_info: ServerStateInfo {
				awaiting_peers: awaiting_peers,
				..Default::default()
			},
			stop: stop,
		})
	}

	/// Asks the server to connect to a peer at the provided network address.
	pub fn connect_peer(&self, addr: SocketAddr) -> Result<(), Error> {
		self.p2p.connect(&addr)?;
		Ok(())
	}

	/// Number of peers
	pub fn peer_count(&self) -> u32 {
		self.p2p.peers.peer_count()
	}

	/// Start mining for blocks on a separate thread. Uses toy miner by default,
	/// mostly for testing, but can also load a plugin from cuckoo-miner
	pub fn start_miner(&self, config: pow::types::MinerConfig) {
		let cuckoo_size = global::sizeshift();
		let proof_size = global::proofsize();
		let currently_syncing = self.currently_syncing.clone();

		let mut miner = miner::Miner::new(
			config.clone(),
			self.chain.clone(),
			self.tx_pool.clone(),
			self.stop.clone(),
		);
		let mining_stats = self.state_info.mining_stats.clone();
		miner.set_debug_output_id(format!("Port {}", self.config.p2p_config.port));
		let _ = thread::Builder::new()
			.name("miner".to_string())
			.spawn(move || {
				// TODO push this down in the run loop so miner gets paused anytime we
				// decide to sync again
				let secs_5 = time::Duration::from_secs(5);
				while currently_syncing.load(Ordering::Relaxed) {
					thread::sleep(secs_5);
				}
				miner.run_loop(config.clone(), mining_stats, cuckoo_size as u32, proof_size);
			});
	}

	/// Start a minimal "stratum" mining service on a separate thread
	pub fn start_stratum_server(&self, config: pow::types::MinerConfig) {
		let cuckoo_size = global::sizeshift();
		let proof_size = global::proofsize();
		let currently_syncing = self.currently_syncing.clone();

		let mut stratum_server = stratumserver::StratumServer::new(
			config.clone(),
			self.chain.clone(),
			self.tx_pool.clone(),
		);
		let stratum_stats = self.state_info.stratum_stats.clone();
		let _ = thread::Builder::new()
			.name("stratum_server".to_string())
			.spawn(move || {
				let secs_5 = time::Duration::from_secs(5);
				while currently_syncing.load(Ordering::Relaxed) {
					thread::sleep(secs_5);
				}
				stratum_server.run_loop(
					config.clone(),
					stratum_stats,
					cuckoo_size as u32,
					proof_size,
				);
			});
	}

	/// The chain head
	pub fn head(&self) -> chain::Tip {
		self.chain.head().unwrap()
	}

	/// The head of the block header chain
	pub fn header_head(&self) -> chain::Tip {
		self.chain.get_header_head().unwrap()
	}

	/// Returns a set of stats about this server. This and the ServerStats
	/// structure
	/// can be updated over time to include any information needed by tests or
	/// other
	/// consumers
	pub fn get_server_stats(&self) -> Result<ServerStats, Error> {
		let mining_stats = self.state_info.mining_stats.read().unwrap().clone();
		let stratum_stats = self.state_info.stratum_stats.read().unwrap().clone();
		let awaiting_peers = self.state_info.awaiting_peers.load(Ordering::Relaxed);

		// Fill out stats on our current difficulty calculation
		// TODO: check the overhead of calculating this again isn't too much
		// could return it from next_difficulty, but would rather keep consensus
		// code clean. This may be handy for testing but not really needed
		// for release
		let diff_stats = {
			let diff_iter = self.chain.difficulty_iter();
			let last_blocks: Vec<Result<(u64, Difficulty), consensus::TargetError>> =
				global::difficulty_data_to_vector(diff_iter)
					.into_iter()
					.skip(consensus::MEDIAN_TIME_WINDOW as usize)
					.take(consensus::DIFFICULTY_ADJUST_WINDOW as usize)
					.collect();

			let mut last_time = last_blocks[0].clone().unwrap().0;
			let tip_height = self.chain.head().unwrap().height as i64;
			let earliest_block_height = tip_height as i64 - last_blocks.len() as i64;

			let mut i = 1;

			let diff_entries: Vec<DiffBlock> = last_blocks
				.iter()
				.skip(1)
				.map(|n| {
					let (time, diff) = n.clone().unwrap();
					let dur = time - last_time;
					let height = earliest_block_height + i + 1;
					i += 1;
					last_time = time;
					DiffBlock {
						block_number: height,
						difficulty: diff.into_num(),
						time: time,
						duration: dur,
					}
				})
				.collect();

			let block_time_sum = diff_entries.iter().fold(0, |sum, t| sum + t.duration);
			let block_diff_sum = diff_entries.iter().fold(0, |sum, d| sum + d.difficulty);
			DiffStats {
				height: tip_height as u64,
				last_blocks: diff_entries,
				average_block_time: block_time_sum / (consensus::DIFFICULTY_ADJUST_WINDOW - 1),
				average_difficulty: block_diff_sum / (consensus::DIFFICULTY_ADJUST_WINDOW - 1),
				window_size: consensus::DIFFICULTY_ADJUST_WINDOW,
			}
		};

		let peer_stats = self.p2p
			.peers
			.connected_peers()
			.into_iter()
			.map(|p| {
				let p = p.read().unwrap();
				PeerStats::from_peer(&p)
			})
			.collect();
		Ok(ServerStats {
			peer_count: self.peer_count(),
			head: self.head(),
			header_head: self.header_head(),
			is_syncing: self.currently_syncing.load(Ordering::Relaxed),
			awaiting_peers: awaiting_peers,
			mining_stats: mining_stats,
			stratum_stats: stratum_stats,
			peer_stats: peer_stats,
			diff_stats: diff_stats,
		})
	}

	/// Stop the server.
	pub fn stop(&self) {
		self.p2p.stop();
		self.stop.store(true, Ordering::Relaxed);
	}
}
