package ptzcamera;

import com.fazecast.jSerialComm.SerialPort;
import com.fazecast.jSerialComm.SerialPortDataListener;
import com.fazecast.jSerialComm.SerialPortEvent;
import com.fazecast.jSerialComm.SerialPortInvalidPortException;

import anywheresoftware.b4a.BA;
import anywheresoftware.b4a.keywords.Bit;
import anywheresoftware.b4a.objects.collections.List;
import anywheresoftware.b4a.objects.collections.Map;
import jGPIO.mycodes;

@BA.ShortName("PTZComPort")
@BA.Events(values= {
		"log(msg as string)",
		"rawdatareceived(bb() as byte)",
		"rawdatasent(bb() as byte)",
		"txrxcount(tx as int, rx as int)"
})

/**
 * Serial Port for PTZ Communication
 * @author rdkartono
 *
 */
public class PTZComPort {
	private BA bax;
	private String event;
	private Object caller;
	private final Object Me = this;
	private boolean need_log_event = false;
	private boolean need_rawdatareceived_event = false;
	private boolean need_txrxcount_event = false;
	private boolean need_rawdatasent_event = false;
			
	private SerialPort myport;
	private String _jserialcomm_version;
	private boolean _jserialcomm_loaded = false;
	private String _portname;
	private int _baudrate = 9600;
	private int rxcount=0;
	private int txcount=0;
	
	//map containing object : PelcoP, PelcoD
	private Map cam_map = new Map();
	
	/**
	 * Initialize PTZ Com Port
	 * @param callerobject : caller object
	 * @param eventname : event name
	 */
	public void Initialize(BA ba, Object callerobject,String eventname) {
		bax = ba;
		caller = callerobject;
		event = eventname.trim();
		cam_map.Initialize();
		if (bax instanceof BA) {
			if (caller!=null) {
				if (!event.isEmpty()) {
					need_log_event = bax.subExists(event+"_log");
					need_rawdatareceived_event = bax.subExists(event+"_rawdatareceived");
					need_txrxcount_event = bax.subExists(event+"_txrxcount");
					need_rawdatasent_event = bax.subExists(event+"_rawdatasent");
				}
			}
		}
		_jserialcomm_version = SerialPort.getVersion();
		_jserialcomm_loaded = _jserialcomm_version.isEmpty() ? false : true;
		if ( _jserialcomm_loaded)
			raise_log("JSerialComm version = "+_jserialcomm_version);
		else	
			raise_log("Unable to load JSerialComm");
		
		 Runtime.getRuntime().addShutdownHook(new Thread() {
	            @Override
	            public void run() {
	                ClosePort();
	            }
	        });
	}
	
	/**
	 * Get SerialPort Port name
	 * @return portname
	 */
	public String getPortName() {
		return _portname;
	}
	
	/**
	 * Close PTZ Com Port
	 */
	public void ClosePort() {
		if (myport instanceof SerialPort) {
			myport.removeDataListener();
			
			myport.closePort();
			myport = null;
		}
	}
	
	/**
	 * 
	 * @param cam
	 * @return
	 */
	@BA.Hide
	public boolean AddToMap(PelcoD cam) {
		if (cam_map instanceof Map) {
			if (cam_map.IsInitialized()) {
				if (cam instanceof PelcoD) {
					cam_map.Put(cam.getID(), cam);
					return true;
				}
			}
		}
		return false;
	}
	
	@BA.Hide
	public void AddToMap(PelcoP cam) {
		if (cam_map instanceof Map) {
			if (cam_map.IsInitialized()) {
				if (cam instanceof PelcoP) {
					cam_map.Put(cam.getID(), cam);
				}
			}
		}
	}
	

	/**
	 * Open PTZ Com Port
	 * @param portname : valid port name
	 * @param baudrate : baud rate, default to 9600
	 * @return true if success
	 */
	public boolean OpenPort(String portname, int baudrate) {
		rxcount = 0;
		if (! _jserialcomm_loaded) {
			raise_log("JSerialComm not loaded");
			return false;
		}
		
		_portname = portname.isEmpty() ? "" : portname.trim();
		
		if (_portname.isEmpty()) {
			raise_log("OpenPort failed, portname is empty");
			return false;
		}
		
		_baudrate = baudrate > 0 ? baudrate : 9600;
		
		try {
			myport = SerialPort.getCommPort(_portname);
		} catch(SerialPortInvalidPortException e) {
			raise_log("Port "+ _portname+" is not accessible, Msg : "+e.getMessage());
			return false;
		}
		
		// sampe sini bisa getCommPort
		if (myport.openPort()) {
			if (myport.setBaudRate(_baudrate)) {
				if (myport.addDataListener(new SerialPortDataListener() {

					@Override
					public int getListeningEvents() {
						// cuma tertarik kalau ada data masuk
						return SerialPort.LISTENING_EVENT_DATA_RECEIVED;
					}

					@Override
					public void serialEvent(SerialPortEvent event) {
						if (event instanceof SerialPortEvent) {
							byte[] bb = event.getReceivedData();
							if (bb!=null) {
								if (bb.length>0) {
									// ada data masuk
									raise_rawdatareceived(bb);
									
									rxcount+= bb.length;
									raise_txrxcount();
									
									process_received_bytes(bb);
								}
							}
						}
					}
					
				}))
					
				{
					
					// success semua
					return true;
				}
				else raise_log("Unable to set DataListener event");
			} else raise_log("Unable to set Baudrate "+ _baudrate);
		} else raise_log("Unable to open Port "+ _portname);
		return false;
	}
	
	private void process_received_bytes(byte[] bb) {
		// TODO : nanti hapus log nya
		BA.Log("PTZComPort Received : "+mycodes.printbytes(bb));
		int ID = -1;
		Object cam;
		if (PelcoP.data_valid(bb)) {
			// is PelcoP data
			ID = Bit.And(bb[1], 0xFF);
			cam = GetCamMapObject(ID);
			if (cam instanceof PelcoP) {
				((PelcoP) cam).DataReceivedFromPTZComPort(bb);
			}
		} else if (PelcoD.data_valid(bb)) {
			// is PelcoD data
			ID = Bit.And(bb[1], 0xFF);
			cam = GetCamMapObject(ID);
			if (cam instanceof PelcoD) {
				((PelcoD) cam).DataReceivedFromPTZComPort(bb);
			}
		}	
	}
	
	private Object GetCamMapObject(int ID) {
		if (cam_map instanceof Map) {
			if (cam_map.IsInitialized()) {
				if (ID>0) {
					if (cam_map.ContainsKey(ID)){
						return cam_map.Get(ID);
					}
				}
			}
		}
		return null;
	}
	
	/**
	 * Check if Serial Port is opened 
	 * @return true if opened
	 */
	public boolean IsOpened() {
		if (_jserialcomm_loaded) {
			if (myport instanceof SerialPort) {
				return myport.isOpen();
			}
		}
		return false;
	}
	
	/**
	 * Get Available Serial Port in system
	 * @return List of Port Names
	 */
	public List GetSerialPorts() {
		List result = new List();
		result.Initialize();
		
		if(_jserialcomm_loaded) {
			SerialPort[] xx = SerialPort.getCommPorts();
			if (xx!=null) {
				if (xx.length>0) {
					for(SerialPort yy : xx) {
						result.Add(yy.getSystemPortName());
					}
				}
			}
		}
		return result;
	}
	
	
	/**
	 * Send Some Data
	 * @param bb : data to send
	 * @return true if success
	 */
	public synchronized boolean SendData(byte[] bb) {
		if (IsOpened()) {
			if (bb!=null) {
				if (bb.length>0) {
					int written = myport.writeBytes(bb, bb.length);
					raise_rawdatasent(bb);
					if (written == bb.length) {
						txcount += written;
						raise_txrxcount();
						return true;
					} else raise_log("SendData failed, counter mismatch");
				} else raise_log("SendData failed, data is zero length");
			} else raise_log("SendData failed, data is null");	
			
		} else raise_log("SendData failed, SerialPort not opened");

		return false;
	}
	
	/**
	 * Change Databit length
	 * @param value : data bit length, default to 8
	 * @return true if sucess
	 */
	public boolean Set_DataBits(int value) {
		if (IsOpened()) {
			if (myport.setNumDataBits(value)) {
				return true;
			} else raise_log("Set_DataBits failed, value is invalid");			
		} else raise_log("Set_DataBits failed, SerialPort not opened");
		
		return false;
	}
	
	/**
	 * Change Stopbit length
	 * @param value : these value 
	 *    1 = 1 stop bit
	 *    2 = 1.5 stop bit
	 *    3 = 2 stop bit
	 *    default to 1
	 * @return true if success
	 */
	public boolean Set_StopBits(int value) {
		if (IsOpened()) {
			if (value<1) value = 1;
			if (value>3) value = 1;
			
			if (myport.setNumStopBits(value)) {
				return true;
			} else raise_log("Set_StopBits failed, value is invalid");
			
		} else raise_log("Set_StopBits failed, SerialPort not opened");
		
		return false;
	}
	
	/**
	 * Change Parity
	 * @param value : these value
	 *     0 = No parity
	 *     1 = Odd Parity
	 *     2 = Even Parity
	 *     3 = Mark Parity
	 *     4 = Space Parity
	 *     Default to No Parity
	 * @return true if success
	 */
	public boolean Set_Parity(int value) {
		if (IsOpened()) {
			if (value<0) value = 0;
			if (value>4) value = 0;
			
			if (myport.setParity(value)) {
				return true;
			} else raise_log("Set_Parity failed, value is invalid");
			
		} else raise_log("Set_Parity failed, SerialPort not opened");
		
		return false;
	}
	
	/**
	 * Set Flow Control
	 * @param value : these value and their combination (OR-ed)
	 *    0 : None
	 *    0x1 : RTS enable
	 *    0x10 : CTS enabled
	 *    0x100 : DSR enabled
	 *    0x1000 : DTR enabled
	 *    0x10000 : XON-XOFF IN enabled
	 *    0x100000 : XON-XOFF OUT enabled
	 *    
	 *    if combine RTS + CTS then (0x1 OR 0x10) = 0x11
	 *    if combine DSR + DTR then (0x100 OR 0x1000) = 0x1100 
	 * @return true if success
	 */
	public boolean Set_FlowControl(int value) {
		if (IsOpened()) {
			if (value<0) value = 0;
			
			if (myport.setFlowControl(value)) {
				return true;
			} else raise_log("Set_FlowControl failed, value is invalid");
			
		} else raise_log("Set_FlowControl failed, SerialPort not opened");
		
		return false;
	}
	
	private void raise_log(String value) {
		if (need_log_event) bax.raiseEventFromDifferentThread(Me, null, 0, event+"_log", false, new Object[] {value});
	}
	
	private void raise_rawdatareceived(byte[] bb) {
		if (need_rawdatareceived_event) bax.raiseEventFromDifferentThread(Me, null, 0, event+"_rawdatareceived", false, new Object[] {bb});
	}
	
	private void raise_txrxcount() {
		if (need_txrxcount_event) bax.raiseEventFromDifferentThread(Me, null, 0, event+"_txrxcount", false, new Object[] {txcount, rxcount});
	}
	
	private void raise_rawdatasent(byte[] bb) {
		if (need_rawdatasent_event) bax.raiseEventFromDifferentThread(Me, null, 0, event+"_rawdatasent", false, new Object[] {bb});
	}
}
