package org.jruby.puma;

import org.jruby.util.ByteList;

import java.nio.charset.StandardCharsets;

/**
 * A set of keys representing HTTP/1.1 header fields and CGI env keys.
 *
 * The set of header fields is derived from RFC 2616 with some additions
 * common to mainstream browsers.
 *
 * This enum is used by {@link Http11 }to access pre-allocated strings for
 * Puma's version of these keys and includes a perfect hash function to quickly
 * find the key for a given range of bytes.
 */
public enum EnvKey {
    ACCEPT,
    ACCEPT_CHARSET,
    ACCEPT_ENCODING,
    ACCEPT_LANGUAGE,
    ACCEPT_RANGES,
    AGE,
    ALLOW,
    AUTHORIZATION,
    CACHE_CONTROL,
    CONNECTION,
    CONTENT_ENCODING,
    CONTENT_LANGUAGE,
    CONTENT_LENGTH(true),
    CONTENT_LOCATION,
    CONTENT_MD5,
    CONTENT_RANGE,
    CONTENT_TYPE(true),
    DATE,
    ETAG,
    EXPECT,
    EXPIRES,
    FRAGMENT(true),
    FROM,
    HOST,
    IF_MATCH,
    IF_MODIFIED_SINCE,
    IF_NONE_MATCH,
    IF_RANGE,
    IF_UNMODIFIED_SINCE,
    KEEP_ALIVE,
    LAST_MODIFIED,
    LOCATION,
    MAX_FORWARDS,
    PRAGMA,
    PROXY_AUTHENTICATE,
    PROXY_AUTHORIZATION,
    QUERY_STRING(true),
    RANGE,
    REFERER,
    REQUEST_METHOD(true),
    REQUEST_PATH(true),
    REQUEST_URI(true),
    RETRY_AFTER,
    SERVER,
    SERVER_PROTOCOL(true),
    TE,
    TRAILER,
    TRANSFER_ENCODING,
    UPGRADE,
    USER_AGENT,
    VARY,
    VIA,
    X_FORWARDED_FOR,
    X_REAL_IP,
    WARNING,
    WWW_AUTHENTICATE;

    final ByteList httpName;

    EnvKey() {
        this(false);
    }

    EnvKey(boolean raw) {
        String httpName = raw ? name() : "HTTP_" + name();
        this.httpName = new ByteList(ByteList.plain(httpName), false);
    }

    /**
     * Using a perfect hash, find the EnvKey for a given field name, or null if none matches.
     *
     * Generated by the gperf tool as C and adapted to Java. Must be regenerated if the set of keys changes.
     *
     * @param buffer the buffer containing the field name
     * @param start  the starting offset of the field name
     * @param len    the length of the field name
     * @return the matching EnvKey, or else null
     */
    public static EnvKey keyForField(byte[] buffer, int start, int len) {
        int key = hash(buffer, start, len);

        if (key <= MAX_HASH_VALUE) {
            EnvKey match = keyList[key];

            if (match != null && equalsIgnoreCase(buffer, start, len, match.name())) {
                return match;
            }
        }
        return null;
    }

    private static boolean equalsIgnoreCase(byte[] buf, int start, int len, String key) {
        int slen = key.length();
        if (len != slen) return false;
        for (int i = 0; i < slen; i++) {
            byte b1 = buf[start + i];
            byte b2 = (byte) key.charAt(i);
            if (Http11.snakeUpcase(b1) != b2) return false;
        }
        return true;
    }

    private static int hash(byte[] buffer, int start, int len) {
        return len
                + asso_values[Byte.toUnsignedInt(Http11.snakeUpcase(buffer[start + len - 1]))]
                + asso_values[Byte.toUnsignedInt(Http11.snakeUpcase(buffer[start]))];
    }

    private static final int MAX_HASH_VALUE = 94;

    private final static int[] asso_values = {
            95, 95, 95, 95, 95, 95, 95, 95, 95, 95,
            95, 95, 95, 95, 95, 95, 95, 95, 95, 95,
            95, 95, 95, 95, 95, 95, 95, 95, 95, 95,
            95, 95, 95, 95, 95, 95, 95, 95, 95, 95,
            95, 95, 95, 95, 95, 95, 95, 95, 95, 95,
            95, 95, 95, 10, 95, 95, 95, 95, 95, 95,
            95, 95, 95, 95, 95, 5, 95, 15, 40, 0,
            0, 10, 10, 15, 95, 0, 20, 5, 25, 95,
            50, 40, 0, 40, 10, 35, 5, 35, 0, 10,
            95, 95, 95, 95, 95, 95, 95, 95, 95, 95,
            95, 95, 95, 95, 95, 95, 95, 95, 95, 95,
            95, 95, 95, 95, 95, 95, 95, 95, 95, 95,
            95, 95, 95, 95, 95, 95, 95, 95, 95, 95,
            95, 95, 95, 95, 95, 95, 95, 95, 95, 95,
            95, 95, 95, 95, 95, 95, 95, 95, 95, 95,
            95, 95, 95, 95, 95, 95, 95, 95, 95, 95,
            95, 95, 95, 95, 95, 95, 95, 95, 95, 95,
            95, 95, 95, 95, 95, 95, 95, 95, 95, 95,
            95, 95, 95, 95, 95, 95, 95, 95, 95, 95,
            95, 95, 95, 95, 95, 95, 95, 95, 95, 95,
            95, 95, 95, 95, 95, 95, 95, 95, 95, 95,
            95, 95, 95, 95, 95, 95, 95, 95, 95, 95,
            95, 95, 95, 95, 95, 95, 95, 95, 95, 95,
            95, 95, 95, 95, 95, 95, 95, 95, 95, 95,
            95, 95, 95, 95, 95, 95, 95, 95, 95, 95,
            95, 95, 95, 95, 95, 95
    };

    private static final EnvKey[] keyList = {
            null, null, null, null, null,
            EnvKey.RANGE,
            null,
            EnvKey.REFERER,
            EnvKey.AGE,
            EnvKey.FROM,
            EnvKey.KEEP_ALIVE,
            EnvKey.RETRY_AFTER,
            EnvKey.TE,
            EnvKey.VIA,
            EnvKey.ETAG,
            EnvKey.X_FORWARDED_FOR,
            EnvKey.EXPECT,
            EnvKey.TRAILER,
            EnvKey.FRAGMENT,
            EnvKey.VARY,
            EnvKey.ACCEPT_LANGUAGE,
            EnvKey.ACCEPT,
            EnvKey.REQUEST_PATH,
            EnvKey.IF_RANGE,
            EnvKey.HOST,
            null,
            EnvKey.REQUEST_URI,
            EnvKey.CONTENT_TYPE,
            EnvKey.CONTENT_RANGE,
            EnvKey.ACCEPT_CHARSET,
            EnvKey.ACCEPT_ENCODING,
            EnvKey.CONTENT_LANGUAGE,
            EnvKey.IF_MODIFIED_SINCE,
            EnvKey.IF_MATCH,
            EnvKey.IF_UNMODIFIED_SINCE,
            null,
            EnvKey.CONTENT_MD5,
            EnvKey.TRANSFER_ENCODING,
            EnvKey.IF_NONE_MATCH,
            EnvKey.CONTENT_LENGTH,
            null,
            EnvKey.CONTENT_ENCODING,
            EnvKey.UPGRADE,
            EnvKey.AUTHORIZATION,
            EnvKey.DATE,
            EnvKey.ALLOW,
            EnvKey.SERVER,
            EnvKey.EXPIRES,
            EnvKey.CACHE_CONTROL,
            null,
            EnvKey.CONNECTION,
            EnvKey.WWW_AUTHENTICATE,
            EnvKey.WARNING,
            EnvKey.LOCATION,
            EnvKey.REQUEST_METHOD,
            EnvKey.USER_AGENT,
            EnvKey.CONTENT_LOCATION,
            EnvKey.MAX_FORWARDS,
            EnvKey.ACCEPT_RANGES,
            EnvKey.X_REAL_IP,
            null,
            EnvKey.PRAGMA,
            EnvKey.QUERY_STRING,
            null, null, null, null, null,
            EnvKey.PROXY_AUTHENTICATE,
            null, null, null, null,
            EnvKey.LAST_MODIFIED,
            null,
            EnvKey.SERVER_PROTOCOL,
            null, null, null, null, null, null, null, null, null,
            null, null, null, null, null, null, null, null, null,
            EnvKey.PROXY_AUTHORIZATION
    };

    static {
        // The perfect hash function must be regenerated if the list above changes
        for (EnvKey key : values()) {
            EnvKey found;
            assert (found = keyForKey(key)) == key : key + " was not matched by perfect hash and found " + found;
        }
    }

    // Only used for verification of the perfect hash, at boot with asserts enabled.
    private static EnvKey keyForKey(EnvKey key) {
        byte[] bytes = key.name().getBytes(StandardCharsets.ISO_8859_1);
        return keyList[hash(bytes, 0, bytes.length)];
    }
}
