package com.hypefiend.javagamebook.client;

import com.hypefiend.javagamebook.common.*;

import java.nio.*;
import java.nio.channels.*;
import java.util.*;
import java.net.*;
import java.io.*;

import org.apache.log4j.*;

/**
 * GameClient.java
 *
 *
 * @author <a href="mailto:bret@hypefiend.com">bret barker</a>
 * @version 1.0
 */
public abstract class GameClient extends Thread{
    /** obiekt rejestracji log4j */
    protected static Logger log = Logger.getLogger("GameClient");

    /** adres of serwera */
    protected InetAddress serverAddress;
    /** połączenie z  serwerem */
    protected SocketChannel channel;

    /** kolejka na przychodzące zdarzenia */
    protected EventQueue inQueue;
    /** kolejka na zdarzenia wychodzące */
    protected EventQueue outQueue;
    
    /** referencja do NIOEventReader, która odczytuje zdarzenia z serwera */
    protected NIOEventReader netReader;

    /** bufor na na zdarzenia wychodzące*/
    protected ByteBuffer writeBuffer;

    /** identyfikator naszego gracza */
    protected String playerId;
    /** identyfikator bieżącego przeciwnika*/
    protected String opponentId;

    /** czy obecnie gramy? */
    protected boolean inGame = false;

    /** nadal działa? */
    protected boolean running = true;

    /** 
     * inicjalizacja
     */
    public void init(String args[]) {    
        inQueue = new EventQueue("GameClient-in");
        outQueue = new EventQueue("GameClient-out");
        writeBuffer = ByteBuffer.allocate(Globals.MAX_EVENT_SIZE );

        try {
            serverAddress = InetAddress.getByName(args[0]);
        }
        catch (UnknownHostException uhe) {
            log.error("nieznany host: " + args[0]);
            System.exit(1);
        }
        this.playerId = args[1];

        // podłączenie do serwera
        if (!connect()) 
            System.exit(1);
        
        // uruchomienie czytania z sieci
        netReader = new NIOEventReader(this, channel, inQueue);
        netReader.start();
    
    }

    public void run() {
        // logowanie
        login();

        // oczeliwanie na LOGIN_ACK
        threadSleep(200L);

        // główna pętla
        while(running) {
            processIncomingEvents();
            writeOutgoingEvents();

            // chwila przerwy, pętla nie musi działać zbyt szybko
            // gdy nic się nie dzieje
            threadSleep(50);
        }
    }


    /** 
     * klasy pochodne musza implementować metodę zwracającą nazwę gry
     */
    public abstract String getGameName();

    /**
     * klasy pochodne muszą implementować tą metodę factory
     */
    public abstract GameEvent createGameEvent();

    /**
     * oraz tą, która tworzy zdarzenie logowania
     */
    public abstract GameEvent createLoginEvent();
    
    /**
     * oraz zdarzenie rozłączenia
     */
    public abstract GameEvent createDisconnectEvent(String reason);
    
    /**
     * obsługa przychodzących GameEvents z EventQueue
     */
    protected abstract void processIncomingEvents();

    /** 
     * zapis wszystkich zdarzeń oczekujących w kolejce wychodzącej
     */
    private void writeOutgoingEvents() {
        GameEvent outEvent;
        while (outQueue.size() > 0) {
            try {
            outEvent = outQueue.deQueue();
            writeEvent(outEvent);
            }
            catch (InterruptedException ie) {}
        }    
    }
    
    /**
     * podłączenie do serwera
     */
    protected boolean connect() {
        log.info("connect()");
        try {
            // otwarcie kanału gniazda
            channel = SocketChannel.open(new InetSocketAddress(serverAddress, Globals.PORT));
            channel.configureBlocking(false);

             // nie korzystamy z algorytmu Nagla
            channel.socket().setTcpNoDelay(true);
            return true;
        }
        catch (ConnectException ce) {
            log.error("Wyjątek przy connect: " + ce.getMessage());
            return false;
        }
        catch (Exception e) {
            log.error("Wyjątek przy podłączaniu", e);
            return false;
        }
    }

    /**
     * wysłanie zdarzenia logowania
     */
    protected void login() {
        GameEvent e = createLoginEvent();
        e.setGameName(getGameName());
        e.setPlayerId(playerId);
        writeEvent(e);
    }

    /**
     * wyłączenie klienta
     * zatrzymanie czytania i zamknięcie kanału
     */
    protected void shutdown() {
        running = false;
        netReader.shutdown();
        //    consoleReader.shutdown();
        try {
            channel.close();
        }
        catch (IOException ioe) {
            log.error("wyjątek przy zamykaniu kanału", ioe);
        }
    }

    /** 
     * wysyłanie zdarzenia do serwera
     */
    protected void writeEvent(GameEvent ge) {
        // ustawienie nazwy gry i identyfikatora gracza
        ge.setGameName(getGameName());
        ge.setPlayerId(playerId);

        NIOUtils.prepBuffer(ge, writeBuffer);
        NIOUtils.channelWrite(channel, writeBuffer);
    }

    /** 
     * metoda użytkowa wywołująca Thread.sleep()
     */
    private void threadSleep(long time) {
        try { 
            Thread.sleep(time); 
        } 
        catch(InterruptedException e) {}
    }

    public void stdOut(String str) {
        if ((str != null) && !str.equals(""))
            System.out.println("\n" + str);
        if (inGame)
            System.out.print( playerId + " kontra " + opponentId + " > " );
        else
            System.out.print( playerId + " > " );
    }
    
    public void stdErr(String str) {
        System.err.println("\n" + str);
        }
        
        public void setOpponent(String opp) {
        opponentId = opp;
        }
}    

