// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.document;

import com.yahoo.document.datatypes.FieldValue;

/**
 * @author thomasg
 */
public class FieldPathEntry {
    public enum Type {
        STRUCT_FIELD,
        ARRAY_INDEX,
        MAP_KEY,
        MAP_ALL_KEYS,
        MAP_ALL_VALUES,
        VARIABLE
    }

    private final Type type;
    private final int lookupIndex;
    private final FieldValue lookupKey;
    private final String variableName;
    private final Field fieldRef;
    private final DataType resultingDataType;

    public DataType getResultingDataType() {
        return resultingDataType;
    }

    public Type getType() {
        return type;
    }

    public Field getFieldRef() {
        return fieldRef;
    }

    public int getLookupIndex() {
        return lookupIndex;
    }

    public FieldValue getLookupKey() {
        return lookupKey;
    }

    public String getVariableName() {
        return variableName;
    }

    public String toString() {
        String retVal = type.toString() + ": ";
        switch (type) {
            case STRUCT_FIELD:
                retVal += getFieldRef();
                break;
            case ARRAY_INDEX:
                retVal += getLookupIndex();
                break;
            case MAP_KEY:
                retVal += getLookupKey();
                break;
            case MAP_ALL_KEYS:
            case MAP_ALL_VALUES:
                break;
            case VARIABLE:
                retVal += getVariableName();
                break;
        }
        return retVal;
    }

    /**
     * Creates a new field path entry that references a struct field.
     * For these kinds of field path entries, getFieldRef() is valid.
     *
     * @param fieldRef The field to look up in the struct.
     * @return The new field path entry
     */
    public static FieldPathEntry newStructFieldEntry(Field fieldRef) {
        return new FieldPathEntry(fieldRef);
    }

    /**
     * Creates a new field path entry that references an array index.
     *
     * @param lookupIndex The index to look up
     * @param resultingDataType The datatype of the contents of the array
     * @return The new field path entry
     */
    public static FieldPathEntry newArrayLookupEntry(int lookupIndex, DataType resultingDataType) {
        return new FieldPathEntry(lookupIndex, resultingDataType);
    }

    /**
     * Creates a new field path entry that references a map or weighted set.
     *
     * @param lookupKey The value of the key in the map or weighted set to recurse into.
     * @param resultingDataType The datatype of values in the map or weighted set.
     * @return The new field path entry
     */
    public static FieldPathEntry newMapLookupEntry(FieldValue lookupKey, DataType resultingDataType) {
        return new FieldPathEntry(lookupKey, resultingDataType);
    }

    /**
     * Creates a new field path entry that digs through all the keys of a map or weighted set.
     *
     * @param resultingDataType The datatype of the keys in the map or weighted set.
     * @return The new field path entry.
     */
    public static FieldPathEntry newAllKeysLookupEntry(DataType resultingDataType) {
        return new FieldPathEntry(true, false, resultingDataType);
    }

    /**
     * Creates a new field path entry that digs through all the values of a map or weighted set.
     *
     * @param resultingDataType The datatype of the values in the map or weighted set.
     * @return The new field path entry.
     */
    public static FieldPathEntry newAllValuesLookupEntry(DataType resultingDataType) {
        return new FieldPathEntry(false, true, resultingDataType);
    }

    /**
     * Creates a new field path entry that digs through all the keys in a map or weighted set, or all the indexes of an array,
     * an sets the given variable name as it does so (or, if the variable is set, uses the set variable to look up the
     * collection.
     *
     * @param variableName The name of the variable to lookup in the collection
     * @param resultingDataType The value type of the collection we're digging through
     * @return The new field path entry.
     */
    public static FieldPathEntry newVariableLookupEntry(String variableName, DataType resultingDataType) {
        return new FieldPathEntry(variableName, resultingDataType);
    }

    private FieldPathEntry(Field fieldRef) {
        type = Type.STRUCT_FIELD;
        lookupIndex = 0;
        lookupKey = null;
        variableName = null;
        this.fieldRef = fieldRef;
        resultingDataType = fieldRef.getDataType();
    }

    private FieldPathEntry(int lookupIndex, DataType resultingDataType) {
        type = Type.ARRAY_INDEX;
        this.lookupIndex = lookupIndex;
        lookupKey = null;
        variableName = null;
        fieldRef = null;
        this.resultingDataType = resultingDataType;
    }

    private FieldPathEntry(FieldValue lookupKey, DataType resultingDataType) {
        type = Type.MAP_KEY;
        lookupIndex = 0;
        this.lookupKey = lookupKey;
        variableName = null;
        fieldRef = null;
        this.resultingDataType = resultingDataType;
    }

    private FieldPathEntry(boolean keysOnly, boolean valuesOnly, DataType resultingDataType) {
        type = keysOnly ? Type.MAP_ALL_KEYS : Type.MAP_ALL_VALUES;
        lookupIndex = 0;
        lookupKey = null;
        variableName = null;
        fieldRef = null;
        this.resultingDataType = resultingDataType;
    }

    private FieldPathEntry(String variableName, DataType resultingDataType) {
        type = Type.VARIABLE;
        lookupIndex = 0;
        lookupKey = null;
        this.variableName = variableName;
        fieldRef = null;
        this.resultingDataType = resultingDataType;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        FieldPathEntry that = (FieldPathEntry) o;

        if (lookupIndex != that.lookupIndex) return false;
        if (fieldRef != null ? !fieldRef.equals(that.fieldRef) : that.fieldRef != null) return false;
        if (lookupKey != null ? !lookupKey.equals(that.lookupKey) : that.lookupKey != null) return false;
        if (resultingDataType != null ? !resultingDataType.equals(that.resultingDataType) : that.resultingDataType != null)
            return false;
        if (type != that.type) return false;
        if (variableName != null ? !variableName.equals(that.variableName) : that.variableName != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = type.hashCode();
        result = 31 * result + lookupIndex;
        result = 31 * result + (lookupKey != null ? lookupKey.hashCode() : 0);
        result = 31 * result + (variableName != null ? variableName.hashCode() : 0);
        result = 31 * result + (fieldRef != null ? fieldRef.hashCode() : 0);
        result = 31 * result + (resultingDataType != null ? resultingDataType.hashCode() : 0);
        return result;
    }

    public static class KeyParseResult {
        public String parsed;
        public int consumedChars;

        public KeyParseResult(String parsed, int consumedChars) {
            this.parsed = parsed;
            this.consumedChars = consumedChars;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            KeyParseResult that = (KeyParseResult) o;

            if (consumedChars != that.consumedChars) return false;
            if (!parsed.equals(that.parsed)) return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = parsed.hashCode();
            result = 31 * result + consumedChars;
            return result;
        }

        @Override
        public String toString() {
            return "KeyParseResult(parsed=\"" + parsed + "\", consumedChars=" + consumedChars + ")";
        }
    }

    private static int parseQuotedString(String key, int offset,
                                          int len, StringBuilder builder)
    {
        for (; offset < len && key.charAt(offset) != '"'; ++offset) {
            if (key.charAt(offset) == '\\') {
                ++offset; // Skip escape backslash
                if (offset == len || key.charAt(offset) != '"') {
                    throw new IllegalArgumentException("Escaped key '" + key + "' has bad quote character escape sequence. Expected '\"'");
                }
            }
            if (offset < len) {
                builder.append(key.charAt(offset));
            }
        }
        if (offset < len && key.charAt(offset) == '"') {
            return offset + 1;
        } else {
            throw new IllegalArgumentException("Escaped key '" + key + "' is incomplete. No matching '\"'");
        }
    }

    private static int skipWhitespace(String str, int offset, int len) {
        while (offset < len && Character.isSpaceChar(str.charAt(offset))) {
            ++offset;
        }
        return offset;
    }

    /**
     * Parse a field path map key of the form {xyz} or {"xyz"} with optional trailing data.
     * If the key contains a '}' or '"' character, the key must be in quotes and all
     * double-quote characters must be escaped. Only '"' chars may be escaped. Any
     * trailing string data past the '}' is ignored.
     *
     * @param key Part of a field path that contains a key at its start
     * @return A parse result containing the parsed/unescaped key and the number
     *     of input characters the parse consumed. Does not include any characters
     *     beyond the '}' char.
     */
    public static KeyParseResult parseKey(String key) {
        StringBuilder parsed = new StringBuilder(key.length());
        // Hooray for ad-hoc parsing
        int len = key.length();
        int i = 0;
        if (i < len && key.charAt(0) == '{') {
            i = skipWhitespace(key, i + 1, len);
            if (i < len && key.charAt(i) == '"') {
                i = parseQuotedString(key, i + 1, len, parsed);
            } else {
                // No quoting, use all of string until '}' verbatim
                while (i < len && key.charAt(i) != '}') {
                    parsed.append(key.charAt(i));
                    ++i;
                }
            }
            i = skipWhitespace(key, i, len);
            if (i < len && key.charAt(i) == '}') {
                return new KeyParseResult(parsed.toString(), i + 1);
            } else {
                throw new IllegalArgumentException("Key '" + key + "' is incomplete. No matching '}'");
            }
        } else {
            throw new IllegalArgumentException("Key '" + key + "' does not start with '{'");
        }
    }
}
