using System;
using System.Runtime.Remoting;  //RemotingServices
using System.Runtime.Remoting.Channels; //ChannelServices
using System.Runtime.Remoting.Channels.Ipc; //IpcServerChannel

/// <summary>
/// IPC(プロセス間通信)を利用して同一アプリ間でコマンドライン引数を渡す仕組みを提供する。
/// IpcManagerクラス、IpcRemoteObjectクラスを含む。
/// </summary>
/// <remarks>
/// IpcManagerクラス:	サーバーチャンネル、クライアントチャンネルの生成。リモートオブジェクトの操作。
/// IpcRemoteObjectクラス:	クライアントのコマンドライン引数を格納し、サーバーへ通知するためのリモートオブジェクト。
/// </remarks>
namespace IpcUtility
{
	/// <summary>
	/// サーバーチャンネル、クライアントチャンネルの生成。リモートオブジェクトの操作。
	/// </summary>
	class IpcManager : IDisposable
	{
		private readonly string ChannelName;
		private const string PortName = "CommandLineArgs";
		private const string RemoteObjectUri = "IpcRemoteObject";
		private IChannel channel;
		private IpcRemoteObject ipcObject;
		private dynamic targetWindow;
		private Action callback;

		//プロパティ
		public bool IsServer { get; private set; }

		/// <summary>
		/// コンストラクタ
		/// </summary>
		/// <param name="channelName">PC内で一意の名前</param>
		/// <remarks>
		/// PC内の他のアプリが生成したチャンネルと名前が偶然かぶらないよう、一意のチャンネル名を用意する必要がある。
		/// </remarks>
		public IpcManager(string channelName)
		{
			ChannelName = channelName;
		}

		/// <summary>
		/// プロセス間通信を確立する
		/// </summary>
		/// <remarks>
		/// まずサーバーチャンネルを生成し、登録を試みる。
		/// 同名チャンネルが登録済みであれば例外が発生するので、続けてクライアントチャンネルを生成、登録する。
		/// ※同名チャンネルが登録済みかの判定が二重起動の判定にもなる。
		/// </remarks>
		/// <param name="targetWindow">クライアント側から制御を戻す先のウインドウ(MainWindowを想定)</param>
		/// <param name="callback">そのウインドウに含まれるコールバック関数</param>
		public void Connect(dynamic targetWindow, Action callback)
		{
			this.targetWindow = targetWindow;
			this.callback = callback;

			try
			{
				//サーバーチャンネルを生成する
				channel = new IpcServerChannel(ChannelName, PortName);
				ChannelServices.RegisterChannel(channel, true);

				//リモートオブジェクトを公開する
				ipcObject = new IpcRemoteObject();
				ipcObject.ClientStarted += Ipc_ClientStarted;	//イベントハンドラを登録する
				RemotingServices.Marshal(ipcObject, RemoteObjectUri);
			}
			catch (RemotingException)
			{
				//クライアントチャンネルを生成する
				channel = new IpcClientChannel();
				ChannelServices.RegisterChannel(channel, true);

				//リモートオブジェクトを取得する
				ipcObject = Activator.GetObject(typeof(IpcRemoteObject), $"ipc://{PortName}/{RemoteObjectUri}") as IpcRemoteObject;

				//コマンドライン引数をサーバーへ渡す
				ipcObject.Args = Environment.GetCommandLineArgs();
				ipcObject.OnClientStarted();	//イベント発生
			}

			IsServer = channel is IpcServerChannel;
		}

		/// <summary>
		/// 2個目の本アプリが起動したことが通知されてきた
		/// </summary>
		/// <remarks>
		/// このメソッドはMainWindowとは別のスレッドで呼び出されるのでTextBoxなどが操作できない。
		/// そのためデリゲートを通し、MainWindowと同じスレッドで処理できるようにする。
		/// </remarks>
		private void Ipc_ClientStarted(object sender, EventArgs e)
		{
			targetWindow.Dispatcher.Invoke(callback);
		}

		/// <summary>
		/// クライアント側から送られたコマンドライン引数を受け取る
		/// </summary>
		public string[] ReceiveArgs()
		{
			return ipcObject.Args;
		}

		/// <summary>
		/// 破棄
		/// </summary>
		public void Dispose()
		{
			if (channel != null)
			{
				ChannelServices.UnregisterChannel(channel);
				channel = null;
			}
		}
	}

	/// <summary>
	/// クライアントのコマンドライン引数を格納し、サーバーへ通知するためのリモートオブジェクト。
	/// </summary>
	class IpcRemoteObject : MarshalByRefObject
	{
		//プロパティ
		public string[] Args { get; set; }

		//イベントハンドラ
		//・サーバー側が、呼んで欲しいメソッドをセットする。
		public event EventHandler ClientStarted;

		//コンストラクタ
		public IpcRemoteObject() { }

		//「クライアントが起動した」というイベントを発生させる
		public void OnClientStarted()
		{
			ClientStarted?.Invoke(this, EventArgs.Empty);
		}
	}
}