package org.ovirt.engine.core.utils.vdshooks;

import java.util.HashMap;
import java.util.Map;

import org.ovirt.engine.core.common.queries.ValueObjectMap;
import org.ovirt.engine.core.compat.StringHelper;
import org.ovirt.engine.core.utils.vdshooks.VdsHooksParser.ParsingResult;

/**
 * Parsers a hooks string to a ValueObjectMap of script directories/events to a ValueObjectMap of script names to a
 * ValueObjectMap of script properties The string is in format of a "toString" invocation on java.util.map object - for
 * example "{before_vm_hibernate={myscript.sh={md5=5d9c4609cd936e80bac8c9ef7b27ea73},
 * 01_log={md5=f388923356e84c2b4149572a44fde2b4}},
 * after_vm_migrate_destination={myscript.sh={md5=677da3bdd8fbd16d4b8917a9fe0f6f89}},
 * after_vm_cont={01_log={md5=f388923356e84c2b4149572a44fde2b4}}}"
 *
 *
 */
public class VdsHooksParser {

    public enum ParsingState {
        START_MAP,
        KEY,
        VALUE
    };

    public static class ParsingResult {
        private Map<String, Object> map;
        private int endIndex;

        public ParsingResult(Map<String, Object> map, int endIndex) {
            this.map = map;
            this.endIndex = endIndex;
        }

        public String toString() {
            return map.toString();
        }

        public Map<String, Object> getMap() {
            return map;
        }

        public int getEndIndex() {
            return endIndex;
        }
    }

    protected static ParsingResult parseMap(char[] chars, int startIndex) {
        if (chars[startIndex] != '{') {
            return null;
        }
        int endIndex = -1;
        StringBuilder keyBuilder = new StringBuilder();
        Object value = null;
        Map<String, Object> resultMap = new HashMap<String, Object>();
        ParsingState state = ParsingState.START_MAP;
        // Goes over all the characters starting at one position after the opening "{"
        for (int counter = startIndex + 1; counter < chars.length; counter++) {
            char current = chars[counter];
            if (current == ' ') {
                // Ignore spaces
                continue;
            }
            // If a closing "}" was found, break from the loop (in order to return the parsing result
            if (endIndex != -1) {
                break;
            }
            switch (state) {
            case START_MAP:
                // If the character is not "}" - then this is a character of a key in the map,
                // Change to "parsing key" state
                if (current != '}') {
                    state = ParsingState.KEY;
                    keyBuilder = new StringBuilder();
                    handleKey(keyBuilder, current);
                } else {
                    endIndex = counter;
                }
                break;
            case KEY:
                // If the character is = - it means the character is the start of a value which is either a recursive
                // map or a string
                // Switch to "parsing value" state
                if (current == '=') {
                    state = ParsingState.VALUE;
                    if (chars[counter + 1] != '{') {
                        value = new StringBuilder();
                    }
                } else { // Otherwise - continue to parse the key
                    handleKey(keyBuilder, current);
                }
                break;
            case VALUE:
                // If the character is "{" - this means this is the start of a recursive (inner) map
                if (current == '{') {
                    // Parse the inner map, and advance the counter to the end of the map, so its characters will not be
                    // re-parsed
                    value = parseMap(chars, counter);
                    counter = ((ParsingResult) value).getEndIndex();

                } else if (current == ',') {
                    // If the character is "," it means that this is the beginning of a next pair of key and value -
                    // return to "parsing key" state
                    value = putValueInMap(keyBuilder, value, resultMap);
                    keyBuilder = new StringBuilder();
                    state = ParsingState.KEY;
                } else if (current == '}') {
                    // If the character is "}" this is the closing of the map - change the value of endIndex not to be
                    // -1
                    endIndex = counter;
                    value = putValueInMap(keyBuilder, value, resultMap);
                } else {
                    handleValue((StringBuilder) value, current);
                }
                break;

            }
        }
        // At this point, the endIndex is not -1, return the parsing result
        ParsingResult result = new ParsingResult(resultMap, endIndex);
        return result;
    }

    private static Object putValueInMap(StringBuilder keyBuilder, Object value, Map<String, Object> resultMap) {
        if (value instanceof ParsingResult) {
            // Place the map that belongs to the parsing result returned from
            // the recursive call
            value = ((ParsingResult) value).getMap();
        } else {
            // value is StringBuilder, convert it to String
            value = value.toString();
        }
        resultMap.put(keyBuilder.toString(), value);
        return value;
    }

    private static void handleValue(StringBuilder valueBuilder, char current) {
        valueBuilder.append(current);
    }

    private static void handleKey(StringBuilder keyBuilder, char current) {
        keyBuilder.append(current);
    }

    /**
     * Method to parse a given hooks string and return a hooks data structure
     *
     * @param hooksStr
     *            hooks string to parse
     * @return hooks data structure
     */
    public static ValueObjectMap parseHooks(String hooksStr) {
        ValueObjectMap result = new ValueObjectMap();
        // If the hooks string is null or empty an empty data structure will be returned
        if (!StringHelper.isNullOrEmpty(hooksStr)) {
            char[] chars = new char[hooksStr.length()];
            hooksStr.getChars(0, hooksStr.length(), chars, 0);
            ParsingResult parsingResult = parseMap(chars, 0);
            return convertParsingResultToValueObjectMap(parsingResult);
        }
        return result;
    }

    public static Map<String, Object> parseHooks2(String hooksStr) {
        char[] chars = new char[hooksStr.length()];
        hooksStr.getChars(0, hooksStr.length(), chars, 0);
        ParsingResult parsingResult = parseMap(chars, 0);
        return parsingResult.getMap();
    }

    private static ValueObjectMap convertParsingResultToValueObjectMap(ParsingResult parsingResult) {
        Map<String, Object> map = parsingResult.getMap();
        ValueObjectMap result = new ValueObjectMap(map, true);
        return result;
    }
}
