package customsocket;



import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.UUID;

import org.apache.commons.validator.routines.InetAddressValidator;

import com.corundumstudio.socketio.AckRequest;
import com.corundumstudio.socketio.Configuration;

import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.listener.ConnectListener;
import com.corundumstudio.socketio.listener.DataListener;
import com.corundumstudio.socketio.listener.DisconnectListener;


import anywheresoftware.b4a.BA;
import anywheresoftware.b4a.objects.collections.List;
import anywheresoftware.b4a.objects.collections.Map;
import jGPIO.mycodes;



@BA.ShortName("jSocketIOServer")
@BA.Events(values= {
		"log(msg as string)",
		"clientconnected(sock as NettySocketWrapper)",
		"clientdisconnected(sock as NettySocketWrapper)",
		"clientmessage(sock as NettySocketWrapper, value as string)"
})
//@BA.DependsOn(values= {
//		"netty-socketio-1.7.23",
//		"slf4j-api-1.7.33",
//		"netty-transport-4.1.80.Final",
//		"netty-common-4.1.80.Final",
//		"netty-handler-4.1.80.Final",
//		"netty-codec-4.1.80.Final",
//		"netty-buffer-4.1.80.Final",
//		"netty-codec-http-4.1.80.Final",
//		"netty-resolver-4.1.80.Final",
//		"netty-transport-classes-epoll-4.1.80.Final",
//		"netty-transport-native-epoll-4.1.80.Final",
//		"netty-transport-native-unix-common-4.1.80.Final",
//		"jackson-core-2.13.4",
//		"jackson-annotations-2.14.0-rc1",
//		"jackson-databind-2.14.0-rc1"
//})


/**
 * Socket.io Server for JAVA
 * Compatible with Socket.io V2, not with V3/V4
 * Latest Socket.io from NPM is 2.4.0
 * @author rdkartono
 *
 */
public class SocketIOServerWrapper {
	private BA bax;
	private Object caller;
	private String event;
	private final Object Me = this;
	private boolean _inited = false;
	private boolean need_log_event = false;
	private boolean need_clientconnected_event = false;
	private boolean need_clientdisconnected_event = false;
	private boolean need_clientmessage_event = false;
	
	
	private SocketIOServer _server;
	
	// source : https://github.com/socketio/engine.io-client-java/blob/master/src/main/java/io/socket/engineio/client/Socket.java
	private final String EVENT_MESSAGE = "message";
	private final String EVENT_ERROR = "error";
	
	
	
	/**
	 * Initialize SocketIO Server
	 * @param callerobject : caller object
	 * @param eventname : event name
	 */
	public void Initialize(BA ba, Object callerobject, String eventname) {
		bax = ba;
		caller = callerobject;
		event = eventname;
		if (bax instanceof BA) {
			if (caller != null) {
				if (event instanceof String) {
					if (!event.isEmpty()) {
						need_log_event = bax.subExists(event+"_log");
						need_clientconnected_event = bax.subExists(event+"_clientconnected");
						need_clientdisconnected_event = bax.subExists(event+"_clientdisconnected");
						need_clientmessage_event = bax.subExists(event+"_clientmessage");
						_inited = true;
					}
				}
			}
		}
		
		Runtime.getRuntime().addShutdownHook(new Thread() {
			public void run() {
				BA.Log("ShutdownHook for jSocketIOServer is running");
				StopListen();
			}
		});
	}
	
	/**
	 * Check if SocketIO Server is initialized or not
	 * @return true if initalized
	 */
	public boolean IsInitialized() {
		return _inited;
	}
	
	/**
	 * ConnectListener callback 
	 */
	private ConnectListener cl = new ConnectListener() {

		@Override
		public void onConnect(SocketIOClient client) {
			NettySocketWrapper _netty = new NettySocketWrapper(client);
			
			raise_clientconnected(_netty);
		}
		
	};
	
	private DisconnectListener dl = new DisconnectListener() {

		@Override
		public void onDisconnect(SocketIOClient client) {
			NettySocketWrapper _netty = new NettySocketWrapper(client);
			raise_clientdisconnected(_netty);
		}
		
	};
	
	private DataListener<String> messagelistener = new DataListener<String>() {

		@Override
		public void onData(SocketIOClient client, String valuestring, AckRequest arg2) throws Exception {
			NettySocketWrapper _netty = new NettySocketWrapper(client);
			raise_clientmessage(_netty, valuestring);
		}
		
	};
	
	private DataListener<String> errorlistener = new DataListener<String>() {

		@Override
		public void onData(SocketIOClient client, String reasonstring, AckRequest arg2) throws Exception {
			NettySocketWrapper _netty = new NettySocketWrapper(client);
			
			raise_log("Error happened on "+_netty.CompleteDescription()+", Reason = "+reasonstring);
		}
		
	};
	
	/**
	 * Start Listening at specified port
	 * @param hostip : Host IP, set valid IP address if want to listen at specific network interface
	 * @param Port : Port number
	 * @return true if server can listening
	 */
	public boolean StartListen(String hostip, int Port) {
		
		if (ServerReady()) {
			raise_log("Closing previous SocketIOServer instance");
			StopListen();
		}
		
		if (Port < 1 || Port > 65535) {
			raise_log("StartListen failed, invalid Port="+Port);
			return false;
		}
		
		if (mycodes.ValidString(hostip)==false) {
			raise_log("StartListen failed, hostip is not valid string");
			return false;
		}
		
		InetAddressValidator validator = InetAddressValidator.getInstance();
		if (validator.isValid(hostip)==false) {
			raise_log("StartListen failed, hostip is not valid ip format");
			return false;
		}
		
		
		InetAddress ip;
		try {
			ip = InetAddress.getByName(hostip);
		} catch (UnknownHostException e) {
			raise_log("StartListen failed, UnknownHostException="+e.getMessage());
			return false;
		}
		
		
		Configuration _config = new Configuration();
		
		_config.setPort(Port);
		// example : https://levelup.gitconnected.com/comparing-java-websockets-jetty-vs-netty-ba128ddca313
		
		if (hostip!=null && hostip.length()>0) {
			if (ip!=null) {
				_config.setHostname(ip.getHostAddress());
			}
		}
		
		SocketIOServer newserver = new SocketIOServer(_config);
		
		newserver.addConnectListener(cl);

		
		newserver.addDisconnectListener(dl);
		
		newserver.addEventListener(EVENT_MESSAGE, String.class, messagelistener);
		
		newserver.addEventListener(EVENT_ERROR, String.class, errorlistener);
		
		
		
		_server = newserver;
		_server.start();
		
		raise_log("jSocketIOServer StartListen at Port = "+Port);
		return true;
	}
	
	/**
	 * Stop SocketIO Server Listening
	 * @return true if can be stopped
	 */
	public void StopListen() {
		raise_log("jSocketIOServer StopListening");
		Disconnect_Clients();
		if (_server!=null) {
			_server.stop();
			_server = null;
		}
		
		
		
	}
	
	
	/**
	 * Get Server Port
	 * @return -1 if server is invalid, or configuration is invalid
	 */
	public int getServerPort() {
		if (ServerReady()) {
			Configuration _config = _server.getConfiguration();
			if (_config instanceof Configuration) {
				return _config.getPort();
			}
		}
		return -1;
	}
	
	/**
	 * Get Server Host Name
	 * @return null if invalid, or host name if valid
	 */
	public String getHostName() {
		if (ServerReady()) {
			Configuration _config = _server.getConfiguration();
			if (_config instanceof Configuration) {
				return _config.getHostname();
			}
		}
		return null;
	}
	
	/**
	 * Check if Server ready or not
	 * @return true if server ready
	 */
	public boolean ServerReady() {
		if (_server != null && _server instanceof SocketIOServer) {
			return true;
		} else return false;
	}
	
	/**
	 * Add a Namespace
	 * @param namespace : value in string
	 * @return true if namespace can be added
	 */
	public boolean Add_Namespace(String namespace) {
		if (ServerReady() && Valid_String(namespace)) {
			_server.addNamespace(namespace);
			return true;
		}
		return false;
	}
	
	/**
	 * Remove a Namespace
	 * @param namespace : value in string
	 * @return true if namespace can be removed
	 */
	public boolean Remove_Namespace(String namespace) {
		if (ServerReady() && Valid_String(namespace)) {
			_server.removeNamespace(namespace);
			return true;
		}
		return false;
	}
	
	/**
	 * Get Server Namespaces
	 * @return List of String , containing namespace
	 */
	public List getServerNameSpaces() {
		List result = new List();
		result.Initialize();
		if (ServerReady()) {
			_server.getAllNamespaces().forEach(xx -> result.Add(xx.getName()));
		};
		
		return result;
	}
	
	/**
	 * Get list of NettySocketWrapper
	 * @return List of String, containing connected client's ID
	 */
	public List getConnectedClientsID() {
		List result = new List();
		result.Initialize();
		
		if (ServerReady()) {
			_server.getAllClients().forEach(xx -> result.Add(xx.getSessionId().toString()));
		}
		return result;
	}
	
	/**
	 * Check if Server has client connected with specific ID
	 * @param ID ID to check
	 * @return true if has client with that ID
	 */
	public boolean HasClientWithID(String ID) {
		if (ServerReady() && Valid_String(ID)) {
			SocketIOClient result =_server.getAllClients().stream().filter(xx -> xx.getSessionId().toString()==ID).findFirst().orElse(null);
			return (result!=null);
		}
		return false;
	}
	
	/**
	 * Get Client with specific ID
	 * @param ID ID string
	 * @return null if not found
	 */
	public NettySocketWrapper GetClient(String ID) {
		if (ServerReady() && Valid_String(ID)) {
			SocketIOClient cl = _server.getClient(UUID.fromString(ID));
			if (cl != null) {
				return new NettySocketWrapper(cl);
			}
		}
		return null;
	}
	
	/**
	 * Send a String to all clients connected
	 * @param emitname event name of the broadcast
	 * @param value data to send
	 * @return true if can be send
	 */
	public boolean Broadcast_String(String emitname, String value) {
		if (ServerReady() && Valid_String(emitname) && Valid_String(value)) {
			_server.getAllClients().forEach(cl -> cl.sendEvent(emitname, value));
			return true;
		}
		return false;
	}
	
	/**
	 * Send a Map to all clients connected
	 * @param emitname event name of the broadcast
	 * @param value data to send
	 * @return true if can be send
	 */
	public boolean Broadcast_Map(String emitname, Map value) {
		if (ServerReady() && Valid_String(emitname) && Valid_Map(value)) {
			_server.getAllClients().forEach(cl -> cl.sendEvent(emitname, value.getObject()));
			return true;
		}
		return false;
	}
	
	/**
	 * Send a List to all clients connected
	 * @param emitname event name of the broadcast
	 * @param value data to send
	 * @return true if can be send
	 */
	public boolean Broadcast_List(String emitname, List value) {
		if (ServerReady() && Valid_String(emitname) && Valid_List(value)) {
			_server.getAllClients().forEach(cl -> cl.sendEvent(emitname, value.getObject()));
			return true;
		}
		return false;
	}
	
	/**
	 * Send an Array to all clients connected
	 * @param emitname event name of the broadcast
	 * @param value data to send
	 * @return true if can be send
	 */
	public boolean Broadcast_Array(String emitname, Object[] value) {
		if (ServerReady() && Valid_String(emitname) && Valid_Array(value)) {
			_server.getAllClients().forEach(cl -> cl.sendEvent(emitname, value));
			return true;
		}
		return false;
	}
	
	/***
	 * Add Custom Event 
	 * <br/>will raise event <b>[event at initalize]_[eventname](netty as NettySocketWrapper, value as Object)</b>
	 * <br/>Value : kalau dari JSON object, value akan dalam bentuk <b>Map</b> --> test pakai <b>Is</b> di B4X
	 * <br/>value lain , value akan dalam bentuk <b>String</b>  --> test pakai <b>Is</b> di B4X
	 * <br/>example : at Initialization , event = testserver. 
	 * <br/>Add_Event(devicehealth)
	 * <br/>Later will raise event <b>testserver_devicehealth(netty as NettySocketWrapper, value as Object, ack as AckRequestWrapper)</b> 
	 * @param eventname eventname
	 * @param printdebug  if true , then will print incoming data from this event, for examination
	 * @return true if Custom Event can be added
	 */	
public boolean Add_Event(final String eventname, boolean printdebug) {
		if (ServerReady()) {
			final String eventtoraise = event+"_"+eventname;
			final boolean need_custom_event = bax.subExists(eventtoraise);
			if (printdebug) {
				if (need_custom_event) {
					printlog("will raise ",eventtoraise);
				} else {
					printlog("Sub ",eventtoraise," is not exists, event will not raised");
					
				}	
			}
			
			
			Remove_Event(eventname);		// remove dulu yang sebelumnya, penting ini !
			
			_server.addEventListener(eventname, Object.class, new DataListener<Object>() {

				@Override
				public void onData(SocketIOClient client, Object value, AckRequest ackSender) throws Exception {
					NettySocketWrapper _netty = new NettySocketWrapper(client);
					AckRequestWrapper ack = new AckRequestWrapper(ackSender);
					
					if (value!=null) {
						String valuetype = value.getClass().getTypeName();
						if (printdebug) {
							printlog("Got event ",eventname,", valuetype =",valuetype,", value =",value.toString());
						}
						if (valuetype.contains("LinkedHashMap")) {
							// untuk javascript tipe JSON Object
							if (need_custom_event) {
								LinkedHashMap<?,?> lhm = (LinkedHashMap<?, ?>) value;
								bax.raiseEventFromDifferentThread(Me, null, 0, eventtoraise, false, new Object[] {_netty,LinkedHashMap_to_B4XMap(lhm).getObject(), ack });
								if (ack.isAckRequested()) waitfor_ack(eventname,ack, 5000, printdebug);
								
							}
						} else if (valuetype.contains("ArrayList")) {
							// untuk javascript tipe array
							if (need_custom_event) {
								ArrayList<?> arr = (ArrayList<?>) value;
								bax.raiseEventFromDifferentThread(Me, null, 0, eventtoraise, false, new Object[] {_netty, ArrayList_to_B4XList(arr).getObject(), ack});
								if (ack.isAckRequested()) waitfor_ack(eventname, ack, 5000, printdebug);
							}
							
						} else {
							// java.lang.String
							// java.lang.Integer
							// java.lang.Boolean
							// java.lang.Double
							// javascript Infinity dan NaN --> null
							if (need_custom_event) {
								if (printdebug) printlog("Raising ",eventtoraise," with "+valuetype+" value and waiting for returnvalue");
								bax.raiseEventFromDifferentThread(Me, null, 0, eventtoraise, false, new Object[] {_netty,value, ack });
								if (ack.isAckRequested()) waitfor_ack(eventname,ack, 5000, printdebug);
							}
						}
					} else {
						// value  = null
						if (printdebug) printlog("Got event ", eventname, ", value = null");
						if (need_custom_event) {
							if (printdebug) printlog("Raising ",eventtoraise," with null value and waiting for returnvalue");
							bax.raiseEventFromDifferentThread(Me, null, 0, eventtoraise, false, new Object[] {_netty, null, ack});
							if (ack.isAckRequested()) waitfor_ack(eventname,ack,5000, printdebug);
						}
					}
					
					
					
					
					
				}
				
			});

			return true;
		}
		return false;
	}

	private void printlog(String... str) {
		if (str!=null) {
			if (str.length==1) {
				BA.Log(str[0]);
			} else if (str.length>1) {
				StringBuilder strbuild = new StringBuilder();
				
				for(String xx : str) strbuild.append(xx);
				BA.Log(strbuild.toString());
			}
		}
	}

	private void waitfor_ack(String functionname, AckRequestWrapper ack, long timeout, boolean printdebug) {
		if (ack != null) {
			synchronized(ack) {
				try {
					ack.wait(timeout);
					if (printdebug) printlog("Function ", functionname," ack.wait finished");
				} catch(IllegalMonitorStateException | InterruptedException | IllegalArgumentException e) {
					printlog("Function ", functionname," ack.wait exception = ", e.getMessage());
				} 
			}
		}
		
	}

	private Map LinkedHashMap_to_B4XMap(LinkedHashMap<?,?> value) {
		Map result = new Map();
		result.Initialize();
		
//		MyMap mmresult = new MyMap();
//		if (value!=null) {
//			if (value.size()>0) {
//				mmresult.putAll(value);
//			}
//		}
//		result.setObject(mmresult);
		if (value!=null) {
			if (value.size()>0) {
				value.forEach((kk,vv)->{
					if (vv!=null) {
						String vvtype = vv.getClass().getTypeName();
						if (vvtype.contains("LinkedHashMap")) {
							// map di dalam map
							result.Put(kk, LinkedHashMap_to_B4XMap((LinkedHashMap<?, ?>) vv).getObject());
						} else if (vvtype.contains("ArrayList")) {
							// array di dalam map
							result.Put(kk, ArrayList_to_B4XList((ArrayList<?>) vv).getObject());
						} else {
							// tipe biasa, masukin langsung
							result.Put(kk, vv);
						}
					}
				});
			}
		}
		return result;
	}
	
	private List ArrayList_to_B4XList(ArrayList<?> value) {
		List result = new List();
		result.Initialize();
		if (value != null) {
			if (value.size()>0) {
				value.forEach(xx -> {
					if (xx != null) {
						String xxtype = xx.getClass().getTypeName();
						if (xxtype.contains("LinkedHashMap")) {
							// array of maps
							result.Add(LinkedHashMap_to_B4XMap((LinkedHashMap<?, ?>) xx).getObject());
						} else if (xxtype.contains("ArrayList")) {
							// array of array, kayaknya gak pernah ada
							result.Add(ArrayList_to_B4XList((ArrayList<?>) xx).getObject());
						} else {
							// tipe biasa, masukin langsung
							result.Add(xx);
						}
					}
				});
			}
			
		}
		return result;
	}

	/**
	 * Remove Custom Event
	 * @param eventname : event name yang mau diremove
	 * @return true kalau bisa di remove
	 */
	public boolean Remove_Event(String eventname) {
		if (ServerReady() && Valid_String(eventname)) {
			_server.removeAllListeners(eventname);
			return true;
		}
		return false;
	}
	
	/**
	 * Check if a string is valid and have content
	 * @param xx String to check
	 * @return true if valid
	 */
	private boolean Valid_String(String xx) {
		if (xx != null) {
			if (xx instanceof String) {
				return xx.length()>0 ;
			}
		}
		return false;
	}
	
	/**
	 * Check if a Map is valid and has content
	 * @param xx Map to check
	 * @return true if valid and have content
	 */
	private boolean Valid_Map(Map xx) {
		if (xx != null) {
			if (xx instanceof Map) {
				if (xx.IsInitialized()) {
					if (xx.getSize()>0) {
						return true;
					}
				}
			}
		}
		return false;
	}
	
	/**
	 * Check if a LIst is valid and has content
	 * @param xx List to check
	 * @return true if valid and have content
	 */
	private boolean Valid_List(List xx) {
		if (xx != null) {
			if (xx instanceof List) {
				if (xx.IsInitialized()) {
					if (xx.getSize()>0) {
						return true;
					}
				}
			}
		}
		return false;
	}
	
	/**
	 * Check if an array is not null and have length
	 * @param xx Array to check
	 * @return true if not null and have length
	 */
	private boolean Valid_Array(Object[] xx) {
		if (xx != null) {
			if (xx.length>0) {
				return true;
			}
		}
		return false;
	}
	
	//////////  Private methods //////////////
	
	/**
	 * Disconnect all SocketIOClient in this SocketIOServer
	 */
	private void Disconnect_Clients() {
		if (_server!=null) {
			_server.getAllClients().forEach(xx -> xx.disconnect());
		}
		
	}
	
	
	
	
	
	////////////  Event ////////////////////
	private void raise_log(String msg) {
		if (need_log_event) bax.raiseEventFromDifferentThread(Me, null, 0, event+"_log", false, new Object[] {msg});
	}
	
	private void raise_clientdisconnected(NettySocketWrapper netty) {
		if (need_clientdisconnected_event) {
			bax.raiseEventFromDifferentThread(Me, null, 0, event+"_clientdisconnected", false, new Object[] {netty});
		}
	}
	
	private void raise_clientconnected(NettySocketWrapper netty) {
		if (need_clientconnected_event) {
			bax.raiseEventFromDifferentThread(Me, null, 0, event+"_clientconnected", false, new Object[] {netty});
		}
	}
	
	
	
	private void raise_clientmessage(NettySocketWrapper netty, String value) {
		if (need_clientmessage_event) {
			bax.raiseEventFromDifferentThread(Me, null, 0, event+"_clientmessage", false, new Object[] {netty, value});
		}
	}
	
	
}
