/**
 * 
 */
package com.homeCenter.service;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Set;

import com.google.gson.JsonObject;
import com.homeCenter.cluster.Cluster;
import com.homeCenter.common.HttpFile;
import com.homeCenter.common.Log;
import com.homeCenter.common.Util;
import com.homeCenter.db.Database;
import com.homeCenter.io.Device;
import com.homeCenter.io.DeviceRef;
import com.homeCenter.io.FhemStream;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;

/**
 * @author Thorsten
 *
 */
public class WebHandler implements HttpHandler {
	protected Database db;
	protected Observer observer;
	protected WebApp app;

	/**
	 * 
	 */
	public WebHandler(WebSessionManagement sessions, Observer observer, Database db, FhemStream connector, Collector collector, Cluster cluster) {
		this.db 			= db;
		this.observer 		= observer;
		this.app			= new WebApp(sessions, this.observer, this.db, connector, collector, cluster);
	}
	

	/* (non-Javadoc)
	 * @see com.sun.net.httpserver.HttpHandler#handle(com.sun.net.httpserver.HttpExchange)
	 */
	@Override
	public void handle(HttpExchange exchange) {
		try {
			String requestMethod = exchange.getRequestMethod();
			String path = exchange.getRequestURI().getPath().trim();
			
		    if (requestMethod.equalsIgnoreCase("GET")) {
		    	if(path == null || path.isEmpty() || path.equals("/")) {
		    		this.renderPage(exchange, null);
		    		return;
		    	}
		    	
		    	if(path.startsWith("/app/data/")) {
		    		this.renderPage(exchange, path.replace("/app/data", ""));
		    	} else {
		    		this.readFile(exchange, path);
		    	}
		    } else if(requestMethod.equalsIgnoreCase("POST")) {
		    	this.processRequest(exchange);
		    } else if(requestMethod.equalsIgnoreCase("PUT")) {
		       	this.processDeviceUpdate(exchange, path.substring(1));
		    } else {
			    byte[] content = ("Method \"" + requestMethod + "\" not supported.").getBytes();
				this.setHeaderContentType(exchange, "text/plain");
				this.setHeaderContentLength(exchange, content);
			    this.writeOutput(exchange, content, HttpURLConnection.HTTP_BAD_METHOD);
		    }
		} catch(Exception e) {
			Log.error(e);
		}
	}
	
	protected void processRequest(HttpExchange exchange) throws IOException {
		HttpFile multipart = new HttpFile();
		Headers headers = exchange.getRequestHeaders();
		BufferedReader reader = new BufferedReader(new InputStreamReader(exchange.getRequestBody()));
		byte[] content = null;
		String sData = null;

        if(multipart.isMultipart(headers)) {
        	multipart.process(headers, exchange);
        	if(multipart.size() > 1) {
        		// take the first entry as source for the query
        		sData = new String(multipart.get().get(0).getData(), "UTF-8");
        	}
        	
    		Log.info("Multipart Request: " + sData);
        } else {
    		String line;
    		StringBuilder out = new StringBuilder();
            while ((line = reader.readLine()) != null) {
                out.append(line);
            }
        	
            sData = out.toString();
    		Log.info("Request: " + sData);
        }

        JsonObject result = this.app.process(exchange, sData, multipart);
	    
	    // Output
	    if(result == null || result.toString() == null || result.toString().getBytes() == null) {
	    	content = ("Request not supported.").getBytes();
			this.setHeaderContentType(exchange, "text/plain");
			this.setHeaderContentLength(exchange, content);
		    this.writeOutput(exchange, content, HttpURLConnection.HTTP_BAD_REQUEST);
		    return;
	    }

	    content = result.toString().getBytes();	    
		this.setHeaderContentType(exchange, "application/json");
		this.setHeaderContentLength(exchange, content);
		this.setDefaultHeader(exchange);
	    this.writeOutput(exchange, content, HttpURLConnection.HTTP_OK);
	}
	
	/**
	 * Update device status. Called only external from fhem for instance.
	 * 
	 * @param {HttpExchange} exchange
	 * @param {String} deviceId
	 * @throws IOException
	 */
	synchronized protected void processDeviceUpdate(HttpExchange exchange, final String deviceId) throws IOException {
		if(deviceId == null || deviceId.isEmpty()) {
			return;
		}
		
		byte[] content = null;
		Device device = DeviceRef.get(deviceId);
    	if(device == null) {
    		content = ("Device \"" + deviceId + "\" not found.").getBytes();
    		this.setHeaderContentType(exchange, "text/plain");
    		this.setHeaderContentLength(exchange, content);
    	    this.writeOutput(exchange, content, HttpURLConnection.HTTP_NO_CONTENT);
    		return;
    	}
    	
		try {
			if(device.update(true)) {
				Log.info("Device \"" + device.getDevice() + "\" updated.");
			}

			content = ("Device will be updated.").getBytes();
			this.setHeaderContentType(exchange, "text/plain");
			this.setHeaderContentLength(exchange, content);
			this.writeOutput(exchange, content, HttpURLConnection.HTTP_OK);
		} catch(Exception e) {
			Log.error(e);
		}
	}
	
	/**
	 * Render object of the web app
	 * 
	 * @param {HttpExchange} exchange
	 * @param {String} path
	 * @throws IOException
	 */
	protected void renderPage(HttpExchange exchange, String path) throws IOException {
		WebRenderObject renderObject = this.app.render(exchange, WebApp.PATH, path);
		
		// Internal error occured
		if(renderObject == null) {
		    this.writeOutput(exchange, "Cannot render object.".getBytes(), HttpURLConnection.HTTP_INTERNAL_ERROR);
			return;
		}
		
		// Set additional fields
		Set<Entry<String,String>> entrySet = renderObject.getFields().entrySet();
		Iterator<Entry<String,String>> it = entrySet.iterator();
		while(it.hasNext()) {
			Entry<String, String> entry = it.next();
			this.setHeader(exchange, entry.getKey(), entry.getValue());
		}
		
		this.setHeaderContentType(exchange, renderObject.getContentType());
		this.setHeaderContentLength(exchange, renderObject.getData());
		this.setDefaultHeader(exchange);
	    this.writeOutput(exchange, renderObject.getData(), HttpURLConnection.HTTP_OK);
	}

	/**
	 * 
	 * @param {HttpExchange} exchange
	 * @param {String} path
	 * @throws IOException
	 */
	protected void readFile(HttpExchange exchange, String path) throws IOException {
		byte[] content = this.app.readFile(WebApp.PATH + path, WebApp.PATH);
		
		String contentType = "text/plain";		
		if(content == null) {
		    content = ("File \"" + path + "\" not found.").getBytes();
			this.setHeaderContentType(exchange, contentType);
			this.setHeaderContentLength(exchange, content);
		    this.writeOutput(exchange, content, HttpURLConnection.HTTP_BAD_REQUEST);
		    return;
		}
		
		switch(Util.getFileExtension(path)) {
		case "css":
			contentType =  "text/css; charset=UTF-8";
			break;
		case "js":
			contentType =  "text/javascript; charset=UTF-8";
			break;
		case "jpg":
			contentType =  "image/jpeg";
			break;
		case "png":
			contentType =  "image/png";
			break;
		case "gif":
			contentType =  "image/gif";
			break;
		}
		
		this.setHeaderContentType(exchange, contentType);
		this.setHeaderContentLength(exchange, content);
		this.setDefaultHeader(exchange);
	    this.writeOutput(exchange, content, HttpURLConnection.HTTP_OK);
	}
	
	protected void setDefaultHeader(HttpExchange exchange) {
		this.setHeader(exchange, "cache-control", "private, max-age=0");
		this.setHeader(exchange, "expires", "-1");
		this.setHeader(exchange, "x-frame-options", "SAMEORIGIN");
		this.setHeader(exchange, "x-xss-protection", "1; mode=block");
	}
	
	protected void setHeaderContentType(HttpExchange exchange, String contentType) {
		this.setHeader(exchange, "Content-Type", contentType);
	}
	
	protected void setHeaderContentLength(HttpExchange exchange, byte[] content) {
		this.setHeader(exchange, "Content-Length", content.length + "");
	}
	
	protected void setHeader(HttpExchange exchange, String key, String value) {
		exchange.getResponseHeaders().set(key, value);
	}
	
	protected void writeOutput(HttpExchange exchange, byte[] content, int responseCode) throws IOException {
//		exchange.getResponseHeaders().set("Content-Encoding", "gzip");
		exchange.getResponseHeaders().set("status", Integer.toString(responseCode));
		exchange.sendResponseHeaders(responseCode, 0);

		//GZIPOutputStream ostream = new GZIPOutputStream(exchange.getResponseBody());
		try {			
			// @url https://stackoverflow.com/questions/33695620/sun-net-httpserver-too-many-bytes-to-write-to-stream
			int BUFFER_SIZE = 2048;
			BufferedOutputStream out = new BufferedOutputStream(exchange.getResponseBody());
			ByteArrayInputStream bis = new ByteArrayInputStream(content);
			byte[] buffer = new byte[BUFFER_SIZE];
            int count;
            while ((count = bis.read(buffer)) != -1) {
                out.write(buffer, 0, count);
            }
			out.flush();
		} catch(Exception e) {
			Log.warning(e.getMessage());
		} finally {
			exchange.close();
		}
	}
	
	public static boolean isMobile(HttpExchange exchange) {
		com.sun.net.httpserver.Headers headers = exchange.getRequestHeaders();
		Iterator<String> it = headers.get("User-agent").iterator();
		boolean result = false;
		String entry = null;
		Set<String> devices = new HashSet<String>();
		
		devices.add("iphone");
		devices.add("ipod");
		devices.add("android");
		devices.add("windows phone");
		
		while(it.hasNext() || !result) {
			entry = it.next();
			entry = entry.toLowerCase();
			
			for (String device : devices) {
				if (entry.contains(device)) {
					result = true;
					break;
				}
			}
		}
		
		return result;
	}
	
	public static boolean isTablet(HttpExchange exchange) {
		com.sun.net.httpserver.Headers headers = exchange.getRequestHeaders();
		Iterator<String> it = headers.get("User-agent").iterator();
		boolean result = false;
		String entry = null;
		Set<String> devices = new HashSet<String>();
		
		devices.add("ipad");
		
		while(it.hasNext() || !result) {
			entry = it.next();
			entry = entry.toLowerCase();
			
			for (String device : devices) {
				if (entry.contains(device)) {
					result = true;
					break;
				}
			}
		}
		
		return result;
	}
	
	
}
