package customsocket;

import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;

import anywheresoftware.b4a.BA;
import anywheresoftware.b4a.keywords.DateTime;

@BA.ShortName("SimpleTCPSocket")
@BA.Events(values= {
		"log(msg as string)",
		"connected(targetip as string, targetport as int)",
		"disconnected(targetip as string, targetport as int, duration as long)",
		"newbytesdata(value() as byte)",
		"txrxstatus(packetsent as int, packetreceived as int, totalsent as long, totalreceived as long)"
		
})

public class SimpleTCPSocket {
	private BA bax;
	private Object Me = this;
	private String event;
	
	private boolean need_log_event = false;
	private boolean need_connected_event = false;
	private boolean need_disconnected_event = false;
	private boolean need_newbytesdata_event = false;
	private boolean need_txrxstatus_event = false;
	
	
	private Socket _socket = null;
	private boolean isprefix = false;
	private Integer packetsent = 0;
	private Integer packetreceived = 0;
	private Long totalsent = 0l;
	private Long totalreceived = 0l;
	/**
	 * Initialize Simple TCP Socket universal
	 * @param event
	 */
	public void Initialize(BA ba, String event) {
		this.bax = ba;
		this.event = event;
		need_log_event = bax.subExists(this.event+"_log");
		need_connected_event = bax.subExists(this.event+"_connected");
		need_disconnected_event = bax.subExists(this.event+"_disconnected");
		need_newbytesdata_event = bax.subExists(this.event+"_newbytesdata");
		need_txrxstatus_event = bax.subExists(this.event+"_txrxstatus");
	}
	
	/**
	 * Close Socket
	 */
	public void Close() {
		if (_socket != null) {
			try {
				_socket.close();
			} catch (IOException e) {
				raise_log_event("Close failed, exception="+e.getMessage());
			}
		}
		_socket = null;
		
	}
	
	/**
	 * Check if Still connected
	 * @return true if connected
	 */
	public boolean IsConnected() {
		if (_socket != null) {
			if (!_socket.isClosed()) {
				if (!_socket.isInputShutdown()) {
					if (!_socket.isOutputShutdown()) {
						return true;
					}
				}
			}
		}
		return false;
	}
	
	/**
	 * Write bytes to Socket
	 * @param data : array of bytes to write
	 * @return -1 if failed, or number of bytes written
	 */
	public int Write(byte[] data) {
		if (IsConnected()) {
			if (data instanceof byte[] && data.length>0) {
				int length = data.length;
				byte[] newdata;
				if (isprefix) {
					newdata = new byte[length+4];
					newdata[0] = (byte)(length & 0xFF);
					newdata[1] = (byte)((length & 0xFF00)>>8);
					newdata[2] = (byte)((length & 0xFF0000)>>16);
					newdata[3] = (byte)((length& 0xFF000000)>>24);
					System.arraycopy(data, 0, newdata, 4, length);
				} else {
					newdata = data;
				}
				
				try {
					
					_socket.getOutputStream().write(newdata);
					packetsent = packetsent  < Integer.MAX_VALUE ? packetsent+1 : 0;
					totalsent = ((Long.MAX_VALUE - totalsent) > newdata.length) ? (totalsent + newdata.length) : newdata.length;
					raise_txrxstatus_event();
					return newdata.length;
				} catch (IOException e) {
					raise_log_event("Write failed, exception="+e.getMessage()+", closing socket");
					if (_socket!=null) {
						String remoteip = _socket.getInetAddress().getHostAddress();
						int remoteport = _socket.getPort();
						raise_disconnected_event(remoteip, remoteport,0);
					}
					Close();
				}
			}
		}
		return -1;
	}
	
	private class socketprefixrunnable implements Runnable{
		private final Socket trysocket;
		private final String remoteip;
		private final int remoteport;
		private final String localip;
		private final int localport;
		private final long _now;
		private InputStream sock_is = null;
		private boolean validrun = false;
		
		public socketprefixrunnable(Socket _sock) {
			trysocket = _sock;
			remoteip = trysocket.getInetAddress().getHostAddress();
			remoteport = trysocket.getPort();
			localip = trysocket.getLocalAddress().getHostAddress();
			localport = trysocket.getLocalPort();
			_now = DateTime.getNow();
			
		}
		
		@Override
		public void run() {
			
			
			
			validrun = false;
			try {
				_socket = trysocket;
				sock_is = _socket.getInputStream();
				validrun = true;
			} catch (IOException e1) {
				raise_log_event("getInputStream failed, exception="+e1.getMessage());
				
			}
			if (validrun) {
				raise_connected_event(remoteip, remoteport);
				raise_log_event("socket prefix connected to "+remoteip+":"+remoteport+", binded with "+localip+":"+localport);
				
				
				
				int readcount = 0;
				boolean need_continue = true;
				while(need_continue) {
					if (_socket==null) break;
					if (_socket.isClosed()) break;
					if (_socket.isInputShutdown()) break;
					ByteBuffer buf = ByteBuffer.allocate(0);
					do {
						readcount = -1;
						byte[] bb = new byte[1024];
						try {
							readcount = sock_is.read(bb);
							
							if (readcount>0) {
								if (readcount>buf.capacity()) {
									ByteBuffer newbuf = ByteBuffer.allocate(buf.capacity()+readcount);
									if (buf.capacity()>0) newbuf.put(buf);
									newbuf.put(bb,0,readcount);
									buf = newbuf;
									raise_log_event("Updating ByteBuffer with "+readcount+" bytes, now capacity="+buf.capacity()+", position="+buf.position()+", limit="+buf.limit());
								}
								
								packetreceived = packetreceived < Integer.MAX_VALUE ? packetreceived+1 : 0;
								totalreceived = ((Long.MAX_VALUE - totalreceived) > readcount) ? (totalreceived+readcount) : readcount;
								raise_txrxstatus_event();
							}
						} catch (IOException e) {
							raise_log_event("InputStream read failed, exception="+e.getMessage());
							need_continue = false;
							break;
						}
						
					} while(readcount!=-1);
					
					if (buf.capacity()>0) {
						raise_log_event("Final ByteBuffer got capacity="+buf.capacity()+", position="+buf.position()+", limit="+buf.limit());
						byte[] bufdata = buf.array();
						int bufdatalength = bufdata.length;
						if (bufdatalength>4) {
							int p_len = Byte.toUnsignedInt(bufdata[0]);
							p_len |= Byte.toUnsignedInt(bufdata[1])<<8;
							p_len |= Byte.toUnsignedInt(bufdata[2])<<16;
							p_len |= Byte.toUnsignedInt(bufdata[3])<<24;
							if (p_len==(bufdatalength-4)) {
								byte[] newbuf = new byte[p_len];
								System.arraycopy(bufdata, 4, newbuf, 0, p_len);
								raise_newbytesdata_event(newbuf);
							} else raise_log_event("Received ByteBuffer prefix length="+p_len+", packetlength="+(bufdatalength-4)+", not valid prefix");
						} else raise_log_event("Received ByteBuffer length less than 4, not valid prefix");
						
					}
				}
			}
			raise_disconnected_event(remoteip, remoteport, (DateTime.getNow()-_now));
		}
		
	}
	
	private class socketrunnable implements Runnable{
		private final Socket trysocket;
		private final String remoteip;
		private final int remoteport;
		private final String localip;
		private final int localport;
		private final long _now;
		private InputStream sock_is = null;
		private boolean validrun = false;
		
		
		public socketrunnable(Socket _sock) {
			
			trysocket = _sock;
			remoteip = trysocket.getInetAddress().getHostAddress();
			remoteport = trysocket.getPort();
			localip = trysocket.getLocalAddress().getHostAddress();
			localport = trysocket.getLocalPort();
			_now = DateTime.getNow();
			try {
				sock_is = trysocket.getInputStream();
				
				validrun = true;
			} catch (IOException e) {
				raise_log_event("Unable to getInputStream from socket, exception="+e.getMessage());
			}
		}
		
		@Override
		public void run() {
			
			
			validrun = false;
			try {
				_socket = trysocket;
				sock_is = _socket.getInputStream();
				validrun = true;
			} catch (IOException e1) {
				raise_log_event("getInputStream failed, exception="+e1.getMessage());
				
			}
			
			if (validrun) {
				raise_connected_event(remoteip, remoteport);
				raise_log_event("socket connected to "+remoteip+":"+remoteport+", binded with "+localip+":"+localport);
				
				int readcount = 0;
				boolean need_continue = true;
				while(need_continue) {
					if (_socket==null) break;
					if (_socket.isClosed()) break;
					if (_socket.isInputShutdown()) break;
					ByteBuffer buf = ByteBuffer.allocate(0);
					do {
						readcount = -1;
						byte[] bb = new byte[1024];
						try {
							readcount = sock_is.read(bb);
							if (readcount>0) {
								if (readcount>buf.capacity()) {
									ByteBuffer newbuf = ByteBuffer.allocate(buf.capacity()+readcount);
									if (buf.capacity()>0) newbuf.put(buf);
									newbuf.put(bb,0,readcount);
									buf = newbuf;
									raise_log_event("Updating ByteBuffer with "+readcount+" bytes, now capacity="+buf.capacity()+", position="+buf.position()+", limit="+buf.limit());
								}
								
								packetreceived = packetreceived < Integer.MAX_VALUE ? packetreceived+1 : 0;
								totalreceived = ((Long.MAX_VALUE - totalreceived) > readcount) ? (totalreceived+readcount) : readcount;
								raise_txrxstatus_event();
							}
						} catch (IOException e) {
							raise_log_event("InputStream read failed, exception="+e.getMessage());
							need_continue = false;
							break;
						}
						
					} while(readcount!=-1);
					
					if (buf.capacity()>0) {
						raise_log_event("Final ByteBuffer got capacity="+buf.capacity()+", position="+buf.position()+", limit="+buf.limit());
						byte[] result = new byte[buf.capacity()];
						System.arraycopy(buf.array(), 0, result, 0, buf.capacity());
						raise_newbytesdata_event(result);
					}
				}
			}
			raise_disconnected_event(remoteip, remoteport, (DateTime.getNow()-_now));
		}
		
	}
	
	/**
	 * Connect to Remote socket
	 * will raise event connected
	 * @param targetip : target remote ip
	 * @param targetport : target remote port
	 * @param timeout : how many miliseconds to wait for connection, put 0 for unlimited
	 * @param isprefix: if true, will connect in prefix mode
	 */
	public void ConnectTo(String targetip, int targetport, int timeout, boolean isprefix) {
		totalsent = 0l;
		totalreceived =0l;
		packetsent = 0;
		packetreceived = 0;
		Socket trysocket = null;
		if (targetip instanceof String && !targetip.isEmpty()) {
			if (targetport>0 && targetport<65536) {
				try {
					trysocket = new Socket();
					InetSocketAddress rem = new InetSocketAddress(targetip, targetport);
					trysocket.connect(rem, timeout);
					
					if (trysocket.isConnected()) {
						this.isprefix = isprefix;
						if (isprefix) {
							BA.submitRunnable(new socketprefixrunnable(trysocket), null, 0);
						} else {
							BA.submitRunnable(new socketrunnable(trysocket), null, 0);
						}
						return;
					}
					
					
				} catch (IOException e) {
					raise_log_event("ConnectTo failed, exception="+e.getMessage());
				}
			}
		}
		raise_connected_event("",0);
	}
	
	/**
	 * Connect to Remote Socket, with local IP address bind
	 * will raise event connected
	 * @param targetip : target remote ip
	 * @param targetport : target remote port
	 * @param localip : local ip address to bind
	 * @param localport : local port to bind, put 0 to set random number
	 * @param timeout : how many milisecods to wait for connection
	 * @param isprefix : if true, will connect in prefix mode
	 */
	public void ConnectTo_withBind(String targetip, int targetport, String localip, int localport, int timeout, boolean isprefix) {
		totalsent = 0l;
		totalreceived =0l;
		packetsent = 0;
		packetreceived = 0;
		Socket trysocket = null;
		if (targetip instanceof String && !targetip.isEmpty()) {
			if (targetport>0 && targetport<65536) {
				
				try {
					trysocket = new Socket();
					InetSocketAddress loc = new InetSocketAddress(localip, localport);
					InetSocketAddress rem = new InetSocketAddress(targetip, targetport);
					trysocket.bind(loc);
					trysocket.connect(rem, timeout);
					if (trysocket.isConnected()) {
						this.isprefix = isprefix;
						if (isprefix) {
							BA.submitRunnable(new socketprefixrunnable(trysocket), null, 0);
						} else {
							BA.submitRunnable(new socketrunnable(trysocket), null, 0);
						}
						return;
					}
					
				} catch (IOException e) {
					raise_log_event("ConnectTo_withBind failed, exception="+e.getMessage());
				}
			}
		}
		raise_connected_event("",0);
	}
	
	private void raise_log_event(String msg) {
		if (need_log_event) bax.raiseEventFromDifferentThread(Me, null, 0, event+"_log", false, new Object[] {msg});
	}
	
	private void raise_connected_event(String targetip, int targetport) {
		if (need_connected_event) bax.raiseEventFromDifferentThread(Me, null, 0, event+"_connected", false, new Object[] {targetip, targetport});
	}
	
	private void raise_disconnected_event(String targetip, int targetport, long duration) {
		if (need_disconnected_event) bax.raiseEventFromDifferentThread(Me, null, 0, event+"_disconnected", false, new Object[] {targetip, targetport, duration});
	}
	
	private void raise_newbytesdata_event(byte[] value) {
		if (need_newbytesdata_event) bax.raiseEventFromDifferentThread(Me, null, 0, event+"_newbytesdata", false, new Object[] {value});
	}
	private void raise_txrxstatus_event() {
		if (need_txrxstatus_event) bax.raiseEventFromDifferentThread(Me, null, 0, event+"_txrxstatus", false, new Object[] {packetsent, packetreceived, totalsent, totalreceived});
	}
}
