package code;


import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;

import com.google.gson.JsonObject;

import anywheresoftware.b4a.BA;



@BA.Events(values= {
		"log(msg as string)"
})

/**
 * General VAPIX Device
 * @author rdkartono
 *
 */
public class VapixDevice {
	protected final CgiClient client;
	private final Map<String,String> parameters;
	
	protected BA ba;
	protected String event;
	protected boolean need_log_event = false;
	protected Object Me = this;
	protected boolean ba_inited = false;
	
	public void InitializeEvent(BA ba, String event) {
		this.ba = ba;
		this.event = event;
		if (this.ba!=null) {
			if (common.ValidString(this.event)) {
				ba_inited = true;
				need_log_event = ba.subExists(this.event+"_log");
			}
		}
	}
	
	/**
	 * Create Generic Vapix Device that point to another VapixDevice
	 * @param other Other Vapix Device
	 */
	protected VapixDevice(VapixDevice other) {
		this.client = (other!=null ? other.client : null);
		this.parameters = (other!=null ? other.parameters : new HashMap<String,String>());	
	}
	
	/**
	 * Create Generic Vapix Device
	 * @param hostname host name or IP address
	 * @param port port number
	 * @param username username to login
	 * @param password password to login
	 */
	protected VapixDevice(String hostname, int port, String username, String password) {
		client = new CgiClient(hostname, port, username, password);
		parameters = new HashMap<String,String>();
	}
	
	/**
	 * Create Generic Vapix Device
	 * @param hostname host name or IP Address
	 * @param port port number
	 * @param username username to login
	 * @param password password to login
	 * @param localip local ip address / ethernet to use for connecting the device
	 */
	protected VapixDevice(String hostname, int port, String username, String password, String localip) {
		this(hostname, port, username, password);
		client.setUsingLocalIP(localip);
		
	}
	
	
	
	public void Close() {
		if (client!=null) {
			client.CloseHttpClient();
		}
	}
	
	/**
	 * Get Serial Number
	 * @return null if failed
	 */
	public String getSerialNumber() {
		return GetParameterValue("Properties.System.SerialNumber");
	}
	
	/**
	 * Get Product Name
	 * @return null if failed
	 */
	public String getProductName() {
		return GetParameterValue("Brand.ProdShortName");
	}
	
	/**
	 * Get Product Type
	 * @return null if failed
	 */
	public String getProductType() {
		return GetParameterValue("Brand.ProdType");
	}
	
	/**
	 * Get IP Address 
	 * @return null if failed
	 */
	public String getIPAddress() {
		return GetParameterValue("Network.eth0.IPAddress");
	}
	
	/**
	 * Get Subnet Mask
	 * @return null if failed
	 */
	public String getSubnetMask() {
		return GetParameterValue("Network.eth0.SubnetMask");
	}
	
	/**
	 * Get MAC Address
	 * @return null if failed
	 */
	public String getMACAddress() {
		return GetParameterValue("Network.eth0.MACAddress");
	}
	
	/**
	 * Get Network Hostname recognized in DHCP Router
	 * @return null if failed
	 */
	public String getHostName() {
		return GetParameterValue("Network.HostName");
	}
	
	/**
	 * Get Network Gateway
	 * @return null if failed
	 */
	public String getGateway() {
		return GetParameterValue("Network.Routing.DefaultRouter");
	}
	
	/**
	 * Get Primary DNS (DNS1)
	 * @return null if failed
	 */
	public String getDNS1() {
		return GetParameterValue("Network.Resolver.NameServer1");
	}
	
	/**
	 * Get Secondary DNS (DNS2)
	 * @return null if faild
	 */
	public String getDNS2() {
		return GetParameterValue("Network.Resolver.NameServer2");
	}
	
	/**
	 * Check if this is an Vapix Audio Device
	 * @return true if correct
	 */
	public boolean IsAudioDevice() {
		return ("yes".equals(GetParameterValue("Properties.Audio.Audio")));
	}
	
	/**
	 * Get HTTP Version
	 * @return string value
	 */
	public String getHTTPVersion() {
		return GetParameterValue("Properties.API.HTTP.Version");
	}
	/**
	 * Get saved parameters with specific key
	 * Call GetParametersFromDevice to save parameters locally
	 * @param key parameter title
	 * @return null if failed
	 */
	public String GetParameterValue(String key) {
		String result = parameters.entrySet().stream()
				.filter(e->e.getKey().contains(key))
				.map(Map.Entry::getValue)
				.findFirst()
				.orElse(null);
		return result;
	}
	
	/**
	 * Replace Parameter Value 
	 * @param key Specific key to replace the value
	 * @param value new value
	 * @return previous value replaced, or null if not available
	 */
	protected String SetParameterValue(String key, String value) {
		for(Map.Entry<String, String> entry : parameters.entrySet()) {
			if (entry.getKey().contains(key)) {
				return entry.setValue(value);
				
			}
		}
		return null;
	}
	
	/**
	 * Get Array of Parameter Keys
	 * @return parameter keys
	 */
	public String[] GetParameterKeys() {
		return parameters.keySet().toArray(new String[0]);
	}
	
	/**
	 * Re-Get parameters from Vapix Device
	 * @return true if success
	 */
	public boolean GetParametersFromDevice(){
		List<String> vv = HTTPGet("/axis-cgi/param.cgi","action=list");
		if (common.ValidListOfString(vv)) {
			raise_log("GetParameters success");
			vv.forEach(ss -> {
				int index = ss.indexOf('=');
				if (index!=-1) {
					// ada '='
					String key = ss.substring(0, index);
					String value = ss.substring(index+1);
					String prev_value = parameters.put(key, value);
					if (common.ValidString(prev_value)) {
						raise_log("key="+key+", old_value="+prev_value+", new_value="+value);
					} 
				}
			});
			return true;
		} else raise_log("GetParameters failed");
		return false;
	}
	
	/**
	 * General Purpose HTTP Get
	 * @param url url path
	 * @param parameters parameters that follow HTTP Get
	 * @return List of String if success, or null if failed
	 */
	protected List<String> HTTPGet(String url, String... parameters){
		List<String> result = null;
		CloseableHttpResponse response = client.HttpGet(url,parameters);
		if (client.GetStatusCode(response)==200) {
			result = client.GetContent(response);
		} 
		client.Close(response);
		return result;
	}
	
	/**
	 * General Purpose HTTP Get
	 * return String as content, or null if falied
	 * @param expected_contenttype expected content type, put null if not expecting 
	 * @param url URL
	 * @param parameters Parameters
	 * @return null if failed 
	 */
	protected String HTTPGet_GetString(String expected_contenttype, String url, String... parameters) {
		CloseableHttpResponse response = client.HttpGet(url, parameters);
		int code = client.GetStatusCode(response);
		//String message = client.GetStatusMessage(response);
		String contenttype = client.GetContentType(response);
		String content = common.ConcatListOfString(client.GetContent(response));
		//raise_log(MessageFormat.format("HTTPGet_GetString to {0}, Code={1}, Message={2}, Content Type={3}, Content={4}", url, code, message, contenttype, content));
		client.Close(response);
		
		
		if (code==200) {
			if (common.Strings_Valid_and_Match(expected_contenttype, contenttype)){
				return content;
			}
		}
		
		return null;
	}
	
	/**
	 * General Purpose HTTP Get, but specifically want to find a string 
	 * @param findme String to find
	 * @param url URL path
	 * @param parameters parameters that follow HTTP Get
	 * @return null if findme not found
	 */
	protected String HTTPGet_FindString(String findme, String url, String... parameters) {
		List<String> xx = HTTPGet(url, parameters);
		if (xx!=null && xx.size()>0) {
			return xx.stream()
					.filter(ss -> ss.contains(findme))
					.findAny()
					.orElse(null);
		}
		return null;
	}
	
	/**
	 * General Purpose HTTP Get, but specifically want to find a string
	 * @param findme String to find
	 * @param url URL Path
	 * @param parameters parameters that follow HTTP Get
	 * @return array of string, contains any string that contains findme
	 */
	protected String[] HTTPGet_FindStrings(String findme, String url, String... parameters) {
		List<String> xx = HTTPGet(url,parameters);
		if (xx!=null && xx.size()>0) {
			List<String> result =  xx.stream()
					.filter(ss -> ss.contains(findme))
					.collect(Collectors.toList());
			return result.toArray(new String[0]);
		}
		return new String[0];
	}
	
	/**
	 * General Purpuse HTTP Get, but dont need the result in Body
	 * Just need status code
	 * @param url URL Path
	 * @param parameters parameters that follow HTTP Get
	 * @return HTTP Get status code
	 */
	protected int HTTPGet_NoResult(String url, String... parameters) {
		CloseableHttpResponse response = client.HttpGet(url,parameters);
		int code = client.GetStatusCode(response);
		String message = client.GetStatusMessage(response);
		client.Close(response);
		raise_log("HTTPGet_NoResult to "+url+", Code="+code+", Message="+message);
		
		return code;
	}
	
	/**
	 * General Purpose HTTP Get, return bytes array from ContentInputStream
	 * @param expected_ContentType expected content type 
	 * @param url URL to download
	 * @param parameters some parameters
	 * @return byte array if success, or null if failed
	 */
	protected byte[] HTTPGet_BytesResult(String expected_ContentType, String url, String... parameters) {
		CloseableHttpResponse response = client.HttpGet(url, parameters);
		int code = client.GetStatusCode(response);
		
		byte[] result = client.GetContent_BytesArray(response);
		client.Close(response);
		if (code==200) {
				return result;
			
		}
		
		return null;
	}
	
	/**
	 * Download file
	 * @param savelocation location path
	 * @param url URL to download
	 * @param parameters some parameters needed
	 * @return DownloadResult object, or null if invalid
	 */
	protected DownloadResult HTTPGet_DownloadFile(String savelocation, String url, String... parameters) {
		CloseableHttpResponse response = client.HttpGet(url, parameters);
		int code = client.GetStatusCode(response);
		if (code==200) {
			HttpEntity entity = client.GetResponseEntity(response);
			if (entity!=null) {
				String filename = client.GetFileName(response);
				if (common.ValidString(filename)) {
					File ff = new File(savelocation, filename);
					try (FileOutputStream fos = new FileOutputStream(ff)) {
						entity.writeTo(fos);
						return new DownloadResult(savelocation, filename, ff.length());
					} catch (IOException e) {
						raise_log("HTTPGet_DownloadFile exception = "+e.getMessage());
					}
				} else raise_log("HTTPGet_DownloadFile filename not available");
			} else raise_log("HTTPGet_DownloadFile HttpEntity not available");
		} else raise_log("HTTPGet_DownloadFile code not 200");
		return null;
	}
	
	
	public class DownloadResult{
		public String path;
		public String filename;
		public long size;

		public DownloadResult(String path, String filename, long size) {
			this.path = path;
			this.filename = filename;
			this.size = size;
		}
	}
	
	/**
	 * HTTP Get to specific url and expect some Content Type to be considered success
	 * @param expected_ContentType content type message to expect
	 * @param url URL
	 * @param parameters parameters
	 * @return true if Code=200 and contain expected_ContentType
	 */
	protected boolean HTTPGet_BooleanResult(String expected_ContentType, String url, String... parameters) {
		CloseableHttpResponse response = client.HttpGet(url, parameters);
		int code=client.GetStatusCode(response);
		String message = client.GetStatusMessage(response);
		String contentype = client.GetContentType(response);
		raise_log(MessageFormat.format("HTTPGet_BooleanResult to {0}, Code={1}, Message={2}, ContentType={3}", url, code, message, contentype));
		client.Close(response);
		if (code==200) {
			if (contentype.contains(expected_ContentType)) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * HTTP Post a Json Object
	 * @param url URL to post
	 * @param data Json Object to post
	 * @return Json Object result
	 */
	protected JsonObject HTTPPost(String url, JsonObject data) {
		CloseableHttpResponse response = client.HttpPost(url, data);
		int code = client.GetStatusCode(response);
		JsonObject result = client.GetContentAsJson(response);
		client.Close(response);
		if (code==200) {
			return result;
		}
		return null;
	}
	
	/**
	 * HTTP Post a Json Object
	 * @param url URL to post
	 * @param data Json Object to post
	 * @return true if success (code 200)
	 */
	protected boolean HTTPPost_BooleanResult(String url, JsonObject data) {
		CloseableHttpResponse response = client.HttpPost(url, data);
		int code = client.GetStatusCode(response);
		client.Close(response);
		return (code==200) ;
		
	}
	
	/**
	 * Upload file using HTTP Post
	 * @param filename file to upload
	 * @param url URL to post
	 * @return clip ID, or -1 if failed
	 */
	protected int HTTPPost_UploadFile(String filename, String url, String... parameters) {
		CloseableHttpResponse response = client.HttpPost_Filename(filename, url, parameters);
		int code = client.GetStatusCode(response);
		List<String> content = client.GetContent(response);
		client.Close(response);
		if (code==200) {
			// bisa dua macam status
			// p1 kalau file sudah ada, tapi di upload ulang
			Pattern p1 = Pattern.compile("updated=MediaClip.M(\\d+)");
			// p2 kalau file belum ada, di upload baru
			Pattern p2 = Pattern.compile("uploaded=(\\d+)");
			
			for(String ss : content) {
				//System.out.println(ss);
				Matcher m1 = p1.matcher(ss);
				Matcher m2 = p2.matcher(ss);
				if (m1.find()) {
					//System.out.println("Found clip "+m1.group(1)+" using updated");
					return Integer.valueOf(m1.group(1));
				} else if (m2.find()) {
					//System.out.println("Found clip "+m2.group(1)+" using uploaded");
					return Integer.valueOf(m2.group(1));
				}
				
			}
			
		}
		return -1;
	}
	
	protected void raise_log(String msg) {
		if (need_log_event) {
			ba.raiseEventFromDifferentThread(Me, null, 0, event+"_log", false, new Object[] {msg});
		} else System.out.println(msg);
	}
	
	
}
