package customsocket;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

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

@BA.ShortName("TCPSocketServer")
@BA.Events(values= {
		"log(msg as string)",
		"newtcpsocket(value as TCPSocket)",
		"listeningstatus(islistening as boolean)",
		"connectedclients(value as int, keys as List)"
})
public class TCPSocketServer implements TcpSocketJavaEvent {

	private BA bax;
	private Object caller;
	private Object Me = this;
	private String event;
	private boolean _inited = false;
	private boolean need_log_event = false;
	private boolean need_newtcpsocket_event = false;
	private boolean need_listeningstatus_event = false;
	private boolean need_connectedclients_event = false;
	private ServerSocket _server;
	
	private Map _socketmap = new Map();
	
	/**
	 * Initialize TCP Socket Server
	 * @param callerobject : caller object
	 * @param eventname : eventname
	 */
	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()) {
						_inited = true;
						need_log_event = bax.subExists(event+"_log");
						need_newtcpsocket_event = bax.subExists(event+"_newtcpsocket");
						need_listeningstatus_event = bax.subExists(event+"_listeningstatus");
						need_connectedclients_event = bax.subExists(event+"_connectedclients");
					}
				}
			}
		}
		
		Runtime.getRuntime().addShutdownHook(new Thread() {
			public void run() {
				BA.Log("ShutdownHook on TCPSocketServer at "+getLocalIPAddress()+":"+getLocalPort());
				StopListening();
				Disconnect_All_TCPSocket();
			}
		});
		
		_socketmap.Initialize();
	}
	
	/**
	 * Check if TCPSocketServer is initialized
	 * @return true if inited
	 */
	public boolean Initialized() {
		return _inited;
	}
	
	/**
	 * Stop Listening for Connections
	 */
	public void StopListening() {
		if (_server instanceof ServerSocket) {
			try {
				_server.close();
			} catch (IOException e) {
				raise_log_error("IOException on StopListening",e);
			}
		}
		_server = null;
	}
	
	/**
	 * Get Local IP Address used for listening
	 * @return empty string if failed
	 */
	public String getLocalIPAddress() {
		if (_server instanceof ServerSocket) {
			if (_server.getInetAddress()!=null) {
				return _server.getInetAddress().getHostAddress();
			}
		}
		return "";
	}
	
	/**
	 * Get Local Port used for Listening
	 * @return zero if failed
	 */
	public int getLocalPort() {
		if (_server instanceof ServerSocket) {
			if (_server.getLocalPort()>0) {
				return _server.getLocalPort();
			}
					
		}
		return 0;
	}
	
	/**
	 * Start Listening
	 * @param port : listen port
	 * @return true if success
	 */
	public boolean StartListening(int port) {
		if (_server instanceof ServerSocket) StopListening();
		try {
			ServerSocket newserver = new ServerSocket(port);
			Thread tx = new Thread(new tcpsocketserverrun(newserver));
			tx.start();
			return true;
		} catch (IOException e) {
			raise_log_error("IOException on StartListening at port "+port,e);
		}
		return false;
	}
	
	/**
	 * Get how many TCPSockets has been received
	 * @return value as integer
	 */
	public int getConnectedTCPSocket() {
		return _socketmap.IsInitialized() ? _socketmap.getSize() : 0;
	}
	
	/**
	 * Get List of IdentifierKey from all connected TCPSockets
	 * @return List of IdentifierKey, or empty List of none connected
	 */
	public List getListOfTCPSocketKeys() {
		List result = new List();
		result.Initialize();
		if (_socketmap.IsInitialized()) {
			if (_socketmap.getSize()>0) {
				for(int ii=0;ii<_socketmap.getSize();ii++) {
					result.Add((String)_socketmap.GetKeyAt(ii));
				}
			}
		}
		return result;
	}
	
	/**
	 * Disconnect all TCPSocket in Map
	 */
	public void Disconnect_All_TCPSocket() {
		List result = getListOfTCPSocketKeys();
		if (result.getSize()>0) {
			for(int ii=0;ii<result.getSize();ii++) {
				Disconnect_TCPSocket((String)result.Get(ii));
			}
		}
		_socketmap.Initialize();
	}
	
	/**
	 * Check if Socket Map contain specific IP address
	 * @param ipvalue : Ip address
	 * @return TCPSocket object, or null if not available
	 */
	public TCPSocket Have_TCPSocket_with_IPAddress(String ipvalue) {
		if (_socketmap instanceof Map) {
			if (_socketmap.IsInitialized()) {
				if (_socketmap.getSize()>0) {
					for (int ii=0;ii<_socketmap.getSize();ii++) {
						String key = (String) _socketmap.GetKeyAt(ii);
						if (key.startsWith(ipvalue)) {
							Object xx = _socketmap.Get(key);
							if (xx instanceof TCPSocket) {
								return (TCPSocket)xx;
							}
							
						}
					}
				}
			}
		}
		return null;
	}
	
	/**
	 * Disconnect TCPSocket and remove it from Map
	 * @param identifierkey : identifierkey in string
	 * @return true if identifierkey can be found
	 */
	public boolean Disconnect_TCPSocket(String identifierkey) {
		if (identifierkey instanceof String) {
			if (!identifierkey.isEmpty()) {
				if (_socketmap.ContainsKey(identifierkey)) {
					Object xx = _socketmap.Get(identifierkey);
					if (xx instanceof TCPSocket) {
						TCPSocket tcp = (TCPSocket)xx;
						tcp.Disconnect();
						tcp = null;
					}
					xx = null;
					_socketmap.Remove(identifierkey);
					raise_connectedclients(_socketmap.getSize(), getListOfTCPSocketKeys());	
				return true;
				}
			}
		}
		return false;
	}
	
	/**
	 * Rename Map identifier from old_id to new_id, but retain the same TCPSocket object
	 * @param old_id : old identifier
	 * @param new_id : new identifier
	 * @return true if Map contain old_id and succesfully renamed, or false if otherwise
	 */
	public boolean Rename_Identifier(String old_id, String new_id) {
		if (_socketmap instanceof Map) {
			if (_socketmap.IsInitialized()) {
				if (_socketmap.ContainsKey(old_id)) {
					Object xx = _socketmap.Get(old_id);
					if (xx instanceof TCPSocket) {
						TCPSocket tcp = (TCPSocket) xx;
						
						_socketmap.Remove(old_id);
						_socketmap.Put(new_id, tcp);
						if (tcp.getIdentifierKey().equals(new_id)==false) {
							tcp.setIdentifierKey(new_id);							
						}
						return true;
					}
				}
			}
		}
		return false;
	}
	
	private class tcpsocketserverrun implements Runnable{
		private boolean validrun = false;
		public tcpsocketserverrun(ServerSocket xx) {
			_server = xx;
			if (_server instanceof ServerSocket) {
				if (!_server.isClosed()) {
					validrun = true;
				}
			}
		}
		
		public void run() {
			if (validrun) {
				raise_listeningstatus(true);
				while(true) {
					if (_server instanceof ServerSocket) {
						if (!_server.isClosed()) {
							try {
								Socket newsocket = _server.accept();
								raise_log("New connection from "+newsocket.getInetAddress().getHostAddress()+":"+newsocket.getPort()+" to TCPSocketServer");
								TCPSocket newtcp = new TCPSocket(newsocket, Me);
								
								raise_newtcpsocket(newtcp);
							} catch (IOException e) {
								raise_log_error("IOException on server.accept", e);
							}
						} else break;
					} else break;
				}
				
			}
			raise_listeningstatus(false);
		}
	}
	
	private void raise_log_error(String msg, Exception e) {
		StringBuffer sbf = new StringBuffer();
		sbf.append(msg);
		if (e instanceof Exception) {
			if (e.getMessage() instanceof String) {
				sbf.append("\r\n");
				sbf.append("Msg : "+e.getMessage());
			}
			if (e.getCause() instanceof Throwable) {
				sbf.append("\r\n");
				sbf.append(e.getCause());
			}
		}
		raise_log(sbf.toString());
	}
	
	private void raise_newtcpsocket(TCPSocket value) {
		if (need_newtcpsocket_event) {
			bax.raiseEventFromDifferentThread(Me, null, 0, event+"_newtcpsocket", false, new Object[] {value});
		}
	}
	
	private void raise_log(String msg) {
		if (need_log_event) {
			bax.raiseEventFromDifferentThread(Me, null, 0, event+"_log", false, new Object[] {msg});
		}
	}
	
	private void raise_listeningstatus(boolean value) {
		if (need_listeningstatus_event) {
			bax.raiseEventFromDifferentThread(Me, null, 0, event+"_listeningstatus", false, new Object[] {value});
		}
	}

	private void raise_connectedclients(int value, List keys) {
		if (need_connectedclients_event) {
			bax.raiseEventFromDifferentThread(Me, null, 0, event+"_connectedclients", false, new Object[] {value, keys});
		}
	}
	
	////////////////  Event from TcpSocketJavaEvent /////////////////////////
	@Override
	@BA.Hide
	public void newidentifierkey(String identifier, TCPSocket Sender) {
		if (!_socketmap.IsInitialized()) _socketmap.Initialize();
		if (identifier instanceof String) {
			if (!identifier.isEmpty()) {
				Object prev_in_map = null;
				if (identifier.contains(":")) {
					// mengandung ip:port
					// coba cek apakah ip nya pernah ada 
					String ipnya = identifier.substring(0, identifier.indexOf(":"));
					prev_in_map = Have_TCPSocket_with_IPAddress(ipnya);
					if (prev_in_map instanceof TCPSocket) {
						TCPSocket oldsock = (TCPSocket) prev_in_map;
						// ada
						raise_log("Found another TCPSocket with same IP. ID 1 = "+identifier+", ID 2="+ oldsock.getIdentifierKey());
						_socketmap.Remove(oldsock.getIdentifierKey());
						raise_log("Removing Old TCPSocket with identifier="+oldsock.getIdentifierKey());
						_socketmap.Put(identifier, Sender);
						raise_log("Save New TCPSocket with identifer="+Sender.getIdentifierKey());
					}
				} else {
					prev_in_map = _socketmap.Put(identifier, Sender);
					raise_log("Save TCPSocket with identifier="+identifier+" to Map");
				}
			
				
				if (prev_in_map != null) {
					if (prev_in_map instanceof TCPSocket) {
						TCPSocket prevsock = (TCPSocket) prev_in_map;
						prevsock.Disconnect();
						prevsock = null;
						raise_log("Disconnect old TCPSocket with the identifier="+identifier);
					}
					prev_in_map = null;
				}	
			}
		}
		
		
	raise_connectedclients(_socketmap.getSize(), getListOfTCPSocketKeys());	
	}

	@Override
	@BA.Hide
	public void disconnected(String identifier) {
		Disconnect_TCPSocket(identifier);
	}

	@Override
	@BA.Hide
	public void renameidentifier(String oldvalue, String newvalue) {
		Rename_Identifier(oldvalue, newvalue);
		
	}
}
