package code;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
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 java.util.stream.Stream;

import org.apache.http.HeaderElement;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.CookieStore;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.ContentProducer;
import org.apache.http.entity.EntityTemplate;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.auth.DigestScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

/**
 * CGI Client to VAPIX Device
 * @author rdkartono
 *
 */
public class CgiClient {
	protected final HttpHost target;
	private final UsernamePasswordCredentials userpasscred;
	private final CredentialsProvider credsProvider;
	private final CookieStore cookieStore;
	protected final CloseableHttpClient httpclient;
	protected final DigestScheme digestAuth;
	protected final AuthCache authCache;
	protected RequestConfig reqconfig;
	protected InetAddress localip = null;
	
	/**
	 * Initialize CGI Client
	 * @param hostname host name or IP address, default to 192.168.1.1 if null
	 * @param port port number, default to 80 if invalid number
	 * @param username specified username , default to "root" if null
	 * @param password specified password, default to "pass" if null
	 */
	public CgiClient(String hostname, int port, String username, String password) {
		if (!common.ValidString(hostname)) hostname="192.168.1.1";
		if (!common.ValidPortNumber(port)) port = 80;
		if (!common.ValidString(username)) username="root";
		if (!common.ValidString(password)) password = "pass";
		
		setUsingLocalIP(null);
		target = new HttpHost(hostname, port, "http");
		userpasscred = new UsernamePasswordCredentials(username, password);
		credsProvider = new BasicCredentialsProvider();
		credsProvider.setCredentials(
                new AuthScope(hostname, port),
                userpasscred);
		cookieStore =  new BasicCookieStore();
		httpclient = HttpClients.custom()
				.setDefaultCookieStore(cookieStore)
                .setDefaultCredentialsProvider(credsProvider)
                .build();
		
	
		digestAuth = new DigestScheme();
		digestAuth.overrideParamter("qop", "auth");
        digestAuth.overrideParamter("nc", "0");
        digestAuth.overrideParamter("cnonce", DigestScheme.createCnonce());
        authCache = new BasicAuthCache();
        authCache.put(target, digestAuth);
        
        
	}
	
	public void setUsingLocalIP(String ip) {
		try {
			localip = common.ValidString(ip) ? InetAddress.getByName(ip) : InetAddress.getLocalHost();
			return;
		} catch (UnknownHostException e) {
			System.out.println("setUsingLocalIP exception on ip="+ip+", Message = "+e.getMessage());
		}
		localip = null;
	}
	
	protected RequestConfig getRequestConfig() {
		
		RequestConfig result = RequestConfig.custom()
				.setLocalAddress(localip)
				.build();
		return result;
	}
	
	/**
	 * Close HTTP Client used to this object
	 * @return true if can be closed
	 */
	public boolean CloseHttpClient() {
		
		if (httpclient!=null) {
			try {
				httpclient.close();
				return true;
			} catch (IOException e) {
				raise_log("CloseHttpClient exception="+e.getMessage());
			} 
		}
		
		return false;
	}
	
//	public CloseableHttpResponse HttpPost_ByteArray(String urlpath, byte[] value) {
//		if (!common.ValidString(urlpath)) urlpath = "/";		// to default
//		raise_log("About to HttpPost from "+urlpath);
//		HttpPost httppost = new HttpPost(urlpath);
//		httppost.setConfig(getRequestConfig());
//		HttpClientContext localContext = HttpClientContext.create();
//		localContext.setAuthCache(authCache);
//		
//		CloseableHttpResponse response = null;
//		
//		
//	}
	
	public CloseableHttpResponse HttpPost_Filename(String filepath, String urlpath, String... values) {
		if (!common.ValidString(urlpath)) urlpath = "/";		// to default
		if (common.ValidArrayOfString(values)) {
			urlpath+=Stream.of(values).reduce("?", (partialstring,nextelement)->partialstring+"&"+nextelement);
		}
		raise_log("About to HttpPost from "+urlpath);
		HttpPost httppost = new HttpPost(urlpath);
		httppost.setConfig(getRequestConfig());
		HttpClientContext localContext = HttpClientContext.create();
		localContext.setAuthCache(authCache);
		
		CloseableHttpResponse response = null;
		
		File fis = new File(filepath);
		if (fis!=null && fis.exists() && fis.isFile()) {
			try {
				
				HttpEntity entity = MultipartEntityBuilder
						.create()
						.addBinaryBody("file_upload", fis)
						.build();
				
				httppost.setEntity(entity);
				httppost.setHeader("Content-Type","multipart/form-data");
				
				response = httpclient.execute(target, httppost, localContext);
				//int code = GetStatusCode(response);
				//String message = GetStatusMessage(response);
				//String content = common.ConcatListOfString(GetContent(response));
				//raise_log("HttpPost_Filename Response Code="+code+", Message="+message+", Content="+content);
				
			} catch (IOException e) {
				raise_log("HttpPost_Filename exception="+e.getMessage());
			}
			
		} else raise_log("HttpPost_Filename failed, invalid filepath="+filepath);
		
		
		
		return response;
	}
	
	public CloseableHttpResponse HttpPost(String ContentType, ContentProducer cp, String urlpath, String... parameters) {
		if (!common.ValidString(urlpath)) urlpath = "/";		// to default
		raise_log("About to HttpPost from "+urlpath);
		HttpPost httppost = new HttpPost(urlpath);
		httppost.setConfig(getRequestConfig());
		
		HttpClientContext localContext = HttpClientContext.create();
		localContext.setAuthCache(authCache);
		
		CloseableHttpResponse response = null;
		
		HttpEntity entity = new EntityTemplate(cp);
		
		httppost.setEntity(entity);
		httppost.setHeader("Content-type", ContentType);
		
		try {
			response = httpclient.execute(target, httppost, localContext);
			return response;
		} catch (IOException e) {
			raise_log("HttpPost exception = "+e.getMessage());
		}
		
		return null;
	}
	
	
	/**
	 * HTTP Post
	 * @param urlpath target URL
	 * @param value Post values that will be written in Body
	 * @return null if failed;
	 */
	public CloseableHttpResponse HttpPost(String urlpath, JsonObject value) {
		if (!common.ValidString(urlpath)) urlpath = "/";		// to default
		//raise_log("About to HttpPost from "+urlpath);
		HttpPost httppost = new HttpPost(urlpath);
		httppost.setConfig(getRequestConfig());
		
		HttpClientContext localContext = HttpClientContext.create();
		localContext.setAuthCache(authCache);
		
		CloseableHttpResponse response = null;
		
		
        try {
        	StringEntity poststring = new StringEntity(value.toString());
        	
        	httppost.setEntity(poststring);
        	httppost.setHeader("Content-type", "application/json");
        	
			response = httpclient.execute(target,httppost, localContext);
			//int code = GetStatusCode(response);
			//String message = GetStatusMessage(response);
			//raise_log("HttpPost Response Code="+code+", Message="+message);
			
		} catch (IOException e) {
			raise_log("HttpPost exception="+e.getMessage());
		}
        return response;
	}
	
	/**
	 * Doing HTTP Get to specified urlpath
	 * @param urlpath url path, default to "/" if null
	 * @return CloseableHttpResponse object or null if failed
	 */
	public CloseableHttpResponse HttpGet(String urlpath, String... parameters){
		if (!common.ValidString(urlpath)) urlpath = "/";		// to default
		if (common.ValidArrayOfString(parameters)) {
			urlpath+=Stream.of(parameters).reduce("?", (partialstring,nextelement)->partialstring+"&"+nextelement);
		}
		//raise_log("About to HttpGet from "+urlpath);
		HttpGet httpget = new HttpGet(urlpath);
		httpget.setConfig(getRequestConfig());
		HttpClientContext localContext = HttpClientContext.create();
        localContext.setAuthCache(authCache);
        CloseableHttpResponse response = null;

        try {
			response = httpclient.execute(target, httpget, localContext);
			int code = GetStatusCode(response);
			//String message = GetStatusMessage(response);
			//raise_log("HttpGet Response Code="+code+", Message="+message);
			if (code==401) {
				// rejected karena authentication
				Map<String, String> wwwauth = GetWWWAuthentication(response);
				if (wwwauth!=null && wwwauth.size()>0) {
					raise_log("HttpGet rejected, Need to update WWW authentication");
					String opaque = wwwauth.get("opaque");
					if (common.ValidString(opaque)) {
						replace_digestauthvalue("opaque", opaque);
					}
					String nonce = wwwauth.get("nonce");
					if (common.ValidString(nonce)) {
						replace_digestauthvalue("nonce",nonce);
						
					}
					String realm = wwwauth.get("Digest realm");
					if (common.ValidString(realm)) {
						replace_digestauthvalue("realm",realm);
					}
				}
				
			}
		} catch (IOException e) {
			raise_log("HttpGet exception="+e.getMessage());
		}
        
		return response;
	}
	
	private void replace_digestauthvalue(String key, String newvalue) {
		if (digestAuth!=null) {
			if (common.ValidString(key)) {
				if (common.ValidString(newvalue)) {
					String oldvalue = digestAuth.getParameter(key);
					if (oldvalue==null) {
						raise_log("Set key="+key+" value="+newvalue);
						digestAuth.overrideParamter(key, newvalue);
					} else if (newvalue.equals(oldvalue)==false) {
						raise_log("Replacing key="+key+", old value="+oldvalue+" with new value="+newvalue);
						digestAuth.overrideParamter(key, newvalue);
					} else raise_log("key="+key+", already same value");
				}
			}
		}
	}
	
	/**
	 * Get HTTP Status Code from CloseableHttpResponse object
	 * @param response CloseableHttpResponse object
	 * @return 0 if failed
	 */
	public int GetStatusCode(CloseableHttpResponse response) {
		if (response!=null) {
			if (response.getStatusLine()!=null) {
				return response.getStatusLine().getStatusCode();
			}
		}
		return 0;
	}
	
	/**
	 * Get HTTP Status Message from CloseableHttpResponse object
	 * @param response CloseableHttpResponse object
	 * @return null if failed
	 */
	public String GetStatusMessage(CloseableHttpResponse response) {
		if (response!=null) {
			if (response.getStatusLine()!=null) {
				return response.getStatusLine().getReasonPhrase();
			}
		}
		return null;
	}
	
	/**
	 * Get HTTP Content Type from CloseableHttpResponse object
	 * @param response CloseableHttpResponse object
	 * @return null if failed
	 */
	public String GetContentType(CloseableHttpResponse response) {
		HttpEntity ent = GetResponseEntity(response);
		return ent!=null ? ent.getContentType().getValue() : null;
	}
	
	/**
	 * Get Content Length
	 * @param response CloseableHttpResponse object
	 * @return 0 if failed, or -1 if unknown from HTTP Response
	 */
	public long GetContentLength(CloseableHttpResponse response) {
		HttpEntity ent = GetResponseEntity(response);
		return ent!=null ? ent.getContentLength() : 0;
		
	}
	
	/**
	 * Get FileName from CloseableHttpResponse
	 * @param response CloseableHttpResponse object
	 * @return null if not available
	 */
	public String GetFileName(CloseableHttpResponse response) {
		if (response!=null) {
			String vv = response.getFirstHeader("Content-Disposition").getValue();
			if (common.ValidString(vv)) {
				Pattern pp = Pattern.compile("filename=\"(\\S+)\"");
				Matcher mm = pp.matcher(vv);
				if (mm.find()) {
					return mm.group(1);
				} 
			}
			
		}
		return null;
	}
	
	/**
	 * Get HTTP Entity from a CloseableHttpResponse
	 * @param response CloseableHttpResponse object
	 * @return null if failed
	 */
	public HttpEntity GetResponseEntity(CloseableHttpResponse response) {
		if (response!=null) {
			return response.getEntity();
		}
		return null;
	}
	
	/**
	 * Close CloseableHttpResponse Object
	 * @param response CloseableHttpResponse object
	 */
	public void Close(CloseableHttpResponse response) {
		if (response!=null) {
			try {
				response.close();
			} catch (IOException e) {
				raise_log("Close exception="+e.getMessage());
			}
			response = null;
		}
	}
	
	/**
	 * Get WWW-Authenticate headers from CloseableHttpResponse
	 * @param response CloseableHttpResponse object
	 * @return Empty Map if failed
	 */
	private Map<String, String> GetWWWAuthentication(CloseableHttpResponse response){
		if (response!=null) {
			Map<String, String> wwwAuth = Arrays
	                .stream(response.getHeaders("WWW-Authenticate")[0]
	                        .getElements())
	                .collect(Collectors.toMap(HeaderElement::getName,
	                        HeaderElement::getValue));
			
			return wwwAuth;
		} else return new HashMap<String, String>();
		
	}
	
	/**
	 * Get Content from CloseableHttpResponse object
	 * @param response CloseableHttpResponse object
	 * @return List of String
	 */
	public List<String> GetContent(CloseableHttpResponse response){
		List<String> result = new ArrayList<String>();
		if (response!=null) {
			HttpEntity entity = response.getEntity();
			if (entity!=null) {
				try(InputStream is = entity.getContent()) {
					BufferedReader buf = new BufferedReader(new InputStreamReader(is));
					buf.lines().forEach(ss->result.add(ss));
				} catch (UnsupportedOperationException | IOException e) {
					raise_log("GetContent exception="+e.getMessage());
				}
				
			}
		}
		return result;
	}
	
	/**
	 * Get Content from body, expect to return Bytes array
	 * @param response CloseableHttpResponse
	 * @return null if failed
	 */
	public byte[] GetContent_BytesArray(CloseableHttpResponse response) {
		if (response!=null) {
			HttpEntity entity = response.getEntity();
			if (entity!=null) {
				try (InputStream is = entity.getContent()) {
					ByteBuffer result = ByteBuffer.allocate(0);
					
					int len = 0;
					while(true) {
						byte[] xx = new byte[1024];
						len = is.read(xx);
						if (len>0) {
							ByteBuffer temp = ByteBuffer.allocate(result.capacity()+len);
							temp.put(result);
							temp.put(xx,0,len);
							
							result = temp;
						} else break;
					}
					
					return result.array();
				} catch (UnsupportedOperationException | IOException e) {
					raise_log("GetContent_BytesArray failed, exception="+e.getMessage());
				}
				
			}
		}
		return null;
	}
	
	/**
	 * Get content as JsonObject
	 * @param response CloseableHttpResponse
	 * @return null if failed
	 */
	public JsonObject GetContentAsJson(CloseableHttpResponse response) {
		if (response!=null) {
			return JsonParser.parseString(common.ConcatListOfString(GetContent(response))).getAsJsonObject();
		}
		return null;
	}
	
	private void raise_log(String msg) {
		System.out.println(msg);
	}
	
	
	
}
