import java.net.DatagramPacket;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;


public class TftpPacket {
    public static final int RREQ = 1;
    public static final int DATA = 3;
    public static final int ACK = 4;
    public static final int ERROR = 5;
    
    private static final Charset UTF8 = Charset.forName("UTF-8");

    private TftpPacket() { }

    public static void packReadRequest(DatagramPacket packet, String filename, String mode) {
        byte[] buf = packet.getData();
        buf[0] = 0;
        buf[1] = RREQ;
        int pos = packString(buf, 2, filename);
        pos = packString(buf, pos, mode);
        packet.setLength(pos);
    }
    
    public static void packData(DatagramPacket packet, int blockNumber, ByteBuffer src) {
        byte[] buf = packet.getData();
        buf[0] = 0;
        buf[1] = DATA;
        buf[2] = (byte) (blockNumber >> 24);
        buf[3] = (byte) (blockNumber >> 16);
        buf[4] = (byte) (blockNumber >> 8);
        buf[5] = (byte) blockNumber;
        
        int srcPos = (blockNumber - 1) * 512;
        int srcLength = Math.min(512, src.limit() - srcPos);
        if (srcLength > 0) {
            src.position(srcPos);
            try {
                src.get(buf, 6, srcLength);
            } catch (Throwable t) {
                System.err.println(srcPos + " " + (srcPos + srcLength) + "/" + src.limit() + ": " + t.toString());
            }
        }
        packet.setLength(6 + srcLength);
    }
    
    public static void packAcknowledgement(DatagramPacket packet, int blockNumber) {
        byte[] buf = packet.getData();
        buf[0] = 0;
        buf[1] = ACK;
        buf[2] = (byte) (blockNumber >> 24);
        buf[3] = (byte) (blockNumber >> 16);
        buf[4] = (byte) (blockNumber >> 8);
        buf[5] = (byte) blockNumber;
        packet.setLength(6);
    }
    
    public static void packError(DatagramPacket packet, int errCode, String errMsg) {
        byte[] buf = packet.getData();
        buf[0] = 0;
        buf[1] = ERROR;
        buf[2] = 0;
        buf[3] = (byte) errCode;
        int pos = packString(buf, 4, errMsg);
        packet.setLength(pos);
    }
    
    public static int unpackCode(DatagramPacket packet) throws TftpException {
        int bufLength = packet.getLength();
        byte[] buf = packet.getData();
        if (bufLength < 2) {
            throw new TftpException(4, "Packet not long enough to contain operation code");
        } else if (buf[0] != 0 || buf[1] < 1 || buf[1] > 5) {
            throw new TftpException(4, "Operations only recognized when <= 5");
        } else {
            return buf[1];
        }
    }
    
    public static String unpackReadRequest(DatagramPacket packet) throws TftpException {
        int bufLength = packet.getLength();
        byte[] buf = packet.getData();
        if (bufLength <= 2 || buf[0] != 0 || buf[1] != RREQ) {
            throw new TftpException(4, "Expected read request message type");
        }
        return unpackString(buf, 2, bufLength);
    }
    
    public static int unpackBlockNumber(DatagramPacket packet) throws TftpException {
        int bufLength = packet.getLength();
        byte[] buf = packet.getData();
        if (bufLength <= 2 || buf[0] != 0 || (buf[1] != DATA && buf[1] != ACK)) {
            throw new TftpException(4, "Expected data/acknowledge type");
        } else if (bufLength < 6) {
            throw new TftpException(0, "Data/ack message too short to have block number");
        } else {
            return ((buf[2] & 0xFF) << 24) | ((buf[3] & 0xFF) << 16) | ((buf[4] & 0xFF) << 8) | (buf[5] & 0xFF);
        }
    }
    
    public static int unpackDataLength(DatagramPacket packet) throws TftpException {
        int bufLength = packet.getLength();
        byte[] buf = packet.getData();
        if (bufLength <= 2 || buf[0] != 0 || buf[1] != DATA) {
            throw new TftpException(4, "Expected data type");
        } else if (bufLength < 6) {
            throw new TftpException(0, "Message too short to have data");
        } else {
            return bufLength - 6;
        }
    }

    public static String unpackError(DatagramPacket packet) throws TftpException {
        int bufLength = packet.getLength();
        byte[] buf = packet.getData();
        if (bufLength >= 2 && (buf[0] != 0 || buf[1] != ERROR)) {
            throw new TftpException(4, "Expected error message type");
        } else if (buf.length < 5) {
            throw new TftpException(0, "Error packet not long enough for message");
        } else {
            return unpackString(buf, 4, bufLength);
        }
    }

    private static String unpackString(byte[] src, int srcPos, int srcLength) throws TftpException {
        for (int i = srcPos; i < srcLength; i++) {
            if (src[i] == 0) {
                return new String(src, srcPos, i - srcPos, UTF8);
            }
        }
        throw new TftpException(0, "Unterminated string");
    }
    
    private static int packString(byte[] dest, int destPos, String message) {
        byte[] out = message.getBytes(UTF8);
        int numOut = Math.min(out.length, dest.length - destPos - 1);
        System.arraycopy(out, 0, dest, destPos, numOut);
        dest[destPos + numOut] = 0;
        return destPos + numOut + 1;
    }
}
