package network;

import java.net.InetAddress;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.logging.Logger;

import utils.ObserverMessage;
import controller.Controller;
import network.message.Message;
import network.message.MessageFactory;

public class Network extends Observable implements ConnectionCallback {

	public static final int PORT = 9084;
	//public static final int PORT = 4809;
	
	private static final int MAX_HANDLED_MESSAGES_SIZE = 10000;
	
	private ConnectionCreator creator;
	private ConnectionAcceptor acceptor;
	
	private List<Peer> connectedPeers;
	
	private SortedSet<String> handledMessages;
	
	private boolean run;
	
	public Network()
	{	
		this.connectedPeers = new ArrayList<Peer>();
		this.run = true;
		
		this.start();
	}
	
	private void start()
	{
		this.handledMessages = Collections.synchronizedSortedSet(new TreeSet<String>());
		
		//START ConnectionCreator THREAD
		creator = new ConnectionCreator(this);
		creator.start();
		
		//START ConnectionAcceptor THREAD
		acceptor = new ConnectionAcceptor(this);
		acceptor.start();
	}

	@Override
	public void onConnect(Peer peer) {
		
		Logger.getGlobal().info("Connection successfull : " + peer.getAddress());
		
		//ADD TO CONNECTED PEERS
		synchronized(this.connectedPeers)
		{
			this.connectedPeers.add(peer);
		}
		
		//ADD TO WHITELIST
		PeerManager.getInstance().addPeer(peer);
		
		//PASS TO CONTROLLER
		Controller.getInstance().onConnect(peer);
		
		//NOTIFY OBSERVERS
		this.setChanged();
		this.notifyObservers(new ObserverMessage(ObserverMessage.ADD_PEER_TYPE, peer));		
		
		this.setChanged();
		this.notifyObservers(new ObserverMessage(ObserverMessage.LIST_PEER_TYPE, this.connectedPeers));		
	}

	@Override
	public void onDisconnect(Peer peer) {

		Logger.getGlobal().info("Connection close : " + peer.getAddress());
		
		//REMOVE FROM CONNECTED PEERS
		synchronized(this.connectedPeers)
		{
			this.connectedPeers.remove(peer);
		}
		
		//PASS TO CONTROLLER
		Controller.getInstance().onDisconnect(peer);
		
		//CLOSE CONNECTION IF STILL ACTIVE
		peer.close();
		peer.interrupt();
		
		//NOTIFY OBSERVERS
		this.setChanged();
		this.notifyObservers(new ObserverMessage(ObserverMessage.REMOVE_PEER_TYPE, peer));		
		
		this.setChanged();
		this.notifyObservers(new ObserverMessage(ObserverMessage.LIST_PEER_TYPE, this.connectedPeers));		
	}
	
	@Override
	public void onError(Peer peer) {
		
		Logger.getGlobal().warning("Connection error : " + peer.getAddress());
		
		//REMOVE FROM CONNECTED PEERS
		synchronized(this.connectedPeers)
		{
			this.connectedPeers.remove(peer);
		}
		
		//ADD TO BLACKLIST
		PeerManager.getInstance().blacklistPeer(peer);
		
		//PASS TO CONTROLLER
		Controller.getInstance().onError(peer);
		
		//CLOSE CONNECTION IF STILL ACTIVE
		peer.close();
		peer.interrupt();
					
		//NOTIFY OBSERVERS
		this.setChanged();
		this.notifyObservers(new ObserverMessage(ObserverMessage.REMOVE_PEER_TYPE, peer));		
		
		this.setChanged();
		this.notifyObservers(new ObserverMessage(ObserverMessage.LIST_PEER_TYPE, this.connectedPeers));		
	}
	
	@Override
	public boolean isConnectedTo(InetAddress address) {
		
		try
		{
			synchronized(this.connectedPeers)
			{
				//FOR ALL connectedPeers
				for(Peer connectedPeer: connectedPeers)
				{
					//CHECK IF ADDRESS IS THE SAME
					if(address.equals(connectedPeer.getAddress()))
					{
						return true;
					}
				}
			}
		}
		catch(Exception e)
		{
			//CONCURRENCY ERROR
		}
		
		return false;
	}
	
	@Override
	public boolean isConnectedTo(Peer peer) {
		
		return this.isConnectedTo(peer.getAddress());
	}
	
	@Override
	public List<Peer> getActiveConnections() {
		
		return this.connectedPeers;
	}
	
	private void addHandledMessage(byte[] hash)
	{
		try
		{
			synchronized(this.handledMessages)
			{
				//CHECK IF LIST IS FULL
				if(this.handledMessages.size() > MAX_HANDLED_MESSAGES_SIZE)
				{
					this.handledMessages.remove(this.handledMessages.first());
				}
				
				this.handledMessages.add(new String(hash));
			}
		}
		catch(Exception e)
		{
			e.printStackTrace();
		}
	}

	@Override
	public void onMessage(Message message) {
	
		//CHECK IF WE ARE STILL PROCESSING MESSAGES
		if(!this.run)
		{
			return;
		}
		
		//ONLY HANDLE BLOCK AND TRANSACTION MESSAGES ONCE
		if(message.getType() == Message.TRANSACTION_TYPE || message.getType() == Message.BLOCK_TYPE)
		{
			synchronized(this.handledMessages)
			{
				//CHECK IF NOT HANDLED ALREADY
				if(this.handledMessages.contains(new String(message.getHash())))
				{
					return;
				}
				
				//ADD TO HANDLED MESSAGES
				this.addHandledMessage(message.getHash());
			}
		}		
		
		switch(message.getType())
		{
		//PING
		case Message.PING_TYPE:
			
			//CREATE PING
			Message response = MessageFactory.getInstance().createPingMessage();
			
			//SET ID
			response.setId(message.getId());
			
			//SEND BACK TO SENDER
			message.getSender().sendMessage(response);
			
			break;
		
		//GETPEERS
		case Message.GET_PEERS_TYPE: 
			
			//CREATE NEW PEERS MESSAGE WITH PEERS
			Message answer = MessageFactory.getInstance().createPeersMessage(PeerManager.getInstance().getKnownPeers());
			answer.setId(message.getId());
			
			//SEND TO SENDER
			message.getSender().sendMessage(answer);
			break;
			
		//SEND TO CONTROLLER
		default:
			
			Controller.getInstance().onMessage(message);
			break;
		}		
	}

	public void broadcast(Message message, List<Peer> exclude) 
	{		
		Logger.getGlobal().info("Broadcasting");
		
		try
		{
			for(int i=0; i < this.connectedPeers.size() ; i++)
			{
				Peer peer = this.connectedPeers.get(i);
				
				//EXCLUDE PEERS
				if(peer != null && !exclude.contains(peer))
				{
					peer.sendMessage(message);
				}
			}	
		}
		catch(Exception e)
		{
			//error broadcasting
			e.printStackTrace();
		}
		
		Logger.getGlobal().info("Broadcasting end");
	}
	
	@Override
	public void addObserver(Observer o)
	{
		super.addObserver(o);
		
		//SEND CONNECTEDPEERS ON REGISTER
		o.update(this, new ObserverMessage(ObserverMessage.LIST_PEER_TYPE, this.connectedPeers));
	}
	
	public static boolean isPortAvailable(int port)
	{
		try 
		{
		    ServerSocket socket = new ServerSocket(port);
		    socket.close();
		    return true;
		} 
		catch (Exception e)
		{
		    return false;
		}
	}

	public void stop() 
	{
		this.run = false;
		this.onMessage(null);
	}
}