/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */

using System;
using System.Threading;
using Apache.NMS.ActiveMQ.Commands;
using Apache.NMS.ActiveMQ.Threads;
using Apache.NMS.ActiveMQ.Util.Synchronization;
using Apache.NMS.Util;

namespace Apache.NMS.ActiveMQ.Transport
{
	/// <summary>
	/// This class make sure that the connection is still alive,
	/// by monitoring the reception of commands from the peer of
	/// the transport.
	/// </summary>
	public class InactivityMonitor : TransportFilter
	{
		private readonly Atomic<bool> monitorStarted = new Atomic<bool>(false);

		private readonly Atomic<bool> commandSent = new Atomic<bool>(false);
		private readonly Atomic<bool> commandReceived = new Atomic<bool>(false);

		private readonly Atomic<bool> failed = new Atomic<bool>(false);
		private readonly Atomic<bool> inRead = new Atomic<bool>(false);
		private readonly Atomic<bool> inWrite = new Atomic<bool>(false);

		private CompositeTaskRunner asyncTasks;
		private AsyncSignalReadErrorkTask asyncErrorTask;
		private AsyncWriteTask asyncWriteTask;

		private readonly Mutex monitor = new Mutex();

		private Timer connectionCheckTimer;

		private DateTime lastReadCheckTime;

		private static int id = 0;
		private readonly int instanceId = 0;
		private bool disposing = false;

		private long readCheckTime;
		public long ReadCheckTime
		{
			get { return this.readCheckTime; }
			set { this.readCheckTime = value; }
		}

		private long writeCheckTime;
		public long WriteCheckTime
		{
			get { return this.writeCheckTime; }
			set { this.writeCheckTime = value; }
		}

		private long initialDelayTime;
		public long InitialDelayTime
		{
			get { return this.initialDelayTime; }
			set { this.initialDelayTime = value; }
		}

		private readonly Atomic<bool> keepAliveResponseRequired = new Atomic<bool>(false);
		public bool KeepAliveResponseRequired
		{
			get { return this.keepAliveResponseRequired.Value; }
			set { keepAliveResponseRequired.Value = value; }
		}

		// Local and remote Wire Format Information
		private WireFormatInfo localWireFormatInfo;
		private WireFormatInfo remoteWireFormatInfo;

		/// <summary>
		/// Constructor or the Inactivity Monitor
		/// </summary>
		/// <param name="next"></param>
		public InactivityMonitor(ITransport next)
			: base(next)
		{
			this.instanceId = ++id;
			Tracer.DebugFormat("Creating Inactivity Monitor: {0}", instanceId);
		}

		~InactivityMonitor()
		{
			Dispose(false);
		}

		protected override void Dispose(bool disposing)
		{
			if(disposing)
			{
				// get rid of unmanaged stuff
			}

			lock(monitor)
			{
				this.localWireFormatInfo = null;
				this.remoteWireFormatInfo = null;
				this.disposing = true;
				StopMonitorThreads();
			}

			base.Dispose(disposing);
		}

		public void CheckConnection(object state)
		{
			// First see if we have written or can write.
			WriteCheck();

			// Now check is we've read anything, if not then we send
			// a new KeepAlive with response required.
			ReadCheck();
		}

		#region WriteCheck Related
		/// <summary>
		/// Check the write to the broker
		/// </summary>
		public void WriteCheck()
		{
			if(this.inWrite.Value || this.failed.Value)
			{
				Tracer.DebugFormat("InactivityMonitor[{0}]: is in write or already failed.", instanceId);
				return;
			}

			CompositeTaskRunner taskRunner = this.asyncTasks;

			if(!commandSent.Value)
			{
				Tracer.DebugFormat("InactivityMonitor[{0}]: No Message sent since last write check. Sending a KeepAliveInfo.", instanceId);
				if(null != this.asyncWriteTask)
				{
					this.asyncWriteTask.IsPending = true;
				}

				if (this.monitorStarted.Value && taskRunner != null) 
				{
					taskRunner.Wakeup();
				}
			}
			else
			{
				Tracer.DebugFormat("InactivityMonitor[{0}]: Message sent since last write check. Resetting flag.", instanceId);
			}

			commandSent.Value = false;
		}
		#endregion

		#region ReadCheck Related
		public void ReadCheck()
		{
			DateTime now = DateTime.Now;
			TimeSpan elapsed = now - this.lastReadCheckTime;
			CompositeTaskRunner taskRunner = this.asyncTasks;

			if(!AllowReadCheck(elapsed))
			{
				Tracer.Debug("InactivityMonitor[" + instanceId + "]: A read check is not currently allowed.");
				return;
			}

			this.lastReadCheckTime = now;

			if(this.inRead.Value || this.failed.Value)
			{
				Tracer.DebugFormat("InactivityMonitor[{0}]: A receive is in progress or already failed.", instanceId);
				return;
			}

			if(!commandReceived.Value)
			{
				Tracer.DebugFormat("InactivityMonitor[{0}]: No message received since last read check! Sending an InactivityException!", instanceId);
				if(null != this.asyncErrorTask)
				{
					this.asyncErrorTask.IsPending = true;
				}

				if (this.monitorStarted.Value && taskRunner != null) 
				{
					taskRunner.Wakeup();
				}
			}
			else
			{
				commandReceived.Value = false;
			}
		}

		/// <summary>
		/// Checks if we should allow the read check(if less than 90% of the read
		/// check time elapsed then we dont do the readcheck
		/// </summary>
		/// <param name="elapsed"></param>
		/// <returns></returns>
		public bool AllowReadCheck(TimeSpan elapsed)
		{
			return (elapsed.TotalMilliseconds > (readCheckTime * 9 / 10));
		}
		#endregion

		public override void Stop()
		{
			StopMonitorThreads();
			next.Stop();
		}

		protected override async System.Threading.Tasks.Task OnCommand(ITransport sender, Command command)
		{
			commandReceived.Value = true;
			inRead.Value = true;
			try
			{
				if(command.IsKeepAliveInfo)
				{
					KeepAliveInfo info = command as KeepAliveInfo;
					if(info.ResponseRequired)
					{
						try
						{
							info.ResponseRequired = false;
							Oneway(info);
						}
						catch(IOException ex)
						{
							OnException(this, ex);
						}
					}
				}
				else if(command.IsWireFormatInfo)
				{
					lock(monitor)
					{
						remoteWireFormatInfo = command as WireFormatInfo;
						try
						{
							StartMonitorThreads();
						}
						catch(IOException ex)
						{
							OnException(this, ex);
						}
					}
				}
				await base.OnCommand(sender, command).Await();
			}
			finally
			{
				inRead.Value = false;
			}
		}

		public override void Oneway(Command command)
		{
			// Disable inactivity monitoring while processing a command.
			// synchronize this method - its not synchronized
			// further down the transport stack and gets called by more
			// than one thread  by this class
			lock(inWrite)
			{
				inWrite.Value = true;
				try
				{
					if(failed.Value)
					{
						throw new IOException("Channel was inactive for too long: " + next.RemoteAddress.ToString());
					}
					if(command.IsWireFormatInfo)
					{
						lock(monitor)
						{
							localWireFormatInfo = command as WireFormatInfo;
							StartMonitorThreads();
						}
					}
					next.Oneway(command);
				}
				finally
				{
					commandSent.Value = true;
					inWrite.Value = false;
				}
			}
		}

		protected override void OnException(ITransport sender, Exception command)
		{
			if(failed.CompareAndSet(false, true) && !this.disposing)
			{
				Tracer.DebugFormat("Exception received in the Inactivity Monitor: {0}", command.Message);
				StopMonitorThreads();
				base.OnException(sender, command);
			}
		}

		private void StartMonitorThreads()
		{
			lock(monitor)
			{
				if(this.IsDisposed || this.disposing)
				{
					return;
				}

				if(monitorStarted.Value)
				{
					return;
				}

				if(localWireFormatInfo == null)
				{
					return;
				}

				if(remoteWireFormatInfo == null)
				{
					return;
				}

				readCheckTime =
					Math.Min(
						localWireFormatInfo.MaxInactivityDuration,
						remoteWireFormatInfo.MaxInactivityDuration);
				initialDelayTime = remoteWireFormatInfo.MaxInactivityDurationInitialDelay > 0 ?
					Math.Min(localWireFormatInfo.MaxInactivityDurationInitialDelay,
						     remoteWireFormatInfo.MaxInactivityDurationInitialDelay) :
                    localWireFormatInfo.MaxInactivityDurationInitialDelay;

				if(readCheckTime > 0)
				{
					Tracer.DebugFormat("InactivityMonitor[{0}]: Read Check time interval: {1}",
								   instanceId, readCheckTime);
					Tracer.DebugFormat("InactivityMonitor[{0}]: Initial Delay time interval: {1}",
									   instanceId, initialDelayTime);

					monitorStarted.Value = true;
					this.asyncTasks = new CompositeTaskRunner("InactivityMonitor[" + instanceId + "].Runner");

					this.asyncErrorTask = new AsyncSignalReadErrorkTask(this, next.RemoteAddress);
					this.asyncWriteTask = new AsyncWriteTask(this);

					this.asyncTasks.AddTask(this.asyncErrorTask);
					this.asyncTasks.AddTask(this.asyncWriteTask);

					writeCheckTime = readCheckTime > 3 ? readCheckTime / 3 : readCheckTime;

					Tracer.DebugFormat("InactivityMonitor[{0}]: Write Check time interval: {1}",
									   instanceId, writeCheckTime);

					this.connectionCheckTimer = new Timer(
						new TimerCallback(CheckConnection),
						null,
						initialDelayTime,
						writeCheckTime);
				}
			}
		}

		private void StopMonitorThreads()
		{
			lock(monitor)
			{
				if(monitorStarted.CompareAndSet(true, false))
				{
					AutoResetEvent shutdownEvent = new AutoResetEvent(false);

					if(null != connectionCheckTimer)
					{
						// Attempt to wait for the Timer to shutdown, but don't wait
						// forever, if they don't shutdown after a few seconds, just quit.
						this.connectionCheckTimer.Dispose(shutdownEvent);
						if(!shutdownEvent.WaitOne(TimeSpan.FromMilliseconds(5000), false))
						{
							Tracer.WarnFormat("InactivityMonitor[{0}]: Timer Task didn't shutdown properly.", instanceId);
						}

						this.connectionCheckTimer = null;
					}

					if(null != this.asyncTasks)
					{
						this.asyncTasks.RemoveTask(this.asyncWriteTask);
						this.asyncTasks.RemoveTask(this.asyncErrorTask);
						this.asyncTasks.Shutdown();
						this.asyncTasks = null;
					}

					this.asyncWriteTask = null;
					this.asyncErrorTask = null;
				}
			}

			Tracer.DebugFormat("InactivityMonitor[{0}]: Stopped Monitor Threads.", instanceId);
		}

		#region Async Tasks
		// Task that fires when the TaskRunner is signaled by the ReadCheck Timer Task.
		class AsyncSignalReadErrorkTask : CompositeTask
		{
			private readonly InactivityMonitor parent;
			private readonly Uri remote;
			private readonly Atomic<bool> pending = new Atomic<bool>(false);

			public AsyncSignalReadErrorkTask(InactivityMonitor parent, Uri remote)
			{
				this.parent = parent;
				this.remote = remote;
			}

			public bool IsPending
			{
				get { return this.pending.Value; }
				set { this.pending.Value = value; }
			}

			public bool Iterate()
			{
				if(this.pending.CompareAndSet(true, false) && this.parent.monitorStarted.Value)
				{
					IOException ex = new IOException("Channel was inactive for too long: " + remote);
					this.parent.OnException(parent, ex);
				}

				return this.pending.Value;
			}
		}

		// Task that fires when the TaskRunner is signaled by the WriteCheck Timer Task.
		class AsyncWriteTask : CompositeTask
		{
			private readonly InactivityMonitor parent;
			private readonly Atomic<bool> pending = new Atomic<bool>(false);

			public AsyncWriteTask(InactivityMonitor parent)
			{
				this.parent = parent;
			}

			public bool IsPending
			{
				get { return this.pending.Value; }
				set { this.pending.Value = value; }
			}

			public bool Iterate()
			{
				Tracer.DebugFormat("InactivityMonitor[{0}] preparing for another Write Check", parent.instanceId);
				if(this.pending.CompareAndSet(true, false) && this.parent.monitorStarted.Value)
				{
					try
					{
						Tracer.DebugFormat("InactivityMonitor[{0}] Write Check required sending KeepAlive.",
										   parent.instanceId);
						KeepAliveInfo info = new KeepAliveInfo();
						info.ResponseRequired = this.parent.keepAliveResponseRequired.Value;
						this.parent.Oneway(info);
					}
					catch(IOException e)
					{
						this.parent.OnException(parent, e);
					}
				}

				return this.pending.Value;
			}
		}
		#endregion
	}

}
