package com.hypefiend.javagamebook.server;

import com.hypefiend.javagamebook.common.GameEvent;
import com.hypefiend.javagamebook.common.Player;
import com.hypefiend.javagamebook.common.Attachment;
import com.hypefiend.javagamebook.server.controller.GameController;
import java.nio.*;
import java.nio.channels.*;
import java.io.*;
import java.util.*;
import java.net.Socket;
import org.apache.log4j.Logger;

/**
 * SelectAndRead.java
 *
 * obsługuje odczyty z wszystkich klientów korzystając z obiektu Selector
 * i przekazuje zdarzenia do odpowiedniego obiektu GameController
 *
 * @author <a href="mailto:bret@hypefiend.com">bret barker</a>
 * @version 1.0
 */
public class SelectAndRead extends Thread {
    /** obiekt rejestracji log4j */
    private static Logger log = Logger.getLogger("SelectAndRead");

    /** oczekujące połączenia */
    private LinkedList newClients;

    //    /** czas opóźnienia dla wywołania  select() z selektora */
    //    private static final long SELECT_TIMEOUT = 250;

    /** selektor, multipleksowany dostęp dla kanałów klienta */
    private Selector selector;

    /** referencja do obiektu GameServer */
    private GameServer gameServer;

    /**
     * Konstruktor.
     */
    public SelectAndRead (GameServer gameServer){
    this.gameServer = gameServer;
    newClients = new LinkedList();
    }
    
    /** 
     * dodaje do listy oczekujących klientów
     */
    public void addNewClient(SocketChannel clientChannel) {
    synchronized (newClients) {
            newClients.addLast(clientChannel);
        }
        // force selector to return
        // so our new client can get in the loop right away
        selector.wakeup();
    }
    
    /** 
     * loop forever, first doing our select() 
     * then check for new connections
     */
    public void run () {
        try {
            selector = Selector.open();

            while (true) {
            select();
            checkNewConnections();

            // sleep just a bit
            try { Thread.sleep(30); } catch (InterruptedException e) {}
            }
        }
        catch (IOException e) {
            log.fatal("exception while opening Selector", e);
              }
        }
        
        /**
         * sprawdzenie nowych połączeń
         * i ich rejestracja w selektorze
         */
    private void checkNewConnections() {
        synchronized(newClients) {
            while (newClients.size() > 0) {
                try {
                    SocketChannel clientChannel = (SocketChannel)newClients.removeFirst();
                    clientChannel.configureBlocking( false);
                    clientChannel.register( selector, SelectionKey.OP_READ, new Attachment());
                }
                catch (ClosedChannelException cce) {
                    log.error("kanał zamknięty", cce);
                }
                catch (IOException ioe) {
                    log.error("wyjątek ioexception w clientChannel", ioe);
                }
            }
        }
    }

    /** 
     * do our select, read from the channels
     * and hand off events to GameControllers
     */
     private void select() {
        try {
            // to jest blokujące wywołanie select, ale jest
            // ono przerywane po dołączeniu się nowych klientów
            selector.select();
            Set readyKeys = selector.selectedKeys();

            Iterator i = readyKeys.iterator();
            while (i.hasNext()) {
            SelectionKey key = (SelectionKey) i.next();
            i.remove();
            SocketChannel channel = (SocketChannel) key.channel();
            Attachment attachment = (Attachment) key.attachment();

            try {
            // odczyt z kanału
                long nbytes = channel.read(attachment.readBuff);
                // sprawdzenie końca kanału
                if (nbytes == -1) {
                log.info("rozłączenie: " + channel.socket().getInetAddress() + 
                     ", koniec kanału");
                channel.close();
                }

                // kontrola kompletności zdarzenia
                try {
                if (attachment.readBuff.position() >= attachment.HEADER_SIZE) {
                    attachment.readBuff.flip();
                    
                    // odczytanie wszystkich dostępnych zdarzeń z bufora
                    while(attachment.eventReady()) {
                        GameEvent event = getEvent(attachment);
                        delegateEvent(event, channel);
                        attachment.reset();
                    }
                    // przygotowanie do kolejnych odczytów kanału
                    attachment.readBuff.compact();
                }
                }
                catch (IllegalArgumentException e) {
                log.error("wyjątek illegalargument", e);
                }
            }
            catch (IOException ioe) {
                log.warn("wyjątek IOException w read(), zamykanie kanału:" + channel.socket().getInetAddress());
                channel.close();
            }
            }
        }
        catch (IOException ioe2) {
            log.warn("wyjątek IOException during select(): " + ioe2.getMessage());
        }
         catch (Exception e) {
             log.error("wyjątek w select()", e);
        }
    }

    /**
     * odczyt zdarzenia z treści załącznika
     */
    private GameEvent getEvent(Attachment attachment) {
        GameEvent event = null;
        ByteBuffer bb = ByteBuffer.wrap(attachment.payload);

        // pobranie kontrolera i zażądanie utworzenia dla nas zdarzenia
        GameController gc = gameServer.getGameControllerByHash(attachment.gameNameHash);
        if (gc == null) {
            return null;
        }
        event = gc.createGameEvent();
        
        // odczyt zdarzenia z treści
        event.read(bb);
        return event;
    }  

    /**
     * przekazanie zdarzenia do odpowiedniego obiektuGameController
     * na podstawie GameName w zdarzeniu
     */
    private void delegateEvent(GameEvent event, SocketChannel channel) {
        if (event != null && event.getGameName() == null) {
            log.error("GameServer.handleEvent() : pusta wartość gameName ");
            return;
    }

    GameController gc = gameServer.getGameController(event.getGameName());
    if (gc == null) {
        log.error("Brak obiektu GameController dla gameName: " + event.getGameName());
        return;
    }

    Player p = gameServer.getPlayerById(event.getPlayerId());
    if (p != null) {
        if (p.getChannel() != channel) {
        log.warn("gracz jest w nowym kanale, musi być powtórnie podłączony.");
        p.setChannel(channel);
        }
    }
    else {
        // gdy pierwszy raz otrzymamy playerId, tworzymy obiekt Player,
        // wypełniamy kanał i dodajemu do naszych list
        p = gc.createPlayer();
        p.setPlayerId(event.getPlayerId());
        p.setChannel(channel);
        gameServer.addPlayer(p);
        log.debug("delegacja zdarzenia, utworzony nowy gracz i ustawony kanał, gracz:" + 
              p.getPlayerId() + ", kanał: " + channel);
    }    

    gc.handleEvent(event);
    }

}// SelectAndRead
