package jxm.JWS.DF;

import jxm.CHRF;
import jxm.JWS.EX.IncompleteHandshakeException;
import jxm.JWS.EX.InvalidDataException;
import jxm.JWS.EX.InvalidHandshakeException;
import jxm.JWS.EX.LimitExedeedException;
import jxm.JWS.FR.CloseFrame;
import jxm.JWS.FR.FrameBuilder;
import jxm.JWS.FR.Framedata;
import jxm.JWS.FR.FramedataImpl1;
import jxm.JWS.WebSocket;
import jxm.JWS.HS.*;

import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;


/**
 * Base class for everything of a websocket specification which is not common such as the way the handshake is read or frames are transfered.
 */
public abstract class Draft {

    public enum HandshakeState {
        /**
         * Handshake matched this Draft successfully
         */
        MATCHED,
        /**
         * Handshake is does not match this Draft
         */
        NOT_MATCHED
    }

    public enum CloseHandshakeType {
        NONE, ONEWAY, TWOWAY
    }

    public static int MAX_FAME_SIZE = 1000 * 1;
    public static int INITIAL_FAMESIZE = 64;

    public static final byte[] FLASH_POLICY_REQUEST = CHRF.utf8Bytes("<policy-file-request/>\0");

    /**
     * In some cases the handshake will be parsed different depending on whether
     */
    protected WebSocket.Role role = null;

    protected Framedata.Opcode continuousFrameType = null;

    public static ByteBuffer readLine(ByteBuffer buf) {
        ByteBuffer sbuf = ByteBuffer.allocate(buf.remaining());
        byte prev = '0';
        byte cur = '0';
        while (buf.hasRemaining()) {
            prev = cur;
            cur = buf.get();
            sbuf.put(cur);
            if (prev == (byte) '\r' && cur == (byte) '\n') {
                sbuf.limit(sbuf.position() - 2);
                sbuf.position(0);
                return sbuf;

            }
        }
        // ensure that there wont be any bytes skipped
        buf.position(buf.position() - sbuf.position());
        return null;
    }

    public static String readStringLine(ByteBuffer buf) {
        ByteBuffer b = readLine(buf);
        return b == null ? null : CHRF.stringAscii(b.array(), 0, b.limit());
    }

    public static HandshakeBuilder translateHandshakeHttp(ByteBuffer buf, WebSocket.Role role) throws InvalidHandshakeException, IncompleteHandshakeException {
        HandshakeBuilder handshake;

        String line = readStringLine(buf);
        if (line == null)
            throw new IncompleteHandshakeException(buf.capacity() + 128);

        String[] firstLineTokens = line.split(" ", 3);// eg. HTTP/1.1 101 Switching the Protocols
        if (firstLineTokens.length != 3) {
            throw new InvalidHandshakeException();
        }

        if (role == WebSocket.Role.CLIENT) {
            // translating/parsing the response from the SERVER
            handshake = new HandshakeImpl1Server();
            ServerHandshakeBuilder serverhandshake = (ServerHandshakeBuilder) handshake;
            serverhandshake.setHttpStatus(Short.parseShort(firstLineTokens[1]));
            serverhandshake.setHttpStatusMessage(firstLineTokens[2]);
        } else {
            // translating/parsing the request from the CLIENT
            ClientHandshakeBuilder clienthandshake = new HandshakeImpl1Client();
            clienthandshake.setResourceDescriptor(firstLineTokens[1]);
            handshake = clienthandshake;
        }

        line = readStringLine(buf);
        while (line != null && line.length() > 0) {
            String[] pair = line.split(":", 2);
            if (pair.length != 2)
                throw new InvalidHandshakeException("not an http header");
            handshake.put(pair[0], pair[1].replaceFirst("^ +", ""));
            line = readStringLine(buf);
        }
        if (line == null)
            throw new IncompleteHandshakeException();
        return handshake;
    }

    public abstract HandshakeState acceptHandshakeAsClient(ClientHandshake request, ServerHandshake response) throws InvalidHandshakeException;

    public abstract HandshakeState acceptHandshakeAsServer(ClientHandshake handshakedata) throws InvalidHandshakeException;

    protected boolean basicAccept(Handshakedata handshakedata) {
        return handshakedata.getFieldValue("Upgrade").equalsIgnoreCase("websocket") && handshakedata.getFieldValue("Connection").toLowerCase(Locale.ENGLISH).contains("upgrade");
    }

    public abstract ByteBuffer createBinaryFrame(Framedata framedata); // TODO Allow to send data on the base of an Iterator or InputStream

    public abstract List<Framedata> createFrames(ByteBuffer binary, boolean mask);

    public abstract List<Framedata> createFrames(String text, boolean mask);

    public List<Framedata> continuousFrame(Framedata.Opcode op, ByteBuffer buffer, boolean fin) {
        if (op != Framedata.Opcode.BINARY && op != Framedata.Opcode.TEXT && op != Framedata.Opcode.TEXT) {
            throw new IllegalArgumentException("Only Opcode.BINARY or  Opcode.TEXT are allowed");
        }

        if (continuousFrameType != null) {
            continuousFrameType = Framedata.Opcode.CONTINUOUS;
        } else {
            continuousFrameType = op;
        }

        FrameBuilder bui = new FramedataImpl1(continuousFrameType);
        try {
            bui.setPayload(buffer);
        } catch (InvalidDataException e) {
            throw new RuntimeException(e); // can only happen when one builds close frames(Opcode.Close)
        }
        bui.setFin(fin);
        if (fin) {
            continuousFrameType = null;
        } else {
            continuousFrameType = op;
        }
        return Collections.singletonList((Framedata) bui);
    }

    public abstract void reset();

    public List<ByteBuffer> createHandshake(Handshakedata handshakedata, WebSocket.Role ownrole) {
        return createHandshake(handshakedata, ownrole, true);
    }

    public List<ByteBuffer> createHandshake(Handshakedata handshakedata, WebSocket.Role ownrole, boolean withcontent) {
        StringBuilder bui = new StringBuilder(100);
        if (handshakedata instanceof ClientHandshake) {
            bui.append("GET ");
            bui.append(((ClientHandshake) handshakedata).getResourceDescriptor());
            bui.append(" HTTP/1.1");
        } else if (handshakedata instanceof ServerHandshake) {
            bui.append("HTTP/1.1 101 " + ((ServerHandshake) handshakedata).getHttpStatusMessage());
        } else {
            throw new RuntimeException("unknow role");
        }
        bui.append("\r\n");
        Iterator<String> it = handshakedata.iterateHttpFields();
        while (it.hasNext()) {
            String fieldname = it.next();
            String fieldvalue = handshakedata.getFieldValue(fieldname);
            bui.append(fieldname);
            bui.append(": ");
            bui.append(fieldvalue);
            bui.append("\r\n");
        }
        bui.append("\r\n");
        byte[] httpheader = CHRF.asciiBytes(bui.toString());

        byte[] content = withcontent ? handshakedata.getContent() : null;
        ByteBuffer bytebuffer = ByteBuffer.allocate((content == null ? 0 : content.length) + httpheader.length);
        bytebuffer.put(httpheader);
        if (content != null)
            bytebuffer.put(content);
        bytebuffer.flip();
        return Collections.singletonList(bytebuffer);
    }

    public abstract ClientHandshakeBuilder postProcessHandshakeRequestAsClient(ClientHandshakeBuilder request) throws InvalidHandshakeException;

    public abstract HandshakeBuilder postProcessHandshakeResponseAsServer(ClientHandshake request, ServerHandshakeBuilder response) throws InvalidHandshakeException;

    public abstract List<Framedata> translateFrame(ByteBuffer buffer) throws InvalidDataException;

    public abstract CloseHandshakeType getCloseHandshakeType();

    /**
     * Drafts must only be by one websocket at all. To prevent drafts to be used more than once the Websocket implementation should call this method in order to create a new usable version of a given draft instance.<br>
     * The copy can be safely used in conjunction with a new websocket connection.
     */
    public abstract Draft copyInstance();

    public Handshakedata translateHandshake(ByteBuffer buf) throws InvalidHandshakeException {
        return translateHandshakeHttp(buf, role);
    }

    public int checkAlloc(int bytecount) throws LimitExedeedException, InvalidDataException {
        if (bytecount < 0)
            throw new InvalidDataException(CloseFrame.PROTOCOL_ERROR, "Negative count");
        return bytecount;
    }

    public void setParseMode(WebSocket.Role role) {
        this.role = role;
    }

    public WebSocket.Role getRole() {
        return role;
    }

}
