package com.homeCenter.cluster;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Date;

import com.homeCenter.cluster.packet.Metadata;
import com.homeCenter.cluster.packet.Packet;
import com.homeCenter.cluster.packet.PacketType;
import com.homeCenter.cluster.packet.SystemInfo;
import com.homeCenter.SystemHandle;
import com.homeCenter.cluster.packet.Image;
import com.homeCenter.common.Log;
import com.homeCenter.io.driver.RaspPiCamera;

public class Client extends Thread {
	protected Socket socket;
	protected ObjectInputStream in;
	protected ObjectOutputStream out;
	protected String startPrefixAddress;
	protected String endPrefixAddress;
	
	private ClientReader reader;
	
	private static final long CONNECT_DELAY = 10000;
	private static final int SEARCH_TIMEOUT = 50;
	
	private Date lastACK;
	private static final long MAX_ACK_INTERVAL = 1000 * 60 * 5;
	
	// Temporary solution
	private RaspPiCamera camera;
	
	/**
	 * @param {String} startPrefixAddress
	 */
	public Client(String startPrefixAddress, String endPrefixAddress) {		
		this.startPrefixAddress = startPrefixAddress;
		this.endPrefixAddress = endPrefixAddress;
		this.socket = null;
		this.reader = null;
		this.lastACK = new Date();
	}
	
	@Override
	protected void finalize() throws Throwable {
		this.close();
	}
	
	@Override
	public void run() {
	    while (!this.isInterrupted()) {
	    	synchronized (this) {
		        try {
					this.onIdle();
					this.wait(Client.CONNECT_DELAY);
				} catch (InterruptedException e) {
					// called by shutdown thread. ignore it
					break;
				} catch(Exception e) {
					Log.error(e);
					break;
				}
			}
	    }
	    
	    this.close();
	}
	
	/**
	 * Waits to connect to cluster 
	 * @throws InterruptedException
	 */
	protected void onIdle() throws InterruptedException {
		if(!this.isConnected()) {
			this.close();
			this.findServer();
			return;
		}
		
		// Check if the last update is too old. If this is the case, we disconnect
		// the socket to get a new usable connection again.
		Date now = new Date();
		if(this.isConnected() && 
			((now.getTime() - this.lastACK.getTime()) > MAX_ACK_INTERVAL)) {
			Log.info("Lost connection. Shutdown client...");
			this.close();
			return;
		}
	}
	
	/**
	 * Connect to cluster.
	 * 
	 * @param {InetSocketAddress} addr
	 * @return {boolean}
	 */
	public boolean connect(InetSocketAddress addr, int timeout) {
		if(addr == null) {
			return false;
		}
		
		boolean result = false;
		try {
			// Create socket
			this.socket = new Socket();
			this.socket.setKeepAlive(true);
			this.socket.setReuseAddress(true);
			
			// connect to cluster server
			if(!this.socket.isConnected()) {
				this.socket.connect(addr, timeout);
			}
			
			// get output stream
		    this.out = new ObjectOutputStream(this.socket.getOutputStream());
		    
		    // Create reader thread

			this.reader = new ClientReader(this);
	    	this.reader.start();
			
			// Send metadata
			Metadata packet = new Metadata();
			this.send(packet);
			
			result = true;
			Log.info("Connected to " + addr.getAddress().getHostAddress());
		} catch (Exception /*| InterruptedException*/ e) {
			this.close();
			Log.warning("Server is not reachable: " + addr.toString());
		} finally {
			
		}
		
		return result;
	}
	
	/**
	 * Send a packet to the server
	 * 
	 * @param {Packet} packet
	 * @throws IOException
	 */
	public void send(Packet packet) throws IOException {
		if(!this.isConnected()) {
			return;
		}
		
		Helper.send(this.out, packet);
	}
	
	/**
	 * Closes the current connection.
	 */
	public void close() {
		// Close output stream
		this.closeOutput();
		
		// Shutdown socket
		this.closeSocket();
		
		// Shutdown reader
		try {
			if(this.reader != null && this.reader.isAlive()) {
				this.reader.interrupt();
				this.reader.join(10000);
			}
		} catch (InterruptedException e) {
			Log.error(e);
		}
		this.reader = null;
		Log.info("Client connection shutdown");
	}
	
	/**
	 * Closes the output stream.
	 */
	public void closeOutput() {
		try {
			if(this.out != null)
				this.out.close();
		} catch (IOException e) {
			Log.warning(e.getMessage());
		} finally {
			this.out = null;
		}
		
		Log.info("Output stream closed.");
	}
	
	/**
	 * Closes the sockets.
	 */
	public void closeSocket() {
		if(this.socket != null && !this.socket.isClosed()) {
			try {				
				this.socket.close();
			} catch (IOException e) {
				Log.error(e);
			}
		}
		this.socket = null;

		Log.info("Socket closed.");
	}
	
	/**
	 * Returns if the node is connected to the cluster.
	 * 
	 * @return {boolean}
	 */
	public boolean isConnected() {
		return this.socket != null && 
				!this.socket.isClosed() && 
				this.socket.isConnected();
	}

	/**
	 * Handle incoming packets and sends the result back to the cluster.
	 * 
	 * @param {Packet} read
	 */
	public void handle(Packet read) {
		Packet response = new Packet(PacketType.Response);
		
		switch(read.getType()) {
		case Job:
			try {
				Log.info("Sending image...");
				if(this.camera == null) {
					this.camera = new RaspPiCamera();
					this.camera.setup();
				}
				
				this.camera.clean();
				this.camera.process();
				response = new Image(this.camera.getWidth(), this.camera.getHeight(), this.camera.getData());
			} catch (Exception e) {				
				Log.error(e);
			}
			break;
		case SystemInfo:
			Log.info("Sending system info...");
			response = new SystemInfo();
			((SystemInfo)response).update();
			break;
		default:
			break;
		}
		
		try {
			// Set identifier again to help the server to execute the corresponding promises
			response.setIdentifier(read.getIdentifier());
			Helper.send(this.out, response);
			Log.info("Package send.");
		} catch (IOException e) {
			Log.error(e);
			this.close();
		} finally {
			// Use the gc call directly
			SystemHandle.gc();
			
			this.lastACK = new Date();
		}
	}
	
	public Socket getSocket() {
		return this.socket;
	}
	
	/**
	 * Search for the cluster. If it find the server, it will return the socket address then.
	 * 
	 * @return {boolean}
	 */
	protected boolean findServer() {
		String addrStr = "";
		int startA = 1;
		int startB = 1;
		int endA = 255;
		int endB = 255;
		
		// Specify net mask
		if(this.startPrefixAddress != null && !this.startPrefixAddress.isEmpty()) {
			if(this.startPrefixAddress.indexOf('.') >= 0) {
				String[] values = this.startPrefixAddress.split("\\.");
				if(values.length >= 1) {
					startA = Integer.valueOf(values[0]);
				}
				
				if(values.length == 2) {
					startB = Integer.valueOf(values[1]);
				}
			} else {
				startA = Integer.valueOf(this.startPrefixAddress);
			}
		}

		// Verify range
		if(startA < 0 || startA > 255)
			startA = 1;
		
		if(startB < 0 || startB > 255)
			startB = 1;
		
		if(this.endPrefixAddress != null && !this.endPrefixAddress.isEmpty()) {
			if(this.endPrefixAddress.indexOf('.') >= 0) {
				String[] values = this.endPrefixAddress.split("\\.");
				if(values.length >= 1) {
					endA = Integer.valueOf(values[0]);
				}
				
				if(values.length == 2) {
					endB = Integer.valueOf(values[1]);
				}
			} else {
				endA = Integer.valueOf(this.endPrefixAddress);
			}
		}

		// Verify range
		if(endA < 0 || endA > 255)
			endA = 1;
		
		if(endB < 0 || endB > 255)
			endB = 1;

		final String addressPrefix = "192.168.";
		Log.info("Search for cluster server(beginning at " + addressPrefix + startA + "." + startB + ")...");
		for(int i = startA; i <= endA; ++i) {
			for(int j = startB; j <= endB;  ++j) {
				addrStr = addressPrefix + i + "." + j;
				//Log.info("Search for cluster server at " + addrStr + "...");
				
				this.connect(new InetSocketAddress(addrStr, Cluster.PORT), Client.SEARCH_TIMEOUT);
				if(this.isConnected()) {
					Log.info("Found cluster server at " + addrStr + ".");
					return true;
				}
				
				this.close();
			}
		}
		
		boolean result = false;
		try {
			Log.info("No server found. Start search again...");
			SystemHandle.gc();
			this.wait(Client.CONNECT_DELAY);
		} catch (InterruptedException e) {
			Log.error(e);
		} finally {
			result = this.findServer();
		}
		
		return result;
	}
}
