/*
 * 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.ingest;

import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
import com.carrotsearch.randomizedtesting.generators.RandomPicks;
import com.carrotsearch.randomizedtesting.generators.RandomStrings;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;

public final class RandomDocumentPicks {

    private RandomDocumentPicks() {

    }

    /**
     * Returns a random field name. Can be a leaf field name or the
     * path to refer to a field name using the dot notation.
     */
    public static String randomFieldName(Random random) {
        int numLevels = RandomNumbers.randomIntBetween(random, 1, 5);
        String fieldName = "";
        for (int i = 0; i < numLevels; i++) {
            if (i > 0) {
                fieldName += ".";
            }
            fieldName += randomString(random);
        }
        return fieldName;
    }

    /**
     * Returns a random leaf field name.
     */
    public static String randomLeafFieldName(Random random) {
        String fieldName;
        do {
            fieldName = randomString(random);
        } while (fieldName.contains("."));
        return fieldName;
    }

    /**
     * Returns a randomly selected existing field name out of the fields that are contained
     * in the document provided as an argument.
     */
    public static String randomExistingFieldName(Random random, IngestDocument ingestDocument) {
        Map<String, Object> source = new TreeMap<>(ingestDocument.getSourceAndMetadata());
        Map.Entry<String, Object> randomEntry = RandomPicks.randomFrom(random, source.entrySet());
        String key = randomEntry.getKey();
        while (randomEntry.getValue() instanceof Map) {
            @SuppressWarnings("unchecked")
            Map<String, Object> map = (Map<String, Object>) randomEntry.getValue();
            Map<String, Object> treeMap = new TreeMap<>(map);
            randomEntry = RandomPicks.randomFrom(random, treeMap.entrySet());
            key += "." + randomEntry.getKey();
        }
        assert ingestDocument.getFieldValue(key, Object.class) != null;
        return key;
    }

    /**
     * Adds a random non existing field to the provided document and associates it
     * with the provided value. The field will be added at a random position within the document,
     * not necessarily at the top level using a leaf field name.
     */
    public static String addRandomField(Random random, IngestDocument ingestDocument, Object value) {
        String fieldName;
        do {
            fieldName = randomFieldName(random);
        } while (canAddField(fieldName, ingestDocument) == false);
        ingestDocument.setFieldValue(fieldName, value);
        return fieldName;
    }

    /**
     * Checks whether the provided field name can be safely added to the provided document.
     * When the provided field name holds the path using the dot notation, we have to make sure
     * that each node of the tree either doesn't exist or is a map, otherwise new fields cannot be added.
     */
    public static boolean canAddField(String path, IngestDocument ingestDocument) {
        String[] pathElements = path.split("\\.");
        Map<String, Object> innerMap = ingestDocument.getSourceAndMetadata();
        if (pathElements.length > 1) {
            for (int i = 0; i < pathElements.length - 1; i++) {
                Object currentLevel = innerMap.get(pathElements[i]);
                if (currentLevel == null) {
                    return true;
                }
                if (currentLevel instanceof Map == false) {
                    return false;
                }
                @SuppressWarnings("unchecked")
                Map<String, Object> map = (Map<String, Object>) currentLevel;
                innerMap = map;
            }
        }
        String leafKey = pathElements[pathElements.length - 1];
        return innerMap.containsKey(leafKey) == false;
    }

    /**
     * Generates a random document and random metadata
     */
    public static IngestDocument randomIngestDocument(Random random) {
        return randomIngestDocument(random, randomSource(random));
    }

    /**
     * Generates a document that holds random metadata and the document provided as a map argument
     */
    public static IngestDocument randomIngestDocument(Random random, Map<String, Object> source) {
        String index = randomString(random);
        String type = randomString(random);
        String id = randomString(random);
        String routing = null;
        if (random.nextBoolean()) {
            routing = randomString(random);
        }
        String parent = null;
        if (random.nextBoolean()) {
            parent = randomString(random);
        }
        return new IngestDocument(index, type, id, routing, parent, source);
    }

    public static Map<String, Object> randomSource(Random random) {
        Map<String, Object> document = new HashMap<>();
        addRandomFields(random, document, 0);
        return document;
    }

    /**
     * Generates a random field value, can be a string, a number, a list of an object itself.
     */
    public static Object randomFieldValue(Random random) {
        return randomFieldValue(random, 0);
    }

    private static Object randomFieldValue(Random random, int currentDepth) {
        switch(RandomNumbers.randomIntBetween(random, 0, 9)) {
            case 0:
                return randomString(random);
            case 1:
                return random.nextInt();
            case 2:
                return random.nextBoolean();
            case 3:
                return random.nextDouble();
            case 4:
                List<String> stringList = new ArrayList<>();
                int numStringItems = RandomNumbers.randomIntBetween(random, 1, 10);
                for (int j = 0; j < numStringItems; j++) {
                    stringList.add(randomString(random));
                }
                return stringList;
            case 5:
                List<Integer> intList = new ArrayList<>();
                int numIntItems = RandomNumbers.randomIntBetween(random, 1, 10);
                for (int j = 0; j < numIntItems; j++) {
                    intList.add(random.nextInt());
                }
                return intList;
            case 6:
                List<Boolean> booleanList = new ArrayList<>();
                int numBooleanItems = RandomNumbers.randomIntBetween(random, 1, 10);
                for (int j = 0; j < numBooleanItems; j++) {
                    booleanList.add(random.nextBoolean());
                }
                return booleanList;
            case 7:
                List<Double> doubleList = new ArrayList<>();
                int numDoubleItems = RandomNumbers.randomIntBetween(random, 1, 10);
                for (int j = 0; j < numDoubleItems; j++) {
                    doubleList.add(random.nextDouble());
                }
                return doubleList;
            case 8:
                Map<String, Object> newNode = new HashMap<>();
                addRandomFields(random, newNode, ++currentDepth);
                return newNode;
            case 9:
                byte[] byteArray = new byte[RandomNumbers.randomIntBetween(random, 1, 1024)];
                random.nextBytes(byteArray);
                return byteArray;
            default:
                throw new UnsupportedOperationException();
        }
    }

    public static String randomString(Random random) {
        if (random.nextBoolean()) {
            return RandomStrings.randomAsciiOfLengthBetween(random, 1, 10);
        }
        return RandomStrings.randomUnicodeOfCodepointLengthBetween(random, 1, 10);
    }

    private static void addRandomFields(Random random, Map<String, Object> parentNode, int currentDepth) {
        if (currentDepth > 5) {
            return;
        }
        int numFields = RandomNumbers.randomIntBetween(random, 1, 10);
        for (int i = 0; i < numFields; i++) {
            String fieldName = randomLeafFieldName(random);
            Object fieldValue = randomFieldValue(random, currentDepth);
            parentNode.put(fieldName, fieldValue);
        }
    }
}
