import java.io.*;
import java.net.*;
import java.util.*;

/**
 * Tato trida umoznuje komunikaci s klientem.
 * 
 * @author Marek Dvoroznak
 * @version 0.1
 */
public class Client
{
    // informace o uzivateli, ktery pouziva klienta
    private User user = new User();
    // vystup ke klientovi
    private PrintStream out;
    // server
    private static ServerThread server;
    // soket, pres ktery je klient pripojen k serveru
    private Socket socket;
    // kanaly, ke kterym je klient pripojen
    private List<Channel> joinedChannels = new ArrayList();
    // neaktivita klienta
    private int inactivity = (int)System.currentTimeMillis();
    // indikuje, zda je klient zaregistrovan
    private boolean registered = false;
    // vychozi zprava o ukonceni klienta
    private String quitText = "Client Quit";
    
    /**
     * Zkonstruuje objekt typu Client.
     * 
     * @param server server, na ktery je klient pripojen
     * @param socket soket, pres ktery je klient pripojen
     */
    public Client(ServerThread server, Socket socket) throws IOException
    {
        setOutputStream(new PrintStream(socket.getOutputStream()));
        this.server = server;
        user.setHost(socket.getInetAddress().getHostAddress());
        this.socket = socket;
    }
    
    /**
     * Nastavi informace o uzivateli.
     * 
     * @param user informace o uzivateli
     */
    public void setUser(User user)
    {
        this.user = user;
    }
    
    /**
     * Nastavi vystupni tok ke klientovi.
     * 
     * @param out vystupni tok
     */
    public void setOutputStream(PrintStream out)
    {
        this.out = out;
    }
    
    /**
     * Vrati informace o uzivateli.
     * 
     * @return informace o uzivateli
     */
    public User getUser()
    {
        return user;
    }
    
    /**
     * Vrati vystupni tok ke klientovi.
     * 
     * @return vystupni tok ke klientovi
     */
    public PrintStream getOutputStream()
    {
        return out;
    }
    
    /**
     * Nastavi, ze je klient registrovan.
     */
    public void setRegistered()
    {
        registered = true;
    }
    
    /**
     * Vrati, jestli je klient zaregistrovan.
     * 
     * @return true - je zaregistrovan, false - neni zaregistrovan
     */
    public boolean getRegistered()
    {
        return registered;
    }
    
    /**
     * Posle klientovi text.
     * 
     * @param text text, ktery se posle klientovi
     */
    public void sendText(String text)
    {
        out.println(text);
    }
    
    /**
     * Posle klientovi zpravu.
     * 
     * @param msg zprava, ktera se posle klientovi
     */
    public void sendMessage(Message msg)
    {
        sendText(msg.getFormatedBody());
    }
    
    /**
     * Pripoji klienta na kanal.
     * 
     * @param channel kanal, na ktery se klient pripoji
     * @throws AlreadyInChannelException v pripade, ze klient uz je na kanalu
     */
    public void joinChannel(String channel) throws AlreadyInChannelException
    {
        Channel myChannel = null;
        String nickname = user.getNickname();
        
        // zjisti, zda takovy kanal uz existuje
        if (server.channelExists(channel)) {
            // existuje
            myChannel = server.getChannel(channel);

            // zjisti, jestli je uz uzivatel uvnitr kanalu
            if (!myChannel.isIn(this)) {
                // neni, probehne pripojeni uzivatele na kanal
                System.out.println(nickname + ": Joining existing channel " + channel);
            } else {
                // je
                System.out.println(nickname + ": User is already in channel " + channel);
                throw new AlreadyInChannelException("User is already in channel");
            }
        } else {
            // neexistuje, vytvori se novy kanal
            System.out.println(nickname + ": Creating new channel " + channel);
            myChannel = server.createChannel(channel);
        }
        
        // pripojeni uzivatele na kanal
        myChannel.addClient(this);
        // informujeme vsechny na kanale, ze se uzivatel pripojil
        server.sendMessageTo(new CommandMessage(user, myChannel, "JOIN", ":" + channel));
        // pridani kanalu do seznamu pripojenych kanalu
        joinedChannels.add(myChannel);
    }
    
    /**
     * Odpoji klienta z kanalu.
     * 
     * @param channel kanal, z ktereho se klient odpoji
     */
    public void partChannel(String channel)
    {
        String nickname = user.getNickname();

        // zjisti, zda takovy kanal uz existuje
        if (server.channelExists(channel)) {
            // existuje
            Channel myChannel = server.getChannel(channel);
            // zjisti, jestli je uzivatel uvnitr kanalu
            if (!myChannel.isIn(this)) {
                // neni
                System.out.println(nickname + ": User is not on channel " + channel);
                sendMessage(new ServerMessage(user, channel + " :You're not on that channel", Settings.ERR_NOTONCHANNEL));
            } else {
                // je, probehne odpojeni uzivatele z kanalu
                System.out.println(nickname + ": User is leaving channel " + channel);

                // informujeme vsechny na kanale, ze se uzivatel odpojil
                server.sendMessageTo(new CommandMessage(user, myChannel, "PART", ":" + channel));
                
                // odebrani uzivatele z kanalu
                myChannel.removeClient(this);
                
                // odebrani kanalu ze seznamu pripojenych kanalu
                joinedChannels.remove(myChannel);

                // zkontroluje kanal
                server.checkChannel(channel);
            }
        } else {
            // neexistuje
            System.out.println(nickname + ": No such channel " + channel);
            sendMessage(new ServerMessage(user, channel + " :No such channel", Settings.ERR_NOSUCHCHANNEL));
        }
    }
    
    /**
     * Vrati seznam kanalu, na ktere je klient pripojen.
     * 
     * @return seznam kanalu, na ktere je klient pripojen
     */
    public List<Channel> getJoinedChannels()
    {
        return joinedChannels;
    }
    
    /**
     * Odpoji klienta ze vsech kanalu, na ktere je pripojen.
     */
    public void partAllJoinedChannels()
    {
        for (Channel channel : joinedChannels) {
            partChannel(channel.getName());
        }
        joinedChannels.clear();
    }
    
    /**
     * Odpoji klienta ze serveru.
     */
    public void quit()
    {
        // v pripade, ze je klient zaregistrovany, oznamime odpojeni klienta vsem, kteri o tom maji vedet
        if (getRegistered()) {
            Set<Client> except = new HashSet();
            except.add(this);
            for (Channel myChannel : getJoinedChannels()) {
                server.sendMessageTo(new CommandMessage(user, myChannel, "QUIT", ":" + quitText), except);
                except.addAll(myChannel.getClients());
    
                myChannel.removeClient(this);
    
                // v pripade, ze je kanal prazdny, smaze ho
                server.checkChannel(myChannel.getName());
                
            }
        }
        // zpravu posleme jeste sobe
        sendClosingLinkText();
    }
    
    /**
     * Posle klientovi zpravu o tom, ze se odpojil.
     */
    public void sendClosingLinkText()
    {
        sendText("ERROR :Closing Link: " + getUser().getHost() + " (" + quitText + ")");
    }
    
    /**
     * Vrati klientovu neaktivitu.
     * 
     * @return klientova neaktivita (v milisekundach)
     */
    public int getInactivity()
    {
        return inactivity;
    }
    
    /**
     * Nastavi klientovu aktivitu.
     */
    public void setNoInactivity()
    {
        inactivity = (int)System.currentTimeMillis();
    }
    
    /**
     * Posle klientovi pozadavek o odezvu.
     */
    public void sendPing()
    {
        sendText("PING :" + Settings.SERVER_NAME);
    }
    
    /**
     * Zavre soket, pres ktery je klient pripojen k serveru.
     */
    public void closeSocket()
    {
        try {
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    /**
     * Nastavi zpravu o ukonceni klienta.
     * 
     * @param quitText zprava o ukonceni klienta
     */
    public void setQuitText(String quitText)
    {
        this.quitText = quitText;
    }
    
    /**
     * Vrati zpravu o ukonceni klienta.
     * 
     * @return zprava o ukonceni klienta
     */
    public String getQuitText()
    {
        return quitText;
    }
    
    /**
     * Vrati retezcovou reprezentaci objektu Client.
     * 
     * @return retezcova reprezentace objektu
     */
    public String toString()
    {
        return getClass().getName() + "[" + socket + "," + getUser() + "]";
    }
}

