package customsocket;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;

import anywheresoftware.b4a.*;

//@BA.ShortName("UDPSocketNew")
@BA.Permissions(values = {"android.permission.INTERNET"})
@BA.Events(values={"PacketArrived (Packet As UDPPacket)"})
@BA.Hide
public class UDPSocket {
	private UDPReader reader;
	private DatagramSocket ds;
	
	/**
	 * Initialize UDPSocket 
	 * @param EventName eventname
	 * @param Port listening port
	 * @param ReceiveBufferSize size of buffer
	 * @throws SocketException
	 */
	public void Initialize(BA ba, String EventName, int Port, int ReceiveBufferSize) throws SocketException {
		Close();
		DatagramSocket newds = null;
		try {
			if (Port==0) {
				newds = new DatagramSocket();
			} else {
				newds = new DatagramSocket(Port);
			}
		} catch(IOException e) {
			BA.Log("UDPSocketNew Initialize failed, exception = "+e.getMessage());
		} finally {
			if (newds instanceof DatagramSocket) {
				init(ba, EventName, ReceiveBufferSize, newds);
			} else BA.Log("Not calling init");
		}
		
		
	}
	
	/**
	 * Initialize UDPSocket with specific bind
	 * @param EventName eventname
	 * @param Port listening port
	 * @param ReceiveBufferSize size of buffer
	 * @param localip Local IP Address bind to UDPSocket
	 * @throws SocketException
	 */
	public void Initialize_withBind(BA ba, String EventName, int Port, int ReceiveBufferSize, String localip) throws SocketException {
		Close();
		DatagramSocket newds = null;
		try {
			InetSocketAddress bindadd = new InetSocketAddress(localip, Port);
			newds = new DatagramSocket(bindadd);
			
		} catch(IOException e) {
			BA.Log("UDPSocketNew Initialize_withBind failed, exception="+e.getMessage());
		} finally {
			if (newds instanceof DatagramSocket) {
				BA.Log("UDPSocketNew Initialize_withBind will call init");
				init(ba, EventName, ReceiveBufferSize, newds);
			} else BA.Log("UDPSocketNew Initialize_withBind not calling init");
		}
		
	}
	
	@BA.Hide
	public void init(BA ba, String EventName, int ReceiveBufferSize, DatagramSocket ds) {
		this.ds = ds;
		if (ReceiveBufferSize > 0) {
			reader = new UDPReader();
			reader.working = true;
			reader.socket = ds;
			reader.receiveLength = ReceiveBufferSize;
			reader.ba = ba;
			reader.eventName = EventName.toLowerCase(BA.cul);
			Thread t = new Thread(reader);
			t.setDaemon(true);
			t.start();
		}
	}
	
	public boolean IsInitialized() {
		return ds != null && !ds.isClosed();
	}
	
	/**
	 * Get Local Port where UDPSocket is listening
	 * @return -1 if UDPsocket closed, or 0 if not bound
	 */
	public int getPort() {
		return ds.getLocalPort();
	}
	
	/**
	 * Get Local IP Address where UDPSocket is bound
	 * @return null if UDPSocket is closed
	 */
	public String getLocalIP() {
		return ds.getLocalAddress().getHostAddress();
	}
	
	/**
	 * Get Remote Port where UDPSocket is connected
	 * @return -1 if not connected
	 */
	public int getRemotePort() {
		return ds.getPort();
	}
	
	/**
	 * Get Remote IP where UDPSocket is connected
	 * @return null if not connnected
	 */
	public String getRemoteIP() {
		return ds.getInetAddress().getHostAddress();
	}
	
	/**
	 * Send UDP Packet
	 * @param Packet packet to send
	 * @throws IOException
	 */
	public void Send(final UDPPacket Packet) throws IOException {
		BA.submitRunnable(new Runnable() {

			@Override
			public void run() {
				try {
					Packet.getObject().packet.setSocketAddress(new InetSocketAddress(Packet.getObject().host, Packet.getObject().port));
					ds.send(Packet.getObject().packet);
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			}

		}, this, 1);
	}
	
	public void Close() {
		if (ds!=null) {
			ds.close();
		}
		ds = null;
		if (reader!=null) {
			reader.working = false;
		}
		reader = null;
	}
	
	@Override
	public String toString() {
		if (ds == null)
			return "Not initialized";
		return getLocalIP()+":"+getPort();
	}
	
	private static class UDPReader implements Runnable {
		volatile boolean working;
		DatagramSocket socket;
		int receiveLength;
		BA ba;
		String eventName;
		@Override
		public void run() {
			while (working) {
				try {
					DatagramPacket p = new DatagramPacket(new byte[receiveLength], receiveLength);
					socket.receive(p);
					UDPPacket u = new UDPPacket();
					u.setObject(new MyDatagramPacket("", 0, p));
					ba.raiseEventFromDifferentThread(null, null, 0, eventName + "_packetarrived", false, new Object[] {u});
				} catch (IOException e) {
					if (working) {
						try {
							e.printStackTrace();
							Thread.sleep(100);
						} catch (InterruptedException e1) {
						}
					}
				}
			}
		}
	}
	
	
	
	/**
	 * A packet of data that is being sent or received.
	 *To send a packet call one of the Initialize methods and then send the packet by passing it to UDPSocket.Send.
	 *When a packet arrives you can get the data in the packet from the available properties.
	 */
//	@BA.ShortName("UDPPacket")
	@BA.Hide
	public static class UDPPacket extends AbsObjectWrapper<MyDatagramPacket> {
		/**
		 * Initializes the packet and makes it ready for sending.
		 *Data - The data that will be send.
		 *Host - The target host name or IP address.
		 *Port - The target port.
		 */
		public void Initialize(byte[] Data, String Host, int Port) throws SocketException {
			Initialize2(Data, 0, Data.length, Host, Port);
		}
		/**
		 * Similar to Initialize. The data sent is based on the Offset and Length values.
		 */
		public void Initialize2(byte[] Data, int Offset, int Length, String Host, int Port) throws SocketException {

			DatagramPacket d = new DatagramPacket(Data, Offset, Length);
			MyDatagramPacket m = new MyDatagramPacket(Host, Port, d);
			setObject(m);
		}
		/**
		 * Gets the length of available bytes in the data. This can be shorter than the array length.
		 */
		public int getLength() {
			return getObject().packet.getLength();
		}
		/**
		 * Gets the data array received.
		 */
		public byte[] getData() {
			return getObject().packet.getData();
		}
		/**
		 * Gets the offset in the data array where the available data starts.
		 */
		public int getOffset() {
			return getObject().packet.getOffset();
		}
		/**
		 * Gets the port of the sending machine.
		 */
		public int getPort() {
			return getObject().packet.getPort();
		}
		/**
		 *<b>This method is deprecated and will not work properly on Android 4+ device.</b>
		 *Use HostAddress instead.
		 */
		public String getHost() {
			return getObject().packet.getAddress().getHostName();
		}
		/**
		 * Gets the IP address of the sending machine.
		 */
		public String getHostAddress() {
			return getObject().packet.getAddress().getHostAddress();
		}
		@Override
		public String toString() {
			if (getObjectOrNull() == null)
				return super.toString();
			return "Length=" + getLength() + ", Offset=" + getOffset() + ", Host=" + getHost() + ", Port=" + getPort();
		}
	}
	@BA.Hide
	public static class MyDatagramPacket {
		public final String host;
		public final int port;
		public final DatagramPacket packet;
		public MyDatagramPacket(String host, int port, DatagramPacket packet) {
			this.host = host;
			this.port = port;
			this.packet = packet;
		}
	}
}
