package customsocket;

import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.regex.Pattern;


import io.socket.client.IO;
import io.socket.client.Socket;
import anywheresoftware.b4a.BA;



@BA.ShortName("jSocketIOClient")
@BA.Events(values= {
		"log(msg as string)",
		"connected(socketid as string, remote as string)",
		"disconnected(socketid as string, remote as string, reason as string)",
		"message(socketid as string, remote as string, value as string)"
})

/**
 * Ini adalah Socket.io client yang inisiatif konek ke Socket.io server
 * @author rdkartono
 *
 */
public class SocketIOClientWrapper implements SocketIOClientInterface {
	
	private final Object Me = this;
	
	private BA bax;
	private Object caller;
	private String event;
	private boolean _inited = false;
	private boolean need_log_event = false;
	private boolean need_connected_event = false;
	private boolean need_disconnected_event = false;
	private boolean need_message_event = false;
	private Socket _client;
	
	private String _remote;
	private String _id;
	
	
	
	/**
	 * Initialize Events for Socket.IO  Client
	 * This is client side Socket that to be use to connect with SocketIO Server
	 * @param callerobject : caller object
	 * @param eventname : event name
	 */
	public void Initialize(final BA ba, final Object callerobject, final 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_connected_event = bax.subExists(event+"_connected");
						need_disconnected_event = bax.subExists(event+"_disconnected");
						need_message_event = bax.subExists(event+"_message");
						_inited = true;
					}
				}
			}
		}
		
		
		Runtime.getRuntime().addShutdownHook(new Thread() {
			public void run() {
				BA.Log("ShutdownHook for SocketIOClient");
				Disconnect();
			}
		});
		
		_id = "";
		_remote = "";
	}
	
	/**
	 * Check if Socket.IO Client is initialized
	 * @return true if inited
	 */
	public boolean IsInitialized() {
		return _inited;
	}
	
	private void printvaluedebug(String eventname, Object...value) {
		if (value!=null) {
			String valuetype = value.getClass().getTypeName();
			int length = value.length;
			String valuestring = Arrays.toString(value);
			if (length>0) {
				BA.Log("Event="+eventname+", type="+valuetype+", length="+length+", value="+valuestring);	
			} else {
				BA.Log("Event="+eventname+" has no value");
			}
			
		} else {
			BA.Log("Event="+eventname+" value is null");
		}
		
	}
	
	/**
	 * Connect to SocketIO Server
	 * Will raise connected event or disconnected event
	 * @param uri : valid uri string, example  ws://192.168.31.2:8080
	 * @param debugmode : if true, will dislay debug log
	 * @param events : array of string, containing event name that want to be intercepted as soon as possible
	 * @return true if socket can be created or false if parameters invalid
	 */
	public boolean ConnectTo(String uri, boolean debugmode, String...events ) {
		
		if (!_id.isEmpty()) Disconnect();		//kalau ID sudah ada, berarti dah connected. Kalau gitu, disconnect dulu
		
		if (uri instanceof String) {
			if (!uri.isEmpty()) {
				try {
					Socket newsock = IO.socket(uri);
					newsock.on(Socket.EVENT_CONNECT,(value)->{
						// di sini, gak ada value, tandanya value.length = 0
						//if (debugmode) printvaluedebug(Socket.EVENT_CONNECT, value);
						
						_id = newsock.id();
						_remote = uri;
						if (_client instanceof Socket) {
							_client.off();
							_client.disconnect();
							_client = null;
						}
						_client = newsock;
						raise_connected(_id, _remote);
					});
					newsock.on(Socket.EVENT_CONNECT_ERROR, (value)->{
						// di sini, ada 1 value, tandanya value.length = 1
						// isinya keterangan kenapa gagal connect
						//if (debugmode) printvaluedebug(Socket.EVENT_CONNECT_ERROR, value);
						
						String _reason = "";						
						if (value!=null) {
							
							if (value.length>0) {
								_reason = String.valueOf(value[0]);
							}
							
						}
						
						raise_disconnected(_id, _remote,_reason);
						_id = "";
						_remote = "";
						if (_client instanceof Socket) {
							_client.off();
							_client.disconnect();
							_client = null;
						}
						
					});
					newsock.on(Socket.EVENT_DISCONNECT, (value)->{
						// di sini ada 1 value, tandanya value.length = 1
						// isinya keterangan kenapa disconnect
						// disconnect artinya sudah pernah connect, beda dengan connect_error
						//if (debugmode) printvaluedebug(Socket.EVENT_DISCONNECT, value);
						
						String _reason = "";
						if (value!=null) {
							
							if (value.length>0) {
								_reason = String.valueOf(value[0]);
							}
							
						}
						
						raise_disconnected(_id, _remote,_reason);
						_id = "";
						_remote = "";
						if (_client instanceof Socket) {
							_client.off();
							_client.disconnect();
							_client = null;
						}
						
						
					});
					
					newsock.on(Socket.EVENT_MESSAGE, (value)->{
						if (debugmode) printvaluedebug(Socket.EVENT_MESSAGE, value);
						
						if (value!=null) {
							if (value.length>0) {
								String msg = String.valueOf(value[0]);
								raise_message(_id, _remote, msg);
							}
							
						}
					});
					
					if (events!=null) {
						if (events.length>0) {
							for(String ev : events) {
								if (!ev.isEmpty()) {
									final String eventtoraise = event+"_"+ev;
									final boolean need_event = bax.subExists(eventtoraise);
									if (need_event) BA.Log("Will raise event "+eventtoraise);
									newsock.on(ev,(value)->{
										if (debugmode) printvaluedebug(ev, value);
										if (need_event) bax.raiseEventFromDifferentThread(Me, null, 0, eventtoraise, false, new Object[] {value});
									});
								}
								
							}
						}
					}
					
					
					newsock.connect();
					return true;
				} catch (URISyntaxException e) {
					raise_log("ConnectTo failed, URISyntaxException for "+uri+", Msg : "+e.getMessage());
				}
				
			}
		}
		return false;
	}
	
	
	/**
	 * Disconnect Socket
	 */
	public void Disconnect() {
		
		if (_client instanceof Socket) {
			Remove_All_Events();		// remove semua event
			_client.disconnect();	// disconnect
		}
		_client = null;
		
		//TODO : konfirmasi di Log nanti, ini perlu di clear di Disconnect() atau di onDisconnect()
		_remote = "";
		_id = "";
	}
	
	/**
	 * Remove all Socket Events
	 */
	public void Remove_All_Events() {
		if (_client instanceof Socket) {
			_client.off();
		}
	}
	
	/***
	 * Check if Client is still connected
	 * @return true if connected
	 */
	public boolean IsConnected() {
		if (_client instanceof Socket) {
			return _client.connected();
		}
		return false;
	}
	
	
	/**
	 * Add Custom Event to Socket
	 * If data arrived, will raise event_eventname(value as Object)
	 * @param eventname : eventname, lowercase characters and numbers only
	 * @param debugmode : if true, will give debug output
	 * @return true if event added
	 */
	public boolean Add_Event(final String eventname, boolean debugmode) {
		if (is_correct_eventname(eventname)) {
			if (_client instanceof Socket) {
				if (_client.hasListeners(eventname)) {
					_client.off(eventname);
					raise_log("Add_Event with eventname = "+eventname+" already exist, removing the previous");
				}
				
				final String eventtoraise = event+"_"+eventname;
				final boolean need_event = bax.subExists(eventtoraise);
				if (need_event && debugmode) BA.Log("Will raise "+eventtoraise);
				
				_client.on(eventname, (value)->{
					if (debugmode) printvaluedebug(eventname, value);
					
					if (need_event) bax.raiseEventFromDifferentThread(Me, null, 0, eventtoraise, false, new Object[] {value});
					
				});
				
				return true;
			} else raise_log("Add_Event failed, Socket not connected");
		} else raise_log("Add_Event failed, eventname is invalid");
		return false;
	}
	
	/**
	 * Remove specified eventname from socket
	 * @param eventname : eventname to remove, lowercase characters and numbers only
	 * @return true if removed
	 */
	public boolean Remove_Event(String eventname) {
		if (is_correct_eventname(eventname)) {
			if (_client instanceof Socket) {
				if (_client.hasListeners(eventname)) {
					_client.off(eventname);
					return true;
				} else raise_log("Remove_Event failed, Socket dont have event : "+eventname);
			} else raise_log("Remove_Event failed, Socket not connected");
		} else raise_log("Remove_Event failed, eventname is invalid");
		return false;
	}
	
	/**
	 * Emit JsonObject to target socket
	 * @param emitname : event name
	 * @param value : JsonObject
	 * @return true if value can be send
	 */
	public boolean Emit_JsonObject(String emitname, JsonObject value) {
		if (emitname instanceof String) {
			if (!emitname.isEmpty()) {
				if (value instanceof JsonObject) {
					if (value.HasSomeValue()) {
						_client.emit(emitname, value.getObject());
						return true;
					}
				}
			}
		}
		return false;
	}
	
	/**
	 * Emit JsonArray to target socket
	 * @param emitname : event name
	 * @param value : JsonArray
	 * @return true if value can be send
	 */
	public boolean Emit_JsonArray(String emitname, JsonArray value) {
		if (emitname instanceof String) {
			if (!emitname.isEmpty()) {
				if (value instanceof JsonArray) {
					if (value.HasSomeValue()) {
						_client.emit(emitname, value.getObject().toList());
						return true;
					}
				}
			}
		}
		return false;
	}
	
	/**
	 * Send simple String to event 'message'
	 * @param value : String to send
	 * @return true if can be send
	 */
	public boolean SendMessage(String value) {
		if (IsConnected()) {
			if (value instanceof String) {
				if (!value.isEmpty()) {
					_client.emit(io.socket.engineio.client.Socket.EVENT_MESSAGE, value);
					return true;
				} else raise_log("SendMessage failed, value is zero length");
			} else raise_log("SendMessage failed, value is not String");
		} else raise_log("SendMessage failed, Socket not connected");
		return false;
	}
	
	private boolean is_correct_eventname(String value) {
		if (value instanceof String) {
			if (!value.isEmpty()) {		
				return Pattern.matches("^[a-z0-9]+$", value);// hanya lowercase dan angka
			}
		}
		return false;
	}
	
	/**
	 * Get SessionID from socket
	 * @return empty string if socket not connected
	 */
	public String getID() {
		return _id;
	}
	
	/**
	 * Get RemoteSocket info, in form of [IP]:[Port]
	 * @return empty string if socket not connected
	 */
	public String getRemote() {
		return _remote;
	}
	
	
	/////////// Events ///////////////
	
	private void raise_log(String msg) {
		if (need_log_event) bax.raiseEventFromDifferentThread(Me, null, 0, event+"_log", false, new Object[] {msg});
	}
	
	private void raise_connected(String id, String remote){
		if (need_connected_event) bax.raiseEventFromDifferentThread(Me, null, 0, event+"_connected", false, new Object[] {id, remote});
	}
	
	private void raise_disconnected(String id, String remote, String reason) {
		if (need_disconnected_event) bax.raiseEventFromDifferentThread(Me, null, 0, event+"_disconnected", false, new Object[] {id,remote, reason});
	}
	
	private void raise_message(String id, String remote, String value) {
		if (need_message_event) bax.raiseEventFromDifferentThread(Me, null, 0, event+"_message", false, new Object[] {id,remote, value});
	}

	@Override
	public boolean Emit(String emitname, Object... value) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public String getLocal() {
		// TODO Auto-generated method stub
		return null;
	}
}
