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.data.DataHolder;
import com.intelligt.modbus.jlibmodbus.exception.ModbusIOException;
import com.intelligt.modbus.jlibmodbus.serial.SerialParameters;
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.serial.SerialUtils;
import com.intelligt.modbus.jlibmodbus.slave.ModbusSlave;
import com.intelligt.modbus.jlibmodbus.slave.ModbusSlaveFactory;
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("jModbusSlave")
@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 Slave
 * in Modbus, Slave is actually a server that hold data.
 * Master is a client, that request data from server.
 * @author rdkartono
 *
 */
public class jModbusSlave {
	
	
	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 ModbusSlave modbus = null;
	private DataHolder dh = null;
	private jModbusCoil coil = null;
	private jModbusHoldingRegister reg = null;
	
	/**
	 * 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;
		dh = new DataHolder();
		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 Slave 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 = ModbusSlaveFactory.createModbusSlaveASCII(sp);
			modbus.addListener(listener);
			if (dh==null) dh = new DataHolder();
			modbus.setDataHolder(dh);
			modbus.listen();
			return true;
		} catch(SerialPortException e) {
			raise_log("OpenSerial_ASCII failed, Msg : "+e.getMessage());
			return false;
		} catch (ModbusIOException e) {
			raise_log("Modbus Listen failed, Msg : "+e.getMessage());
			return false;
		}
	}
	
	/**
	 * Open Modbus Slave 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 = ModbusSlaveFactory.createModbusSlaveRTU(sp);
			
			modbus.addListener(listener);
			if (dh==null) dh = new DataHolder();
			modbus.setDataHolder(dh);
			modbus.listen();
			return true;
		} catch (SerialPortException e) {
			raise_log("OpenSerial_RTU failed, Msg : "+e.getMessage());
			return false;
		} catch (ModbusIOException e) {
			raise_log("Modbus Listen failed, Msg : "+e.getMessage());
			return false;
		}
	}
	
	/**
	 * Open Modbus Slave in TCP 
	 * @param hostaddress : network adapter IP address, or empty string for default ip address
	 * @param port : port number, if negative number, will use default TCP port 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 {
			if (hostaddress.isEmpty()) {
				host = InetAddress.getLocalHost();
			} else {
				host = InetAddress.getByName(hostaddress);
			}
		} catch (UnknownHostException | SecurityException e) {
			raise_log("Unable to get InetAddress for host="+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 = ModbusSlaveFactory.createModbusSlaveTCP(tp);
		if (dh==null) dh = new DataHolder();
		
		modbus.setDataHolder(dh);
		modbus.addListener(listener);
		try {
			modbus.listen();
		} catch (ModbusIOException e) {
			raise_log("Unable to start Modbus Listen, Msg : "+e.getMessage());
			return false;
		}
		return true;
	}
	
	/**
	 * Get / Set Slave ID.
	 * On getting, when returning -1, means modbus not yet opened
	 */
	public void setID(int value) {
		if (modbus!=null) {
			modbus.setServerAddress(value);
		}
	}
	
	public int getID() {
		if (modbus!=null) {
			return modbus.getServerAddress();
		} else return -1;
	}
	
	/**
	 * Initialize coils
	 * coil is binary (true/false) value, 1 bit
	 * @param size : how many coils want to initialize. If < 1, default to 1024
	 */
	public void Initialize_Coils(int size) {
		if (size>0)
			coil = new jModbusCoil(size);
		else
			coil = new jModbusCoil();
		
		if (dh==null) dh = new DataHolder();
		dh.setCoils(coil);
	}
	
	/**
	 * Get Coils that already been initialized
	 * @return null if failed
	 */
	public jModbusCoil getCoils() {
		return coil;
	}
	
	/**
	 * Get Registers that already been initialized
	 * @return null if failed
	 */
	public jModbusHoldingRegister getRegisters() {
		return reg;
	}
	
	/**
	 * Initialize Register
	 * Register is 16 bit value, can read and write
	 * @param size : how many registers want to initialize. If < 1, default to 1024
	 */
	public void Initialize_Registers(int size) {
		if (size>0)
		   reg = new jModbusHoldingRegister(size);
		else
			reg = new jModbusHoldingRegister();
		
		if (dh==null) dh = new DataHolder();
		dh.setHoldingRegisters(reg);
	}
	
	/**
	 * Close Modbus Serial Slave RTU
	 * @return true if success
	 */
	public boolean Close() {
		if (modbus!=null) {
			try {
				modbus.removeListeners();
				modbus.shutdown();
			} catch (ModbusIOException e) {
				raise_log("Modbus Close failed, Msg : "+e.getMessage());
				return false;
			}
		}
		return true;
	}
	
	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());
		}
		
		
	};
	
	
}
