package devices.PZEM004;

import java.nio.ByteBuffer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import anywheresoftware.b4a.BA;
import anywheresoftware.b4a.keywords.Bit;
import jGPIO.SerialPort.SerialComm;
import jGPIO.SerialPort.jSerialPort_Event;

@BA.ShortName("PZEM004T_V3")
@BA.Events(values= {
		"log(msg as string)",
		"measurement(value as PZEM004T_MeasurenmentData)",
		"alarmpower(value as int)",
		"addresschanged(value as int)",
		"resetenergy(success as boolean)",
		"invalidserialcom(serialportname as string)"
})

public class PZEM004T implements jSerialPort_Event {
	private BA bax;
	private Object caller;
	private String event;
	private final Object Me = this;
	private boolean need_log_event = false;
	private boolean need_measurement_event = false;
	private boolean need_alarmpower_event = false;
	private boolean need_addresschanged_event = false;
	private boolean need_resetenergy_event = false;
	private boolean need_invalidserialcom_event = false;
	
	private SerialComm sercom;
	private String currentserialportname;
	private ExecutorService executor = Executors.newSingleThreadExecutor();
	
	private boolean inited = false;
	private int slaveid = 0xF8;
	
	private int txcount = 0;
	private int txfail = 0;
	private int rxcount = 0;
	private int rxfail = 0;
	
	private ByteBuffer rxbuffer = ByteBuffer.allocate(100);
	
	private final int[] CRC_Table = {
			0X0000, 0XC0C1, 0XC181, 0X0140, 0XC301, 0X03C0, 0X0280, 0XC241,
		    0XC601, 0X06C0, 0X0780, 0XC741, 0X0500, 0XC5C1, 0XC481, 0X0440,
		    0XCC01, 0X0CC0, 0X0D80, 0XCD41, 0X0F00, 0XCFC1, 0XCE81, 0X0E40,
		    0X0A00, 0XCAC1, 0XCB81, 0X0B40, 0XC901, 0X09C0, 0X0880, 0XC841,
		    0XD801, 0X18C0, 0X1980, 0XD941, 0X1B00, 0XDBC1, 0XDA81, 0X1A40,
		    0X1E00, 0XDEC1, 0XDF81, 0X1F40, 0XDD01, 0X1DC0, 0X1C80, 0XDC41,
		    0X1400, 0XD4C1, 0XD581, 0X1540, 0XD701, 0X17C0, 0X1680, 0XD641,
		    0XD201, 0X12C0, 0X1380, 0XD341, 0X1100, 0XD1C1, 0XD081, 0X1040,
		    0XF001, 0X30C0, 0X3180, 0XF141, 0X3300, 0XF3C1, 0XF281, 0X3240,
		    0X3600, 0XF6C1, 0XF781, 0X3740, 0XF501, 0X35C0, 0X3480, 0XF441,
		    0X3C00, 0XFCC1, 0XFD81, 0X3D40, 0XFF01, 0X3FC0, 0X3E80, 0XFE41,
		    0XFA01, 0X3AC0, 0X3B80, 0XFB41, 0X3900, 0XF9C1, 0XF881, 0X3840,
		    0X2800, 0XE8C1, 0XE981, 0X2940, 0XEB01, 0X2BC0, 0X2A80, 0XEA41,
		    0XEE01, 0X2EC0, 0X2F80, 0XEF41, 0X2D00, 0XEDC1, 0XEC81, 0X2C40,
		    0XE401, 0X24C0, 0X2580, 0XE541, 0X2700, 0XE7C1, 0XE681, 0X2640,
		    0X2200, 0XE2C1, 0XE381, 0X2340, 0XE101, 0X21C0, 0X2080, 0XE041,
		    0XA001, 0X60C0, 0X6180, 0XA141, 0X6300, 0XA3C1, 0XA281, 0X6240,
		    0X6600, 0XA6C1, 0XA781, 0X6740, 0XA501, 0X65C0, 0X6480, 0XA441,
		    0X6C00, 0XACC1, 0XAD81, 0X6D40, 0XAF01, 0X6FC0, 0X6E80, 0XAE41,
		    0XAA01, 0X6AC0, 0X6B80, 0XAB41, 0X6900, 0XA9C1, 0XA881, 0X6840,
		    0X7800, 0XB8C1, 0XB981, 0X7940, 0XBB01, 0X7BC0, 0X7A80, 0XBA41,
		    0XBE01, 0X7EC0, 0X7F80, 0XBF41, 0X7D00, 0XBDC1, 0XBC81, 0X7C40,
		    0XB401, 0X74C0, 0X7580, 0XB541, 0X7700, 0XB7C1, 0XB681, 0X7640,
		    0X7200, 0XB2C1, 0XB381, 0X7340, 0XB101, 0X71C0, 0X7080, 0XB041,
		    0X5000, 0X90C1, 0X9181, 0X5140, 0X9301, 0X53C0, 0X5280, 0X9241,
		    0X9601, 0X56C0, 0X5780, 0X9741, 0X5500, 0X95C1, 0X9481, 0X5440,
		    0X9C01, 0X5CC0, 0X5D80, 0X9D41, 0X5F00, 0X9FC1, 0X9E81, 0X5E40,
		    0X5A00, 0X9AC1, 0X9B81, 0X5B40, 0X9901, 0X59C0, 0X5880, 0X9841,
		    0X8801, 0X48C0, 0X4980, 0X8941, 0X4B00, 0X8BC1, 0X8A81, 0X4A40,
		    0X4E00, 0X8EC1, 0X8F81, 0X4F40, 0X8D01, 0X4DC0, 0X4C80, 0X8C41,
		    0X4400, 0X84C1, 0X8581, 0X4540, 0X8701, 0X47C0, 0X4680, 0X8641,
		    0X8201, 0X42C0, 0X4380, 0X8341, 0X4100, 0X81C1, 0X8081, 0X4040
	};
	
	/**
	 * Initialize PZEM-004T V3
	 * @param callerobject : callback object
	 * @param eventname : eventname
	 * @param idnumber : can set 1 - 247, or 248 for single device installation
	 */
	public void Initialize(BA ba, Object callerobject, String eventname, int idnumber) {
		bax = ba;
		caller = callerobject;
		event = eventname;
		
		
		if (bax!=null) {
			if (caller!=null) {
				if (!event.isEmpty()) {
					need_log_event = bax.subExists(event+"_log");
					need_measurement_event = bax.subExists(event+"_measurement");
					need_alarmpower_event = bax.subExists(event+"_alarmpower");
					need_addresschanged_event = bax.subExists(event+"_addresschanged");
					need_resetenergy_event = bax.subExists(event+"_resetenergy");
					need_invalidserialcom_event = bax.subExists(event+"_invalidserialcom");
				}
			}
		}
		
		
		if (idnumber>0) {
			if (idnumber<0xF9) {
				slaveid = idnumber;
				raise_log("ID set to "+slaveid);
			} else raise_log("Invalid ID number "+idnumber+", back to default = 1");
		} else raise_log("Invalid ID number "+idnumber+", back to default = 1");
		
		
		inited = true;
		
		// auto close kalau java program closed
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                Close();
            }
        });
	}
	
	/**
	 * Check if inited
	 * @return true if inited
	 */
	public boolean IsInitialized() {
		return inited;
	}
	
	public void Close() {
		if (executor!=null) {
			executor.shutdown();
		}
		executor = null;
	}
	
	/**
	 * Set SerialComm for communication
	 * targetcom must be initialized and opened before
	 * @param targetcom : SerialComm object
	 * @return true if targetcom is initialized and opened
	 */
	public boolean SetSerialComm(SerialComm targetcom) {
		if (targetcom!=null) {
			if (targetcom instanceof SerialComm) {
				if (targetcom.PortIsReady()) {
					sercom = targetcom;
					// untuk dapetin event log dan newdata
					sercom.SetJavaEvent(this);
					return true;
				}	
			}
		}
		raise_invalidserialcom();
		return false;
	}
	
	/**
	 * Check if SerialComm assigned is ready
	 * @return true if ready
	 */
	public boolean SerialComm_Ready() {
		if (sercom!=null) {
			if (sercom instanceof SerialComm) {
				return sercom.PortIsReady();
			}	
		}
		return false;
	}
	
	/**
	 * Reset Energy (KWH) counter.
	 * On success, will raise event resetenergy success = true
	 * on Failed, will raise event resetenergy success = false
	 */
	public void Reset_Energy() {
		byte[] bb = {(byte) slaveid, 0x42,0,0};
		if (executor != null) 
			executor.execute(new serthread(bb));
		else {
			raise_log("Reset_Energy failed, Executor is null");
			raise_resetenergy(false);
		}
	}
	
	public void Reset_Energy_SerialComm(SerialComm target ) {
		SetSerialComm(target);
		Reset_Energy();
	}
	
	/**
	 * Get Measurement
	 * On success, will raise event measurement
	 * value is PZEM004T_MeasurenmentData class
	 * On failed, will raise event measurement, value.isvalid = false
	 */
	public void Read_Measurement() {
		byte[] bb = {(byte) slaveid, 4, 0, 0, 0, 10,0,0};
		if (executor != null) 
			executor.execute(new serthread(bb));
		else {
			raise_log("Read_Measurement failed, Executor is null");
			raise_measurement(new PZEM004T_MeasurenmentData());			
		}
	}
	
	public void Read_Measurement_SerialComm(SerialComm target) {
		SetSerialComm(target);
		Read_Measurement();
	}
	
//	/**
//	 * Change Slave ID 
//	 * On success, event addresschanged will raised
//	 * On failed, event addresschanged will raise, with value -1
//	 * @param value : 01 - F7
//	 */
//	public void Change_Slave_Address(int value) {
//		//byte[] bb = {(byte) slaveid, 6, 0, 2, 0, (byte) value,0,0};
//		byte[] bb = {(byte) 0, 6, 0, 2, 0, (byte) value, 0, 0 };			// change address , pakai broadcast address (0)
//		if (executor != null) 
//			executor.execute(new serthread(bb));
//		else {
//			raise_log("Change_Slave_Address failed, Executor is null");
//			raise_addresschanged(-1);
//		}
//	}
//	
//	public void Change_Slave_Address_SerialComm(int value , SerialComm target) {
//		SetSerialComm(target);
//		Change_Slave_Address(value);
//	}
	
	/**
	 * Change power alarm threshold
	 * On success will raise event alarmpower with value = value inserted in this function
	 * On failed, will raise event alarmpower with value = -1
	 * @param value : value in Wh
	 */
	public void Set_Power_Alarm(int value) {
		byte[] bb = {(byte) slaveid, 0x06, 0, 1, (byte)(value>>8), (byte) value,0,0};
		if (executor != null) 
			executor.execute(new serthread(bb));
		else {
			raise_log("Set_Power_Alarm failed, Executor is null");
			this.raise_alarmpower(-1);
		}
		
	}
	
	private int CalculateCRC(byte[] datanya, int length) {
		int indextable;
		int CRCresult = 0xFFFF;
		int ii=0;
		while(length>0) {
			indextable = (datanya[ii] ^ CRCresult) & 0xFF;
			ii++;
			CRCresult = (CRCresult >> 8) & 0xFFFF;
			CRCresult = (CRCresult ^ CRC_Table[indextable]) & 0xFFFF;
			length--;
		}
		return CRCresult;
	}
	
	
	
	private class serthread implements Runnable{
		private byte[] bb;
		private int bblength=0;
		serthread(byte[] datanya){
			bb = datanya;
			
			bblength = datanya.length;
			int crc1 = CalculateCRC(datanya, bblength-2);
			bb[bblength-2] = (byte) (crc1);
			bb[bblength-1] = (byte) (crc1 >> 8);
			
			if (rxbuffer==null) rxbuffer = ByteBuffer.allocate(100);
			rxbuffer.clear();
		}
		@Override
		public void run() {
			if (SerialComm_Ready()) {
				synchronized(rxbuffer) {

					if (sercom.Write(bb)>0) {

						// berhasil tulis
						if (txcount<Integer.MAX_VALUE) txcount++; else txcount = 0;

						// tunggu sebentar, sampe event newdata trigger dari SerialComm
						try {
							rxbuffer.wait(75);
						} catch(InterruptedException e) {

						}

						switch(bb[1]) {
						case 4 :
							// command was read measurement
							if (!(rxbuffer instanceof ByteBuffer)) {
								raise_log("Read Measurement failed, rxbuffer invalid");
								PZEM004T_MeasurenmentData tmp = new PZEM004T_MeasurenmentData(null);
								raise_measurement(tmp);
								if (rxfail < Integer.MAX_VALUE) rxfail++; else rxfail = 0;

							} else if (rxbuffer.position()==0) {
								raise_log("Read Measurement failed, no reply from sensor");
								PZEM004T_MeasurenmentData tmp = new PZEM004T_MeasurenmentData(null);
								raise_measurement(tmp);
								if (rxfail < Integer.MAX_VALUE) rxfail++; else rxfail = 0;
							} else {
								process_data();
							}
							break;
						case 6 :
							if (bb[3]==1) {
								// command was set power alarm
								if (!(rxbuffer instanceof ByteBuffer)) {
									raise_log("Set Power Alarm failed, rxbuffer invalid");
									raise_alarmpower(-1);
									if (rxfail < Integer.MAX_VALUE) rxfail++; else rxfail = 0;
								} else if (rxbuffer.position()==0) {
									raise_log("Set Power Alarm failed, no reply from sensor");
									raise_alarmpower(-1);
									if (rxfail < Integer.MAX_VALUE) rxfail++; else rxfail = 0;
								} else {
									process_data();
								}
							} else if (bb[3]==2) {
								// command was change slave address
								if (!(rxbuffer instanceof ByteBuffer)) {
									raise_log("Change Slave Address failed, rxbuffer invalid");
									raise_addresschanged(-1);
									if (rxfail < Integer.MAX_VALUE) rxfail++; else rxfail = 0;
								} else if (rxbuffer.position()==0) {
									raise_log("Change Slave Address failed, no reply from sensor");
									raise_addresschanged(-1);
									if (rxfail < Integer.MAX_VALUE) rxfail++; else rxfail = 0;
								} else {
									process_data();
								}
							}
							break;
						case 0x42:
							//BA.Log("Sending Reset energy command : "+mycodes.printbytes(bb));
							
							// command was reset energy
							if (!(rxbuffer instanceof ByteBuffer)) {
								raise_log("Reset Energy failed, rxbuffer invalid" );
								raise_resetenergy(false);
								if (rxfail < Integer.MAX_VALUE) rxfail++; else rxfail = 0;
							} else if (rxbuffer.position()==0) {
								raise_log("Reset Energy failed, no reply from sensor" );
								raise_resetenergy(false);
								if (rxfail < Integer.MAX_VALUE) rxfail++; else rxfail = 0;
							} else {
								process_data();
							}
							break;
						}

						// return di sini, artinya sercom.write berhasil
						return;
					}
				}
			}
			// gagal tulis, entah karena sercom nya invalid, atau sercom nya PortIsReady = false, atau sercom write gagal
			if (txfail < Integer.MAX_VALUE) txfail++; else txfail = 0;
		}
		
	}
	
	private void raise_log(String msg) {
		if (need_log_event) {
			if (bax instanceof BA) bax.raiseEventFromDifferentThread(Me, null, 0, event+"_log", false, new Object[] {msg});
		}
	}
	
	private void raise_measurement(PZEM004T_MeasurenmentData value) {
		if (need_measurement_event) {
			if (bax instanceof BA) bax.raiseEventFromDifferentThread(Me, null, 0, event+"_measurement", false, new Object[] {value});
		}
	}
	
	private void raise_alarmpower(int value) {
		if (need_alarmpower_event) {
			if (bax instanceof BA) bax.raiseEventFromDifferentThread(Me, null, 0, event+"_alarmpower", false, new Object[] {value});
		}
	}
	
	private void raise_addresschanged(int value) {
		if (need_addresschanged_event) {
			if (bax instanceof BA) bax.raiseEventFromDifferentThread(Me, null, 0, event+"_addresschanged", false, new Object[] {value});
		}
	}

	private void raise_resetenergy(boolean success) {
		if (need_resetenergy_event) {
			if (bax instanceof BA) {
				bax.raiseEventFromDifferentThread(Me, null, 0, event+"_resetenergy", false, new Object[] {success});
			}	
		}
	}
	
	private void raise_invalidserialcom() {
		if (need_invalidserialcom_event) {
			bax.raiseEventFromDifferentThread(Me, null, 0, event+"_invalidserialcom", false, new Object[] {currentserialportname});
		}
	}
	
	private void process_data() {
		if (rxbuffer instanceof ByteBuffer) {
			if (rxbuffer.position()!=0) {
				rxbuffer.flip();
				byte[] bb = new byte[rxbuffer.remaining()];
				rxbuffer.get(bb);
				rxbuffer.compact();
				//printbytes("RXbuffer : ",bb);
				
				if (bb.length>=5) {		// minimal 4 byte == 1 ID, 1 function code, 1 CRC H , 1 CRC L
					if (bb[0]==slaveid) {
						int cmd = bb[1] & 0xFF;
						switch(cmd) {
							case 4 :
								// read measurement result
									PZEM004T_MeasurenmentData value = new PZEM004T_MeasurenmentData(bb);
									if (value!=null) {
										raise_measurement(value);
									}
									if (rxcount< Integer.MAX_VALUE) rxcount++; else rxcount = 0;
								
								
								break;
							case 6 :
								// slave parameter result
								if (bb.length>=8) {
									//int numbytes = bb[2];	// gak kepake
									int reg_address = bb[3];
									int reg_value = Bit.ShiftLeft(bb[4], 8) | Bit.And(bb[5], 0xFF);
									switch(reg_address) {
										case 1 :
													// power alarm threshold
													raise_alarmpower(reg_value);
													raise_log("Alarm Power Threshold changed to "+reg_value);
													if (rxcount< Integer.MAX_VALUE) rxcount++; else rxcount = 0;
												
											break;
										case 2 :
													// slave address change
													raise_addresschanged(reg_value);
													slaveid = (byte) reg_value;
													raise_log("Slave Address changed to "+reg_value);
													if (rxcount< Integer.MAX_VALUE) rxcount++; else rxcount = 0;
												
											break;
										default:
												raise_log("Invalid Register Addres="+reg_address+", Value="+reg_value+", on command 6");
									}
								}
								break;
							case 0x42:
								// reset energy result
									raise_resetenergy(true);
									raise_log("Energy reset success");
									if (rxcount< Integer.MAX_VALUE) rxcount++; else rxcount = 0;
								
								break;
							default:
								if (cmd>0x80) {
									// error command
									cmd = cmd &0x7F;
									int code = bb[2];
									raise_log("Command "+cmd+" error, code="+code);
								} else {
									// unsuported command
									raise_log("Unsupported command="+cmd);
								}
						}
					}
				}	

			}
		}	
	}
	
	// event from SerialComm
	@Override
	@BA.Hide
	public void Log(String msg) {
		raise_log("SerialComm log: "+msg);
	}

	
	@Override
	@BA.Hide
	public void newdata(byte[] bb) {
		if (bb!=null) {
			if (rxbuffer instanceof ByteBuffer) {
				rxbuffer.put(bb);
			}
		}
		
	}
	
	
}
