package customsocket;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.concurrent.Callable;

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

@BA.ShortName("TCPSocketPrefixMode")
@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)",
		"newstringdata(value as String)",
		
})
public class TCPSocketPrefixMode {
	private BA bax;
	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_newstringdata_event = false;
	private boolean inited = false;
	private Socket _socket;
	
	private long _connected_tick = 0;
	
	private int _txokcount = 0;
	private long _txbytescount = 0;
	private int _rxokcount = 0;
	private long _rxbytescount = 0;
	
	private String _remoteip = "";
	private int _remoteport = 0;
	private String _localip = "";
	private int _localport = 0;
	
	
	
	/**
	 * Initialize TCP Socket Prefix Mode
	 * @param EventName Eventname
	 */
	public void Initialize(BA ba, String EventName) {
		this.bax = ba;
		this.event = EventName.toLowerCase();
		if (bax!=null) {
			if (event != null && event.length() > 0) {
				need_log_event = bax.subExists(event+"_log");
				need_connected_event = bax.subExists(event+"_connected");
				need_disconnected_event = bax.subExists(event+"_disconnected");
				need_newbytesdata_event = bax.subExists(event+"_newbytesdata");
				need_newstringdata_event = bax.subExists(event+"_newstringdata");
				inited = true;
			}
		}
	}
	
	public boolean IsInitialized() {
		return inited;
	}
	
	public String getRemoteIP() {
		return _remoteip;
	}
	
	public int getRemotePort() {
		return _remoteport;
	}
	
	public String getLocalIP() {
		return _localip;
	}
	
	public int getLocalPort() {
		return _localport;
	}
	
	public void ConnectTo(String targetip, int port) {
		if (targetip != null && targetip.length()>0) {
			if (port>0 && port < 65536) {
				BA.runAsync(bax, TCPSocketPrefixMode.this, event+"_connected", new Object[] {"",0}, new Callable<Object[]>(){

					@Override
					public Object[] call() throws Exception {
						try {
							Socket trysock = new Socket();
							SocketAddress rem = new InetSocketAddress(targetip, port);
							trysock.connect(rem);
							Thread tx = new Thread(new tcpsocketrun(trysock));
							tx.start();
							return new Object[] {targetip,port};
						} catch (UnknownHostException e) {
							raise_log_error("UnknownHostException on ConnectTo ["+targetip+":"+port+"]",e);
							
						} catch (IOException e) {
							raise_log_error("IOException on ConnectTo ["+targetip+"]:["+port+"]",e);
						}
						return new Object[] {"",0};
					}
					
				});
				return;
			} else raise_log("ConnectTo failed, invalid targetport");
		} else raise_log("ConnectTo failed, Invalid targetip");
		raise_connected("",0);
	}
	
	public void  ConnectTo_withBind(String targetip, int port, String localip) {
		if (targetip != null && !targetip.isEmpty()) {
			if (port>0 && port < 65536) {
				if (localip!=null && !localip.isEmpty()) {
					BA.runAsync(bax, TCPSocketPrefixMode.this, event+"_connected", new Object[] {"",0}, new Callable<Object[]>() {

						@Override
						public Object[] call() throws Exception {
							try {
								SocketAddress loc = new InetSocketAddress(localip, port);
								SocketAddress rem = new InetSocketAddress (targetip,port);
								Socket trysocket = new Socket();
								trysocket.bind(loc);
								trysocket.connect(rem);
								Thread tx = new Thread(new tcpsocketrun(trysocket));
								tx.start();
								return new Object[] {targetip,port};
							} catch(IOException e) {
								raise_log_error("IOException on ConnectTo_withBind ["+targetip+"]:]"+port+"] Bind="+localip,e);
							}
							return new Object[] {"",0};
						}
						
					});
				} else raise_log("ConnectTo_withBind failed, invalid localip");
			} else raise_log("ConnectTo_withBind failed, invalid targetport");
		} else raise_log("ConnectTo_withBind failed, Invalid targetip");
		raise_connected("",0);
	}
	
	public void Disconnect() {
		
		
		if (_socket != null) {
			try {
				_socket.close();
				raise_disconnected(_remoteip, _remoteport);
			} catch (IOException e) {
				raise_log_error("IOException on Disconnect",e);
				raise_disconnected("",0);
			}
		} else {
			raise_log("Disconnect has not effect, Socket is not created yet");
			raise_disconnected("",0);
		}
		
		_socket = null;
		_remoteip = "";
		_localip = "";
		_remoteport =0;
		_localport = 0;
		
		
	}
	
	public boolean WriteBytes(byte[] data) {
		if (IsConnected()) {
			if (data != null && data.length>0) {
				ByteBuffer bb = ByteBuffer.allocate(data.length+4);
				bb.order(ByteOrder.BIG_ENDIAN);
				bb.putInt(data.length);
				bb.put(data);
				try {
					_socket.getOutputStream().write(bb.array());
					
					_txokcount++;
					_txbytescount += data.length;
					return true;
				} catch (IOException e) {
					raise_log_error("IOException on WriteBytes", e);
					Disconnect();
				}
			} else raise_log("WriteBytes failed, data is null or empty");
		} else raise_log("WriteBytes failed, Socket is not connected");
		return false;
	}
	
	public boolean WriteString(String data) {
		try {
			byte[] xx = data.getBytes("UTF-8");
			return WriteBytes(xx);
		} catch (UnsupportedEncodingException e) {
			raise_log_error("UnsupportedEncodingException on WriteString", e);
		}
		return false;
	}
	
	public long ConnectionDuration() {
		if (_connected_tick>0) {
			long now = DateTime.getNow();
			if ((now - _connected_tick)>0) {
				return ((now - _connected_tick) / DateTime.TicksPerSecond);
			}
		}
		return 0;
	}
	
	public String Connected_DateTime_String() {
		if (_connected_tick>0) {
			return mycodes.Tick_To_DDMMYYYY_HHMMSS(_connected_tick);
		}
		return "";
	}
	
	public int getTX_OK_Counter() {
		
		return _txokcount;
	}
	
	public int getRX_OK_Counter() {
		return _rxokcount;
	}
	
	public long getTX_Bytes_Counter() {
		return _txbytescount;
	}
	
	public long getRX_Bytes_Counter() {
		return _rxbytescount;
	}
	
	/**
	 * Check if Socket is connected
	 * @return true if connected
	 */
	public boolean IsConnected() {
		if (_socket != null) {
			if (_socket.isConnected()) {
				if (!_socket.isInputShutdown()) {
					if (!_socket.isOutputShutdown()) {
                        return !_socket.isClosed();
					}
				}
			}
		}
		return false;
	}
	
	
	
	class tcpsocketrun implements Runnable{
		private boolean validrun = false;
		private InputStream _input;
		public tcpsocketrun(Socket xx) { 
			// reset
			_remoteip = "";
			_remoteport = 0;
			_localip = "";
			_localport = 0;
			_connected_tick = 0;
			_rxbytescount = 0;
			_rxokcount = 0;
			_txbytescount = 0;
			_txokcount = 0;
			
			try {
				BA.Log("tcpsocketrun about to call Disconnect on previous _socket if available");
				if (_socket != null) Disconnect();
				_socket = xx;
				_input = _socket.getInputStream();
				
				_remoteip = _socket.getInetAddress() == null ? "" : _socket.getInetAddress().getHostAddress();
				_localip = _socket.getLocalAddress() == null ? "" : _socket.getLocalAddress().getHostAddress();
				_remoteport = _socket.isClosed() ? 0 : _socket.getPort();
				_localport = _socket.getLocalPort();
				
				validrun = true;
				
			} catch(IOException e) {
				raise_log_error("IOException on tcpsocketrun",e);
			}
			
			if (validrun) {
				/// shutdownhook taruh di sini, buat auto close ketika socket connected
				Runtime.getRuntime().addShutdownHook(new Thread() {
					public void run() {
						
						Disconnect();
					}
				});
			}
			
		}
		
		public void run() {
			
			if (validrun) {
				raise_log("tcpsocketrun running");
				//raise_connected(_socket.getInetAddress().getHostAddress(), _socket.getPort());
				_connected_tick = DateTime.getNow();
				
				int readcount;
				byte[] xx;
				while(IsConnected()) {
					xx = new byte[15000];
					readcount = 0;
					try {
						
						readcount = _input.read(xx); // ngeblok di sini sampai dapat data
						
					} catch (IOException e) {
						raise_log_error("IOException on tcpsocketrun read",e);
						break;
					}
					
					// prefix mode, 4 byte awal adalah size, sisanya adalah data
					// jadi minimal length = 4
					if (readcount>4) {
						ByteBuffer bb = ByteBuffer.wrap(xx);
						bb.order(ByteOrder.BIG_ENDIAN);
						try {
							int datasize = bb.getInt();
							byte[] data = new byte[datasize];
							bb.get(data);
							_rxokcount++;
							_rxbytescount += readcount;
							byte[] yy = new byte[readcount];
							System.arraycopy(xx, 0, yy, 0, readcount);
							raise_newbytesdata(yy);
							try {
								String zz = new String(yy, "UTF-8");
								raise_newstringdata(zz);
							} catch (UnsupportedEncodingException e) {
								raise_log_error("UnsupportedEncodingException on tcpsocketrun read", e);
							}
						} catch (BufferUnderflowException e) {
							
							raise_log_error("BufferUnderflowException on tcpsocketrun read, prefix invalid", e);
							break;
						}
						
						
					} 
				}
				Disconnect();
				

			} else raise_disconnected("",0);
			raise_log("tcpsocketrun ended");
			
		}
	}
	
	
	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());
		e.printStackTrace();
	}
	
	private void raise_log(String msg) {
		if (need_log_event) bax.raiseEventFromDifferentThread(TCPSocketPrefixMode.this, null, 0, event+"_log", false, new Object[] {msg});
	}
	
	private void raise_connected(String remoteip, int remoteport) {
		if (need_connected_event) {
			bax.raiseEventFromDifferentThread(TCPSocketPrefixMode.this, null, 0, event+"_connected", false, new Object[] {remoteip, remoteport});
		}
	}
	
	private void raise_disconnected(String remoteip, int remoteport) {
		if (need_disconnected_event) {
			if (remoteport > 0 && remoteip.length() > 0) {
				// pernah connect
				long tt = 0;
				if (_connected_tick > 0) {
					tt = (DateTime.getNow() - _connected_tick) / DateTime.TicksPerSecond;
				}
				bax.raiseEventFromDifferentThread(TCPSocketPrefixMode.this, null, 0, event+"_disconnected", false, new Object[] {remoteip,remoteport, tt / DateTime.TicksPerSecond });
			}
			
		}
	}
	
	private void raise_newbytesdata(byte[] value) {
		if (need_newbytesdata_event) {
			bax.raiseEventFromDifferentThread(TCPSocketPrefixMode.this, null, 0, event+"_newbytesdata", false, new Object[] {value});
		}
	}
	
	private void raise_newstringdata(String value) {
		if (need_newstringdata_event) {
			bax.raiseEventFromDifferentThread(TCPSocketPrefixMode.this, null, 0, event+"_newstringdata", false, new Object[] {value});
		}
	}
}
