import java.net.*;
import java.util.*;
import java.io.*;
import javax.swing.Timer;
import java.awt.event.*;
import java.util.concurrent.*;

/**
 * Jde o IRC server. Po spusteni vlakna zacne server poslouchat na urcitem portu. Kdyz se k nemu pripoji nejaky klient,
 * prijme ho a priradi mu operatora. Operator klienta obsluhuje.
 * 
 * @author Marek Dvoroznak
 * @version 0.1
 */
public class ServerThread extends Thread
{
    // port, na kterem server posloucha
    private int port;
    // soket
    private ServerSocket ss;
    // indikuje, zda server bezi
    private boolean running = false;
    // pinger
    private Timer pinger;
    // seznam vsech klientu, kteri jsou pripojeni k serveru
    private List<Client> clients = new ArrayList();
    // mapa vsech zalozenych kanalu na serveru
    private Map<String, Channel> channels = new ConcurrentHashMap();

    /**
     * Zkonstruuje objekt typu ServerThread.
     * 
     * @param port port, na kterem server posloucha
     */
    public ServerThread(int port)
    {
        this.port = port;
    }
    
    /**
     * Spusti pinger a poslouchani na urcitem portu.
     */
    public void run()
    {
        running = true;
        startPinger();
        listen();
    }
    
    /**
     * Spusti pinger.
     * Pinger - kazdych PING_TIME sekund kontroluje aktivitu klientu tim,
     *          ze jim posila pozadavek o odezvu (ping). V pripade, ze klient neodpovi
     *          do PING_TIMEOUT sekund, server klienta odpoji.
     */
    private void startPinger()
    {
        pinger = new Timer(Settings.PING_TIME, new
            ActionListener()
            {
                public void actionPerformed(ActionEvent event)
                {
                    synchronized (clients) {
                        int inactivity;
                        for (Client client : clients) {
                            inactivity = (int)System.currentTimeMillis() - client.getInactivity();
                            
                            if (client.getRegistered()) {
                                // registrovany klient
                                if (inactivity >= Settings.PING_TIMEOUT) {
                                    // odpojeni klienta
                                    System.out.println("Disconnecting client " + client);
                                    client.setQuitText("Ping timeout: " + (int)(inactivity/1000) + " seconds");
                                    client.sendClosingLinkText();
                                    client.closeSocket();
                                } else
                                if (inactivity >= Settings.MAX_INACTIVITY_BEFORE_PING) {
                                    System.out.println("Sending ping to client " + client);
                                    client.sendPing();
                                }
                            } else {
                                // neregistrovany klient
                                if (inactivity >= Settings.PING_TIMEOUT) {
                                    // odpojeni klienta
                                    System.out.println("Disconnecting client " + client);
                                    client.setQuitText("Connection timed out");
                                    client.sendClosingLinkText();
                                    client.closeSocket();
                                }
                            }
//                             System.out.println(client.getInactivity());
                        }
                    }
                }
            });
        pinger.start();
        System.out.println("Pinger started.");
    }
    
    /**
     * Zastavi pinger.
     */
    public void stopPinger()
    {
        pinger.stop();
        System.out.println("Pinger stopped.");
    }
    
    /**
     * Posloucha na portu <port>, akceptuje klienty a prirazuje klientum operatory.
     */
    private void listen()
    {
        try {
            ss = new ServerSocket(port);

            System.out.println("Server is listening on " + ss);
      
            while (true) {
                // akceptuje klienta
                Socket s = ss.accept();
                System.out.println("Accepting connection from " + s);
            
                Client client = new Client(this, s);
            
                new OperatorThread(this, s, client);

                addClient(client);
            }
        } catch (SocketException e) {
            if (e.getMessage().equals("Socket closed")) {
                // soket byl cilene uzavren kvuli zastaveni serveru
            } else {
                e.printStackTrace();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // ukoncime vsechny klienty
            synchronized (getClients()) {
                for (Client client : getClients()) {
                    client.setQuitText("Server stop");
                    client.sendClosingLinkText();
                    client.closeSocket();
                }
            }
            try {
                ss.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println("Server stopped.");
        }
    }
      
    /**
     * Prida klienta do seznamu klientu pripojenych k serveru.
     * 
     * @param client klient
     */
    public void addClient(Client client)
    {
        getClients().add(client);
    }

    /**
     * Odebere klienta ze seznamu klientu pripojenych k serveru.
     * 
     * @param client klient
     */
    public void removeClient(Client client) {
        getClients().remove(client);
    }

    /**
     * Odebere klienta ze seznamu klientu pripojenych k serveru.
     * 
     * @param nickname prezdivka uzivatele, ktery pouziva klienta
     */
    public void removeClient(String nickname) {
        removeClient(getClient(nickname));
    }
 
    /**
     * Vrati klienta ze seznamu klientu pripojenych k serveru.
     * 
     * @param nickname prezdivka uzivatele, ktery pouziva klienta
     * @return klient
     */
    public Client getClient(String nickname)
    {
        for (Client client : getClients()) {
            if (client.getUser().getNickname().equals(nickname)) return client;
        }
        return null;
    }

    /**
     * Vytvori na serveru novy kanal.
     * 
     * @param channel nazev kanalu
     */
    public Channel createChannel(String channel)
    {
        Channel myChannel = new Channel(channel);
        channels.put(channel, myChannel);
        return myChannel;
    }
    
    /**
     * Smaze kanal na serveru.
     * 
     * @param channel nazev kanalu
     */
    public void removeChannel(String channel)
    {
        channels.remove(channel);
    }
    
    /**
     * Vrati kanal na serveru.
     * 
     * @param channel nazev kanalu
     * @return kanal
     */
    public Channel getChannel(String channel)
    {
        return channels.get(channel);
    }
    
    /**
     * Smaze kanal, kdyz je prazdny.
     * 
     * @param channel nazev kanalu
     */
    public void checkChannel(String channel)
    {
        Channel myChannel = getChannel(channel);
        if (myChannel != null)
        if (myChannel.isEmpty()) {
            System.out.println("Removing empty channel " + channel);
            removeChannel(channel);
        }
    }
    
    /**
     * Zjisti, jestli je prezdivka uz nekym pouzivana.
     * 
     * @param nickname prezdivka
     * @return true - je pouzivana, false - neni pouzivana
     */
    public boolean isNicknameInUse(String nickname)
    {
        return clientExists(nickname);
    }
    
    /**
     * Posle zpravu.
     * 
     * @param msg zprava
     * @param except mnozina klientu, kterym se zprava neposle
     */
    public void sendMessageTo(Message msg, Set<Client> except)
    {
        Subject to = msg.getTo();
        if (to instanceof Channel) {
            // zprava pro kanal
            Channel channel = (Channel)to;
            for (Client client : channel.getClients()) {
                if ((except != null && !except.contains(client)) || except == null) client.sendMessage(msg);
            }
        } else
        if (to instanceof User) {
            // zprava pro uzivatele
            Client client = getClient(to.getName());
            if ((except != null && !except.contains(client)) || except == null) client.sendMessage(msg);
        }
    }
    
    /**
     * Posle zpravu.
     * 
     * @param msg zprava
     */
    public void sendMessageTo(Message msg)
    {
        sendMessageTo(msg, null);
    }
    
    /**
     * Zjisti, jestli uz kanal existuje.
     * 
     * @param channel nazev kanalu
     * @return true - kanal existuje, false - kanal neexistuje
     */
    public boolean channelExists(String channel)
    {
        return channels.containsKey(channel);
    }
    
    /**
     * Zjisti, jestli existuje klient s urcitou prezdivkou.
     * 
     * @param nickname prezdivka
     * @return true - klient existuje, false - klient neexistuje
     */
    public boolean clientExists(String nickname) {
        return getClient(nickname) == null ? false : true;
    }
    
    /**
     * Zastavi server.
     */
    public void stopIt()
    {
        try {
            ss.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        stopPinger();
        running = false;
    }
    
    /**
     * Zjisti, jestli server bezi.
     * 
     * @return true - bezi, false - nebezi
     */
    public boolean isRunning()
    {
        return running;
    }
    
    /**
     * Vrati seznam klientu pripojenych k serveru.
     * 
     * @return seznam klientu
     */
    private List<Client> getClients()
    {
        synchronized (clients) {
            return clients;
        }
    }
    
}
