/*
Copyright (C) 2005 David Green <green@couchpotato.net>
All Rights Reserved.

This file is part of Aelfengard.

Aelfengard is proprietary software. You may not redistribute it without
prior written permission from the copyright holder.
*/

package server;

import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;

import javax.crypto.Cipher;

import server.token.ResolvedTokenString;

import common.ByteSource;


/**
 * Represents all state for a connected client.
 */
class Client implements Readable, Writable {
    
    private final SelectionKey key;
    private final SocketChannel channel;

    private final Cipher encipher;
    private final Cipher decipher;
    
    // We'll reuse this little buffer for length indicators
    private final ByteBuffer lengthIndicatorBuffer = ByteBuffer.allocate(4);
    private LinkedList<ByteSource> writeQueue = new LinkedList<ByteSource>();
    private ByteBuffer[] writeBuffers = new ByteBuffer[] {};
    private int writeOffset;
    private ByteBuffer readBuffer = lengthIndicatorBuffer;
    
    private ResolvedTokenString lastRTS = null;

    private boolean closed = false;
    
    public static Client currentClient;
    private ClientState clientState;

    public Client(SelectionKey key, ClientState clientState, Cipher encipher, Cipher decipher) {
        this.key = key;
        this.channel = (SocketChannel) key.channel();
        this.clientState = clientState;
        this.encipher = encipher;
        this.decipher = decipher;
        key.interestOps(SelectionKey.OP_READ);
    }

    public void close() {
        if (closed) return;
        closed = true;
        key.cancel();
        try { key.channel().close(); } catch (IOException ex) { /* ignore */ }
        clientState.close(false);
    }

    public void doRead() {
        try {
            if (channel.read(readBuffer) < 0) { // EOF
                throw new EOFException();
            }
            while (!readBuffer.hasRemaining()) {
                if (readBuffer == lengthIndicatorBuffer) {
                    int length = readBuffer.getInt(0);
                    Utils.checkInputLength(length);
                    readBuffer = ByteBuffer.allocate(length);
                }
                else {
                    byte[] b = readBuffer.array();
                    b = decipher.doFinal(b);
                    clientState.process(b);
                    // prepare for reading next event from client
                    readBuffer = lengthIndicatorBuffer;
                    readBuffer.clear();
                }
            }
        }
        catch (Exception ex) {
            ex.printStackTrace();
            close();
        }
    }


    public void doWrite() {
        try {
            if (writeOffset >= writeBuffers.length) {
                writeBuffers = new ByteBuffer[writeQueue.size() * 2];
                int ptr = 0;
                for (ByteSource bs : writeQueue) {
                    if (bs == lastRTS) {
                        lastRTS = null; // can't combine with it anymore
                    }
                    byte[] b = bs.toByteArray();
                    b = encipher.doFinal(b);
                    writeBuffers[ptr] = ByteBuffer.allocate(4);
                    writeBuffers[ptr++].putInt(0, b.length);
                    writeBuffers[ptr++] = ByteBuffer.wrap(b);
                }
                writeOffset = 0;
                writeQueue = new LinkedList<ByteSource>();
                if (writeBuffers.length == 0) {
                    key.interestOps(SelectionKey.OP_READ);
                    return; // done writing
                }
            }
            channel.write(writeBuffers, writeOffset, writeBuffers.length - writeOffset);
            int idx;
            for (idx = writeOffset; idx < writeBuffers.length; idx++) {
                if (writeBuffers[idx].hasRemaining()) {
                    break;
                }
                writeBuffers[idx] = null;
            }
            writeOffset = idx;
        }
        catch (Exception ex) {
            ex.printStackTrace();
            close();
        }
    }
    
    void send(ByteSource bs) {
        if (bs instanceof ResolvedTokenString) {
            ResolvedTokenString rst = (ResolvedTokenString) bs;
            if (lastRTS != null && lastRTS.combineWith(rst)) {
                return;
            }
            lastRTS = rst;
        }
        writeQueue.add(bs);
        key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
    }
    
}