/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed 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 test.java.util.Map;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.function.Supplier;

import org.testng.annotations.DataProvider;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.assertEquals;

public class MapWithCollisionsProviders {

    private static final int TEST_SIZE
            = Boolean.valueOf(System.getProperty("test.map.collisions.shortrun"))
            ? 2500
            : 5000;

    private static final IntKey EXTRA_INTKEY_VAL
            = new IntKey(TEST_SIZE, Integer.MAX_VALUE);

    private static final String EXTRA_STRING_VAL = "Extra Value";

    public static final class IntKey implements Comparable<IntKey> {

        private final int value;
        private final int hashmask; //yes duplication

        IntKey(int value, int hashmask) {
            this.value = value;
            this.hashmask = hashmask;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof IntKey) {
                IntKey other = (IntKey) obj;

                return other.value == value;
            }

            return false;
        }

        @Override
        public int hashCode() {
            return value % hashmask;
        }

        @Override
        public int compareTo(IntKey o) {
            return value - o.value;
        }

        @Override
        public String toString() {
            return Integer.toString(value);
        }

        public int getValue() {
            return value;
        }
    }

    private static Object[] createUniqueObjectKeys() {
        IntKey UNIQUE_OBJECTS[] = new IntKey[TEST_SIZE];
        for (int i = 0; i < TEST_SIZE; i++) {
            UNIQUE_OBJECTS[i] = new IntKey(i, Integer.MAX_VALUE);
        }
        return UNIQUE_OBJECTS;
    }

    private static Object[] createUniqueStringKeys() {
        String UNIQUE_STRINGS[] = new String[TEST_SIZE];
        for (int i = 0; i < TEST_SIZE; i++) {
            UNIQUE_STRINGS[i] = unhash(i);
        }
        return UNIQUE_STRINGS;
    }

    private static Object[] createCollidingObjectKeys() {
        IntKey COLLIDING_OBJECTS[] = new IntKey[TEST_SIZE];
        for (int i = 0; i < TEST_SIZE; i++) {
            COLLIDING_OBJECTS[i] = new IntKey(i, 10);
        }
        return COLLIDING_OBJECTS;
    }

    private static Object[] createCollidingStringKeys() {
        String COLLIDING_STRINGS[] = new String[TEST_SIZE];
        String UNIQUE_STRINGS[] = new String[TEST_SIZE];
        for (int i = 0; i < TEST_SIZE; i++) {
            UNIQUE_STRINGS[i] = unhash(i);
            COLLIDING_STRINGS[i] = (0 == i % 2)
                    ? UNIQUE_STRINGS[i / 2]
                    : "\u0000\u0000\u0000\u0000\u0000" + COLLIDING_STRINGS[i - 1];
        }
        return COLLIDING_STRINGS;
    }

    /**
     * Returns a string with a hash equal to the argument.
     *
     * @return string with a hash equal to the argument.
     */
    private static String unhash(int target) {
        StringBuilder answer = new StringBuilder();
        if (target < 0) {
            // String with hash of Integer.MIN_VALUE, 0x80000000
            answer.append("\\u0915\\u0009\\u001e\\u000c\\u0002");

            if (target == Integer.MIN_VALUE) {
                return answer.toString();
            }
            // Find target without sign bit set
            target = target & Integer.MAX_VALUE;
        }

        unhash0(answer, target);
        return answer.toString();
    }

    private static void unhash0(StringBuilder partial, int target) {
        int div = target / 31;
        int rem = target % 31;

        if (div <= Character.MAX_VALUE) {
            if (div != 0) {
                partial.append((char) div);
            }
            partial.append((char) rem);
        } else {
            unhash0(partial, div);
            partial.append((char) rem);
        }
    }

    private static <T> Map<T, T> fillMap(Map<T, T> m, T[] keys) {
        for (T k : keys) {
            m.put(k, k);
            assertTrue(m.containsKey(k));
            assertTrue(m.containsValue(k));
        }
        assertEquals(m.size(), keys.length);
        return m;
    }

    private static <T> Supplier<Map<T, T>> createMap(Map<T, T> m, T[] keys) {
        return () -> fillMap(m, keys);
    }

    private static <T> Object[] createCase(String desc, Map<T, T> m, T[] keys, T val) {
        return new Object[]{desc, createMap(m, keys), val};
    }

    private static <T> Collection<Object[]> makeMapsMoreTypes(String desc,
            T[] keys,
            T val) {
        Collection<Object[]> cases = new ArrayList<>();
        cases.add(createCase("Hashtable with " + desc,
                new Hashtable<>(), keys, val));
        cases.add(createCase("IdentityHashMap with " + desc,
                new IdentityHashMap<>(), keys, val));
        cases.add(createCase("TreeMap with " + desc,
                new TreeMap<>(), keys, val));
        cases.add(createCase("WeakHashMap with " + desc,
                new WeakHashMap<>(), keys, val));
        cases.add(createCase("ConcurrentHashMap with " + desc,
                new ConcurrentHashMap<>(), keys, val));
        cases.add(createCase("ConcurrentSkipListMap with " + desc,
                new ConcurrentSkipListMap<>(), keys, val));
        return cases;
    }

    private static <T> Collection<Object[]> makeMapsHashMap(String desc,
            T[] keys,
            T val) {
        Collection<Object[]> cases = new ArrayList<>();
        cases.add(createCase("HashMap with " + desc,
                new HashMap<>(), keys, val));
        cases.add(createCase("LinkedHashMap with " + desc,
                new LinkedHashMap<>(), keys, val));
        return cases;
    }

    private static <T> Collection<Object[]> makeMaps(String desc, T[] keys, T val) {
        Collection<Object[]> cases = new ArrayList<>();
        cases.addAll(makeMapsHashMap(desc, keys, val));
        cases.addAll(makeMapsMoreTypes(desc, keys, val));
        return cases;
    }

    private static <T> Collection<Object[]> makeObjectsCases(String desc, T[] keys) {
        return makeMaps(desc, keys, EXTRA_INTKEY_VAL);
    }

    private static <T> Collection<Object[]> makeStringsCases(String desc,
            T[] keys) {
        return makeMaps(desc, keys, EXTRA_STRING_VAL);
    }

    private static final Collection<Object[]> mapsWithObjectsCases
            = new ArrayList<>() {
        {
            addAll(makeObjectsCases("unique objects", createUniqueObjectKeys()));
            addAll(makeObjectsCases("colliding objects", createCollidingObjectKeys()));
        }
    };

    private static final Collection<Object[]> mapsWithStringsCases
            = new ArrayList<>() {
        {
            addAll(makeStringsCases("unique strings", createUniqueStringKeys()));
            addAll(makeStringsCases("colliding strings", createCollidingStringKeys()));
        }
    };

    private static final Collection<Object[]> mapsWithObjectsAndStringsCases
            = new ArrayList<>() {
        {
            addAll(mapsWithObjectsCases);
            addAll(mapsWithStringsCases);
        }
    };

    private static final Collection<Object[]> hashMapsWithObjectsCases
            = new ArrayList<>() {
        {
            addAll(makeMapsHashMap("unique objects",
                    createUniqueObjectKeys(), EXTRA_INTKEY_VAL));
            addAll(makeMapsHashMap("collisions objects",
                    createCollidingObjectKeys(), EXTRA_INTKEY_VAL));
        }
    };

    @DataProvider
    public Iterator<Object[]> mapsWithObjects() {
        return mapsWithObjectsCases.iterator();
    }

    @DataProvider
    public Iterator<Object[]> mapsWithStrings() {
        return mapsWithStringsCases.iterator();
    }

    @DataProvider
    public Iterator<Object[]> mapsWithObjectsAndStrings() {
        return mapsWithObjectsAndStringsCases.iterator();
    }

    @DataProvider
    public Iterator<Object[]> hashMapsWithObjects() {
        return hashMapsWithObjectsCases.iterator();
    }

}