package org.java_websocket.drafts;

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

import org.java_websocket.WebSocket.Role;
import org.java_websocket.exceptions.IncompleteHandshakeException;
import org.java_websocket.exceptions.InvalidDataException;
import org.java_websocket.exceptions.InvalidHandshakeException;
import org.java_websocket.exceptions.LimitExedeedException;
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.HandshakeImpl1Client;
import org.java_websocket.handshake.HandshakeImpl1Server;
import org.java_websocket.handshake.Handshakedata;
import org.java_websocket.handshake.ServerHandshake;
import org.java_websocket.handshake.ServerHandshakeBuilder;
import org.java_websocket.util.Charsetfunctions;

/**
 * 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 CloseHandshakeType
    {
        NONE, ONEWAY, TWOWAY
    }

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

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

    public static int          INITIAL_FAMESIZE     = 64;

    public static int          MAX_FAME_SIZE        = 1000 * 1;

    protected Opcode           continuousFrameType  = null;

    /**
     * In some cases the handshake will be parsed different depending on whether
     */
    protected Role             role                 = 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 : Charsetfunctions.stringAscii(b.array(), 0, b.limit());
    }

    public static HandshakeBuilder translateHandshakeHttp(ByteBuffer buf, 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 == 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;
    }

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

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

    public abstract HandshakeState acceptHandshakeAsServer(ClientHandshake handshakedata)
        throws InvalidHandshakeException;

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

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

        if (continuousFrameType != null)
        {
            continuousFrameType = 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);
    }

    /**
     * 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 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<ByteBuffer> createHandshake(Handshakedata handshakedata, Role ownrole)
    {
        return createHandshake(handshakedata, ownrole, true);
    }

    public List<ByteBuffer> createHandshake(Handshakedata handshakedata, 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 = Charsetfunctions.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 CloseHandshakeType getCloseHandshakeType();

    public Role getRole()
    {
        return role;
    }

    public abstract ClientHandshakeBuilder postProcessHandshakeRequestAsClient(ClientHandshakeBuilder request)
        throws InvalidHandshakeException;

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

    public abstract void reset();

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

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

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

}
