/*
 * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET)
 * 
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */
using System;
using System.ServiceModel;
using System.Threading;
using SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Communication.Messages;
using SafeExamBrowser.Contracts.Communication.Responses;
using SafeExamBrowser.Contracts.Logging;
namespace SafeExamBrowser.Core.Communication
{
	/// 
	/// The base implementation of an . Runs the host on a new, separate thread.
	/// 
	[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Single)]
	public abstract class BaseHost : ICommunication, ICommunicationHost
	{
		private const int TWO_SECONDS = 2000;
		private readonly object @lock = new object();
		private string address;
		private ILogger logger;
		private ServiceHost host;
		private Thread hostThread;
		protected Guid? CommunicationToken { get; private set; }
		protected ILogger Logger { get; private set; }
		public bool IsRunning
		{
			get
			{
				lock (@lock)
				{
					return host?.State == CommunicationState.Opened;
				}
			}
		}
		public BaseHost(string address, ILogger logger)
		{
			this.address = address;
			this.logger = logger;
		}
		protected abstract bool OnConnect(Guid? token);
		protected abstract void OnDisconnect();
		protected abstract Response OnReceive(Message message);
		protected abstract Response OnReceive(SimpleMessagePurport message);
		public ConnectionResponse Connect(Guid? token = null)
		{
			lock (@lock)
			{
				logger.Debug($"Received connection request with authentication token '{token}'.");
				var response = new ConnectionResponse();
				var connected = OnConnect(token);
				if (connected)
				{
					response.CommunicationToken = CommunicationToken = Guid.NewGuid();
					response.ConnectionEstablished = true;
				}
				logger.Debug($"{(connected ? "Accepted" : "Denied")} connection request.");
				return response;
			}
		}
		public DisconnectionResponse Disconnect(DisconnectionMessage message)
		{
			lock (@lock)
			{
				var response = new DisconnectionResponse();
				logger.Debug($"Received disconnection request with message '{ToString(message)}'.");
				if (IsAuthorized(message?.CommunicationToken))
				{
					OnDisconnect();
					CommunicationToken = null;
					response.ConnectionTerminated = true;
				}
				return response;
			}
		}
		public Response Send(Message message)
		{
			lock (@lock)
			{
				var response = new SimpleResponse(SimpleResponsePurport.Unauthorized) as Response;
				if (IsAuthorized(message?.CommunicationToken))
				{
					switch (message)
					{
						case SimpleMessage simpleMessage when simpleMessage.Purport == SimpleMessagePurport.Ping:
							response = new SimpleResponse(SimpleResponsePurport.Acknowledged);
							break;
						case SimpleMessage simpleMessage:
							response = OnReceive(simpleMessage.Purport);
							break;
						default:
							response = OnReceive(message);
							break;
					}
				}
				logger.Debug($"Received message '{ToString(message)}', sending response '{ToString(response)}'.");
				return response;
			}
		}
		public void Start()
		{
			lock (@lock)
			{
				var exception = default(Exception);
				var startedEvent = new AutoResetEvent(false);
				hostThread = new Thread(() => TryStartHost(startedEvent, out exception));
				hostThread.SetApartmentState(ApartmentState.STA);
				hostThread.IsBackground = true;
				hostThread.Start();
				var success = startedEvent.WaitOne(TWO_SECONDS);
				if (!success)
				{
					throw new CommunicationException($"Failed to start communication host for endpoint '{address}' within {TWO_SECONDS / 1000} seconds!", exception);
				}
			}
		}
		public void Stop()
		{
			lock (@lock)
			{
				var success = TryStopHost(out Exception exception);
				if (success)
				{
					logger.Debug($"Terminated communication host for endpoint '{address}'.");
				}
				else
				{
					throw new CommunicationException($"Failed to terminate communication host for endpoint '{address}'!", exception);
				}
			}
		}
		private bool IsAuthorized(Guid? token)
		{
			return CommunicationToken == token;
		}
		private void TryStartHost(AutoResetEvent startedEvent, out Exception exception)
		{
			exception = null;
			try
			{
				host = new ServiceHost(this);
				host.AddServiceEndpoint(typeof(ICommunication), new NetNamedPipeBinding(NetNamedPipeSecurityMode.Transport), address);
				host.Closed += Host_Closed;
				host.Closing += Host_Closing;
				host.Faulted += Host_Faulted;
				host.Opened += Host_Opened;
				host.Opening += Host_Opening;
				host.UnknownMessageReceived += Host_UnknownMessageReceived;
				host.Open();
				logger.Debug($"Successfully started communication host for endpoint '{address}'.");
				startedEvent.Set();
			}
			catch (Exception e)
			{
				exception = e;
			}
		}
		private bool TryStopHost(out Exception exception)
		{
			var success = false;
			exception = null;
			try
			{
				host?.Close();
				success = hostThread.Join(TWO_SECONDS);
			}
			catch (Exception e)
			{
				exception = e;
				success = false;
			}
			return success;
		}
		private void Host_Closed(object sender, EventArgs e)
		{
			logger.Debug("Communication host has been closed.");
		}
		private void Host_Closing(object sender, EventArgs e)
		{
			logger.Debug("Communication host is closing...");
		}
		private void Host_Faulted(object sender, EventArgs e)
		{
			logger.Error("Communication host has faulted!");
		}
		private void Host_Opened(object sender, EventArgs e)
		{
			logger.Debug("Communication host has been opened.");
		}
		private void Host_Opening(object sender, EventArgs e)
		{
			logger.Debug("Communication host is opening...");
		}
		private void Host_UnknownMessageReceived(object sender, UnknownMessageReceivedEventArgs e)
		{
			logger.Warn($"Communication host has received an unknown message: {e?.Message}.");
		}
		private string ToString(Message message)
		{
			return message != null ? message.ToString() : "";
		}
		private string ToString(Response response)
		{
			return response != null ? response.ToString() : "";
		}
	}
}