package network;

import java.io.DataInputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

import settings.Settings;
import network.message.Message;
import network.message.MessageFactory;

public class Peer extends Thread{

	private InetAddress address;
	private ConnectionCallback callback;
	private Socket socket;
	private OutputStream out;
	private Pinger pinger;
	
	private Map<Integer, BlockingQueue<Message>> messages;
	
	public Peer(InetAddress address)
	{
		this.address = address;
		this.messages = Collections.synchronizedMap(new HashMap<Integer, BlockingQueue<Message>>());
	}
	
	public Peer(ConnectionCallback callback, Socket socket)
	{
		try
		{	
			this.callback = callback;
			this.socket = socket;
			this.address = socket.getInetAddress();
			this.messages = Collections.synchronizedMap(new HashMap<Integer, BlockingQueue<Message>>());
			
			//ENABLE KEEPALIVE
			//this.socket.setKeepAlive(true);
			
			//TIMEOUT
			this.socket.setSoTimeout(1000*60*60);
			
			//CREATE STRINGWRITER
			this.out = socket.getOutputStream();
			
			//START COMMUNICATON THREAD
			this.start();
			
			//START PINGER
			this.pinger = new Pinger(this);
			
			//ON SOCKET CONNECT
			this.callback.onConnect(this);			
		}
		catch(Exception e)
		{
			//FAILED TO CONNECT NO NEED TO BLACKLIST
			Logger.getGlobal().info("Failed to connect to : " + address);
		}
	}
	
	public InetAddress getAddress()
	{
		return address;
	}
	
	public long getPing()
	{
		return this.pinger.getPing();
	}
	
	public void connect(ConnectionCallback callback)
	{
		this.callback = callback;
		
		try
		{
			//OPEN SOCKET
			this.socket = new Socket(address, Network.PORT);
			
			//ENABLE KEEPALIVE
			//this.socket.setKeepAlive(true);
			
			//TIMEOUT
			this.socket.setSoTimeout(1000*60*60);
			
			//CREATE STRINGWRITER
			this.out = socket.getOutputStream();
			
			//START COMMUNICATON THREAD
			this.start();
			
			//START PINGER
			this.pinger = new Pinger(this);
			
			//ON SOCKET CONNECT
			this.callback.onConnect(this);			
		}
		catch(Exception e)
		{
			//FAILED TO CONNECT NO NEED TO BLACKLIST
			Logger.getGlobal().info("Failed to connect to : " + address);
		}
	}
	
	public void run()
	{
		try 
		{
			DataInputStream in = new DataInputStream(socket.getInputStream());
			
			while(true)
			{
				//READ FIRST 4 BYTES
				byte[] messageMagic = new byte[Message.MAGIC_LENGTH];
				in.readFully(messageMagic);
				
				if(Arrays.equals(messageMagic, Message.MAGIC))
				{
					//PROCESS NEW MESSAGE
					Message message = MessageFactory.getInstance().parse(this, in);
					
					//Logger.getGlobal().info("received message " + message.getType() + " from " + this.address.toString());
					
					//CHECK IF WE ARE WAITING FOR A MESSAGE WITH THAT ID
					if(message.hasId() && this.messages.containsKey(message.getId()))
					{
						//ADD TO OUR OWN LIST
						this.messages.get(message.getId()).add(message);
					}
					else
					{
						//CALLBACK
						this.callback.onMessage(message);
					}
				}
				else
				{
					Logger.getGlobal().warning("received message with wrong magic");
					
					//ERROR
					callback.onError(this);
					return;
				}
			}
		} 
		catch (Exception e) 
		{
			//e.printStackTrace();
			
			//DISCONNECT
			callback.onDisconnect(this);
			return;
		}
	}
	
	public boolean sendMessage(Message message)
	{
		try 
		{
			//CHECK IF SOCKET IS STILL ALIVE
			if(!this.socket.isConnected())
			{
				//ERROR
				callback.onError(this);
				
				return false;
			}
			
			//SEND MESSAGE
			synchronized(this.out)
			{
				this.out.write(message.toBytes());
				this.out.flush();
			}
			
			//RETURN
			return true;
		}
		catch (Exception e) 
		{
			//ERROR
			callback.onError(this);
			
			//RETURN
			return false;
		}
	}
	
	public Message getResponse(Message message)
	{
		//GENERATE ID
		int id = (int) ((Math.random() * 1000000) + 1);
		
		//SET ID
		message.setId(id);
		
		//PUT QUEUE INTO MAP SO WE KNOW WE ARE WAITING FOR A RESPONSE
		BlockingQueue<Message> blockingQueue = new ArrayBlockingQueue<Message>(1);
		this.messages.put(id, blockingQueue);
		
		//WHEN FAILED TO SEND MESSAGE
		if(!this.sendMessage(message))
		{
			return null;
		}
		
		try 
		{
			Message response = blockingQueue.poll(Settings.getInstance().getConnectionTimeout(), TimeUnit.MILLISECONDS);
			this.messages.remove(id);
			
			return response;
		} 
		catch (InterruptedException e)
		{
			//NO MESSAGE RECEIVED WITHIN TIME;
			return null;
		}
	}
	
	public void onPingFail()
	{
		//DISCONNECTED
		this.callback.onDisconnect(this);
	}

	public void close() 
	{
		try
		{
			//STOP PINGER
			if(this.pinger != null)
			{
				this.pinger.stopPing();
			}
			
			//CHECK IS SOCKET EXISTS
			if(socket != null)
			{
				//CHECK IF SOCKET IS CONNECTED
				if(socket.isConnected())
				{
					//CLOSE SOCKET
					socket.close();
				}
			}
		}
		catch(Exception e)
		{
			
		}		
	}
}
