package network;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;

import anywheresoftware.b4a.BA;
import anywheresoftware.b4a.keywords.Bit;

@BA.ShortName("jUDPSocket")
@BA.Events(values= {
		"log(msg as string)",
		"socketstatus(isopen as boolean)",
		"newdata(bb() as byte, sourceip as string, sourceport as int)",
		"txrxstatistic(txcount as int, txfail as int, rxcount as int, rxfail as int)"
})

/**
 * UDP Socket by me, with more functions
 * @author rdkartono
 *
 */
public class jUDPSocket {

	private BA ba;
	private Object caller;
	private String event;
	private final Object Me = this;
	private boolean need_log_event = false;
	private boolean need_socketstatus_event = false;
	private boolean need_newdata_event = false;
	private boolean need_txrxstatistic_event = false;
	private DatagramSocket udp;
	private boolean isopened = false;
	
	private int txfail=0, txcount=0, rxfail=0, rxcount=0;
	
	/**
	 * Initialize UDP Socket (made by RDK)
	 * @param callerobject : caller object
	 * @param eventname : event name
	 */
	public void Initialize(BA bax, Object callerobject, String eventname) {
		ba = bax;
		caller = callerobject;
		event = eventname.trim();
		if (ba instanceof BA) {
			if (caller != null) {
				if (!event.isEmpty()) {
					need_log_event = ba.subExists(event+"_log");
					need_socketstatus_event = ba.subExists(event+"_socketstatus");
					need_newdata_event = ba.subExists(event+"_newdata");
					need_txrxstatistic_event = ba.subExists(event+"_txrxstatistic");
				}
			}
		}
	}
	
	/**
	 * Check if UDP already opened
	 * @return true if opened
	 */
	public boolean getUDPIsOpened() {
		return isopened;
	}
	
	/**
	 * Close UDP Communication
	 * will raise socketstatus event
	 */
	public void Close() {
		isopened = false;
		if (udp instanceof DatagramSocket) {
			udp.close();
		}
		
		udp = null;
		raise_socketstatus(false);
	}
	
	/**
	 * Get IPV4 bytes from ip string
	 * @param ipstring : ip string to translate
	 * @return 0.0.0.0 if failed
	 */
	public byte[] Get_IPV4_Bytes(String ipstring) {
		if (!ipstring.isEmpty()) {
			try {
				InetAddress xx = InetAddress.getByName(ipstring);
				return xx.getAddress();
			} catch (UnknownHostException e) {
				raise_log("Unable to Get_IPV4_Bytes, UnknownHostException Msg : "+e.getMessage());
			} catch(SecurityException e) {
				raise_log("Unable to Get_IPV4_Bytes, SecurityException Msg : "+e.getMessage());
			}
		}
		return new byte[4];
	}
	
	/**
	 * Ping a target IP
	 * @param ipstring : target IP
	 * @return true if reachable
	 */
	public boolean PingTo(String ipstring) {
		try {
			InetAddress target = InetAddress.getByName(ipstring);
			return target.isReachable(1000);
		} catch (UnknownHostException e) {
			raise_log("Unable to PingTo "+ipstring+", UnknownHostException Msg : "+e.getMessage());
		} catch (IOException e) {
			raise_log("Unable to PingTo "+ipstring+", IOException Msg : "+e.getMessage());
		}
		return false;
	}
	
	/**
	 * Get Local MAC address
	 * @return empty string if failed
	 */
	public String GetLocalMAC() {
		
		try {
			InetAddress local = InetAddress.getLocalHost();
			NetworkInterface ni = NetworkInterface.getByInetAddress(local);
			byte[] result = ni.getHardwareAddress();
			if (result!=null) {
				if (result.length>0) {
					StringBuilder str = new StringBuilder();
					for(byte xx : result) {
						if (str.length()>0) str.append(":");
						int xt = Bit.And(xx, 0xFF);		// jadiin positif dulu 
						if (xt<10) str.append("0");		// kalau di bawah 10, tambahin 0 , biar double digit
						str.append(Bit.ToHexString(xt));
					}
					return str.toString().toUpperCase();
				}
			}
		} catch (UnknownHostException e) {
			raise_log("Unable to GetLocalMAC, UnknownHostException Msg : "+e.getMessage());
		} catch (SocketException e) {
			raise_log("Unable to GetLocalMAC, SocketException Msg : "+e.getMessage());
		} catch(NullPointerException e) {
			raise_log("Unable to GetLocalMAC, NullPointerException Msg : "+e.getMessage());
		}	
		return "";
	}
	
	/**
	 * Get Local IP Address 
	 * @return empty string failed
	 */
	public String GetLocalIP() {
		try {
			InetAddress local = InetAddress.getLocalHost();
			return local.getHostAddress();
		} catch (UnknownHostException e) {
			raise_log("Unable to GetLocalIP, UnknownHostException Msg : "+e.getMessage());
		}
		
		return "";
	}
	
	/**
	 * Get Local Port bounded/opened with UDP Socket
	 * @return -1 if UDP is not opened yet
	 */
	public int GetLocalPort() {
		if (udp instanceof DatagramSocket) {
			return udp.getLocalPort();
		}
		return -1;
	}
	
	/**
	 * Open UDP Socket
	 * will raise socketstatus event
	 * @param port : port number to listen
	 * @param maxpackagesize : max packagesize. Default to 1000 bytes
	 * @return true if port can be opened
	 */
	public boolean Open(int port, int maxpackagesize) {
		if (maxpackagesize<1) maxpackagesize = 1000;
		
		if (udp instanceof DatagramSocket) Close();		// kalau sudah pernah open, close dulu 
		try {
			udp = new DatagramSocket(port);
			Thread tx = new Thread(new udprun(udp, maxpackagesize));
			tx.start();
			raise_socketstatus(true);
			return true;
		} catch (SocketException e) {
			raise_log("Unable to Open UDP at port "+port+", SocketException Msg : "+e.getMessage());
			raise_socketstatus(false);
		} catch(SecurityException e) {
			raise_log("Unable to Open UDP at port "+port+", SecurityException Msg : "+e.getMessage());
			raise_socketstatus(false);
		}
		return false;
	}
	
	private class udprun implements Runnable {
		private final int maxudpsize;
		private final DatagramSocket udp;
		
		public udprun(DatagramSocket udpsock, int maxsize) {
			udp = udpsock;
			maxudpsize = maxsize;
			isopened = true;
		}
		
		@Override
		public void run() {
			
			while(isopened) {
				if (udp instanceof DatagramSocket) {
					byte[] bb = new byte[maxudpsize];
					DatagramPacket xx = new DatagramPacket(bb, maxudpsize);
					try {
						udp.receive(xx);
						byte[] sourcedata = xx.getData();
						if (sourcedata.length > xx.getLength()) {
							byte[] newdata = new byte[xx.getLength()];
							for(int ii=0;ii<xx.getLength();ii++) {
								newdata[ii] = sourcedata[ii];
							}
							
							raise_newdata(newdata, xx.getAddress().getHostAddress(), xx.getPort());
						} else {
							raise_newdata(sourcedata, xx.getAddress().getHostAddress(), xx.getPort());
								
						}
						
						if (rxcount<Integer.MAX_VALUE) rxcount+=1; else rxcount = 0;
						raise_txrxstatistic();
					} catch (IOException e) {
						raise_log("Unable to Receive Data, IOException Msg : "+e.getMessage());
						if (rxfail < Integer.MAX_VALUE) rxfail+=1; else rxfail = 0;
						raise_txrxstatistic();
						break;
					}
				} else break;
			}
			Close();
		}
		
	}
	
	/**
	 * Send Data to Target
	 * @param bb : data to send
	 * @param targetip : IP address or host name
	 * @param targetport : target UDP Port
	 * @return true if success
	 */
	public boolean SendData(byte[] bb, String targetip, int targetport) {
		if (!targetip.isEmpty()) {
			if (targetport>0) {
				if (bb!=null) {
					if (bb.length>0) {
						if (udp instanceof DatagramSocket) {
							try {
								InetAddress t_ip = InetAddress.getByName(targetip);
								DatagramPacket pp = new DatagramPacket(bb, bb.length, t_ip, targetport);	
								udp.send(pp);
								if (txcount<Integer.MAX_VALUE) txcount+=1; else txcount = 0;
								raise_txrxstatistic();
								return true;
							} catch (UnknownHostException e) {
								raise_log("Unable to SendData to "+targetip+":"+targetport+", UnknownHostException Msg : "+e.getMessage());
							} catch (IOException e) {
								raise_log("Unable to SendData to "+targetip+":"+targetport+", IOException Msg : "+e.getMessage());
							}
							
						} else raise_log("Unable to SendData to "+targetip+":"+targetport+", udp is not opened");						
					} else raise_log("Unable to SendData to "+targetip+":"+targetport+", data size is zero");
				} else raise_log("Unable to SendData to "+targetip+":"+targetport+", data is null");
			} else raise_log("Unable to SendData, targetport is negative value");
		} else raise_log("Unable to SendData, targetip is empty");
		
		if (txfail<Integer.MAX_VALUE) txfail+=1; else txfail = 0;
		raise_txrxstatistic();
		return false;
	}
	
	/**
	 * Get Succesfull transmission counter
	 * @return tx sent 
	 */
	public int getTX_Sent() {
		return txcount;
	}
	
	/**
	 * Get Failed Transmission counter
	 * @return tx failed
	 */
	public int getTX_Failed() {
		return txfail;
	}
	
	/**
	 * Get Successfull reception counter
	 * @return rx received
	 */
	public int getRX_Received() {
		return rxcount;
	}
	
	/**
	 * Get Failed reception counter
	 * @return rx fail
	 */
	public int getRX_Failed() {
		return rxfail;
	}
	
	private void raise_log(String msg) {
		if (need_log_event) ba.raiseEventFromDifferentThread(Me, null, 0, event+"_log", false, new Object[] {msg});
	}
	private void raise_socketstatus(boolean isopen) {
		if (need_socketstatus_event) ba.raiseEventFromDifferentThread(Me, null, 0, event+"_socketstatus", false, new Object[] {isopen});
	}
	
	private void raise_newdata(byte[] bb, String sourceip, int sourceport) {
		if (need_newdata_event) ba.raiseEventFromDifferentThread(Me,null, 0, event+"_newdata", false, new Object[] {bb, sourceip, sourceport});
	}
	
	private void raise_txrxstatistic() {
		if (need_txrxstatistic_event) ba.raiseEventFromDifferentThread(Me, null, 0, event+"_txrxstatistic", false, new Object[] {txcount, txfail, rxcount, rxfail});
	}
}
