/* 
 * Copyright (C) 2017 www.quorrabot.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.java_websocket.drafts;

import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;

import org.java_websocket.exceptions.InvalidDataException;
import org.java_websocket.exceptions.InvalidFrameException;
import org.java_websocket.exceptions.InvalidHandshakeException;
import org.java_websocket.exceptions.LimitExedeedException;
import org.java_websocket.exceptions.NotSendableException;
import org.java_websocket.framing.CloseFrame;
import org.java_websocket.framing.FrameBuilder;
import org.java_websocket.framing.Framedata;
import org.java_websocket.framing.Framedata.Opcode;
import org.java_websocket.framing.FramedataImpl1;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.handshake.ClientHandshakeBuilder;
import org.java_websocket.handshake.HandshakeBuilder;
import org.java_websocket.handshake.ServerHandshake;
import org.java_websocket.handshake.ServerHandshakeBuilder;
import org.java_websocket.util.Charsetfunctions;

public class Draft_75 extends Draft {

    /**
     * The byte representing CR, or Carriage Return, or \r
     */
    public static final byte CR = (byte) 0x0D;
    /**
     * The byte representing LF, or Line Feed, or \n
     */
    public static final byte LF = (byte) 0x0A;
    /**
     * The byte representing the beginning of a WebSocket text frame.
     */
    public static final byte START_OF_FRAME = (byte) 0x00;
    /**
     * The byte representing the end of a WebSocket text frame.
     */
    public static final byte END_OF_FRAME = (byte) 0xFF;

    /**
     * Is only used to detect protocol violations
     */
    protected boolean readingState = false;

    protected List<Framedata> readyframes = new LinkedList<Framedata>();
    protected ByteBuffer currentFrame;

    private final Random reuseableRandom = new Random();

    @Override
    public HandshakeState acceptHandshakeAsClient(ClientHandshake request, ServerHandshake response) {
        return request.getFieldValue("WebSocket-Origin").equals(response.getFieldValue("Origin")) && basicAccept(response) ? HandshakeState.MATCHED : HandshakeState.NOT_MATCHED;
    }

    @Override
    public HandshakeState acceptHandshakeAsServer(ClientHandshake handshakedata) {
        if (handshakedata.hasFieldValue("Origin") && basicAccept(handshakedata)) {
            return HandshakeState.MATCHED;
        }
        return HandshakeState.NOT_MATCHED;
    }

    @Override
    public ByteBuffer createBinaryFrame(Framedata framedata) {
        if (framedata.getOpcode() != Opcode.TEXT) {
            throw new RuntimeException("only text frames supported");
        }

        ByteBuffer pay = framedata.getPayloadData();
        ByteBuffer b = ByteBuffer.allocate(pay.remaining() + 2);
        b.put(START_OF_FRAME);
        pay.mark();
        b.put(pay);
        pay.reset();
        b.put(END_OF_FRAME);
        b.flip();
        return b;
    }

    @Override
    public List<Framedata> createFrames(ByteBuffer binary, boolean mask) {
        throw new RuntimeException("not yet implemented");
    }

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

    @Override
    public ClientHandshakeBuilder postProcessHandshakeRequestAsClient(ClientHandshakeBuilder request) throws InvalidHandshakeException {
        request.put("Upgrade", "WebSocket");
        request.put("Connection", "Upgrade");
        if (!request.hasFieldValue("Origin")) {
            request.put("Origin", "random" + reuseableRandom.nextInt());
        }

        return request;
    }

    @Override
    public HandshakeBuilder postProcessHandshakeResponseAsServer(ClientHandshake request, ServerHandshakeBuilder response) throws InvalidHandshakeException {
        response.setHttpStatusMessage("Web Socket Protocol Handshake");
        response.put("Upgrade", "WebSocket");
        response.put("Connection", request.getFieldValue("Connection")); // to respond to a Connection keep alive
        response.put("WebSocket-Origin", request.getFieldValue("Origin"));
        String location = "ws://" + request.getFieldValue("Host") + request.getResourceDescriptor();
        response.put("WebSocket-Location", location);
        // TODO handle Sec-WebSocket-Protocol and Set-Cookie
        return response;
    }

    protected List<Framedata> translateRegularFrame(ByteBuffer buffer) throws InvalidDataException {

        while (buffer.hasRemaining()) {
            byte newestByte = buffer.get();
            if (newestByte == START_OF_FRAME) { // Beginning of Frame
                if (readingState) {
                    throw new InvalidFrameException("unexpected START_OF_FRAME");
                }
                readingState = true;
            } else if (newestByte == END_OF_FRAME) { // End of Frame
                if (!readingState) {
                    throw new InvalidFrameException("unexpected END_OF_FRAME");
                }
                // currentFrame will be null if END_OF_FRAME was send directly after
                // START_OF_FRAME, thus we will send 'null' as the sent message.
                if (this.currentFrame != null) {
                    currentFrame.flip();
                    FramedataImpl1 curframe = new FramedataImpl1();
                    curframe.setPayload(currentFrame);
                    curframe.setFin(true);
                    curframe.setOptcode(Opcode.TEXT);
                    readyframes.add(curframe);
                    this.currentFrame = null;
                    buffer.mark();
                }
                readingState = false;
            } else if (readingState) { // Regular frame data, add to current frame buffer //TODO This code is very expensive and slow
                if (currentFrame == null) {
                    currentFrame = createBuffer();
                } else if (!currentFrame.hasRemaining()) {
                    currentFrame = increaseBuffer(currentFrame);
                }
                currentFrame.put(newestByte);
            } else {
                return null;
            }
        }

        // if no error occurred this block will be reached
        /*if( readingState ) {
			checkAlloc(currentFrame.position()+1);
		}*/
        List<Framedata> frames = readyframes;
        readyframes = new LinkedList<Framedata>();
        return frames;
    }

    @Override
    public List<Framedata> translateFrame(ByteBuffer buffer) throws InvalidDataException {
        List<Framedata> frames = translateRegularFrame(buffer);
        if (frames == null) {
            throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR);
        }
        return frames;
    }

    @Override
    public void reset() {
        readingState = false;
        this.currentFrame = null;
    }

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

    public ByteBuffer createBuffer() {
        return ByteBuffer.allocate(INITIAL_FAMESIZE);
    }

    public ByteBuffer increaseBuffer(ByteBuffer full) throws LimitExedeedException, InvalidDataException {
        full.flip();
        ByteBuffer newbuffer = ByteBuffer.allocate(checkAlloc(full.capacity() * 2));
        newbuffer.put(full);
        return newbuffer;
    }

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