package SBC;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Enumeration;

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

@BA.Events(values= {
		"log(msg as string)",
		"isvalidip(toip as string, isvalid as boolean)",
		"isreachableip(toip as string, isreachable as boolean)",
		"networkstatistic(value as NetworkMeterInfo)",
		"cpuusage(value as Map)"
})

public abstract class BasicSBCInfo {
	
	
	private boolean need_log_event = false;
	private boolean need_isvalidip_event = false;
	private boolean need_isreachableip_event = false;
	private boolean need_networkstatistic_event = false;
	private boolean need_cpuusage_event = false;
	private Object caller;
	private String eventname;
	private BA ba;
	private Object Me;
	private String hardware = "";
	private String revision = "";
	private String serialnumber = "";
	
	protected BasicSBCInfo(){
		if (jGPIO.osname.isEmpty()) jGPIO.detectOS();
		
		Map data = new Map();

		data.Initialize();

		try {
			boolean result = Get_CPUINFO(data);
			if (result) {

				if (data.ContainsKey("Hardware"))
					hardware = (String) data.Get("Hardware");
				if (data.ContainsKey("Revision"))
					revision = (String) data.Get("Revision");
				if (data.ContainsKey("Serial"))
					serialnumber = (String) data.Get("Serial");
			} else {
				hardware = "";
				revision = "";
				serialnumber = "";
			}
		} catch (IOException e) {
			hardware = "";
			revision = "";
			serialnumber = "";
		}
		
		 Runtime.getRuntime().addShutdownHook(new Thread() {
	            @Override
	            public void run() {
	                Stop_CPU_Usage_Monitoring();
	                Stop_Network_Meter();
	            }
	        });
		 
		
	}
	
	public abstract double Get_CPU_CoreVolt();
	
	
	protected void setup_events(final BA bax, final Object callerobject, final String event, final Object Sender) {
		ba = bax;
		caller = callerobject;
		eventname = event;
		Me = Sender;
		if (ba instanceof BA) {
			if (caller != null) {
				if (eventname instanceof String) {
					if (!eventname.isEmpty()) {
						if (Me != null) {
							need_isvalidip_event = ba.subExists(eventname + "_isvalidip");
							need_log_event = ba.subExists(eventname + "_log");
							need_isreachableip_event = ba.subExists(eventname + "_isreachableip");
							need_networkstatistic_event = ba.subExists(eventname+"_networkstatistic");
							need_cpuusage_event = ba.subExists(eventname+"_cpuusage");							
						}
					}
				}	
			}
		}
	}
	
	
	protected double pembulatan_1desimal(double value) {
		int temp = (int)(value * 10);
		return (temp / 10.0);
	}
	
	private long split_meminfo_string(String value) {
		value = value.trim();
		String result[] = value.split("\\s+");
		if (result.length==2) {
			result[0] = result[0].trim();
			result[1] = result[1].trim();
			try {
				Long xx = Long.parseLong(result[0]);
				return xx;	
			} catch(NumberFormatException e) {
				BA.Log("Failed to Long.parseLong "+result[0]+", Message="+e.getMessage()+", Caused="+e.getCause());
				return -1;
			}
			
		} else return -1;
	}
	
	/**
	 * Get CPU Hardware Model
	 * 
	 * @return empty string if failed to get
	 */
	public String getCPU_Hardware() {
		return hardware;
	}

	/**
	 * Get CPU Revision Code
	 * 
	 * @return empty string if failed to get
	 */
	public String getCPU_Revision() {
		return revision;
	}

	/**
	 * Get CPU Serial Number
	 * 
	 * @return empty string if failed to get
	 */
	public String getCPU_SerialNumber() {
		return serialnumber;
	}
	
	/**
	 * Get CPU INFO Provide Map object to contain result : key="Hardware", value =
	 * CPU hardware, if available key="Revision", value = CPU revision code, if
	 * available key="Serial", value = CPU serial number, if available
	 * 
	 * @param data : Map of <String, String> to contain result
	 * @return true if success
	 * @throws IOException
	 */
	protected boolean Get_CPUINFO(Map data) throws IOException {
		boolean result = false;
		if (!jGPIO.IsLinux) {
			return result;
		}
		if (data == null)
			return result;
		if (data.IsInitialized() == false)
			return result;
		if (!jGPIO.IsLinux) {
			return result;
		}
		
		data.Clear();
		File cpuinfo = new File("/proc/cpuinfo");
		if (cpuinfo.exists()) {
			if (cpuinfo.canRead()) {

				BufferedReader reader = new BufferedReader(new FileReader(cpuinfo.getAbsolutePath()));
				String line ;
				do {
					line = reader.readLine();
					if (line!=null) {
						line = line.trim();
						if (line.startsWith("Hardware")) {
							String[] vv = split_procinfo_string(line);
							if (vv != null) {
								data.Put(vv[0], vv[1]);
							}
						}
						if (line.startsWith("Revision")) {
							String[] vv = split_procinfo_string(line);
							if (vv != null) {
								data.Put(vv[0], vv[1]);
							}
						}
						if (line.startsWith("Serial")) {
							String[] vv = split_procinfo_string(line);
							if (vv != null) {
								data.Put(vv[0], vv[1]);
							}
						}
					}
					
				} while(line!=null);
				

				reader.close();
				result = true;
			} else {
				BA.Log("/proc/cpuinfo can't be read");
			}
		} else {
			BA.Log("/proc/cpuinfo not found");
		}

		return result;
	}
	
	private String[] split_procinfo_string(String value) {
		value = value.trim();
		String result[] = value.split(":");
		if (result.length == 2) {
			result[0] = result[0].trim();
			result[1] = result[1].trim();
			return result;
		} else {
			return null;
		}
	}
	
	private final double memory_MB = 1024;
	private final double memory_GB = memory_MB * 1024;
	
	/**
	 * Convert Memory size (in KB) for easy reading
	 * @param value : memory size
	 * @return string value
	 */
	public String Convert_MemorySize_toString(double value) {
		if (value < memory_MB) {
			return pembulatan_1desimal(value)+ " KB";
		} else if (value < memory_GB) {
			return pembulatan_1desimal(value / memory_MB)+" MB";
		} else return pembulatan_1desimal(value / memory_GB)+" GB";
	}
	
	/**
	 * Same as Get_MemoryInfo, but with dedicated RAM_Info class
	 * @return RAM_Info class, or null if invalid
	 */
	public RAM_Info Get_RAMInfo() {
		Map vv = Get_MemoryInfo();
		if (vv instanceof Map) {
			if (vv.IsInitialized()) {
				if (vv.getSize()>0) {
					RAM_Info result = new RAM_Info(Get_MemoryInfo());
					return result;
				}
			}
		}
		return null;
	}
	
	/**
	 * Get Memory Info. 
	 * Map will contain keys :
	 * 	key = "JVMTotalMemory"		, value (Double) in KB
	 * 	key = "JVMFreeMemory"		, value (Double) in KB
	 * 	key = "JVMMaxMemory"		, value (Double) in KB
	 *  key = "JVMFreePercentage"	, value (Double) in Percentage
	 * 	key = "MemTotal"			, value (Double) in KB
	 * 	key = "MemFree"				, value (Double) in KB
	 * 	key = "MemAvailable"		, value (Double) in KB
	 *  key = "MemFreePercentage"	, value (Double) in Percentage
	 *  key = "SwapTotal"			, value (Double) in KB
	 *  key = "SwapFree"			, value (Double) in KB
	 *  
	 *  Use function Convert_MemorySize_toString for easy reading
	 * @return Map result
	 */
	public Map Get_MemoryInfo() {
		Map data = new Map();
		data.Initialize();
		
		Runtime jvm = java.lang.Runtime.getRuntime();
		double jvmtotalmemory = jvm.totalMemory() / 1024.0;
		double jvmfreememory = jvm.freeMemory() / 1024.0 ;
		double jvmmaxmemory = jvm.maxMemory() / 1024.0 ;
		
		data.Put("JVMTotalMemory", pembulatan_1desimal(jvmtotalmemory));
		data.Put("JVMFreeMemory", pembulatan_1desimal(jvmfreememory));
		data.Put("JVMMaxMemory", pembulatan_1desimal(jvmmaxmemory));
		data.Put("JVMFreePercentage", pembulatan_1desimal((jvmfreememory/jvmtotalmemory)*100));
		
		File meminfo = new File("/proc/meminfo");
		if (meminfo.exists()) {
			if (meminfo.canRead()) {
				try {
					BufferedReader reader = new BufferedReader(new FileReader(meminfo.getAbsolutePath()));
					String line;
					do {
						line = reader.readLine();
						if (line!=null) {
							line = line.trim();
							if (line.startsWith("MemTotal:")) {
								double memtotal = split_meminfo_string(line.substring(line.indexOf(":")+1));
								if (memtotal!=-1) data.Put("MemTotal", pembulatan_1desimal( memtotal));
							}
							if (line.startsWith("MemFree:")) {
								double memfree = split_meminfo_string(line.substring(line.indexOf(":")+1));
								if (memfree!=-1) data.Put("MemFree", pembulatan_1desimal(memfree));
							}
							if (line.startsWith("MemAvailable:")) {
								double memavailable = split_meminfo_string(line.substring(line.indexOf(":")+1));
								if (memavailable!=-1) data.Put("MemAvailable", pembulatan_1desimal(memavailable));
							}
							if (line.startsWith("SwapTotal:")) {
								double swaptotal = split_meminfo_string(line.substring(line.indexOf(":")+1));
								if (swaptotal!=-1) data.Put("SwapTotal", pembulatan_1desimal(swaptotal));
							}
							if (line.startsWith("SwapFree:")) {
								double swapfree = split_meminfo_string(line.substring(line.indexOf(":")+1));
								if (swapfree!=-1) data.Put("SwapFree", pembulatan_1desimal(swapfree));
							}
						}
					} while(line!=null);
					
					reader.close();
					
					if (data.ContainsKey("MemTotal")) {
						if (data.ContainsKey("MemFree")) {
							double memtotal = (double)data.Get("MemTotal");
							double memfree = (double)data.Get("MemFree");
							data.Put("MemFreePercentage",pembulatan_1desimal( (memfree/memtotal)*100));
						}
					}
				} catch(IOException e) {
					raise_log("Error reading /proc/meminfo, Msg : "+e.getMessage());
				}
				
			} else {
				raise_log("/proc/meminfo can't be read");
			}
		} else {
			raise_log("/proc/meminfo not found");
		}
		
		return data;
	}
	
	/**
	 * Same as Get_StorageInfo, but with dedicated Storage_Info class
	 * @param folderpath : path to check
	 * @return Storage_Info class, or null if failed
	 */
	public Storage_Info Get_StorageInformation(String folderpath) {
		Map vv = Get_StorageInfo(folderpath);
		if (vv instanceof Map) {
			if (vv.IsInitialized()) {
				if (vv.getSize()>0) {
					Storage_Info result = new Storage_Info(vv);
					return result;
				}
			}
		}
		return null;
	}
	
	/**
	 * Get Storage Info
	 * Map will contain keys :
	 *   key = "Path"           , requested path
	 *   key = "Total"			, value (Double) in bytes
	 *   key = "Free",			, value (Double) in bytes
	 *   key = "FreePercentage"	, value (Double) in Percentage
	 *   
	 *  Use function Convert_FileSize_toString for easy reading string
	 * @param folderpath : path to check
	 * @return Map result
	 */
	public Map Get_StorageInfo(String folderpath) {
		Map data = new Map();
		data.Initialize();
		
		File dir = new File(folderpath);
		if (dir.exists()) {
			if (dir.isDirectory()) {
				double total = dir.getTotalSpace();
				double free = dir.getUsableSpace();
				double percentage = (free / total) * 100;
				data.Put("Path", folderpath);
				data.Put("Total", total);
				data.Put("Free", free);
				data.Put("FreePercentage", pembulatan_1desimal(percentage));
				
			}	
		}
		return data;
	}
	
	
	private final double file_KB = 1024;
	private final double file_MB = file_KB * 1024;
	private final double file_GB = file_MB * 1024;
	private final double file_TB = file_GB * 1024;
	/**
	 * Convert a filesize to easy to read string
	 * @param value : size in bytes 
	 * @return string of filesize 
	 */
	public String Convert_FileSize_toString(double value) {
		if (value < file_KB) {
			return pembulatan_1desimal(value)+" B";
		} else if (value < file_MB) {
			return pembulatan_1desimal(value/file_KB)+" KB";
		} else if (value < file_GB) {
			return pembulatan_1desimal(value / file_MB)+" MB";
		} else if (value < file_TB) {
			return pembulatan_1desimal(value / file_GB)+" GB";
		} else return pembulatan_1desimal(value / file_TB)+" TB";
	}
	
	/**
	 * Get MAC Address
	 * @param networkdevice : network device name eth0 / wlan0
	 * @return MAC (String) if success, or empty string if failed
	 */
	public String Get_MAC(String networkdevice) {
		try {
			NetworkInterface inf = NetworkInterface.getByName(networkdevice);
			if (inf==null) return "";
			
			byte[] mac = inf.getHardwareAddress();
			StringBuilder str = new StringBuilder();
			for (byte bb : mac) {
				int bx = Bit.And(bb, 0xFF);
				String hx = "";

				if (bx < 10) {
					hx += "0"; // tambah nol di depan, biar 2 digit
				}
				hx += Bit.ToHexString(bx).toUpperCase();

				if (str.length() > 0)
					str.append("-");
				str.append(hx);
			}
			return str.toString();
		} catch (SocketException e) {
			raise_log("Get_MAC device="+networkdevice+" exception, Msg : "+e.getMessage());
			return "";
		}
		
		
	}
	
	/**
	 * Get Network Status if Up or Down
	 * @param networkdevice : network device name eth0 / wlan0
	 * @return true if UP, or false if DOWN
	 */
	public boolean NetworkIsActive(String networkdevice) {
		
		try {
			NetworkInterface inf = NetworkInterface.getByName(networkdevice);
			if (inf==null) return false;
			return inf.isUp();
		} catch (SocketException e) {
			raise_log("NetworkIsActive device="+networkdevice+" exception, Msg : "+e.getMessage());
			return false;
		}	
	}
	
	/**
	 * Get IP Address in String
	 * Revision 12072020: Sometimes an interface may contain more than 1 IP addres
	 * so this function will return List instead of one String
	 * @param networkdevice : network device name eth0 / wlan0
	 * @return List contain IP Address (String) if success, or empty List if failed
	 */
	public List Get_IPAddress(String networkdevice) {
		List result = new List();
		result.Initialize();
		try {
			NetworkInterface inf = NetworkInterface.getByName(networkdevice);
			
			if (inf instanceof NetworkInterface) {
				if (inf.isUp()) {
					Enumeration<InetAddress> inetAddress = inf.getInetAddresses();
					InetAddress currentAddress;
					
					while (inetAddress.hasMoreElements()) {
						currentAddress = inetAddress.nextElement();
						if (currentAddress instanceof Inet4Address && !currentAddress.isLoopbackAddress()) {
							String ip = currentAddress.getHostAddress();
							if (!ip.isEmpty())
								result.Add(ip);
						}
					}
				}
			}
			
		} catch (SocketException e) {
			raise_log("Get_IPAddress device="+networkdevice+" exception, Msg : "+e.getMessage());
		}
		
		return result;
	}
	
	/**
	 * Get IP Bytes
	 * @param networkdevice : network device name eth0 / wlan0
	 * @return IP Bytes (4 integers) if success, or null if failed
	 */
	public int[] Get_IP_Bytes(String networkdevice) {
		try {
			NetworkInterface inf = NetworkInterface.getByName(networkdevice);
			if (inf instanceof NetworkInterface) {
				if (inf.isUp()) {
					Enumeration<InetAddress> inetAddress = inf.getInetAddresses();
					InetAddress currentAddress;
					while (inetAddress.hasMoreElements()) {
						currentAddress = inetAddress.nextElement();
						if (currentAddress instanceof Inet4Address && !currentAddress.isLoopbackAddress()) {
							byte[] result = currentAddress.getAddress();
							if (result == null)
								return null;
							if (result.length != 4)
								return null;
							int[] intresult = new int[4];
							for (int ii = 0; ii < 4; ii++) {
								intresult[ii] = Bit.And(result[ii], 0xFF);
							}
							return intresult;
						}
					}
				}
			}
			return null;
		} catch (SocketException e) {
			raise_log("Get_IP_Bytes device="+networkdevice+" exception, Msg : "+e.getMessage());
			return null;
		}
	}
	
	private boolean cpu_monitoring = false;
	
	public boolean Start_CPU_Usage_Monitoring() {
		if (jGPIO.IsLinux) {
			Thread tx = new Thread(new cpumonitorrunnable());
			tx.start();
			return true;
		}
		return false;
	}
	
	private class cpumonitorrunnable implements Runnable{
		private Map cpu_usage = new Map();		// yang terakhir kali, isinya class cpudata
		private boolean success_result = false;
		cpumonitorrunnable(){
			cpu_usage.Initialize();
		}
		
		@Override
		public void run() {
			raise_log("Start CPU Usage Monitoring");
			cpu_monitoring = true;
			while(cpu_monitoring) {
				Map cpumap = new Map();		//yang sekarang , isinya class cpudata
				cpumap.Initialize();
				Map result = new Map();		// hasil akhir, isinya double values
				result.Initialize();
				success_result = false;		// anggap gagal dulu
				
				try {
					if (Get_CPUDATA(cpumap)) {
						for(int ii=0;ii<cpumap.getSize();ii++) {
							String cpuname = (String)cpumap.GetKeyAt(ii);
							cpudata cpu = (cpudata) cpumap.GetDefault(cpuname, null);		// ambil datanya 
							
							cpudata prev_cpu = (cpudata) cpu_usage.Put(cpuname, cpu);	// replace yang sekarang, yang terbuang akan keluar di prev_cpu
							
							
							result.Put(cpuname, cpu.getCPU_Usage(prev_cpu));	// masukin ke result, <nama cpu, percentage (double)>
						}
						success_result = true;
					}	
				} catch(IOException e) {
					raise_log("Failed to Get_CPUDATA, Msg : "+e.getMessage());
					break;
				}
				
				if (success_result) raise_cpuusage(result);
				
				
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					raise_log("CPU Usage Monitoring interrupted, Msg : "+e.getMessage());
					break;
				}
			}
			raise_log("Finished CPU Usage Monitoring");
		}
		
		
		private boolean Get_CPUDATA(Map data) throws IOException {
			boolean result = false;
			if (data == null)
				return false;
			if (data.IsInitialized() == false)
				return false;
			if (!jGPIO.IsLinux) {
				return result;
			}
			data.Clear();
			File statinfo = new File("/proc/stat");

			if (statinfo.exists()) {
				if (statinfo.canRead()) {
					BufferedReader reader = new BufferedReader(new FileReader(statinfo.getAbsolutePath()));
					
					String line;
					do {
						line = reader.readLine();
						if (line!=null) {
							line = line.trim();
							cpudata cpu = new cpudata(line);
							if (cpu.correct_data) {
								data.Put(cpu.name, cpu);
								// Untuk dapat Total CPU Usage since boot, tinggal
								// cpu.getCPU_Usage(null);
								// untuk dapat realtime CPU usage, harus buat prev_cpudata dan current_cpudata,
								// kemudian kurangkan
								// current_cpudata.getCPU_Usage(prev_cpudata)
								// supaya bisa jalan baik, harus pakai thread
							}
						}
					} while (line!=null);
					
					
					reader.close();
					if (data.getSize() > 0)
						result = true;
				}
			}

			return result;
		}
	}
	
	public void Stop_CPU_Usage_Monitoring() {
		cpu_monitoring = false;
	}
	
	
	
	
	
	/**
	 * Check if a value is valid IP address
	 * will raise event isvalidip(toip as string, isvalid as boolean)
	 * @param value : target host ip or name
	 */
	public void IP_isValid(String value) {
		if (value.isEmpty()) {
			raise_isvalidip(value, false);
			return;
		}
		Thread tx = new Thread(new CheckIPValid(value));
		tx.start();		
	}
	
	private class CheckIPValid implements Runnable{
		final String targetip;
		
		public CheckIPValid(String value){
			targetip = value.trim();
			
		}
		
		
		
		@Override
		public void run() {
			try {
				InetAddress result = InetAddress.getByName(targetip);
				if (result instanceof InetAddress) {
					raise_isvalidip(targetip, true);			// ikut default
				} else {
					raise_isvalidip(targetip, false);
				}
				
			} catch(UnknownHostException | SecurityException  e) {
				raise_log("CheckIPValid "+targetip+" Exception, Msg : "+e.getMessage());
				raise_isvalidip(targetip, false);
			}
		}
		
	};
	
	private class CheckIPReachable implements Runnable{
		final String targetip;
		final String networkdev;
		
		// revisi 31052021
		// timeout berubah dari 2000 ke 5000
		final int pingtimeout = 5000;
		public CheckIPReachable(String value, String networkdevice){
			targetip = value;
			networkdev = networkdevice;
		}
		
		
		@Override
		public void run() {
			
			try {
				NetworkInterface nef = NetworkInterface.getByName(networkdev);
				if (nef instanceof NetworkInterface) {
					InetAddress ip = InetAddress.getByName(targetip);
					if (ip instanceof InetAddress) {
						boolean result = ip.isReachable(nef, 0, pingtimeout);
						raise_isreachableip(targetip, result);			// ikut default
						return;
					}
				}
				
				
			} catch (SocketException e1) {		// dari NetworkInterface.getByName
				raise_log("CheckIPReachable IP="+targetip+" Device="+networkdev+" Exception, Msg : "+e1.getMessage());
				
			} catch (UnknownHostException | SecurityException e) {	// dari InetAddress.getByName
				raise_log("CheckIPReachable IP="+targetip+" Device="+networkdev+" Exception, Msg : "+e.getMessage());
				
				
			} catch (IOException e) {		// dari InetAddress.IsReachable
				raise_log("CheckIPReachable IP="+targetip+" Device="+networkdev+" Exception, Msg : "+e.getMessage());
			}
			
			raise_isreachableip(targetip, false);			// ikut default
		}
	}
	
	
	/**
	 * Check if IP address is reachable / Ping-able
	 * will raise event isreachableip(toip as string, isreachable as boolean)
	 * @param value : Ip address
	 * @param networkdevice : network device name eth0 /wlan0
	 */
	public void IP_isReachable(String value, String networkdevice) {
		if (value.isEmpty()) {
			raise_isreachableip(value, false);
			return;
		}
		
		if (networkdevice.isEmpty()) {
			raise_isreachableip(value, false);
			return;
		}
		
		Thread tx = new Thread(new CheckIPReachable(value, networkdevice));
		tx.start();
		
	}
	
	
	
	/**
	 * Get Single Board Computer Model
	 * @return empty string if not available
	 */
	public String read_SBC_model() {
		if (!jGPIO.IsLinux) return "";
		
		File ff = new File("/sys/firmware/devicetree/base/model");
		String result = "";
		if (ff.exists()) {
			if (ff.canRead()) {
				try {
					BufferedReader reader = new BufferedReader(new FileReader(ff.getAbsolutePath()));
					result = reader.readLine();
					if (result!=null) {
						result = result.trim();
					} else result = "";
					reader.close();
					
				} catch  (IOException e) {
					raise_log("Failed to read from "+ff.getAbsolutePath()+", Message="+e.getMessage()+", Caused="+e.getCause());
				}
				
			}
		}
		return result;
	}
	
	/**
	 * Get CPU Temperature
	 * On multiple cores , Map will contains several keys
	 * Map key = name of thermal zone, start from 0 (thermal_zone0)
	 * Map value = CPU temperature in double
	 * @return Map containing values if success, or empty map if failed
	 */
	public Map Get_CPU_Temp() {
		Map result = new Map();
		result.Initialize();
		final String childstring = "thermal_zone";
		final String parentpath = "/sys/devices/virtual/thermal";
		final String tempstring = "temp";
		
		File parent = new File(parentpath);
		if (parent.exists()) {
			String parentcontent[] = parent.list();
			if (parentcontent!=null) {
				if (parentcontent.length>0) {
					for(String xx: parentcontent) {
						if (xx.contains(childstring)) {
							// ketemu nya thermal_zoneX
							
							File parentchild = new File(parentpath, xx);
							if (parentchild.isDirectory()) {
								File tempfile = new File(parentchild.getAbsolutePath(), tempstring);
								if (tempfile.exists()) {
									if (tempfile.canRead()) {
										try {
											BufferedReader reader = new BufferedReader(new FileReader(tempfile.getAbsolutePath()));
											String readresult = reader.readLine();
											if (readresult!=null) {
												readresult = readresult.trim();
											} else readresult = "";
											reader.close();
											
											double doubleresult = Double.valueOf(readresult) / 1000.0;
											// pembulatan 1 desimal
											int pembulatan = (int) (doubleresult * 10);
											doubleresult = pembulatan / 10.0;
											
											result.Put(xx,  doubleresult);
										} catch(NumberFormatException e) {
											// readresult bukan angka
											result.Put(xx, -1);
											raise_log("NumberFormatException "+e.getMessage());
										}
										catch ( IOException e) {
											raise_log("Failed to access "+tempfile.getAbsolutePath()+", Msg:"+e.getMessage());	
											result.Put(xx, -1);
											
										}
									}
								} else {
									
									result.Put(xx, -1);
									raise_log(tempfile.getAbsolutePath()+" is not exist");
								}
							}
						}
					}
				}
			}
		}
		
		return result;
	}
	
	protected boolean network_metering = false;
	private final String rx_bytes_file = "rx_bytes";
	private final String tx_bytes_file = "tx_bytes";
	private final String rx_packets_file = "rx_packets";
	private final String tx_packets_file = "tx_packets";
	/**
	 * Start Network Metering
	 * Will raise networkstatistic(ifname as string, rx_pps as long, tx_pps as long, rx_bps as long, tx_bps as long)
	 * @param ifname : valid interface name
	 * @return true if started
	 */
	public boolean Start_Network_Meter(String ifname) {
		if (jGPIO.IsLinux) {
			if (NetworkIsActive(ifname)) {
				String networkpath = "/sys/class/net/"+ifname+"/statistics";
				File netdir = new File(networkpath);
				if (netdir.exists()) {
					if (netdir.isDirectory()) {
						if (FileExistAndReadable(networkpath, rx_bytes_file)) {
							if (FileExistAndReadable(networkpath, tx_bytes_file)) {
								if (FileExistAndReadable(networkpath, rx_packets_file)) {
									if (FileExistAndReadable(networkpath, tx_packets_file)) {
										Thread tx = new Thread(new networkmeterrunnable(ifname,networkpath));
										tx.start();
										return true;
									} else raise_log("Unable to access "+tx_packets_file);
								} else raise_log("Unable to access "+rx_packets_file);
							} else raise_log("Unable to access "+tx_bytes_file);
						} else  raise_log("Unable to access "+rx_bytes_file);
					} else raise_log(networkpath+" is not directory");
				} else raise_log(networkpath+" is not exist");
				
			} else raise_log(ifname+" is down");	
		} else raise_log("System is not Linux");
		return false;
	}
	
	public void Stop_Network_Meter() {
		network_metering = false;
		
	}
	
	/**
	 * Update manually Linux Date and Time
	 * @param datenya : must in format yyyy-MM-dd
	 * 	year only valid from 2000 - 2999
	 * @param timenya : must in format hh:mm:ss
	 * @return true if success
	 */
	public boolean Set_Linux_DateTime(String datenya, String timenya) {
		if (!Regex.IsMatch("^(2[0-9]{3})-([0]?[1-9]|[1][0-2])-([0]?[1-9]|[1|2][0-9]|[3][0-1])$", datenya)) {
			raise_log("Date Format must be yyyy-MM-dd");
			return false;
		}
		
		if (!Regex.IsMatch("^([0]?[0-9]|[1][0-9]|[2][0-3]):([0]?[0-9]|[1-5][0-9]):([0]?[0-9]|[1-5][0-9])$", timenya)) {
			raise_log("Time Format must be hh:mm:ss");
			return false;
		}
		try {
			Process changetime = Runtime.getRuntime().exec("sudo date -s '"+datenya+" "+timenya+"'");
			if (changetime!=null) {
				if (changetime.exitValue()==0) {
					return true;
				}
			}
		} catch (Exception e) {
			raise_log("SetDateTime Exception, Msg : "+e.getMessage());
			
		}
		return false;
	}
	
	/**
	 * Manually set Linux Date and Time
	 * @param DayOfMonth : start from 1
	 * 		Month = 1, 3, 5, 7, 8 10, 12 --> Day Of Month up to 31
	 * 		Month = 4, 6, 9, 11		--> Day of Month up to 30
	 * 		Month = 2, normal 28, kabisat 29
	 * @param Month : 1 - 12
	 * @param Year : 2000 - 2999
	 * @param Hour : 0 - 23
	 * @param Minute : 0 - 59
	 * @param Second : 0 - 59
	 * @return true if success
	 */
	public boolean Set_Linux_Date_and_Time(byte DayOfMonth, byte Month, int Year, byte Hour, byte Minute, byte Second) {
		if (Hour<0) return false;
		if (Hour>23) return false;
		if (Minute<0) return false;
		if (Minute>59) return false;
		if (Second<0) return false;
		if (Second>59) return false;
		
		if (Year < 2000) return false;
		if (Year > 2999) return false;
		if (Month<1) return false;
		if (Month>12) return false;
		
		if (DayOfMonth<1) return false;
		switch(Month) {
		case 2 :
			// february
			if ((Year % 4)==0) {
				// kabisat
				if (DayOfMonth>29) return false;
			} else {
				if (DayOfMonth>28) return false;
			}

			break;
			// bulan -bulan yang berakhir di 31
		case 1: 
		case 3:
		case 5:
		case 7: 
		case 8:
		case 10: 
		case 12 :
			if (DayOfMonth>31) return false;			
			break;
			// sisanya, berarti bulan yang berakhir di 30
		default:
			if (DayOfMonth>30) return false;
			break;
		}
		
		// sampe sini dah valid
		// bentuk datevalue dengan format yyyy-MM-dd
		String datevalue = Create_YYYY_MM_DD(DayOfMonth, Month, Year);
		// bentuk timevalue dengan format hh:mm:ss
		String timevalue = Create_HH_MM_SS(Hour, Minute, Second);
		return Set_Linux_DateTime(datevalue, timevalue);
		
	}
	
	private String Create_YYYY_MM_DD(byte DD, byte MM, int YYYY) {
		StringBuilder str = new StringBuilder();
		if (DD<10) str.append("0");
		str.append(DD);
		str.append("-");
		if (MM<10) str.append("0");
		str.append(MM);
		str.append("-");
		str.append(YYYY);
		return str.toString();
	}
	
	private String Create_HH_MM_SS(byte HH, byte MM, byte SS) {
		StringBuilder str = new StringBuilder();
		if (HH<10) str.append("0");
		str.append(HH);
		str.append(":");
		if (MM<10) str.append("0");
		str.append(MM);
		str.append(":");
		if (SS <10) str.append("0");
		str.append(SS);
		return str.toString();
	}
	
	protected boolean FileExistAndReadable(String path, String filename) {
		File ff = new File(path, filename);
		if (ff.exists()) {
			if (ff.canRead()) {
				return true;
			}
		}
		return false;
	}

	private class networkmeterrunnable implements Runnable{
		
		private final String ifname;
		private final String rx_bytes_path;
		private final String tx_bytes_path;
		private final String rx_packets_path;
		private final String tx_packets_path;
		private NetworkMeterInfo previous = null;
		networkmeterrunnable(String ifname, String pathname){
			this.ifname = ifname;
			this.rx_bytes_path = new File(pathname,rx_bytes_file).getAbsolutePath();
			this.tx_bytes_path = new File(pathname,tx_bytes_file).getAbsolutePath();
			this.rx_packets_path = new File(pathname,rx_packets_file).getAbsolutePath();
			this.tx_packets_path = new File(pathname,tx_packets_file).getAbsolutePath();
		}
		@Override
		public void run() {
			raise_log("Network Meter started");
			network_metering = true;
			while(network_metering) {
				NetworkMeterInfo current = new NetworkMeterInfo(ifname, DateTime.getNow(),
						read_value(rx_bytes_path), read_value(tx_bytes_path), 
						read_value(rx_packets_path), read_value(tx_packets_path));
				
				if (current.IsValidValue()) {
					current.calculate(previous);
					previous = current;
					raise_networkstatistic(current);
				}
				
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					break;
				}
				
				
			}
			raise_log("Network Meter stopped");
			
		}
		
		private long read_value(String path) {
			long result = 0;
			try {
				BufferedReader reader = new BufferedReader(new FileReader(path));
				String line = reader.readLine();
				if (line!=null){
					line = line.trim();
				} else line = "";
				reader.close();
				result = Long.valueOf(line);
				return result;
			}  catch (IOException | NumberFormatException e) {
				result = -1;
			}
			
			return result;
		}
		
	};
	
	protected void raise_log(String msg) {
		if (need_log_event) {
			ba.raiseEventFromDifferentThread(Me, null, 0, eventname + "_log", false, new Object[] { msg });
		}
	}

	protected void raise_isreachableip(String toip, boolean isreachable) {
		if (need_isreachableip_event) {
			ba.raiseEventFromDifferentThread(Me, null, 0, eventname + "_isreachableip", false,
					new Object[] { toip, isreachable });
		}
	}
	
//	protected void raise_isreachableip(String toip, boolean isreachable, BA targetba, Object targetcaller, String targetevent) {
//		if (targetba != null) {
//			if (targetcaller!=null) {
//				if (!targetevent.isEmpty()) {
//					ba.raiseEventFromDifferentThread(targetcaller, null, 0, targetevent + "_isreachableip", false,
//							new Object[] { toip, isreachable });
//				}
//			}	
//		}	
//	}

	protected void raise_isvalidip(String toip, boolean isvalid) {
		if (need_isvalidip_event) {
			ba.raiseEventFromDifferentThread(Me, null, 0, eventname + "_isvalidip", false,
					new Object[] { toip, isvalid });
		}
	}
	
//	protected void raise_isvalidip(String toip, boolean isvalid, BA targetba,  Object targetcaller, String targetevent) {
//		if (targetba != null) {
//			if (targetcaller!=null) {
//				if (!targetevent.isEmpty()) {
//					ba.raiseEventFromDifferentThread(targetcaller, null, 0, targetevent + "_isvalidip", false,
//							new Object[] { toip, isvalid });
//				}
//			}	
//		}
//		
//		
//	}

	protected void raise_networkstatistic(NetworkMeterInfo value) {
		if (need_networkstatistic_event) {
			ba.raiseEventFromDifferentThread(Me, null, 0, eventname+"_networkstatistic", false, new Object[] {value});
		}
		
	}
	
	protected void raise_cpuusage(Map value) {
		if (need_cpuusage_event) {
			ba.raiseEventFromDifferentThread(Me, null, 0, eventname+"_cpuusage", false, new Object[] {value});
		}
	}
	
	
	/**
	 * Untuk write hasil 'Sub Application_Error (Error As Exception, StackTrace As String) As Boolean' ke file
	 * hasil nya file di run directory, dengan format Crash-ddmmyyyy-hhmmss.log
	 * @param error : exception dari Application Error
	 * @param stacktrace : stacktrace dari Application Error
	 * @return true kalau success create file
	 */
	public boolean Create_Crash_Log(Exception error, String stacktrace) {
		if (error instanceof Exception) {
			final String errormessage = error.getMessage();
			if (errormessage instanceof String) {
				if (!errormessage.isEmpty()) {
					if (stacktrace instanceof String) {
						if (!stacktrace.isEmpty()) {
							File crashfile = new File(jGPIO.rundir, create_crashlog_name());					
							try {
								BufferedWriter writer = new BufferedWriter(new FileWriter(crashfile.getAbsolutePath()));
								writer.write(error.getMessage());
								writer.newLine();
								writer.write(stacktrace);
								writer.close();
								return true;
							} catch (IOException e) {
								BA.Log("Unable to Create Crash Log, Msg : "+e.getMessage());
							}
						}
					}
				}
			}
		}
		return false;

	}
	
	
	/**
	 * Get Current Running Directory
	 * @return current running directory
	 */
	public String getRunDirectory() {
		return jGPIO.rundir;
	}
	
	private String create_crashlog_name() {
		// simpan format awal
		String dateformat = DateTime.getDateFormat();
		String timeformat = DateTime.getTimeFormat();
		
		// ganti format Datetime
		DateTime.setDateFormat("ddMMyyyy");
		DateTime.setTimeFormat("HHmmss");
		
		StringBuilder result = new StringBuilder();
		long tt = DateTime.getNow();
		result.append("Crash-");
		result.append(DateTime.Date(tt));
		result.append("-");
		result.append(DateTime.Time(tt));
		result.append(".log");
		
		// balikin format Datetime ke format awal
		DateTime.setDateFormat(dateformat);
		DateTime.setTimeFormat(timeformat);
		return result.toString();
	}
	
	public void WriteLog(String appname, String msg) {
		File logfile = new File(getRunDirectory(), create_applog_name(appname));
		
		try {
			BufferedWriter writer = new BufferedWriter(new FileWriter(logfile.getAbsolutePath(),true));
			writer.write(create_log_time());
			writer.write(msg);
			writer.newLine();
			writer.close();
		} catch (IOException e) {
			BA.Log("Unable to Create Crash Log, Msg : "+e.getMessage());
		}
	}
	
	private String create_log_time() {
		String timeformat = DateTime.getTimeFormat();
		DateTime.setTimeFormat("HH:mm:ss");
		String result = DateTime.Time(DateTime.getNow())+"\t";
		DateTime.setTimeFormat(timeformat);
		return result;
	}
	
	private String create_applog_name(String appname) {
		// simpan format awal
		String dateformat = DateTime.getDateFormat();

		// ganti format Datetime
		DateTime.setDateFormat("ddMMyyyy");

		StringBuilder result = new StringBuilder();
		long tt = DateTime.getNow();
		result.append(appname.trim());
		result.append("-");
		result.append(DateTime.Date(tt));
		result.append(".log");

		// balikin format Datetime ke format awal
		DateTime.setDateFormat(dateformat);
		return result.toString();
	}
	
}
