import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;

public class TftpServer implements Runnable {
    private DatagramSocket connection;
    private String filename;
    private ByteBuffer fileMap;
    private DatagramPacket packIn;
    private DatagramPacket packOut;
    private int estimateBlocks = 0;
    private double estimateRtt = 1000.0;
    private double estimateDev = 0.0;
    
    public TftpServer(DatagramSocket connection, InetAddress clientAddr, int clientPort,
            String filename, ByteBuffer fileMap) {
        this.connection = connection;
        this.filename = filename;
        this.fileMap = fileMap;
        
        packIn = new DatagramPacket(new byte[600], 600, clientAddr, clientPort);
        packOut = new DatagramPacket(new byte[600], 600, clientAddr, clientPort);
    }
    
    public void run() {
        try {
            log("received request for '%s'", filename);
            
            // repeatedly send a data packet and handle a received packet
            boolean isAlive = true;
            int blockNumber = 1;
            int numBlocks = (fileMap.limit() / 512) + 1;
            int sendTimeouts = 0;
            boolean suppressNextSend = false;
            long sendTime = System.nanoTime();
            while (isAlive && blockNumber <= numBlocks && sendTimeouts < 10) {
                if (suppressNextSend) {
                    suppressNextSend = false;
                } else {
                    log("send block %d of %d", blockNumber, numBlocks);
                    TftpPacket.packData(packOut, blockNumber, fileMap);
                    try {
                        connection.send(packOut);
                    } catch (IOException e) {
                        log("I/O error when sending: %s", e.toString());
                        break;
                    }
                }
                
                try {
                    connection.receive(packIn);
                } catch (SocketTimeoutException e) {
                    log("block %d response timed out", blockNumber);
                    sendTimeouts++;
                    continue; // try to re-send packet
                } catch (IOException e) {
                    log("I/O error when receiving packet");
                    return;
                }
                                    
                // interpret response received
                switch (TftpPacket.unpackCode(packIn)) {
                case TftpPacket.ACK:
                    if (sendTimeouts == 0) {
                        updateTimeoutInterval(System.nanoTime() - sendTime);
                    }
                    int ackBlock = TftpPacket.unpackBlockNumber(packIn);
                    if (ackBlock == blockNumber) {
                        blockNumber++;
                        sendTimeouts = 0;
                        sendTime = System.nanoTime();
                    } else if (ackBlock >= 0 && ackBlock < blockNumber) {
                        log("received duplicate acknowledgement for %d", ackBlock);
                        suppressNextSend = true; // avoid sorcerer's apprentice syndrome
                    } else {
                        throw new TftpException(0, "wrong packet acknowledged");
                    }
                    break;
                case TftpPacket.ERROR:
                    log("client sent error '%s'", TftpPacket.unpackError(packIn));
                    isAlive = false;
                    break;
                default:
                    throw new TftpException(0, "expected acknowledgement or error message");
                }
            }
            if (sendTimeouts == 10) {
                log("connection apparently lost - 10 tries failed");
                throw new TftpException(0, "connection apparently lost");
            }
        } catch (TftpException e) {
            log("server error '%s'", e.getMessage());
            TftpPacket.packError(packOut, e.getErrorCode(), e.getMessage());
            try {
                connection.send(packOut);
            } catch (IOException e2) {
                log("I/O error when sending error packet");
            }
        }
    }
    
    private void log(String format, Object... parms) {
        System.out.printf("server: %s\n", String.format(format, parms));
    }

    private void updateTimeoutInterval(long nanos) {
        int millis = (int) (nanos / 1000000);
        int blocks = estimateBlocks;
        double rtt = estimateRtt;
        double dev = estimateDev;
        double weight = blocks <= 4 ? 1.0 / (blocks + 1) : 0.125;
        rtt = (1.0 - weight) * rtt + weight * millis;
        dev = (1.0 - weight) * dev + weight * Math.abs(millis - rtt);
        estimateBlocks = blocks + 1;
        estimateRtt = rtt;
        estimateDev = dev;
        try {
            connection.setSoTimeout((int) (rtt + 4 * dev));
        } catch (SocketException e) { }
    }
}
