/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package io.undertow.protocols.ajp;

import io.undertow.util.HeaderMap;
import io.undertow.util.HttpString;

import java.io.IOException;
import java.nio.ByteBuffer;

import static io.undertow.protocols.ajp.AjpConstants.FRAME_TYPE_END_RESPONSE;
import static io.undertow.protocols.ajp.AjpConstants.FRAME_TYPE_REQUEST_BODY_CHUNK;
import static io.undertow.protocols.ajp.AjpConstants.FRAME_TYPE_SEND_BODY_CHUNK;
import static io.undertow.protocols.ajp.AjpConstants.FRAME_TYPE_SEND_HEADERS;

/**
 * Parser used for the client (i.e. load balancer) side of the AJP connection.
 *
 * @author Stuart Douglas
 */
class AjpResponseParser {

    public static final AjpResponseParser INSTANCE = new AjpResponseParser();

    private static final int AB = ('A' << 8) + 'B';

    //states
    public static final int BEGIN = 0;
    public static final int READING_MAGIC_NUMBER = 1;
    public static final int READING_DATA_SIZE = 2;
    public static final int READING_PREFIX_CODE = 3;
    public static final int READING_STATUS_CODE = 4;
    public static final int READING_REASON_PHRASE = 5;
    public static final int READING_NUM_HEADERS = 6;
    public static final int READING_HEADERS = 7;
    public static final int READING_PERSISTENT_BOOLEAN = 8;
    public static final int READING_BODY_CHUNK_LENGTH = 9;
    public static final int DONE = 10;

    //parser states
    int state;
    byte prefix;
    int numHeaders = 0;
    HttpString currentHeader;

    //final states
    int statusCode;
    String reasonPhrase;
    HeaderMap headers = new HeaderMap();
    int readBodyChunkSize;

    public boolean isComplete() {
        return state == DONE;
    }

    public void parse(final ByteBuffer buf) throws IOException {
        if (!buf.hasRemaining()) {
            return;
        }
        switch (this.state) {
            case BEGIN: {
                IntegerHolder result = parse16BitInteger(buf);
                if (!result.readComplete) {
                    return;
                } else {
                    if (result.value != AB) {
                        throw new IOException("Wrong magic number");
                    }
                }
            }
            case READING_DATA_SIZE: {
                IntegerHolder result = parse16BitInteger(buf);
                if (!result.readComplete) {
                    this.state = READING_DATA_SIZE;
                    return;
                }
            }
            case READING_PREFIX_CODE: {
                if (!buf.hasRemaining()) {
                    this.state = READING_PREFIX_CODE;
                    return;
                } else {
                    final byte prefix = buf.get();
                    this.prefix = prefix;
                    if (prefix == FRAME_TYPE_END_RESPONSE) {
                        this.state = READING_PERSISTENT_BOOLEAN;
                        break;
                    } else if (prefix == FRAME_TYPE_SEND_BODY_CHUNK) {
                        this.state = READING_BODY_CHUNK_LENGTH;
                        break;
                    } else if (prefix != FRAME_TYPE_SEND_HEADERS && prefix != FRAME_TYPE_REQUEST_BODY_CHUNK) {
                        this.state = DONE;
                        return;
                    }
                }
            }
            case READING_STATUS_CODE: {
                //this state is overloaded for the request size
                //when reading state=6 (read_body_chunk requests)

                IntegerHolder result = parse16BitInteger(buf);
                if (result.readComplete) {
                    if (this.prefix == FRAME_TYPE_SEND_HEADERS) {
                        statusCode = result.value;
                    } else {
                        //read body chunk or end result
                        //a bit hacky
                        this.state = DONE;
                        this.readBodyChunkSize = result.value;
                        return;
                    }
                } else {
                    this.state = READING_STATUS_CODE;
                    return;
                }
            }
            case READING_REASON_PHRASE: {
                StringHolder result = parseString(buf, false);
                if (result.readComplete) {
                    reasonPhrase = result.value;
                } else {
                    this.state = READING_REASON_PHRASE;
                    return;
                }
            }
            case READING_NUM_HEADERS: {
                IntegerHolder result = parse16BitInteger(buf);
                if (!result.readComplete) {
                    this.state = READING_NUM_HEADERS;
                    return;
                } else {
                    this.numHeaders = result.value;
                }
            }
            case READING_HEADERS: {
                int readHeaders = this.readHeaders;
                while (readHeaders < this.numHeaders) {
                    if (this.currentHeader == null) {
                        StringHolder result = parseString(buf, true);
                        if (!result.readComplete) {
                            this.state = READING_HEADERS;
                            this.readHeaders = readHeaders;
                            return;
                        }
                        if (result.header != null) {
                            this.currentHeader = result.header;
                        } else {
                            this.currentHeader = HttpString.tryFromString(result.value);
                        }
                    }
                    StringHolder result = parseString(buf, false);
                    if (!result.readComplete) {
                        this.state = READING_HEADERS;
                        this.readHeaders = readHeaders;
                        return;
                    }
                    headers.add(this.currentHeader, result.value);
                    this.currentHeader = null;
                    ++readHeaders;
                }
                break;
            }
        }

        if (state == READING_PERSISTENT_BOOLEAN) {
            if (!buf.hasRemaining()) {
                return;
            }
            currentIntegerPart = buf.get();
            this.state = DONE;
            return;
        } else if (state == READING_BODY_CHUNK_LENGTH) {
            IntegerHolder result = parse16BitInteger(buf);
            if (result.readComplete) {
                this.currentIntegerPart = result.value;
                this.state = DONE;
            }
            return;
        } else {
            this.state = DONE;
        }
    }

    protected HttpString headers(int offset) {
        return AjpConstants.HTTP_HEADERS_ARRAY[offset];
    }

    public HeaderMap getHeaders() {
        return headers;
    }

    public int getStatusCode() {
        return statusCode;
    }

    public String getReasonPhrase() {
        return reasonPhrase;
    }

    public int getReadBodyChunkSize() {
        return readBodyChunkSize;
    }

    public static final int STRING_LENGTH_MASK = 1 << 31;

    /**
     * The length of the string being read
     */
    public int stringLength = -1;

    /**
     * The current string being read
     */
    public StringBuilder currentString;

    /**
     * when reading the first byte of an integer this stores the first value. It is set to -1 to signify that
     * the first byte has not been read yet.
     */
    public int currentIntegerPart = -1;
    boolean containsUrlCharacters = false;
    public int readHeaders = 0;

    public void reset() {

        state = 0;
        prefix = 0;
        numHeaders = 0;
        currentHeader = null;

        statusCode = 0;
        reasonPhrase = null;
        headers = new HeaderMap();
        stringLength = -1;
        currentString = null;
        currentIntegerPart = -1;
        readHeaders = 0;
    }

    protected IntegerHolder parse16BitInteger(ByteBuffer buf) {
        if (!buf.hasRemaining()) {
            return new IntegerHolder(-1, false);
        }
        int number = this.currentIntegerPart;
        if (number == -1) {
            number = (buf.get() & 0xFF);
        }
        if (buf.hasRemaining()) {
            final byte b = buf.get();
            int result = ((0xFF & number) << 8) + (b & 0xFF);
            this.currentIntegerPart = -1;
            return new IntegerHolder(result, true);
        } else {
            this.currentIntegerPart = number;
            return new IntegerHolder(-1, false);
        }
    }

    protected StringHolder parseString(ByteBuffer buf, boolean header) {
        boolean containsUrlCharacters = this.containsUrlCharacters;
        if (!buf.hasRemaining()) {
            return new StringHolder(null, false, false);
        }
        int stringLength = this.stringLength;
        if (stringLength == -1) {
            int number = buf.get() & 0xFF;
            if (buf.hasRemaining()) {
                final byte b = buf.get();
                stringLength = ((0xFF & number) << 8) + (b & 0xFF);
            } else {
                this.stringLength = number | STRING_LENGTH_MASK;
                return new StringHolder(null, false, false);
            }
        } else if ((stringLength & STRING_LENGTH_MASK) != 0) {
            int number = stringLength & ~STRING_LENGTH_MASK;
            stringLength = ((0xFF & number) << 8) + (buf.get() & 0xFF);
        }
        if (header && (stringLength & 0xFF00) != 0) {
            this.stringLength = -1;
            return new StringHolder(headers(stringLength & 0xFF));
        }
        if (stringLength == 0xFFFF) {
            //OxFFFF means null
            this.stringLength = -1;
            return new StringHolder(null, true, false);
        }
        StringBuilder builder = this.currentString;

        if (builder == null) {
            builder = new StringBuilder();
            this.currentString = builder;
        }
        int length = builder.length();
        while (length < stringLength) {
            if (!buf.hasRemaining()) {
                this.stringLength = stringLength;
                this.containsUrlCharacters = containsUrlCharacters;
                return new StringHolder(null, false, false);
            }
            char c = (char) buf.get();
            if(c == '+' || c == '%') {
                containsUrlCharacters = true;
            }
            builder.append(c);
            ++length;
        }

        if (buf.hasRemaining()) {
            buf.get(); //null terminator
            this.currentString = null;
            this.stringLength = -1;
            this.containsUrlCharacters = false;
            return new StringHolder(builder.toString(), true, containsUrlCharacters);
        } else {
            this.stringLength = stringLength;
            this.containsUrlCharacters = containsUrlCharacters;
            return new StringHolder(null, false, false);
        }
    }

    protected static class IntegerHolder {
        public final int value;
        public final boolean readComplete;

        private IntegerHolder(int value, boolean readComplete) {
            this.value = value;
            this.readComplete = readComplete;
        }
    }

    protected static class StringHolder {
        public final String value;
        public final HttpString header;
        public final boolean readComplete;
        public final boolean containsUrlCharacters;

        private StringHolder(String value, boolean readComplete, boolean containsUrlCharacters) {
            this.value = value;
            this.readComplete = readComplete;
            this.containsUrlCharacters = containsUrlCharacters;
            this.header = null;
        }

        private StringHolder(HttpString value) {
            this.value = null;
            this.readComplete = true;
            this.header = value;
            this.containsUrlCharacters = false;
        }
    }
}
