/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.common.xcontent.support;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.regex.Regex;
import org.elasticsearch.common.unit.TimeValue;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 *
 */
public class XContentMapValues {

    /**
     * Extracts raw values (string, int, and so on) based on the path provided returning all of them
     * as a single list.
     */
    public static List<Object> extractRawValues(String path, Map<String, Object> map) {
        List<Object> values = Lists.newArrayList();
        String[] pathElements = Strings.splitStringToArray(path, '.');
        if (pathElements.length == 0) {
            return values;
        }
        extractRawValues(values, map, pathElements, 0);
        return values;
    }

    @SuppressWarnings({"unchecked"})
    private static void extractRawValues(List values, Map<String, Object> part, String[] pathElements, int index) {
        if (index == pathElements.length) {
            return;
        }

        String key = pathElements[index];
        Object currentValue = part.get(key);
        int nextIndex = index + 1;
        while (currentValue == null && nextIndex != pathElements.length) {
            key += "." + pathElements[nextIndex];
            currentValue = part.get(key);
            nextIndex++;
        }

        if (currentValue == null) {
            return;
        }

        if (currentValue instanceof Map) {
            extractRawValues(values, (Map<String, Object>) currentValue, pathElements, nextIndex);
        } else if (currentValue instanceof List) {
            extractRawValues(values, (List) currentValue, pathElements, nextIndex);
        } else {
            values.add(currentValue);
        }
    }

    @SuppressWarnings({"unchecked"})
    private static void extractRawValues(List values, List<Object> part, String[] pathElements, int index) {
        for (Object value : part) {
            if (value == null) {
                continue;
            }
            if (value instanceof Map) {
                extractRawValues(values, (Map<String, Object>) value, pathElements, index);
            } else if (value instanceof List) {
                extractRawValues(values, (List) value, pathElements, index);
            } else {
                values.add(value);
            }
        }
    }

    public static Object extractValue(String path, Map<String, Object> map) {
        String[] pathElements = Strings.splitStringToArray(path, '.');
        if (pathElements.length == 0) {
            return null;
        }
        return extractValue(pathElements, 0, map);
    }

    @SuppressWarnings({"unchecked"})
    private static Object extractValue(String[] pathElements, int index, Object currentValue) {
        if (index == pathElements.length) {
            return currentValue;
        }
        if (currentValue == null) {
            return null;
        }
        if (currentValue instanceof Map) {
            Map map = (Map) currentValue;
            String key = pathElements[index];
            Object mapValue = map.get(key);
            int nextIndex = index + 1;
            while (mapValue == null && nextIndex != pathElements.length) {
                key += "." + pathElements[nextIndex];
                mapValue = map.get(key);
                nextIndex++;
            }
            return extractValue(pathElements, nextIndex, mapValue);
        }
        if (currentValue instanceof List) {
            List valueList = (List) currentValue;
            List newList = new ArrayList(valueList.size());
            for (Object o : valueList) {
                Object listValue = extractValue(pathElements, index, o);
                if (listValue != null) {
                    newList.add(listValue);
                }
            }
            return newList;
        }
        return null;
    }

    public static Map<String, Object> filter(Map<String, Object> map, String[] includes, String[] excludes) {
        Map<String, Object> result = Maps.newHashMap();
        filter(map, result, includes == null ? Strings.EMPTY_ARRAY : includes, excludes == null ? Strings.EMPTY_ARRAY : excludes, new StringBuilder());
        return result;
    }

    private static void filter(Map<String, Object> map, Map<String, Object> into, String[] includes, String[] excludes, StringBuilder sb) {
        if (includes.length == 0 && excludes.length == 0) {
            into.putAll(map);
            return;
        }
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            String key = entry.getKey();
            int mark = sb.length();
            if (sb.length() > 0) {
                sb.append('.');
            }
            sb.append(key);
            String path = sb.toString();

            if (Regex.simpleMatch(excludes, path)) {
                sb.setLength(mark);
                continue;
            }

            boolean exactIncludeMatch = false; // true if the current position was specifically mentioned
            boolean pathIsPrefixOfAnInclude = false; // true if potentially a sub scope can be included
            if (includes.length == 0) {
                // implied match anything
                exactIncludeMatch = true;
            } else {
                for (String include : includes) {
                    // check for prefix matches as well to see if we need to zero in, something like: obj1.arr1.* or *.field
                    // note, this does not work well with middle matches, like obj1.*.obj3
                    if (include.charAt(0) == '*') {
                        if (Regex.simpleMatch(include, path)) {
                            exactIncludeMatch = true;
                            break;
                        }
                        pathIsPrefixOfAnInclude = true;
                        continue;
                    }
                    if (include.startsWith(path)) {
                        if (include.length() == path.length()) {
                            exactIncludeMatch = true;
                            break;
                        } else if (include.length() > path.length() && include.charAt(path.length()) == '.') {
                            // include might may match deeper paths. Dive deeper.
                            pathIsPrefixOfAnInclude = true;
                            continue;
                        }
                    }
                    if (Regex.simpleMatch(include, path)) {
                        exactIncludeMatch = true;
                        break;
                    }
                }
            }

            if (!(pathIsPrefixOfAnInclude || exactIncludeMatch)) {
                // skip subkeys, not interesting.
                sb.setLength(mark);
                continue;
            }


            if (entry.getValue() instanceof Map) {
                Map<String, Object> innerInto = Maps.newHashMap();
                // if we had an exact match, we want give deeper excludes their chance
                filter((Map<String, Object>) entry.getValue(), innerInto, exactIncludeMatch ? Strings.EMPTY_ARRAY : includes, excludes, sb);
                if (exactIncludeMatch || !innerInto.isEmpty()) {
                    into.put(entry.getKey(), innerInto);
                }
            } else if (entry.getValue() instanceof List) {
                List<Object> list = (List<Object>) entry.getValue();
                List<Object> innerInto = new ArrayList<>(list.size());
                // if we had an exact match, we want give deeper excludes their chance
                filter(list, innerInto, exactIncludeMatch ? Strings.EMPTY_ARRAY : includes, excludes, sb);
                into.put(entry.getKey(), innerInto);
            } else if (exactIncludeMatch) {
                into.put(entry.getKey(), entry.getValue());
            }
            sb.setLength(mark);
        }
    }

    private static void filter(List<Object> from, List<Object> to, String[] includes, String[] excludes, StringBuilder sb) {
        if (includes.length == 0 && excludes.length == 0) {
            to.addAll(from);
            return;
        }

        for (Object o : from) {
            if (o instanceof Map) {
                Map<String, Object> innerInto = Maps.newHashMap();
                filter((Map<String, Object>) o, innerInto, includes, excludes, sb);
                if (!innerInto.isEmpty()) {
                    to.add(innerInto);
                }
            } else if (o instanceof List) {
                List<Object> innerInto = new ArrayList<>();
                filter((List<Object>) o, innerInto, includes, excludes, sb);
                if (!innerInto.isEmpty()) {
                    to.add(innerInto);
                }
            } else {
                to.add(o);
            }
        }
    }

    public static boolean isObject(Object node) {
        return node instanceof Map;
    }

    public static boolean isArray(Object node) {
        return node instanceof List;
    }

    public static String nodeStringValue(Object node, String defaultValue) {
        if (node == null) {
            return defaultValue;
        }
        return node.toString();
    }

    public static float nodeFloatValue(Object node, float defaultValue) {
        if (node == null) {
            return defaultValue;
        }
        return nodeFloatValue(node);
    }

    public static float nodeFloatValue(Object node) {
        if (node instanceof Number) {
            return ((Number) node).floatValue();
        }
        return Float.parseFloat(node.toString());
    }

    public static double nodeDoubleValue(Object node, double defaultValue) {
        if (node == null) {
            return defaultValue;
        }
        return nodeDoubleValue(node);
    }

    public static double nodeDoubleValue(Object node) {
        if (node instanceof Number) {
            return ((Number) node).doubleValue();
        }
        return Double.parseDouble(node.toString());
    }

    public static int nodeIntegerValue(Object node) {
        if (node instanceof Number) {
            return ((Number) node).intValue();
        }
        return Integer.parseInt(node.toString());
    }

    public static int nodeIntegerValue(Object node, int defaultValue) {
        if (node == null) {
            return defaultValue;
        }
        if (node instanceof Number) {
            return ((Number) node).intValue();
        }
        return Integer.parseInt(node.toString());
    }

    public static short nodeShortValue(Object node, short defaultValue) {
        if (node == null) {
            return defaultValue;
        }
        return nodeShortValue(node);
    }

    public static short nodeShortValue(Object node) {
        if (node instanceof Number) {
            return ((Number) node).shortValue();
        }
        return Short.parseShort(node.toString());
    }

    public static byte nodeByteValue(Object node, byte defaultValue) {
        if (node == null) {
            return defaultValue;
        }
        return nodeByteValue(node);
    }

    public static byte nodeByteValue(Object node) {
        if (node instanceof Number) {
            return ((Number) node).byteValue();
        }
        return Byte.parseByte(node.toString());
    }

    public static long nodeLongValue(Object node, long defaultValue) {
        if (node == null) {
            return defaultValue;
        }
        return nodeLongValue(node);
    }

    public static long nodeLongValue(Object node) {
        if (node instanceof Number) {
            return ((Number) node).longValue();
        }
        return Long.parseLong(node.toString());
    }

    public static boolean nodeBooleanValue(Object node, boolean defaultValue) {
        if (node == null) {
            return defaultValue;
        }
        return nodeBooleanValue(node);
    }

    public static boolean nodeBooleanValue(Object node) {
        if (node instanceof Boolean) {
            return (Boolean) node;
        }
        if (node instanceof Number) {
            return ((Number) node).intValue() != 0;
        }
        String value = node.toString();
        return !(value.equals("false") || value.equals("0") || value.equals("off"));
    }

    public static TimeValue nodeTimeValue(Object node, TimeValue defaultValue) {
        if (node == null) {
            return defaultValue;
        }
        return nodeTimeValue(node);
    }

    public static TimeValue nodeTimeValue(Object node) {
        if (node instanceof Number) {
            return TimeValue.timeValueMillis(((Number) node).longValue());
        }
        return TimeValue.parseTimeValue(node.toString(), null);
    }

    public static Map<String, Object> nodeMapValue(Object node, String desc) {
        if (node instanceof Map) {
            return (Map<String, Object>) node;
        } else {
            throw new ElasticsearchParseException(desc + " should be a hash but was of type: " + node.getClass());
        }
    }

    /**
     * Returns an array of string value from a node value.
     *
     * If the node represents an array the corresponding array of strings is returned.
     * Otherwise the node is treated as a comma-separated string.
     */
    public static String[] nodeStringArrayValue(Object node) {
        if (isArray(node)) {
            List list = (List) node;
            String[] arr = new String[list.size()];
            for (int i = 0; i < arr.length; i++) {
                arr[i] = nodeStringValue(list.get(i), null);
            }
            return arr;
        } else {
            return Strings.splitStringByCommaToArray(node.toString());
        }
    }
}
