/*******************************************************************************
 * Copyright  2015 rzorzorzo@users.sf.net
 *
 * 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.
 *******************************************************************************/
package org.rzo.yajsw.os.ms.win.w32;

import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.lang3.StringUtils;
import org.rzo.yajsw.os.ms.win.w32.Cluster.Clusapi.ClusterGroupState;
import org.rzo.yajsw.os.ms.win.w32.WindowsXPProcess.MyKernel32;

import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.WString;
import com.sun.jna.ptr.IntByReference;

public class Cluster
{
	static Logger _log = Logger.getLogger(Cluster.class.getCanonicalName());
	ExecutorService threadPool = Executors.newSingleThreadExecutor();

	public interface Clusapi extends com.sun.jna.win32.StdCallLibrary
	{
		Clusapi INSTANCE = (Clusapi) Native.loadLibrary("ClusApi",
				Clusapi.class);

		/*
		 * HCLUSTER WINAPI OpenCluster( __in_opt LPCWSTR lpszClusterName );
		 */
		Pointer OpenCluster(WString lpszClusterName);

		/*
		 * BOOL WINAPI CloseCluster( __in HCLUSTER hCluster );
		 */
		boolean CloseCluster(Pointer hCluster);

		/*
		 * HCHANGE WINAPI CreateClusterNotifyPort( __in HCHANGE hChange, __in
		 * HCLUSTER hCluster, __in DWORD dwFilter, __in DWORD_PTR dwNotifyKey );
		 */
		Pointer CreateClusterNotifyPort(Pointer hChange, Pointer hCluster,
				int dwFilter, IntByReference dwNotifyKey);

		final static int CLUSTER_CHANGE_GROUP_STATE = 0x00001000;
		final static int CLUSTER_CHANGE_HANDLE_CLOSE = 0x80000000;
		final static int CLUSTER_CHANGE_CLUSTER_RECONNECT = 0x00080000;
		final static int CLUSTER_CHANGE_CLUSTER_STATE = 0x20000000;
		final static int CLUSTER_CHANGE_GROUP_DELETED = 0x00002000;
		final static int CLUSTER_CHANGE_GROUP_ADDED = 0x00004000;

		// Results
		final static int WAIT_TIMEOUT = 0x102;
		final static int ERROR_SUCCESS = 0x0;
		final static int ERROR_NO_MORE_ITEMS = 0x103;

		// GetClusterGroupState results
		final static int CLUSTER_GROUP_STATE_UNKNOWN = -1;
		final static int CLUSTER_GROUP_ONLINE = 0;
		final static int CLUSTER_GROUP_OFFLINE = 1;
		final static int CLUSTER_GROUP_FAILED = 2;
		final static int CLUSTER_GROUP_PARTIAL_ONLINE = 3;
		final static int CLUSTER_GROUP_PENDING = 4;

		enum ClusterGroupState
		{
			Unknown, Online, Offline, Failed, PartialOnline, Pending;
			public static ClusterGroupState parse(int val)
			{
				switch (val)
				{
				case CLUSTER_GROUP_STATE_UNKNOWN:
					return Unknown;
				case CLUSTER_GROUP_ONLINE:
					return Online;
				case CLUSTER_GROUP_OFFLINE:
					return Offline;
				case CLUSTER_GROUP_FAILED:
					return Failed;
				case CLUSTER_GROUP_PARTIAL_ONLINE:
					return PartialOnline;
				case CLUSTER_GROUP_PENDING:
					return Pending;
				default:
					_log.severe("unknown cluster state: " + val);
					return Unknown;
				}
			}
		}

		/*
		 * DWORD WINAPI GetClusterNotify( __in HCHANGE hChange, __out DWORD_PTR
		 * *lpdwNotifyKey, __out LPDWORD lpdwFilterType, __out LPWSTR lpszName,
		 * __inout LPDWORD lpcchName, __in_opt DWORD dwMilliseconds );
		 */
		int GetClusterNotify(Pointer hChange, IntByReference lpdwNotifyKey,
				IntByReference lpdwFilterType, Memory lpszName,
				IntByReference lpcchName, int dwMilliseconds);

		/*
		 * BOOL WINAPI CloseClusterNotifyPort( __in HCHANGE hChange );
		 */
		boolean CloseClusterNotifyPort(Pointer hChange);

		/*
		 * HNODE WINAPI OpenClusterNode( __in HCLUSTER hCluster, __in LPCWSTR
		 * lpszNodeName );
		 */
		Pointer OpenClusterNode(Pointer hCluster, WString lpszNodeName);

		/*
		 * BOOL WINAPI CloseClusterNode( __in HNODE hNode );
		 */
		boolean CloseClusterNode(Pointer hNode);

		/*
		 * CLUSTER_NODE_STATE WINAPI GetClusterNodeState( __in HNODE hNode );
		 */
		int GetClusterNodeState(Pointer hNode);

		/*
		 * HCLUSENUM WINAPI ClusterOpenEnum( __in HCLUSTER hCluster, __in DWORD
		 * dwType );
		 */
		Pointer ClusterOpenEnum(Pointer hCluster, int dwType);

		Pointer ClusterNodeOpenEnum(Pointer hNode, int dwType);

		static int CLUSTER_ENUM_NODE = 1;
		static int CLUSTER_ENUM_RESOURCE = 4;
		static int CLUSTER_ENUM_NETINTERFACE = 32;
		static int CLUSTER_ENUM_GROUP = 8;

		/*
		 * DWORD WINAPI ClusterCloseEnum( __in HCLUSENUM hEnum );
		 */
		int ClusterCloseEnum(Pointer hEnum);

		/*
		 * DWORD WINAPI ClusterEnum( __in HCLUSENUM hEnum, __in DWORD dwIndex,
		 * __out LPDWORD lpdwType, __out LPWSTR lpszName, __inout LPDWORD
		 * lpcchName );
		 */
		int ClusterEnum(Pointer hEnum, int dwIndex, IntByReference lpdwType,
				Memory lpszName, IntByReference lpcchName);

		int ClusterNodeEnum(Pointer hEnum, int dwIndex,
				IntByReference lpdwType, Memory lpszName,
				IntByReference lpcchName);

		/*
		 * HRESOURCE WINAPI OpenClusterResource( __in HCLUSTER hCluster, __in
		 * LPCWSTR lpszResourceName );
		 */
		Pointer OpenClusterResource(Pointer hCluster, WString lpszResourceName);

		/*
		 * HGROUP WINAPI OpenClusterGroup( __in HCLUSTER hCluster, __in LPCWSTR
		 * lpszGroupName );
		 */
		Pointer OpenClusterGroup(Pointer hCluster, WString lpszGroupName);

		/*
		 * CLUSTER_GROUP_STATE WINAPI GetClusterGroupState( __in HGROUP hGroup,
		 * __out_opt LPWSTR lpszNodeName, __inout_opt LPDWORD lpcchNodeName );
		 */
		int GetClusterGroupState(Pointer hGroup, Memory lpszNodeName,
				IntByReference lpcchNodeName);
	}

	ArrayList<ClusterNodeChangeListener> _listeners = new ArrayList<ClusterNodeChangeListener>();
	boolean _stopped = true;

	public String getActiveNode()
	{
		String activeNode = null;

		try
		{
			Pointer cluster = Clusapi.INSTANCE.OpenCluster(null);
			Pointer hEnum = Clusapi.INSTANCE.ClusterOpenEnum(cluster,
					Clusapi.CLUSTER_ENUM_GROUP);
			int dwIndex = 0;
			IntByReference lpdwType = new IntByReference();
			IntByReference lpcchName = new IntByReference();
			Memory lpszName = new Memory(256);
			lpszName.clear();
			lpcchName.setValue(256);
			int result = 0;
			do
			{
				result = Clusapi.INSTANCE.ClusterEnum(hEnum, dwIndex, lpdwType,
						lpszName, lpcchName);
				if (result == Clusapi.ERROR_SUCCESS)
				{
					String group = lpszName.getWideString(0);
					ClusterGroupInfo info = getGroupNodeInfo(cluster, group);
					if (info != null)
						activeNode = info.getLocation();
				}
				dwIndex++;
			}
			while (result == 0);
		}
		catch (Exception ex)
		{
			_log.log(Level.SEVERE, "Error getting cluster information", ex);
		}
		return activeNode;
	}

	private ClusterGroupInfo getGroupNodeInfo(Pointer cluster, String groupName)
	{
		ClusterGroupInfo result = null;
		try
		{
			Pointer hGroup = Clusapi.INSTANCE.OpenClusterGroup(cluster,
					new WString(groupName));

			if (hGroup == null)
				throw new RuntimeException(
						"Clusapi call to OpenClusterGroup returned err code "
								+ MyKernel32.INSTANCE.GetLastError());

			IntByReference lpcchNodeName = new IntByReference();
			Memory lpszNodeName = new Memory(256);
			lpszNodeName.clear();
			lpcchNodeName.setValue(256);

			int state = Clusapi.INSTANCE.GetClusterGroupState(hGroup,
					lpszNodeName, lpcchNodeName);
			String location = lpszNodeName.getWideString(0);

			if (state == Clusapi.CLUSTER_GROUP_STATE_UNKNOWN)
				_log.severe("unknown group state for group " + groupName
						+ " err code " + MyKernel32.INSTANCE.GetLastError());

			result = new ClusterGroupInfo(groupName, state, location);

			MyKernel32.INSTANCE.CloseHandle(hGroup);
		}
		catch (Exception e)
		{
			_log.log(Level.SEVERE, "Error while getting GroupActiveNode", e);
		}
		return result;
	}

	public Set<ClusterGroupInfo> getGroupInfo()
	{
		Pointer hCluster = Clusapi.INSTANCE.OpenCluster(null);
		if (hCluster == null)
			throw new RuntimeException(
					"Clusapi call to OpenClusterGroup returned err code "
							+ MyKernel32.INSTANCE.GetLastError());

		Pointer hEnum = Clusapi.INSTANCE.ClusterOpenEnum(hCluster,
				Clusapi.CLUSTER_ENUM_GROUP);
		if (hEnum == null)
			throw new RuntimeException(
					"Clusapi call to ClusterOpenEnum returned err code "
							+ MyKernel32.INSTANCE.GetLastError());

		Set<ClusterGroupInfo> result = new LinkedHashSet<ClusterGroupInfo>();

		try
		{
			IntByReference lpdwType = new IntByReference();
			IntByReference lpcchName = new IntByReference(0);
			Memory lpszName = new Memory(256);

			int dwIndex = 0;

			int returnValue = 0;
			do
			{
				lpdwType.setValue(0);
				lpcchName.setValue(0);
				lpszName.clear();
				lpcchName.setValue(256);

				returnValue = Clusapi.INSTANCE.ClusterEnum(hEnum, dwIndex,
						lpdwType, lpszName, lpcchName);

				if (returnValue == Clusapi.ERROR_SUCCESS)
				{
					String group = lpszName.getWideString(0);
					ClusterGroupInfo info = getGroupNodeInfo(hCluster, group);
					if (info != null)
						result.add(info);
				}

				if ((returnValue != Clusapi.ERROR_NO_MORE_ITEMS)
						&& (returnValue != Clusapi.ERROR_SUCCESS))
					_log.log(Level.SEVERE, "strange returnValue from ClusApi"
							+ returnValue);

				dwIndex++;
			}
			while (returnValue == 0);
		}
		catch (Exception e)
		{
			_log.log(Level.SEVERE,
					"Error while getting Cluster group information", e);
		}
		finally
		{
			MyKernel32.INSTANCE.CloseHandle(hEnum);
			MyKernel32.INSTANCE.CloseHandle(hCluster);
		}
		return result;
	}

	public void start()
	{
		Runnable check = null;
		synchronized (this)
		{
			if (_stopped)
			{
				check = new Runnable()
				{
					public void run()
					{
						IntByReference lpdwNotifyKey = new IntByReference();
						IntByReference lpdwFilterType = new IntByReference();
						IntByReference lpcchName = new IntByReference();
						IntByReference dwNotifyKey = new IntByReference();
						Memory lpszName = new Memory(256);
						Pointer minusOne = Pointer.createConstant(-1);
						int dwMilliseconds = 300 * 1000;
						final int dwFilter = Clusapi.CLUSTER_CHANGE_GROUP_STATE
								| Clusapi.CLUSTER_CHANGE_HANDLE_CLOSE
								| Clusapi.CLUSTER_CHANGE_GROUP_DELETED
								| Clusapi.CLUSTER_CHANGE_CLUSTER_STATE
								| Clusapi.CLUSTER_CHANGE_CLUSTER_RECONNECT
								| Clusapi.CLUSTER_CHANGE_GROUP_ADDED;

						while (!_stopped)
						{
							Pointer hCluster = null;
							Pointer hChange = null;

							long started = System.currentTimeMillis();

							try
							{
								lpdwNotifyKey.setValue(0);
								lpdwFilterType.setValue(0);
								lpcchName.setValue(0);
								dwNotifyKey.setValue(0);
								lpszName.clear();
								lpcchName.setValue(256);

								hCluster = Clusapi.INSTANCE.OpenCluster(null);
								if (hCluster == null)
									_log.severe("ClusApi.OpenCluster returned err code "
											+ MyKernel32.INSTANCE
													.GetLastError());
								else
								{
									hChange = Clusapi.INSTANCE
											.CreateClusterNotifyPort(minusOne,
													hCluster, dwFilter,
													dwNotifyKey);
									if (hChange == null)
										_log.severe("ClusApi.CreateClusterNotifyPort returned err code "
												+ MyKernel32.INSTANCE
														.GetLastError());
								}

								if (hCluster == null || hChange == null)
									Thread.sleep(5000);
								else
								{
									int result = Clusapi.INSTANCE
											.GetClusterNotify(hChange,
													lpdwNotifyKey,
													lpdwFilterType, lpszName,
													lpcchName, dwMilliseconds);

									if (result == Clusapi.ERROR_SUCCESS)
										doListeners(null,
												lpdwFilterType.getValue(),
												lpszName.getWideString(0));
									else if (result != Clusapi.WAIT_TIMEOUT) // 258
																				// =
																				// Wait
																				// Time
																				// Out
										_log.warning("ClusApi.GetClusterNotify result="
												+ result);
								}
							}
							catch (Throwable e)
							{
								_log.log(Level.SEVERE,
										"Error getting ClusterInformation", e);
							}
							finally
							{
								_log.info("check cluster took "
										+ (System.currentTimeMillis() - started)
										+ " ms");
								if (hChange != null)
								{
									try
									{
										Clusapi.INSTANCE
												.CloseClusterNotifyPort(hChange);
									}
									catch (Throwable e2)
									{
										e2.printStackTrace();
									}

									MyKernel32.INSTANCE.CloseHandle(hChange);
								}

								if (hCluster != null)
									MyKernel32.INSTANCE.CloseHandle(hCluster);
							}
						}
					}
				};
				_stopped = false;
			}
		}
		new Thread(check, "cluster listener thread").start();
	}

	private void doListeners(String activeNode, int lpdwFilterType,
			String lpszName)
	{
		// LOGGING
		try
		{
			switch (lpdwFilterType)
			{
			case Clusapi.CLUSTER_CHANGE_GROUP_ADDED:
				_log.severe("cluster group added: " + lpszName);
				break;

			case Clusapi.CLUSTER_CHANGE_GROUP_DELETED:
				_log.severe("cluster group deleted: " + lpszName);
				break;

			case Clusapi.CLUSTER_CHANGE_GROUP_STATE:
				_log.severe("cluster group state changed: " + lpszName);
				break;

			case Clusapi.CLUSTER_CHANGE_HANDLE_CLOSE:
				_log.severe("The queue receives a notification when a handle associated with a cluster object is closed. "
						+ lpszName);
				break;

			case Clusapi.CLUSTER_CHANGE_CLUSTER_RECONNECT:
				_log.severe("The queue receives a notification when the connection to the cluster "
						+ "identified by hCluster is reestablished after a brief disconnect. Some events "
						+ "generated immediately before or after this event may have been lost. val="
						+ lpszName);
				break;

			case Clusapi.CLUSTER_CHANGE_CLUSTER_STATE:
				_log.severe("all attempts to communicate with the cluster failed, val="
						+ lpszName);
				break;

			default:
				_log.severe("unknown event id="
						+ Integer.toHexString(lpdwFilterType) + ", val="
						+ lpszName);
				break;
			}
		}
		catch (Throwable e)
		{
			_log.log(Level.SEVERE, "Error in Cluster Logging", e);
		}

		threadPool.execute(new Runnable()
		{
			public void run()
			{
				ArrayList<ClusterNodeChangeListener> listeners = new ArrayList<ClusterNodeChangeListener>();
				synchronized (_listeners)
				{
					listeners.addAll(_listeners);
				}

				for (ClusterNodeChangeListener l : listeners)
					try
					{
						l.nodeChanged();
					}
					catch (Throwable e)
					{
						_log.log(
								Level.SEVERE,
								"Error in ClusterNodeChangeListener.nodeChanged()",
								e);
					}
			}
		});
	}

	public void addNodeChangeListener(ClusterNodeChangeListener listener)
	{
		synchronized (_listeners)
		{
			_listeners.add(listener);
		}
	}

	public void stop()
	{
		_stopped = true;
	}

	public static void main(String[] args) throws UnknownHostException
	{
		final Cluster c = new Cluster();
		c.addNodeChangeListener(new ClusterNodeChangeListener()
		{
			public void nodeChanged()
			{
				try
				{
					System.out.println("new GroupInfo" + c.getGroupInfo());
				}
				catch (Exception e)
				{
					e.printStackTrace();
				}
			}
		});
		c.start();
		try
		{
			Thread.sleep(Integer.MAX_VALUE);
		}
		catch (InterruptedException e)
		{
			e.printStackTrace();
		}
	}

	public class ClusterGroupInfo
	{
		final private String _groupName;
		final private String _location;
		ClusterGroupState _state = ClusterGroupState.Unknown;

		public ClusterGroupInfo(String groupName, int state, String location)
		{
			_groupName = groupName;
			_location = location;
			_state = ClusterGroupState.parse(state);
		}

		public String getGroupName()
		{
			return _groupName;
		}

		public String getLocation()
		{
			return _location;
		}

		public ClusterGroupState getState()
		{
			return _state;
		}

		public boolean equals(ClusterGroupInfo info)
		{
			if (super.equals(info))
				return true;

			if (StringUtils.equals(_location, info._location)
					&& StringUtils.equals(_groupName, info._groupName)
					&& _state == info._state)
				return true;

			return false;
		}

		@Override
		public String toString()
		{
			return _groupName + "(" + _state + ", " + _location + ")";
		}
	}
}
