// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 *******************************************************************************
 * Copyright (C) 1996-2016, International Business Machines Corporation and
 * others. All Rights Reserved.
 *******************************************************************************
 */
package com.ibm.icu.dev.test;

import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.StringJoiner;
import java.util.TreeMap;
import java.util.TreeSet;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import com.ibm.icu.impl.UnicodeMap;
import com.ibm.icu.impl.UnicodeMapIterator;
import com.ibm.icu.impl.Utility;
import com.ibm.icu.lang.UCharacter;
import com.ibm.icu.lang.UProperty;
import com.ibm.icu.text.UTF16;
import com.ibm.icu.text.UnicodeSet;

/**
 * @test
 * @summary General test of UnicodeSet
 */
@RunWith(JUnit4.class)
public class UnicodeMapTest extends TestFmwk {

    static final int MODIFY_TEST_LIMIT = 32;
    static final int MODIFY_TEST_ITERATIONS = 100000;

    @Test
    public void TestIterations() {
        UnicodeMap<Double> foo = new UnicodeMap<>();
        checkToString(foo, "");
        foo.put(3, 6d).put(5, 10d);
        checkToString(foo, "0003=6.0\n0005=10.0\n");
        foo.put(0x10FFFF, 666d);
        checkToString(foo, "0003=6.0\n0005=10.0\n10FFFF=666.0\n");
        foo.put("neg", -555d);
        checkToString(foo, "0003=6.0\n0005=10.0\n10FFFF=666.0\n006E,0065,0067=-555.0\n");

        double i = 0;
        for (UnicodeMap.EntryRange<Double> entryRange : foo.entryRanges()) {
            i += entryRange.value;
        }
        assertEquals("EntryRange<T>", 127d, i);
    }

    public void checkToString(UnicodeMap<Double> foo, String expected) {
        StringJoiner joiner = new StringJoiner("\n");
        for (UnicodeMap.EntryRange<Double> range : foo.entryRanges()) {
            joiner.add(range.toString());
        }
        assertEquals("EntryRange<T>", expected, joiner.toString() + (foo.size() == 0 ? "" : "\n"));
        assertEquals("EntryRange<T>", expected, foo.toString());
    }

    @Test
    public void TestRemove() {
        UnicodeMap<Double> foo = new UnicodeMap<Double>()
                .putAll(0x20, 0x29, -2d)
                .put("abc", 3d)
                .put("xy", 2d)
                .put("mark", 4d)
                .freeze();
        UnicodeMap<Double> fii = new UnicodeMap<Double>()
                .putAll(0x21, 0x25, -2d)
                .putAll(0x26, 0x28, -3d)
                .put("abc", 3d)
                .put("mark", 999d)
                .freeze();

        UnicodeMap<Double> afterFiiRemoval = new UnicodeMap<Double>()
                .put(0x20, -2d)
                .putAll(0x26, 0x29, -2d)
                .put("xy", 2d)
                .put("mark", 4d)
                .freeze();

        UnicodeMap<Double> afterFiiRetained = new UnicodeMap<Double>()
                .putAll(0x21, 0x25, -2d)
                .put("abc", 3d)
                .freeze();

        UnicodeMap<Double> test = new UnicodeMap<Double>().putAll(foo)
                .removeAll(fii);
        assertEquals("removeAll", afterFiiRemoval, test);

        test = new UnicodeMap<Double>().putAll(foo)
                .retainAll(fii);
        assertEquals("retainAll", afterFiiRetained, test);
    }

    @Test
    public void TestAMonkey() {
        SortedMap<String,Integer> stayWithMe = new TreeMap<String,Integer>(OneFirstComparator);

        UnicodeMap<Integer> me = new UnicodeMap<Integer>().putAll(stayWithMe);
        // check one special case, removal near end
        me.putAll(0x10FFFE, 0x10FFFF, 666);
        me.remove(0x10FFFF);

        int iterations = 100000;
        SortedMap<String,Integer> test = new TreeMap<>();

        Random rand = new Random(0);
        String other;
        Integer value;
        // try modifications
        for (int i = 0; i < iterations ; ++i) {
            switch(rand.nextInt(20)) {
            case 0:
                logln("clear");
                stayWithMe.clear();
                me.clear();
                break;
            case 1:
                fillRandomMap(rand, 5, test);
                logln("putAll\t" + test);
                stayWithMe.putAll(test);
                me.putAll(test);
                break;
            case 2: case 3: case 4: case 5: case 6: case 7: case 8:
                other = getRandomKey(rand);
                //                if (other.equals("\uDBFF\uDFFF") && me.containsKey(0x10FFFF) && me.get(0x10FFFF).equals(me.get(0x10FFFE))) {
                //                    System.out.println("Remove\t" + other + "\n" + me);
                //                }
                logln("remove\t" + other);
                stayWithMe.remove(other);
                try {
                    me.remove(other);
                } catch (IllegalArgumentException e) {
                    errln("remove\t" + other + "\tfailed: " + e.getMessage() + "\n" + me);
                    me.clear();
                    stayWithMe.clear();
                }
                break;
            default:
                other = getRandomKey(rand);
                value = rand.nextInt(50)+50;
                logln("put\t" + other + " = " + value);
                stayWithMe.put(other, value);
                me.put(other,value);
                break;
            }
            checkEquals(me, stayWithMe);
        }
    }

    /**
     * @param rand
     * @param nextInt
     * @param test
     * @return
     */
    private SortedMap<String, Integer> fillRandomMap(Random rand, int max, SortedMap<String, Integer> test) {
        test.clear();
        max = rand.nextInt(max);
        for (int i = 0; i < max; ++i) {
            test.put(getRandomKey(rand), rand.nextInt(50)+50);
        }
        return test;
    }

    /**
     * @param me
     * @param stayWithMe
     */
    private void checkEquals(UnicodeMap<Integer> me, SortedMap<String, Integer> stayWithMe) {
        Set<Map.Entry<String, Integer>> temp = new HashSet<>();
        for (Map.Entry<String, Integer> e : me.entrySet()) {
            temp.add(e);
        }
        Set<Map.Entry<String, Integer>> entrySet = stayWithMe.entrySet();
        if (!entrySet.equals(temp)) {
            logln(me.entrySet().toString());
            logln(me.toString());
            assertEquals("are in parallel", entrySet, temp);
            // we failed. Reset and start again
            entrySet.clear();
            temp.clear();
            return;
        }
        for (String key : stayWithMe.keySet()) {
            assertEquals("containsKey", stayWithMe.containsKey(key), me.containsKey(key));
            Integer value = stayWithMe.get(key);
            assertEquals("get", value, me.get(key));
            assertEquals("containsValue", stayWithMe.containsValue(value), me.containsValue(value));
            int cp = UnicodeSet.getSingleCodePoint(key);
            if (cp != Integer.MAX_VALUE) {
                assertEquals("get", value, me.get(cp));
            }
        }
        Set<String> nonCodePointStrings = stayWithMe.tailMap("").keySet();
        if (nonCodePointStrings.size() == 0) nonCodePointStrings = null; // for parallel api
        assertEquals("getNonRangeStrings", nonCodePointStrings, me.getNonRangeStrings());

        TreeSet<Integer> values = new TreeSet<Integer>(stayWithMe.values());
        TreeSet<Integer> myValues = new TreeSet<Integer>(me.values());
        assertEquals("values", myValues, values);

        for (String key : stayWithMe.keySet()) {
            assertEquals("containsKey", stayWithMe.containsKey(key), me.containsKey(key));
        }
    }

    static Comparator<String> OneFirstComparator = new Comparator<String>() {
        @Override
        public int compare(String o1, String o2) {
            int cp1 = UnicodeSet.getSingleCodePoint(o1);
            int cp2 = UnicodeSet.getSingleCodePoint(o2);
            int result = cp1 - cp2;
            if (result != 0) {
                return result;
            }
            if (cp1 == Integer.MAX_VALUE) {
                return o1.compareTo(o2);
            }
            return 0;
        }

    };

    /**
     * @param rand
     * @param others
     * @return
     */
    private String getRandomKey(Random rand) {
        int r = rand.nextInt(30);
        if (r == 0) {
            return UTF16.valueOf(r);
        } else if (r < 10) {
            return UTF16.valueOf('A'-1+r);
        } else if (r < 20) {
            return UTF16.valueOf(0x10FFFF - (r-10));
            //        } else if (r == 20) {
            //            return "";
        }
        return "a" + UTF16.valueOf(r + 'a'-1);
    }

    @Test
    public void TestModify() {
        Random random = new Random(0);
        UnicodeMap<String> unicodeMap = new UnicodeMap<>();
        HashMap<Integer, String> hashMap = new HashMap<>();
        String[] values = {null, "the", "quick", "brown", "fox"};
        for (int count = 1; count <= MODIFY_TEST_ITERATIONS; ++count) {
            String value = values[random.nextInt(values.length)];
            int start = random.nextInt(MODIFY_TEST_LIMIT); // test limited range
            int end = random.nextInt(MODIFY_TEST_LIMIT);
            if (start > end) {
                int temp = start;
                start = end;
                end = temp;
            }
            int modCount = count & 0xFF;
            if (modCount == 0 && isVerbose()) {
                logln("***"+count);
                logln(unicodeMap.toString());
            }
            unicodeMap.putAll(start, end, value);
            if (modCount == 1 && isVerbose()) {
                logln(">>>\t" + Utility.hex(start) + ".." + Utility.hex(end) + "\t" + value);
                logln(unicodeMap.toString());
            }
            for (int i = start; i <= end; ++i) {
                hashMap.put(i, value);
            }
            if (!hasSameValues(unicodeMap, hashMap)) {
                errln("Failed at " + count);
            }
        }
    }

    private boolean hasSameValues(UnicodeMap<String> unicodeMap, HashMap<Integer, String> hashMap) {
        for (int i = 0; i < MODIFY_TEST_LIMIT; ++i) {
            String unicodeMapValue = unicodeMap.getValue(i);
            String hashMapValue = hashMap.get(i);
            if (unicodeMapValue != hashMapValue) {
                return false;
            }
        }
        return true;
    }

    @Test
    public void TestCloneAsThawed11721 () {
        UnicodeMap<Integer> test = new UnicodeMap<Integer>().put("abc", 3).freeze();
        UnicodeMap<Integer> copy = test.cloneAsThawed();
        copy.put("def", 4);
        assertEquals("original-abc", (Integer) 3, test.get("abc"));
        assertNull("original-def", test.get("def"));
        assertEquals("copy-def", (Integer) 4, copy.get("def"));
    }

    private static final int LIMIT = 0x15; // limit to make testing more realistic in terms of collisions
    private static final int ITERATIONS = 1000000;
    private static final boolean SHOW_PROGRESS = false;
    private static final boolean DEBUG = false;

    SortedSet<String> log = new TreeSet<String>();
    static String[] TEST_VALUES = {"A", "B", "C", "D", "E", "F"};
    static Random random = new Random(12345);

    @Test
    public void TestUnicodeMapRandom() {
        // do random change to both, then compare
        random.setSeed(12345); // reproducible results
        logln("Comparing against HashMap");
        UnicodeMap<String> map1 = new UnicodeMap<>();
        Map<Integer, String> map2 = new HashMap<Integer, String>();
        for (int counter = 0; counter < ITERATIONS; ++counter) {
            int start = random.nextInt(LIMIT);
            String value = TEST_VALUES[random.nextInt(TEST_VALUES.length)];
            String logline = Utility.hex(start) + "\t" + value;
            if (SHOW_PROGRESS) logln(counter + "\t" + logline);
            log.add(logline);
            if (DEBUG && counter == 144) {
                System.out.println(" debug");
            }
            map1.put(start, value);
            map2.put(start, value);
            check(map1, map2, counter);
        }
        checkNext(map1, map2, LIMIT);
    }

    private static final int propEnum = UProperty.GENERAL_CATEGORY;

    @Test
    public void TestUnicodeMapGeneralCategory() {
        logln("Setting General Category");
        UnicodeMap<String> map1 = new UnicodeMap<>();
        Map<Integer, String> map2 = new HashMap<Integer, String>();
        //Map<Integer, String> map3 = new TreeMap<Integer, String>();
        map1 = new UnicodeMap<String>();
        map2 = new TreeMap<Integer,String>();
        for (int cp = 0;;) {
              int enumValue = UCharacter.getIntPropertyValue(cp, propEnum);
              //if (enumValue <= 0) continue; // for smaller set
              String value = UCharacter.getPropertyValueName(propEnum,enumValue, UProperty.NameChoice.LONG);
              map1.put(cp, value);
              map2.put(cp, value);
              cp++;
              // Unicode is huge, skip over large parts of it.
              if (cp == 0x08FF) {           // General Scripts Area.
                  cp = 0xD700;              // Hangul Syllables Area.
              } else if (cp == 0x100FF) {   // General Scripts Area.
                  cp = 0x1F000;             // Symbols Area.
              } else if (cp == 0x200FF) {   // CJK Unified Ideographs Extensions.
                  cp = 0x10FF00;            // Supplementary Private Use Area-B.
              } else if (cp == 0x10FFFF) {  // The end of Unicode.
                  break;
              }
        }
        checkNext(map1, map2, Integer.MAX_VALUE);

        logln("Comparing General Category");
        check(map1, map2, -1);
        logln("Comparing Values");
        Set<String> values1 = map1.getAvailableValues(new TreeSet<String>());
        Set<String> values2 = new TreeSet<String>(map2.values());
        if (!TestBoilerplate.verifySetsIdentical(this, values1, values2)) {
            throw new IllegalArgumentException("Halting");
        }
        logln("Comparing Sets");
        for (Iterator<String> it = values1.iterator(); it.hasNext();) {
            String value = it.next();
            logln(value == null ? "null" : value);
            UnicodeSet set1 = map1.keySet(value);
            UnicodeSet set2 = TestBoilerplate.getSet(map2, value);
            if (!TestBoilerplate.verifySetsIdentical(this, set1, set2)) {
                throw new IllegalArgumentException("Halting");
            }
        }
    }

    @Test
    public void TestAUnicodeMap2() {
        UnicodeMap<String> foo = new UnicodeMap<>();
        @SuppressWarnings("unused")
        int hash = foo.hashCode(); // make sure doesn't NPE
        @SuppressWarnings("unused")
        Set<String> fii = foo.stringKeys(); // make sure doesn't NPE
    }

    @Test
    public void TestAUnicodeMapInverse() {
        UnicodeMap<Character> foo1 = new UnicodeMap<Character>()
                .putAll('a', 'z', 'b')
                .put("ab", 'c')
                .put('x', 'b')
                .put("xy", 'c')
                ;
        Map<Character, UnicodeSet> target = new HashMap<Character, UnicodeSet>();
        foo1.addInverseTo(target);
        UnicodeMap<Character> reverse = new UnicodeMap<Character>().putAllInverse(target);
        assertEquals("", foo1, reverse);
    }

    private void checkNext(UnicodeMap<String> map1, Map<Integer,String> map2, int limit) {
        logln("Comparing nextRange");
        Map<Integer, String> localMap = new TreeMap<>();
        UnicodeMapIterator<String> mi = new UnicodeMapIterator<String>(map1);
        while (mi.nextRange()) {
            logln(Utility.hex(mi.codepoint) + ".." + Utility.hex(mi.codepointEnd) + " => " + mi.value);
            for (int i = mi.codepoint; i <= mi.codepointEnd; ++i) {
                //if (i >= limit) continue;
                localMap.put(i, mi.value);
            }
        }
        checkMap(map2, localMap);

        logln("Comparing next");
        mi.reset();
        localMap = new TreeMap<>();
//        String lastValue = null;
        while (mi.next()) {
//            if (!UnicodeMap.areEqual(lastValue, mi.value)) {
//                // System.out.println("Change: " + Utility.hex(mi.codepoint) + " => " + mi.value);
//                lastValue = mi.value;
//            }
            //if (mi.codepoint >= limit) continue;
            localMap.put(mi.codepoint, mi.value);
        }
        checkMap(map2, localMap);
    }

    public void check(UnicodeMap<String> map1, Map<Integer,String> map2, int counter) {
        for (int i = 0; i < LIMIT; ++i) {
            String value1 = map1.getValue(i);
            String value2 = map2.get(i);
            if (!UnicodeMap.areEqual(value1, value2)) {
                errln(counter + " Difference at " + Utility.hex(i)
                     + "\t UnicodeMap: " + value1
                     + "\t HashMap: " + value2);
                errln("UnicodeMap: " + map1);
                errln("Log: " + TestBoilerplate.show(log));
                errln("HashMap: " + TestBoilerplate.show(map2));
            }
        }
    }

    void checkMap(Map<Integer, String> m1, Map<Integer, String> m2) {
        if (m1.equals(m2)) return;
        StringBuilder buffer = new StringBuilder();
        Set<Map.Entry<Integer, String>> m1entries = m1.entrySet();
        Set<Map.Entry<Integer, String>> m2entries = m2.entrySet();
        getEntries("\r\nIn First, and not Second", m1entries, m2entries, buffer, 20);
        getEntries("\r\nIn Second, and not First", m2entries, m1entries, buffer, 20);
        errln(buffer.toString());
    }

    static Comparator<Map.Entry<Integer, String>> ENTRY_COMPARATOR = new Comparator<Map.Entry<Integer, String>>() {
        @Override
        public int compare(Map.Entry<Integer, String> o1, Map.Entry<Integer, String> o2) {
            if (o1 == o2) return 0;
            if (o1 == null) return -1;
            if (o2 == null) return 1;
            Map.Entry<Integer, String> a = o1;
            Map.Entry<Integer, String> b = o2;
            int result = compare2(a.getKey(), b.getKey());
            if (result != 0) return result;
            return compare2(a.getValue(), b.getValue());
        }
        private <T extends Comparable<T>> int compare2(T o1, T o2) {
            if (o1 == o2) return 0;
            if (o1 == null) return -1;
            if (o2 == null) return 1;
            return o1.compareTo(o2);
        }
    };

    private void getEntries(String title, Set<Map.Entry<Integer,String>> m1entries, Set<Map.Entry<Integer, String>> m2entries, StringBuilder buffer, int limit) {
        Set<Map.Entry<Integer, String>> m1_m2 = new TreeSet<Map.Entry<Integer, String>>(ENTRY_COMPARATOR);
        m1_m2.addAll(m1entries);
        m1_m2.removeAll(m2entries);
        buffer.append(title + ": " + m1_m2.size() + "\r\n");
        for (Map.Entry<Integer, String> entry : m1_m2) {
            if (limit-- < 0) return;
            buffer.append(entry.getKey()).append(" => ")
             .append(entry.getValue()).append("\r\n");
        }
    }
}
