package devices.ups.ica.sin2100c;

import com.fazecast.jSerialComm.SerialPort;

import com.fazecast.jSerialComm.SerialPortEvent;
import com.fazecast.jSerialComm.SerialPortMessageListenerWithExceptions;

import anywheresoftware.b4a.BA;

@BA.ShortName("UPSMegatecProtocol")
@BA.Events(values= {
		"log(msg as string)",
		"newdata(datanya as Megatec_Data)",
		"failedoperation(operationname as string)"
		
})


// Source : https://networkupstools.org/protocols/megatec.html
public class MegatecProtocol implements Megatec_Data_Event {

	private BA bax;
	private Object caller;
	private String event;
	private final Object Me = this;
	private boolean need_log_event = false;
	private boolean need_newdata_event = false;
	private boolean need_failedoperation_event = false;
	
	private boolean _inited = false;
	private int _txrxfail = 0;
	private int _txrxok = 0;
	
	private SerialPort _serialport;
	private Megatec_Data _data = new Megatec_Data();
	
	/**
	 * Initialize UPS Megatec Protocol
	 * @param callerobject : caller object
	 * @param eventname : eventname
	 */
	public void Initialize(BA ba, Object callerobject, String eventname) {
		bax = ba;
		caller = callerobject;
		event = eventname;
		if (bax instanceof BA) {
			if (caller != null) {
				if (event instanceof String) {
					if (!event.isEmpty()) {
						need_log_event = bax.subExists(event+"_log");
						need_newdata_event = bax.subExists(event+"_newdata");
						need_failedoperation_event = bax.subExists(event+"_failedoperation");
						_inited = true;
						_data.SetJavaEvent(this);
					}
				}
			}
		}
	}
	
	/**
	 * Check if UPSMegatecProtocol is initalized or not
	 * @return true if inited
	 */
	public boolean IsInitialized() {
		return _inited;
	}
	
	/**
	 * Get Serial Port used for communication
	 * @return empty string if none used
	 */
	public String getSerialPortUsed() {
		return (_serialport instanceof SerialPort ? _serialport.getSystemPortName() : "");
	}
	
	/**
	 * Open SerialPort for UPS Data
	 * @param portname : SerialPort name to use
	 * @return true if SerialPort is found and can be opened
	 */
	public boolean Open(String portname) {
		
		SerialPort[] PortList = SerialPort.getCommPorts();
		if (PortList != null) {
			if (PortList.length>0) {
				for(SerialPort xx : PortList) {
					if (xx.getSystemPortName().equalsIgnoreCase(portname)) {
						// sampai sini ketemu
						
						if (xx.openPort()) {
							if (xx.setBaudRate(2400)) {
								if (xx.setNumDataBits(8)) {
									if (xx.setParity(SerialPort.NO_PARITY)) {
										if (xx.setNumStopBits(SerialPort.ONE_STOP_BIT)) {
											if (xx.addDataListener(listener)) {
												
												_serialport = xx;
												
												return true;	
											}
										} else raise_log("Unable to set StopBit 1 to "+portname);
									} else raise_log("Unable to set NO_PARITY to "+portname);
								} else raise_log("Unable to set 8Bit to "+portname);
							} else raise_log("Unable to set baudrate 2400 to "+portname);
						} else raise_log("Unable to open SerialPort "+portname);
					}
				}
			} else raise_log("Start_Scanning can't found SerialPort");
		} else raise_log("Start_Scanning GetCommPorts return null");
		return false;
	}
	
	/**
	 * Close SerialPort
	 */
	public void Close() {
		if (_serialport instanceof SerialPort) {
			_serialport.removeDataListener();
			_serialport.closePort();
		}
		_serialport = null;
	}
	
	/**
	 * Get Measurement using command Q1
	 * If there are new data, will raise event newdata(datanya as Megatec_Data)
	 */
	public void Get_Measurement() {
		if (_serialport instanceof SerialPort) {
			byte[] cmd = new byte[3];
			cmd[0] = 'Q';
			cmd[1] = '1';
			cmd[2] = 13;
			Thread tx = new Thread(new SerialSendExpectReply("Get_Measurement",cmd));
			tx.start();
		} else {
			_txrxfail+=1;
		}
	}
	
	/**
	 * Get Measurement using command I
	 * If there are new data, will raise event newdata(datanya as Megatec_Data)
	 */
	public void Get_Information() {
		if (_serialport instanceof SerialPort) {
			byte[] cmd = new byte[2];
			cmd[0] = 'I';
			cmd[1] = 13;
			Thread tx = new Thread(new SerialSendExpectReply("Get_Information",cmd));
			tx.start();
		} else {
			_txrxfail+=1;
		}
	}
	
	/**
	 * Get UPS Rating using command F
	 * If there are new data, will raise event newdata(datanya as Megatec_Data)
	 */
	public void Get_Rating() {
		if (_serialport instanceof SerialPort) {
			byte[] cmd = new byte[2];
			cmd[0] = 'F';
			cmd[1] = 13;
			Thread tx = new Thread(new SerialSendExpectReply("Get_Rating",cmd));
			tx.start();
		} else {
			_txrxfail+=1;
		}
	}
	
	/**
	 * Toggle UPS Beep using command Q
	 */
	public void Toggle_Beep() {
		if (_serialport instanceof SerialPort) {
			synchronized(_serialport) {
				byte[] cmd = new byte[2];
				cmd[0] = 'Q';
				cmd[1] = 13;
				if (_serialport.writeBytes(cmd, cmd.length) == cmd.length) {   // dont need reply
					_txrxok+=1;
					return;
				}
			}
		} 
		_txrxfail+=1;
		raise_failedoperation("Toggle_Beep");
	}
	
	/**
	 * Cancel Output Shutdown using command C
	 */
	public void Cancel_Shutdown() {
		if (_serialport instanceof SerialPort) {
			synchronized(_serialport) {
				byte[] cmd = new byte[2];
				cmd[0] = 'C';
				cmd[1] = 13;
				if (_serialport.writeBytes(cmd, cmd.length) == cmd.length) {	// dont need reply
					_txrxok+=1;
					return;
				};	
			}	
		}
		_txrxfail+=1;
		raise_failedoperation("Cancel_Shutdown");
	}
	
	/**
	 * Shutdown Output after delay in minutes, using command S(delay)
	 * delay value can vary 0.x minute up to 10 minutes
	 * @param delay : in minutes
	 */
	public void Shutdown_Output(float delay) {
		if (_serialport instanceof SerialPort) {
			synchronized(_serialport) {
				if (delay<=0) delay = 0.1f;		// minimal 0.1 minute
				if (delay>10) delay = 10f;
				
				byte[] cmd = new byte[4];
				cmd[0] = 'S';
				cmd[3] = 13;
				
				if (delay<1f) {
					cmd[1] = '.';
					cmd[2] = (byte)((int)(delay*10)+0x30);
				} else if (delay<10f) {
					cmd[1] = '0';
					cmd[2] = (byte)((int)delay+0x30);
				} else {
					cmd[1] = '1';
					cmd[2] = '0';
				}
				
				if (_serialport.writeBytes(cmd, cmd.length)==cmd.length) {	// dont need reply
					_txrxok+=1;
					return;
				};		
			}
			
		} 
		
		_txrxfail+=1;
		raise_failedoperation("Shutdown_Output");
	} 
	
	/**
	 * Get TX RX OK Counter
	 * @return number of succesfull transmission
	 */
	public int getTXRX_OK() {
		return _txrxok;
	}
	
	/**
	 * Get TX RX FAIL counter
	 * @return number of failed transmission
	 */
	public int getTXRX_FAIL() {
		return _txrxfail;
	}
	
	private class SerialSendExpectReply implements Runnable{
		private final byte[] datanya;
		private final boolean validdata;
		private final String _operationname;
		public SerialSendExpectReply(String operationname, byte[] bytetosend) {
			datanya = bytetosend;
			_operationname = operationname;
			if (datanya!=null) {
				if (datanya.length>0) {
					if (_serialport instanceof SerialPort) {
						if (_serialport.isOpen()) {
							validdata = true;
							return;
						}
					}
				}
			}
			validdata = false;
		}
		
		@Override
		public void run() {
			if (validdata) {
				synchronized(_serialport) {
					
					if (_serialport.writeBytes(datanya, datanya.length) == datanya.length) {
						try {
							_serialport.wait(5000);
							_txrxok += 1;
							return;
						} catch (InterruptedException e) {
							raise_log("Reply for "+_operationname+" was not coming in 5 second interval");
						}	
					}
					
				}
			}
		
			_txrxfail+=1;
			raise_failedoperation(_operationname);
		}
		
	}
	
	
	SerialPortMessageListenerWithExceptions listener = new SerialPortMessageListenerWithExceptions() {

		@Override
		public byte[] getMessageDelimiter() {
			byte[] limiter = new byte[1];
			limiter[0] = 13;			// CR
			return limiter;
		}

		@Override
		public boolean delimiterIndicatesEndOfMessage() {
			return true;		// chr 13 (CR)
		}

		@Override
		public int getListeningEvents() {
			int event = SerialPort.LISTENING_EVENT_DATA_RECEIVED;
			return event;
		}

		@Override
		public void serialEvent(SerialPortEvent event) {
			if (_serialport instanceof SerialPort) {
				synchronized(_serialport) {
					_serialport.notifyAll();		// notify , supaya yang wait bisa selesai
				}
			}
			
			// sampe sini dapat data
			if (event instanceof SerialPortEvent) {
				byte[] bb = event.getReceivedData();
				
				if (bb!=null) {
					if (bb.length>0) {
						int bblen = bb.length;
						
						//TODO : hapus aja ini , buat ngecek doang
						String rawstring = new String(bb);
						BA.Log("RawString length="+rawstring.length()+" content = "+rawstring);
						
						if (bb[0]==40) {				// kurung buka '('  = 40
							if (bb[bblen-1]==13) {		// CR = 13
								String result = new String(bb,1,bblen-2);
								BA.Log("Result = "+result);
								process_Q1_Result(result);
								return;
							}
						}
						
						if (bb[0]==35) {				// pagar # = 35
							if (bb[bblen-1]==13) {	// CR = 13
								String result = new String(bb,1,bblen-2);
								BA.Log("Result = "+result);
								process_I_F_Result(result);
								return;
							}
						}
					}
				}
			}
			
		}

		@Override
		public void catchException(Exception e) {
			raise_log("SerialPort Exception : "+e.getMessage());
		}		
	};
	
	private void process_Q1_Result(String cmd) {
		String[] fields = cmd.split(" ");
		if (fields != null) {
			if (fields.length==8) {
				// ada 8 fields
				_data.setInputVoltage(Float.valueOf(fields[0]));
				_data.setInputFaultVoltage(Float.valueOf(fields[1]));
				_data.setOutputVoltage(Float.valueOf(fields[2]));
				_data.setOutputCurrentPercentage(Float.valueOf(fields[3]));
				_data.setInputFrequency(Float.valueOf(fields[4]));
				_data.setBatteryVoltage(Float.valueOf(fields[5]));
				_data.setTemperature(Float.valueOf(fields[6]));
				
				if (fields[7].length()==8) {
					int binaryvalue = 0;
					if (fields[7].charAt(0)=='1') binaryvalue |= (1<<7);
					if (fields[7].charAt(1)=='1') binaryvalue |= (1<<6);
					if (fields[7].charAt(2)=='1') binaryvalue |= (1<<5);
					if (fields[7].charAt(3)=='1') binaryvalue |= (1<<4);
					if (fields[7].charAt(4)=='1') binaryvalue |= (1<<3);
					if (fields[7].charAt(5)=='1') binaryvalue |= (1<<2);
					if (fields[7].charAt(6)=='1') binaryvalue |= (1<<1);
					if (fields[7].charAt(7)=='1') binaryvalue |= (1<<0);
					_data.setBinaryStatus(binaryvalue);
				}
			}
		}
	}
	
	private void process_I_F_Result(String cmd) {
		String[] fields = cmd.split(" ");
		if (fields!=null) {
			if (fields.length==3) {
				// kemungkinan command I
				_data.setCompanyName(fields[0]);
				_data.setUPSModel(fields[1]);
				_data.setVersion(fields[2]);
				
			} else if (fields.length==4) {
				// kemungkinan command F
				_data.setRatingBatteryVoltage(Float.valueOf(fields[0]));
				_data.setRatingCurrent(Float.valueOf(fields[1]));
				_data.setRatingBatteryVoltage(Float.valueOf(fields[2]));
				_data.setRatingFrequency(Float.valueOf(fields[3]));
			}
		}
	}
		
	private void raise_log(String msg) {
		if (need_log_event) bax.raiseEventFromDifferentThread(Me, null, 0, event+"_log", false, new Object[] {msg});
	}
	
	private void raise_newdata(Megatec_Data datanya) {
		if (need_newdata_event) bax.raiseEventFromDifferentThread(Me, null, 0, event+"_newdata", false, new Object[] {datanya});
	}

	private void raise_failedoperation(String operationname) {
		if (need_failedoperation_event) bax.raiseEventFromDifferentThread(Me, null, 0, event+"_failedoperation", false, new Object[] {operationname});
	}
	
	//////////////// Event from Megatec_Data_Event ////////////////////
	@Override
	@BA.Hide
	public void newdatastring(String field, String olddata, String newdata) {
		if (_data instanceof Megatec_Data) {
			raise_newdata(_data);
		}
		raise_log("Field "+field+", Old Data="+olddata+", New Data="+newdata);
	}

	@Override
	@BA.Hide
	public void newdatafloat(String field, float olddata, float newdata) {
		if (_data instanceof Megatec_Data) {
			raise_newdata(_data);
		}
		raise_log("Field "+field+", Old Data="+olddata+", New Data="+newdata);
	}
	
	///////////////////////////////////////////////////////////////////////
}
