package jxm.JWS.DF;

import jxm.CHRF;
import jxm.JWS.FR.Framedata;
import jxm.JWS.FR.FramedataImpl1;
import jxm.JWS.WebSocket;
import jxm.JWS.PBase64;
import jxm.JWS.EX.*;
import jxm.JWS.FR.CloseFrameBuilder;
import jxm.JWS.FR.FrameBuilder;
import jxm.JWS.HS.*;

import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;


public class Draft_10 extends Draft {

    private class IncompleteException extends Throwable {

        /**
         * It's Serializable.
         */
        private static final long serialVersionUID = 7330519489840500997L;

        private int preferedsize;

        public IncompleteException(int preferedsize) {
            this.preferedsize = preferedsize;
        }

        public int getPreferedSize() {
            return preferedsize;
        }
    }

    public static int readVersion(Handshakedata handshakedata) {
        String vers = handshakedata.getFieldValue("Sec-WebSocket-Version");
        if (vers.length() > 0) {
            int v;
            try {
                v = new Integer(vers.trim());
                return v;
            } catch (NumberFormatException e) {
                return -1;
            }
        }
        return -1;
    }

    private ByteBuffer incompleteframe;
    private Framedata fragmentedframe = null;

    private final Random reuseableRandom = new Random();

    @Override
    public HandshakeState acceptHandshakeAsClient(ClientHandshake request, ServerHandshake response) throws InvalidHandshakeException {
        if (!request.hasFieldValue("Sec-WebSocket-Key") || !response.hasFieldValue("Sec-WebSocket-Accept"))
            return HandshakeState.NOT_MATCHED;

        String seckey_answere = response.getFieldValue("Sec-WebSocket-Accept");
        String seckey_challenge = request.getFieldValue("Sec-WebSocket-Key");
        seckey_challenge = generateFinalKey(seckey_challenge);

        if (seckey_challenge.equals(seckey_answere))
            return HandshakeState.MATCHED;
        return HandshakeState.NOT_MATCHED;
    }

    @Override
    public HandshakeState acceptHandshakeAsServer(ClientHandshake handshakedata) throws InvalidHandshakeException {
        // Sec-WebSocket-Origin is only required for browser clients
        int v = readVersion(handshakedata);
        if (v == 7 || v == 8)// g
            return basicAccept(handshakedata) ? HandshakeState.MATCHED : HandshakeState.NOT_MATCHED;
        return HandshakeState.NOT_MATCHED;
    }

    @Override
    public ByteBuffer createBinaryFrame(Framedata framedata) {
        ByteBuffer mes = framedata.getPayloadData();
        boolean mask = role == WebSocket.Role.CLIENT; // framedata.getTransfereMasked();
        int sizebytes = mes.remaining() <= 125 ? 1 : mes.remaining() <= 65535 ? 2 : 8;
        ByteBuffer buf = ByteBuffer.allocate(1 + (sizebytes > 1 ? sizebytes + 1 : sizebytes) + (mask ? 4 : 0) + mes.remaining());
        byte optcode = fromOpcode(framedata.getOpcode());
        byte one = (byte) (framedata.isFin() ? -128 : 0);
        one |= optcode;
        buf.put(one);
        byte[] payloadlengthbytes = toByteArray(mes.remaining(), sizebytes);
        assert (payloadlengthbytes.length == sizebytes);

        if (sizebytes == 1) {
            buf.put((byte) ((byte) payloadlengthbytes[0] | (mask ? (byte) -128 : 0)));
        } else if (sizebytes == 2) {
            buf.put((byte) ((byte) 126 | (mask ? (byte) -128 : 0)));
            buf.put(payloadlengthbytes);
        } else if (sizebytes == 8) {
            buf.put((byte) ((byte) 127 | (mask ? (byte) -128 : 0)));
            buf.put(payloadlengthbytes);
        } else
            throw new RuntimeException("Size representation not supported/specified");

        if (mask) {
            ByteBuffer maskkey = ByteBuffer.allocate(4);
            maskkey.putInt(reuseableRandom.nextInt());
            buf.put(maskkey.array());
            for (int i = 0; mes.hasRemaining(); i++) {
                buf.put((byte) (mes.get() ^ maskkey.get(i % 4)));
            }
        } else
            buf.put(mes);
        // translateFrame ( buf.array () , buf.array ().length );
        assert (buf.remaining() == 0) : buf.remaining();
        buf.flip();

        return buf;
    }

    @Override
    public List<Framedata> createFrames(ByteBuffer binary, boolean mask) {
        FrameBuilder curframe = new FramedataImpl1();
        try {
            curframe.setPayload(binary);
        } catch (InvalidDataException e) {
            throw new NotSendableException(e);
        }
        curframe.setFin(true);
        curframe.setOptcode(Framedata.Opcode.BINARY);
        curframe.setTransferemasked(mask);
        return Collections.singletonList((Framedata) curframe);
    }

    @Override
    public List<Framedata> createFrames(String text, boolean mask) {
        FrameBuilder curframe = new FramedataImpl1();
        try {
            curframe.setPayload(ByteBuffer.wrap(CHRF.utf8Bytes(text)));
        } catch (InvalidDataException e) {
            throw new NotSendableException(e);
        }
        curframe.setFin(true);
        curframe.setOptcode(Framedata.Opcode.TEXT);
        curframe.setTransferemasked(mask);
        return Collections.singletonList((Framedata) curframe);
    }

    private byte fromOpcode(Framedata.Opcode opcode) {
        if (opcode == Framedata.Opcode.CONTINUOUS)
            return 0;
        else if (opcode == Framedata.Opcode.TEXT)
            return 1;
        else if (opcode == Framedata.Opcode.BINARY)
            return 2;
        else if (opcode == Framedata.Opcode.CLOSING)
            return 8;
        else if (opcode == Framedata.Opcode.PING)
            return 9;
        else if (opcode == Framedata.Opcode.PONG)
            return 10;
        throw new RuntimeException("Don't know how to handle " + opcode.toString());
    }

    private String generateFinalKey(String in) {
        String seckey = in.trim();
        String acc = seckey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
        MessageDigest sh1;
        try {
            sh1 = MessageDigest.getInstance("SHA1");
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
        return PBase64.encodeBytes(sh1.digest(acc.getBytes()));
    }

    @Override
    public ClientHandshakeBuilder postProcessHandshakeRequestAsClient(ClientHandshakeBuilder request) {
        request.put("Upgrade", "websocket");
        request.put("Connection", "Upgrade"); // to respond to a Connection keep alives
        request.put("Sec-WebSocket-Version", "8");

        byte[] random = new byte[16];
        reuseableRandom.nextBytes(random);
        request.put("Sec-WebSocket-Key", PBase64.encodeBytes(random));

        return request;
    }

    @Override
    public HandshakeBuilder postProcessHandshakeResponseAsServer(ClientHandshake request, ServerHandshakeBuilder response) throws InvalidHandshakeException {
        response.put("Upgrade", "websocket");
        response.put("Connection", request.getFieldValue("Connection")); // to respond to a Connection keep alives
        response.setHttpStatusMessage("Switching Protocols");
        String seckey = request.getFieldValue("Sec-WebSocket-Key");
        if (seckey == null)
            throw new InvalidHandshakeException("missing Sec-WebSocket-Key");
        response.put("Sec-WebSocket-Accept", generateFinalKey(seckey));
        return response;
    }

    private byte[] toByteArray(long val, int bytecount) {
        byte[] buffer = new byte[bytecount];
        int highest = 8 * bytecount - 8;
        for (int i = 0; i < bytecount; i++) {
            buffer[i] = (byte) (val >>> (highest - 8 * i));
        }
        return buffer;
    }

    private Framedata.Opcode toOpcode(byte opcode) throws InvalidFrameException {
        switch (opcode) {
            case 0:
                return Framedata.Opcode.CONTINUOUS;
            case 1:
                return Framedata.Opcode.TEXT;
            case 2:
                return Framedata.Opcode.BINARY;
            // 3-7 are not yet defined
            case 8:
                return Framedata.Opcode.CLOSING;
            case 9:
                return Framedata.Opcode.PING;
            case 10:
                return Framedata.Opcode.PONG;
            // 11-15 are not yet defined
            default:
                throw new InvalidFrameException("unknow optcode " + (short) opcode);
        }
    }

    @Override
    public List<Framedata> translateFrame(ByteBuffer buffer) throws LimitExedeedException, InvalidDataException {
        List<Framedata> frames = new LinkedList<Framedata>();
        Framedata cur;

        if (incompleteframe != null) {
            // complete an incomplete frame
            while (true) {
                try {
                    buffer.mark();
                    int available_next_byte_count = buffer.remaining();// The number of bytes received
                    int expected_next_byte_count = incompleteframe.remaining();// The number of bytes to complete the incomplete frame

                    if (expected_next_byte_count > available_next_byte_count) {
                        // did not receive enough bytes to complete the frame
                        incompleteframe.put(buffer.array(), buffer.position(), available_next_byte_count);
                        buffer.position(buffer.position() + available_next_byte_count);
                        return Collections.emptyList();
                    }
                    incompleteframe.put(buffer.array(), buffer.position(), expected_next_byte_count);
                    buffer.position(buffer.position() + expected_next_byte_count);

                    cur = translateSingleFrame((ByteBuffer) incompleteframe.duplicate().position(0));
                    frames.add(cur);
                    incompleteframe = null;
                    break; // go on with the normal frame receival
                } catch (IncompleteException e) {
                    // extending as much as suggested
                    int oldsize = incompleteframe.limit();
                    ByteBuffer extendedframe = ByteBuffer.allocate(checkAlloc(e.getPreferedSize()));
                    assert (extendedframe.limit() > incompleteframe.limit());
                    incompleteframe.rewind();
                    extendedframe.put(incompleteframe);
                    incompleteframe = extendedframe;

                    return translateFrame(buffer);
                }
            }
        }

        while (buffer.hasRemaining()) {// Read as much as possible full frames
            buffer.mark();
            try {
                cur = translateSingleFrame(buffer);
                frames.add(cur);
            } catch (IncompleteException e) {
                // remember the incomplete data
                buffer.reset();
                int pref = e.getPreferedSize();
                incompleteframe = ByteBuffer.allocate(checkAlloc(pref));
                incompleteframe.put(buffer);
                break;
            }
        }
        return frames;
    }

    public Framedata translateSingleFrame(ByteBuffer buffer) throws IncompleteException, InvalidDataException {
        int maxpacketsize = buffer.remaining();
        int realpacketsize = 2;
        if (maxpacketsize < realpacketsize)
            throw new IncompleteException(realpacketsize);
        byte b1 = buffer.get( /*0*/);
        boolean FIN = b1 >> 8 != 0;
        byte rsv = (byte) ((b1 & ~(byte) 128) >> 4);
        if (rsv != 0)
            throw new InvalidFrameException("bad rsv " + rsv);
        byte b2 = buffer.get( /*1*/);
        boolean MASK = (b2 & -128) != 0;
        int payloadlength = (byte) (b2 & ~(byte) 128);
        Framedata.Opcode optcode = toOpcode((byte) (b1 & 15));

        if (!FIN) {
            if (optcode == Framedata.Opcode.PING || optcode == Framedata.Opcode.PONG || optcode == Framedata.Opcode.CLOSING) {
                throw new InvalidFrameException("control frames may no be fragmented");
            }
        }

        if (payloadlength >= 0 && payloadlength <= 125) {
        } else {
            if (optcode == Framedata.Opcode.PING || optcode == Framedata.Opcode.PONG || optcode == Framedata.Opcode.CLOSING) {
                throw new InvalidFrameException("more than 125 octets");
            }
            if (payloadlength == 126) {
                realpacketsize += 2; // additional length bytes
                if (maxpacketsize < realpacketsize)
                    throw new IncompleteException(realpacketsize);
                byte[] sizebytes = new byte[3];
                sizebytes[1] = buffer.get( /*1 + 1*/);
                sizebytes[2] = buffer.get( /*1 + 2*/);
                payloadlength = new BigInteger(sizebytes).intValue();
            } else {
                realpacketsize += 8; // additional length bytes
                if (maxpacketsize < realpacketsize)
                    throw new IncompleteException(realpacketsize);
                byte[] bytes = new byte[8];
                for (int i = 0; i < 8; i++) {
                    bytes[i] = buffer.get( /*1 + i*/);
                }
                long length = new BigInteger(bytes).longValue();
                if (length > Integer.MAX_VALUE) {
                    throw new LimitExedeedException("Payloadsize is to big...");
                } else {
                    payloadlength = (int) length;
                }
            }
        }

        // int maskskeystart = foff + realpacketsize;
        realpacketsize += (MASK ? 4 : 0);
        // int payloadstart = foff + realpacketsize;
        realpacketsize += payloadlength;

        if (maxpacketsize < realpacketsize)
            throw new IncompleteException(realpacketsize);

        ByteBuffer payload = ByteBuffer.allocate(checkAlloc(payloadlength));
        if (MASK) {
            byte[] maskskey = new byte[4];
            buffer.get(maskskey);
            for (int i = 0; i < payloadlength; i++) {
                payload.put((byte) ((byte) buffer.get( /*payloadstart + i*/) ^ (byte) maskskey[i % 4]));
            }
        } else {
            payload.put(buffer.array(), buffer.position(), payload.limit());
            buffer.position(buffer.position() + payload.limit());
        }

        FrameBuilder frame;
        if (optcode == Framedata.Opcode.CLOSING) {
            frame = new CloseFrameBuilder();
        } else {
            frame = new FramedataImpl1();
            frame.setFin(FIN);
            frame.setOptcode(optcode);
        }
        payload.flip();
        frame.setPayload(payload);
        return frame;
    }

    @Override
    public void reset() {
        incompleteframe = null;
    }

    @Override
    public Draft copyInstance() {
        return new Draft_10();
    }

    @Override
    public CloseHandshakeType getCloseHandshakeType() {
        return CloseHandshakeType.TWOWAY;
    }
}
