package ModbusWrapper;

import java.net.InetAddress;
import java.net.UnknownHostException;

import com.fazecast.jSerialComm.SerialPort;
import com.intelligt.modbus.jlibmodbus.Modbus;
import com.intelligt.modbus.jlibmodbus.exception.ModbusIOException;
import com.intelligt.modbus.jlibmodbus.exception.ModbusNumberException;
import com.intelligt.modbus.jlibmodbus.exception.ModbusProtocolException;
import com.intelligt.modbus.jlibmodbus.master.ModbusMaster;
import com.intelligt.modbus.jlibmodbus.master.ModbusMasterFactory;
import com.intelligt.modbus.jlibmodbus.serial.SerialParameters;
import com.intelligt.modbus.jlibmodbus.serial.SerialUtils;
import com.intelligt.modbus.jlibmodbus.serial.SerialPort.BaudRate;
import com.intelligt.modbus.jlibmodbus.serial.SerialPort.Parity;
import com.intelligt.modbus.jlibmodbus.serial.SerialPortException;
import com.intelligt.modbus.jlibmodbus.tcp.TcpParameters;
import com.intelligt.modbus.jlibmodbus.utils.FrameEvent;
import com.intelligt.modbus.jlibmodbus.utils.FrameEventListener;

import anywheresoftware.b4a.BA;
import anywheresoftware.b4a.keywords.Bit;
import anywheresoftware.b4a.objects.collections.List;

@BA.ShortName("jModbusMaster")
@BA.Events(values= {
		"log(msg as string)",
		"datasent(bb() as byte)",
		"datareceived(bb() as byte)"
})
//@BA.DependsOn(values = { "jSerialComm-2.6.2","jlibmodbus-1.2.9.7" })

/**
 * Create Modbus Master
 * in Modbus, Master is actually a client that request data from server (Slave)
 * @author rdkartono
 *
 */
public class jModbusMaster {
	private Object caller;
	private String event;
	private BA ba;
	private final Object Me = this;
	
	private boolean need_log_event = false;
	private boolean need_datasent_event = false;
	private boolean need_datareceived_event = false;
	
	private ModbusMaster modbus;
	
	/**
	 * Initialize ModbusSerialSlaveRTU
	 * @param callerobject : caller object
	 * @param eventname : event name
	 */
	public void Initialize(BA ba, Object callerobject, String eventname) {
		this.ba = ba;
		event = eventname.trim();
		caller = callerobject;
		
		if (this.ba!=null) {
			if (caller!=null) {
				if (!event.isEmpty()) {
					need_log_event = this.ba.subExists(event+"_log");
					need_datasent_event = this.ba.subExists(event+"_datasent");
					need_datareceived_event = this.ba.subExists(event+"_datareceived");
				}
			}
		}
		
		 Runtime.getRuntime().addShutdownHook(new Thread() {
	            @Override
	            public void run() {
	                Close();
	            }
	        });
	}
	
	/**
	 * Get available Serial Port Names
	 * @return List of port names. If exists, List size > 0
	 */
	public List GetSerialPortNames() {
		List result = new List();
		result.Initialize();
		
		
		SerialPort[] sp = SerialPort.getCommPorts();
		if (sp!=null) {
			if (sp.length>0) {
				for(SerialPort xx : sp) {
					result.Add(xx.getSystemPortName());
				}
			}
		}
		return result;
	}
	
	/**
	 * Open Modbus Master in Serial ASCII format
	 * @param serialname : serial port name
	 * @param baudrate : baud rate, typically 4800, 9600, 144400, 19200, 38400, 57600, 115200
	 * @param databit : data bit length , typically 8
	 * @param stopbit : typically 1
	 * @param parity : 0 = None, 1 = Odd, 2 = Even, 3 = Mark, 4 = Space
	 * @return true if success
	 */
	public boolean OpenSerial_ASCII(String serialname, int baudrate, int databit, int stopbit, int parity) {
		SerialParameters sp = new SerialParameters();
		
		sp.setDataBits(databit);
		sp.setDevice(serialname);
		sp.setStopBits(stopbit);
		sp.setBaudRate(BaudRate.getBaudRate(baudrate));
		sp.setParity(Parity.getParity(parity));
		
		SerialUtils.setSerialPortFactoryJSerialComm();
		
		try {
			modbus = ModbusMasterFactory.createModbusMasterASCII(sp);
			modbus.addListener(listener);
			modbus.connect();
			return true;
		} catch (SerialPortException e) {
			raise_log("OpenSerial_ASCII failed, Msg : "+e.getMessage());
			return false;
		} catch (ModbusIOException e) {
			raise_log("Modbus Connect failed, Msg : "+e.getMessage());
			return false;
		}
		
	}
	
	/**
	 * Open Modbus Master in Serial RTU format
	 * @param serialname : serial port name
	 * @param baudrate : baud rate, typically 4800, 9600, 144400, 19200, 38400, 57600, 115200
	 * @param databit : data bit length , typically 8
	 * @param stopbit : typically 1
	 * @param parity : 0 = None, 1 = Odd, 2 = Even, 3 = Mark, 4 = Space
	 * @return true if success
	 */
	public boolean OpenSerial_RTU(String serialname, int baudrate, int databit, int stopbit, int parity) {
		SerialParameters sp = new SerialParameters();
		
		sp.setDataBits(databit);
		sp.setDevice(serialname);
		sp.setStopBits(stopbit);
		sp.setBaudRate(BaudRate.getBaudRate(baudrate));
		sp.setParity(Parity.getParity(parity));
		
		SerialUtils.setSerialPortFactoryJSerialComm();
		
		try {
			modbus = ModbusMasterFactory.createModbusMasterRTU(sp);
			modbus.addListener(listener);
			modbus.connect();
			return true;
		} catch (SerialPortException e) {
			raise_log("OpenSerial_RTU failed, Msg : "+e.getMessage());
			return false;
		} catch (ModbusIOException e) {
			raise_log("Modbus Connect failed, Msg : "+e.getMessage());
			return false;
		}
		
	}
	
	/**
	 * Open Modbus Master in TCP 
	 * @param hostaddress : target slave IP address
	 * @param port : tcp port. if negative, will use default 502
	 * @param enable_keepalive : set true to enable keep alive connection
	 * @return true if success
	 */
	public boolean OpenTCP(String hostaddress, int port, boolean enable_keepalive) {
		InetAddress host;
		try {
			host = InetAddress.getByName(hostaddress);
		} catch (UnknownHostException e) {
			raise_log("OpenTCP failed, Invalid hostaddress="+hostaddress+", Msg : "+e.getMessage());
			return false;
		}
		
		if (port<1) port = Modbus.TCP_PORT;
		TcpParameters tp = new TcpParameters();
		tp.setPort(port);
		tp.setHost(host);
		tp.setKeepAlive(enable_keepalive);
		
		modbus = ModbusMasterFactory.createModbusMasterTCP(tp);
		modbus.addListener(listener);
		try {
			modbus.connect();
			return true;
		} catch (ModbusIOException e) {
			raise_log("Modbus Connect failed, Msg : "+e.getMessage());
			return false;
		}
	}
	
	/**
	 * Close Modbus Master connection
	 * @return true if success
	 */
	public boolean Close() {
		if (modbus instanceof ModbusMaster) {
			modbus.removeListeners();
			try {
				modbus.disconnect();
			} catch (ModbusIOException e) {
				raise_log("Modbus Disconnect failed, Msg : "+e.getMessage());
				return false;
			}
		}
		return true;
	}
	
	/***
	 * Write Single Coil (command = 5)
	 * @param slave_address : slave address
	 * @param coil_address : coil address
	 * @param is_on : true = 1, false = 0
	 * @return true if success
	 */
	public boolean WriteSingleCoil(int slave_address, int coil_address, boolean is_on) {
		if (modbus instanceof ModbusMaster) {
			if (modbus.isConnected()) {
				try {
					modbus.writeSingleCoil(slave_address, coil_address, is_on);
					return true;
				} catch (ModbusProtocolException | ModbusNumberException | ModbusIOException e) {
					raise_log("WriteSingleCoil failed, Msg : "+e.getMessage());

				} 
			}
		}
		return false;
	}
	
	/**
	 * Write Single Register (command = 6)
	 * @param slave_address : slave address
	 * @param register_address : register address
	 * @param value : register value , 16 bit
	 * @return true if success
	 */
	public boolean WriteSingleRegister(int slave_address, int register_address, int value) {
		if (modbus instanceof ModbusMaster) {
			if (modbus.isConnected()) {
				try {
					modbus.writeSingleRegister(slave_address, register_address, value);
					return true;
				} catch (ModbusProtocolException | ModbusNumberException | ModbusIOException e) {
					raise_log("WriteSingleRegister failed, Msg : "+e.getMessage());
				}
			}
		}
		return false;
	}
	
	/**
	 * Write multiple coils (command = 15)
	 * @param slave_address : slave address
	 * @param start_address : start address of coils
	 * @param values : array of boolean for coils, true = 1, false = 0
	 * @return true if success
	 */
	public boolean WriteMultipleCoils(int slave_address, int start_address, boolean[] values) {
		if (modbus instanceof ModbusMaster) {
			if (modbus.isConnected()) {
				try {
					modbus.writeMultipleCoils(slave_address, start_address, values);
					return true;
				} catch (ModbusProtocolException | ModbusNumberException | ModbusIOException e) {
					raise_log("WriteMultipleCoils failed, Msg : "+e.getMessage());
				}
			}
		}
		return false;
	}
	
	/**
	 * Write Multiple Register (command = 16)
	 * @param slave_address : slave address
	 * @param start_address : start address of register
	 * @param values : array of int (16 bit) for registers
	 * @return true if success
	 */
	public boolean WriteMultipleRegisters(int slave_address, int start_address, int[] values) {
		if (modbus instanceof ModbusMaster) {
			if (modbus.isConnected()) {
				try {
					modbus.writeMultipleRegisters(slave_address, start_address, values);
					return true;
				} catch (ModbusProtocolException | ModbusNumberException | ModbusIOException e) {
					raise_log("WriteMultipleRegisters failed, Msg : "+e.getMessage());
				}
			}
		}
		return false;
	}
	
	/**
	 * Read Single coil (command = 1)
	 * @param slave_address : slave address
	 * @param coil_address : coil address
	 * @return -1 = failed, 0 = false, 1 = true
	 */
	public int ReadSingleCoil(int slave_address, int coil_address) {
		if (modbus instanceof ModbusMaster) {
			if (modbus.isConnected()) {
				try {
					boolean[] result = modbus.readCoils(slave_address, coil_address, 1);
					if (result!=null) {
						if (result.length>0) {
							if (result[0]) return 1; else return 0;
						}
					}
				} catch (ModbusProtocolException | ModbusNumberException | ModbusIOException e) {
					raise_log("ReadSingleCoil failed, Msg : "+e.getMessage());
				}
			}
		}
		return -1;
	}
	
	/**
	 * Read single Register (16 bit) (command = 3)
	 * @param slave_address : slave address
	 * @param register_address : register address
	 * @return -1 = failed, other = register value
	 */
	public int ReadSingleRegister(int slave_address, int register_address) {
		if (modbus instanceof ModbusMaster) {
			if (modbus.isConnected()) {
				try {
					int[] result = modbus.readHoldingRegisters(slave_address, register_address, 1);
					if (result!=null) {
						if (result.length>0) {
							return Bit.And(result[0], 0xFFFF);
						}
					}
				} catch (ModbusProtocolException | ModbusNumberException | ModbusIOException e) {
					raise_log("ReadSingleRegister failed, Msg : "+e.getMessage());
				}
			}
		}
		return -1;
	}
	
	/**
	 * Read multiple coils
	 * @param slave_address : slave address
	 * @param start_address : coil start address
	 * @param quantity : how many coils want to be read
	 * @return array of boolean, or null if failed.
	 */
	public boolean[] ReadMultipleCoils(int slave_address, int start_address, int quantity) {
		if (modbus instanceof ModbusMaster) {
			if (modbus.isConnected()) {
				try {
					boolean[] result = modbus.readCoils(slave_address, start_address, quantity);
					if (result!=null) {
						if (result.length>0) {
							return result;
						}
					}
				} catch (ModbusProtocolException | ModbusNumberException | ModbusIOException e) {
					raise_log("ReadMultipleCoils failed, Msg : "+e.getMessage());
				}
				
			}
		}
		return null;
	}
	
	/**
	 * Read multiple registers
	 * @param slave_address : slave address
	 * @param start_address : register start address
	 * @param quantity : how many registers want to be read
	 * @return array of int, or null if failed
	 */
	public int[] ReadMultipleRegisters(int slave_address, int start_address, int quantity) {
		if (modbus instanceof ModbusMaster) {
			if (modbus.isConnected()) {
				try {
					int[] result = modbus.readHoldingRegisters(slave_address, start_address, quantity);
					if (result!=null) {
						if (result.length>0) {
							return result;
						}
					}
				} catch (ModbusProtocolException | ModbusNumberException | ModbusIOException e) {
					raise_log("ReadMultipleRegisters failed, Msg : "+e.getMessage());
				}
			}
		}
		return null;
	}
	
	
	private void raise_log(String msg) {
		if (need_log_event) ba.raiseEventFromDifferentThread(Me, null, 0, event+"_log", false, new Object[] {msg});
	}
	
	private void raise_datasent(byte[] value) {
		if (need_datasent_event) ba.raiseEventFromDifferentThread(Me, null, 0, event+"_datasent", false, new Object[] {value});
	}
	
	private void raise_datareceived(byte[] value) {
		if (need_datareceived_event) ba.raiseEventFromDifferentThread(Me, null, 0, event+"_datareceived", false, new Object[] {value});
	}
	
	private String printbytes(byte[] bb) {
		if (bb!=null) {
			if (bb.length>0) {
				StringBuilder str = new StringBuilder();
				for(byte xx: bb) {
					str.append(Bit.ToHexString(Bit.And(xx, 0xFF))).append(" ");
				}
				return str.toString();		
			}
		}
		return "";
	}
	
	FrameEventListener listener = new FrameEventListener() {

		@Override
		public void frameSentEvent(FrameEvent event) {
			
			raise_log("Data Sent : "+printbytes(event.getBytes()));
			raise_datasent(event.getBytes());
		}

		@Override
		public void frameReceivedEvent(FrameEvent event) {
			raise_log("Data Received : "+printbytes(event.getBytes()));
			raise_datareceived(event.getBytes());
		}
		
		
	};
}
