import java.lang.reflect.Field;
import java.util.*;

/**
 * HashMap internals, observed rather than described. Reads the private `table` field by reflection,
 * so run with:  java --add-opens java.base/java.util=ALL-UNNAMED HashMapInternals
 */
public class HashMapInternals {
    // ---- helpers: peek inside HashMap -------------------------------------------------------
    static Object[] table(HashMap<?, ?> m) throws Exception {
        Field f = HashMap.class.getDeclaredField("table"); f.setAccessible(true);
        return (Object[]) f.get(m);
    }
    static int capacity(HashMap<?, ?> m) throws Exception { Object[] t = table(m); return t == null ? 0 : t.length; }
    static int spread(Object key) { int h = key.hashCode(); return h ^ (h >>> 16); }   // same as HashMap.hash()

    // ---- keys used by the demos -------------------------------------------------------------
    /** Every instance collides: hashCode is constant. Comparable so a tree bin can order them. */
    record Colliding(int id) implements Comparable<Colliding> {
        @Override public int hashCode() { return 1; }
        @Override public int compareTo(Colliding o) { return Integer.compare(id, o.id); }
    }
    static class MutableKey {
        int v; MutableKey(int v) { this.v = v; }
        @Override public boolean equals(Object o) { return o instanceof MutableKey k && k.v == v; }
        @Override public int hashCode() { return v; }
    }

    public static void main(String[] args) throws Exception {
        // 1. hash spreading and bucket index
        System.out.println("== 1. hash spreading and bucket index (table length 16) ==");
        for (String k : new String[] { "apple", "banana", "cherry" }) {
            int raw = k.hashCode(), sp = spread(k);
            System.out.printf("%-7s hashCode=%11d  spread=%11d  index=(16-1)&spread=%d%n", k, raw, sp, (16 - 1) & sp);
        }
        System.out.println("null key hash   = 0, so it always lands in bucket 0");

        // 2. lazy table, load factor, resize
        System.out.println("\n== 2. the table is created lazily; resize happens when size exceeds threshold (12 for 16 x 0.75) ==");
        HashMap<Integer, String> m = new HashMap<>();
        System.out.println("after new HashMap()      : capacity=" + capacity(m));
        for (int i = 1; i <= 13; i++) {
            int before = capacity(m);
            m.put(i, "v" + i);
            if (capacity(m) != before) System.out.println("put #" + i + " changed capacity " + before + " -> " + capacity(m));
        }
        System.out.println("new HashMap<>(100) first put -> capacity " + capacityAfterFirstPut(new HashMap<>(100)));
        System.out.println("new HashMap<>(17)  first put -> capacity " + capacityAfterFirstPut(new HashMap<>(17)));

        // 3. collisions and treeification
        System.out.println("\n== 3. collisions: list bin, resize instead of tree below 64 slots, then TreeNode ==");
        HashMap<Colliding, Integer> c = new HashMap<>();
        for (int i = 1; i <= 11; i++) {
            c.put(new Colliding(i), i);
            Object first = table(c)[1 & (table(c).length - 1)];
            System.out.printf("size=%2d capacity=%2d bin node = %s%n", i, capacity(c), first.getClass().getName().replace("java.util.", ""));
        }
        System.out.println("get(Colliding(7)) still works: " + c.get(new Colliding(7)));

        // 4. mutable keys
        System.out.println("\n== 4. a key mutated after insertion is lost ==");
        HashMap<MutableKey, String> mk = new HashMap<>();
        MutableKey key = new MutableKey(1);
        mk.put(key, "found");
        System.out.println("get before mutation : " + mk.get(key));
        key.v = 2;
        System.out.println("get after  mutation : " + mk.get(key) + "   (size still " + mk.size() + ")");
        key.v = 1;
        System.out.println("get after restoring : " + mk.get(key));

        // 5. iteration order is not insertion order
        System.out.println("\n== 5. iteration order follows buckets, not insertion ==");
        HashMap<String, Integer> order = new HashMap<>();
        for (String s : new String[] { "banana", "apple", "cherry", "date" }) order.put(s, s.length());
        System.out.println("HashMap       : " + order.keySet());
        System.out.println("LinkedHashMap : " + new LinkedHashMap<>(new TreeMap<>(Map.of("a", 1))).keySet() + " (insertion order is a different class)");
    }
    static int capacityAfterFirstPut(HashMap<Integer, Integer> m) throws Exception { m.put(1, 1); return capacity(m); }
}
