001package squidpony.squidmath;
002
003import squidpony.ArrayTools;
004
005import java.util.Collection;
006import java.util.Iterator;
007import java.util.SortedSet;
008
009/**
010 * An ordered multi-directional "map" that keeps 1 or more keysets organized so you can look up one key given a key in
011 * another keyset, and do the same for the indices of keys. Does not have generic type parameters, which is needed
012 * because we handle arbitrary counts of keysets, and each keyset could have its own type. You can use most of the
013 * normal Map methods here, though they often need an int as their first argument, {@code which}, that specifies which
014 * keyset the method applies to. For example, {@link #contains(int, Object)} checks for the presence of the second
015 * parameter in the keyset specified by the first parameter. Adding items to a MultiKey uses {@link #put(Object...)},
016 * and that does not take a {@code which} parameter because it needs to add an item to every keyset to succeed, and will
017 * do nothing if the array or varargs passed to it has a different length than the {@link #keyCount} of the MultiKey.
018 * Created by Tommy Ettinger on 10/23/2016.
019 */
020@SuppressWarnings("unchecked")
021public class MultiKey {
022    public final int keyCount;
023    private Arrangement[] keys;
024
025    /**
026     * Constructs an empty MultiKey.
027     */
028    public MultiKey()
029    {
030        this(3, 16, 0.5f);
031    }
032
033    /**
034     * Constructs a MultiKey with the expected number of indices to hold (the number of items in each keyset is always
035     * the same, and this will be more efficient if expected is greater than that number).
036     * @param expected how many items this should be ready to hold; can resize if needed
037     */
038    public MultiKey(int keyCount, int expected)
039    {
040        this(keyCount, expected, 0.5f);
041    }
042
043    /**
044     * Constructs a MultiKey with the expected number of indices to hold (the number of items in each keyset is always
045     * the same, and this will be more efficient if expected is greater than that number) and the load factor to use,
046     * between 0.1f and 0.8f usually (using load factors higher than 0.8f can cause problems).
047     * @param expected the amount of indices (the number of items in each keyset is always the same) this should hold
048     * @param f the load factor, probably between 0.1f and 0.8f
049     */
050    public MultiKey(int keyCount, int expected, float f)
051    {
052        this.keyCount = keyCount;
053        keys = new Arrangement[keyCount];
054        for (int i = 0; i < keyCount; i++) {
055            keys[i] = new Arrangement(expected, f);
056        }
057    }
058
059    /**
060     * Constructs a MultiKey from a Collection of Iterables that will be processed in tandem, adding a unique item from
061     * each keyset in keysets if and only if it can also add a unique item from all other keysets, otherwise skipping
062     * that iteration in each Iterable.
063     * @param keysets a Collection of Iterable data structures, each containing items that should all be unique
064     */
065    public MultiKey(Collection<Iterable> keysets) {
066        if (keysets == null) {
067            keyCount = 0;
068            keys = new Arrangement[0];
069        } else {
070            keyCount = keysets.size();
071            keys = new Arrangement[keyCount];
072            for (int i = 0; i < keyCount; i++) {
073                keys[i] = new Arrangement();
074            }
075            putAll(keysets);
076        }
077    }
078
079    public MultiKey(MultiKey other)
080    {
081        if(other == null)
082        {
083            keyCount = 0;
084            keys = new Arrangement[0];
085        }
086        else
087        {
088            keyCount = other.keyCount;
089            keys = new Arrangement[keyCount];
090            for (int i = 0; i < keyCount; i++) {
091                keys[i] = new Arrangement(other.keys[i]);
092            }
093        }
094    }
095
096    public MultiKey(Arrangement[] keysets)
097    {
098        if(keysets == null)
099        {
100            keyCount = 0;
101            keys = new Arrangement[0];
102        }
103        else
104        {
105            keyCount = keysets.length;
106            keys = new Arrangement[keyCount];
107            int minLength = Integer.MAX_VALUE;
108            for (int k = 0; k < keyCount; k++) {
109                if(keysets[k] == null)
110                    return;
111                minLength = Math.min(minLength, keysets[k].size);
112            }
113            for (int k = 0; k < keyCount; k++) {
114                keys[k] = keysets[k].take(minLength);
115            }
116        }
117    }
118    /**
119     * Returns true if this contains key in the keyset specified by which.
120     * @param which which keyset to check in
121     * @param key the key to check the presence of
122     * @return true if key is present in the keyset at which; false otherwise
123     */
124    public boolean contains(int which, Object key) {
125        if(which >= 0 && which < keyCount)
126            return keys[which].containsKey(key);
127        return false;
128    }
129
130    /**
131     * Returns true if index is between 0 (inclusive) and {@link #size()} (exclusive), or false otherwise.
132     * @param index the index to check
133     * @return true if index is a valid index in the ordering of this MultiKey
134     */
135    public boolean containsIndex(int index) {
136        if(keyCount > 0) return keys[0].containsValue(index);
137        return false;
138    }
139
140    /**
141     * Given an index of a keyset (which) and an Object key, finds the position in the ordering that key has in the
142     * keyset at which, or -1 if key is not present.
143     * <br>
144     * Unlike {@link java.util.List#indexOf(Object)}, this runs in constant time.
145     * @param which which keyset to check in
146     * @param key the key to find the position of
147     * @return the int index of key in the ordering, or -1 if it is not present
148     */
149    public int indexOf(int which, Object key)
150    {
151        if(which >= 0 && which < keyCount)
152            return keys[which].getInt(key);
153        return -1;
154    }
155
156    /**
157     * Given an index of a keyset (which) and an int index, finds the associated key in the keyset specified by which
158     * (using index as a point in the ordering).
159     * @param which which keyset to get from
160     * @param index an int index into this MultiKey
161     * @return the key (in the keyset found by which) with index for its position in the ordering, or null if index or which was invalid
162     */
163    public Object getAt(int which, int index)
164    {
165        if(which >= 0 && which < keyCount)
166            return keys[which].keyAt(index);
167        return null;
168    }
169
170    /**
171     * Given an int index, finds the associated keys at all keysets (using index as a point in the ordering) and returns
172     * them as a newly-allocated Object array.
173     * @param index an int index into this MultiKey
174     * @return the array of keys with index for their position in the ordering, or an array of null if index was invalid
175     * @see #getAllAt(int, Object[]) getAllAt can avoid allocating a new array each time
176     */
177    public Object[] getAllAt(int index) {
178        Object[] os = new Object[keyCount];
179        for (int i = 0; i < keyCount; i++) {
180            os[i] = keys[i].keyAt(index);
181        }
182        return os;
183    }
184
185    /**
186     * Given an int index and an Object array to reuse, finds the associated keys at all keysets (using index as a point
187     * in the ordering) and fills into with those keys, up to keyCount items.
188     * @param index an int index into this MultiKey
189     * @param into an Object array to reuse and fill with items; will be returned as-is if smaller than keyCount
190     * @return the array of keys with index for their position in the ordering, or an array of null if index was invalid
191     */
192    public Object[] getAllAt(int index, Object[] into) {
193        if (into != null && into.length >= keyCount) {
194            for (int i = 0; i < keyCount; i++) {
195                into[i] = keys[i].keyAt(index);
196            }
197        }
198        return into;
199    }
200
201    /**
202     * Given an index of the keyset to look up a key in (lookingUp), an index of the keyset to get from (getting), and
203     * an Object key to look up (key), finds the Object key in the keyset specified by getting that is associated with
204     * key in the keyset specified by lookingUp. key and the returned value will be at the same point in the ordering.
205     * @param lookingUp which keyset to look up the {@code key} parameter in
206     * @param getting which keyset to get a value from
207     * @param key an object to use as a key, which should be the right type for the keyset at lookingUp
208     * @return the object from getting's keyset that is associated with key, or null if key was not present
209     */
210    public Object getFrom(int lookingUp, int getting, Object key)
211    {
212        if(lookingUp >= 0 && lookingUp < keyCount && getting >= 0 && getting < keyCount)
213            return keys[getting].keyAt(keys[lookingUp].getInt(key));
214        return null;
215    }
216
217    /**
218     * Gets a random key from the keyset specified by which using the given IRNG.
219     * @param which which keyset to get a random key from
220     * @param random generates a random index to get a key with
221     * @return a randomly chosen key from the keyset specified, or null if this is empty
222     */
223    public Object randomKey(int which, IRNG random)
224    {
225        if(which >= 0 && which < keyCount)
226            return keys[which].randomKey(random);
227        return null;
228    }
229    /**
230     * Gets a random key from a random keyset in this MultiKey using the given IRNG.
231     * @param random generates a random keyset index and random item index, to get a random key
232     * @return a randomly chosen Object key from possibly any keyset in this, or null if this is empty
233     */
234    public Object randomKey(IRNG random)
235    {
236        if(keyCount > 0)
237            return keys[random.nextInt(keyCount)].randomKey(random);
238        return null;
239    }
240
241    /**
242     * In the keyset specified by {@code which}, changes an existing key, {@code past}, to another key, {@code future},
243     * if past exists in that keyset and future does not yet exist in that keyset. This will retain past's point in the
244     * ordering for future, so the associated other key(s) will still be associated in the same way.
245     * @param which which keyset to alter the items in
246     * @param past a key, that must exist in the keyset specified by which, and will be changed
247     * @param future a key, that cannot currently exist in the keyset specified by which, but will if this succeeds
248     * @return this for chaining
249     */
250    public MultiKey alter(int which, Object past, Object future)
251    {
252        if(which >= 0 && which < keyCount && keys[which].containsKey(past) && !keys[which].containsKey(future))
253            keys[which].alter(past, future);
254        return this;
255    }
256    /**
257     * In the keyset specified by {@code which}, changes the key at {@code index} to another key, {@code future}, if
258     * index is valid and future does not yet exist in that keyset. The position in the ordering for future will be the
259     * same as index, and the same as the key this replaced, if this succeeds, so the other key(s) at that position will
260     * still be associated in the same way.
261     * @param which which keyset to alter the items in
262     * @param index a position in the ordering to change; must be at least 0 and less than {@link #size()}
263     * @param future a key, that cannot currently exist in the keyset specified by which, but will if this succeeds
264     * @return this for chaining
265     */
266    public MultiKey alterAt(int which, int index, Object future)
267    {
268        if(which >= 0 && which < keyCount && !keys[which].containsKey(future) && index >= 0 && index < keys[which].size)
269            keys[which].alter(keys[which].keyAt(index), future);
270        return this;
271    }
272
273    /**
274     * Adds a key to each keyset at the same point in the ordering (the end) of this MultiKey. The length of k must
275     * match the keyCount of this MultiKey, and the nth item in k will go into the nth keyset. No item in k can be
276     * present in the matching keyset in this before this is called. If you want to change or update an existing key,
277     * use {@link #alter(int, Object, Object)} or {@link #alterAt(int, int, Object)}.
278     * @param k an array or varargs of keys to add after the last index of this MultiKey; length must equal keyCount
279     * @return true if this collection changed as a result of this call
280     */
281    public boolean put(Object... k)
282    {
283        if(k != null && keyCount > 0 && k.length == keyCount) {
284            for (int i = 0; i < keyCount; i++) {
285                if(keys[i].containsKey(k[i]))
286                    return false;
287            }
288            for (int i = 0; i < keyCount; i++) {
289                keys[i].add(k[i]);
290            }
291            return true;
292        }
293        return false;
294    }
295
296    /**
297     * Goes through all Iterable items in {@code k} and adds their unique items into their corresponding keyset at the
298     * end. If an item from the nth Iterable is already present in the nth keyset in this when this would add one, this
299     * will not put any keys at that point in the iteration order, and will place the next unique group of items it
300     * finds in the arguments at that position instead.
301     * @param k a Collection of Iterable s of keys to add to their respective keysets; should all be unique (like a Set)
302     * @return true if this collection changed as a result of this call
303     */
304    public boolean putAll(Collection<Iterable> k)
305    {
306        if(k == null || k.size() != keyCount) return false;
307        boolean changed = false;
308        Iterator[] its = new Iterator[keyCount];
309        int idx = 0;
310        for (Iterable it : k) {
311            its[idx++] = it.iterator();
312        }
313        Object[] os = new Object[keyCount];
314        while (true)
315        {
316            for (int i = 0; i < keyCount; i++) {
317                if(!its[i].hasNext())
318                    return changed;
319                os[i] = its[i].next();
320            }
321            changed = put(os) || changed;
322        }
323    }
324    /**
325     * Puts all unique keys in {@code other} into this MultiKey, respecting other's ordering. If a key in other is
326     * already present when this would add one, this will not put the keys at that point in the iteration
327     * order, and will place the next unique items it finds in other at that position instead.
328     * @param other another MultiKey collection with the same keyCount
329     * @return true if this collection changed as a result of this call
330     */
331    public boolean putAll(MultiKey other)
332    {
333        if(other == null || other.keyCount != keyCount) return false;
334        boolean changed = false;
335        int sz = other.size();
336        Object[] os = new Object[keyCount];
337        for (int i = 0; i < sz; i++) {
338            changed = put(other.getAllAt(i, os)) || changed;
339        }
340        return changed;
341    }
342
343    /**
344     * Adds a key to each keyset at the given index in the ordering of this MultiKey. The length of k must
345     * match the keyCount of this MultiKey, and the nth item in k will go into the nth keyset. No item in k can be
346     * present in the matching keyset in this before this is called. If you want to change or update an existing key,
347     * use {@link #alter(int, Object, Object)} or {@link #alterAt(int, int, Object)}.
348     * @param k an array or varargs of keys to add after the last index of this MultiKey; length must equal keyCount
349     * @return true if this collection changed as a result of this call
350     */
351    public boolean putAt(int index, Object... k)
352    {
353        if(k != null && keyCount > 0 && k.length == keyCount) {
354            for (int i = 0; i < keyCount; i++) {
355                if(keys[i].containsKey(k[i]))
356                    return false;
357            }
358            for (int i = 0; i < keyCount; i++) {
359                keys[i].addAt(index, k[i]);
360            }
361            return true;
362        }
363        return false;
364    }
365
366    /**
367     * Removes a given Object key from the keyset specified by which, if {@code removing} exists in that keyset, and
368     * also removes any keys associated with its point in the ordering.
369     * @param which which keyset to remove the item from; if {@code removing} isn't in that keyset, this does nothing
370     * @param removing the key to remove
371     * @return this for chaining
372     */
373    public MultiKey remove(int which, Object removing)
374    {
375        if(which >= 0 && which < keyCount) {
376            int i = keys[which].removeInt(removing);
377            if(i >= 0) {
378                for (int j = 0; j < keyCount; j++) {
379                    if(j != which)
380                        keys[j].removeAt(i);
381                }
382            }
383        }
384        return this;
385    }
386
387    /**
388     * Removes a given point in the ordering, if {@code index} is at least 0 and less than {@link #size()}.
389     * @param index the position in the ordering to remove
390     * @return this for chaining
391     */
392    public MultiKey removeAt(int index)
393    {
394        if(index >= 0 && keyCount > 0 && index < keys[0].size) {
395            for (int i = 0; i < keyCount; i++) {
396                keys[i].removeAt(index);
397            }
398        }
399        return this;
400    }
401
402    /**
403     * Reorders this MultiKey using {@code ordering}, which have the same length as this MultiKey's {@link #size()}
404     * and can be generated with {@link ArrayTools#range(int)} (which, if applied, would produce no
405     * change to the current ordering), {@link IRNG#randomOrdering(int)} (which gives a random ordering, and if
406     * applied immediately would be the same as calling {@link #shuffle(IRNG)}), or made in some other way. If you
407     * already have an ordering and want to make a different ordering that can undo the change, you can use
408     * {@link ArrayTools#invertOrdering(int[])} called on the original ordering.
409     * @param ordering an int array or vararg that should contain each int from 0 to {@link #size()} (or less)
410     * @return this for chaining
411     */
412    public MultiKey reorder(int... ordering)
413    {
414        if(ordering != null) {
415            for (int i = 0; i < keyCount; i++) {
416                keys[i].reorder(ordering);
417            }
418        }
419        return this;
420    }
421
422    /**
423     * Generates a random ordering with rng and applies the same ordering to all kinds of keys this has; they will
424     * maintain their current association to other keys but their ordering/indices will change.
425     * @param rng an IRNG to produce the random ordering this will use
426     * @return this for chaining
427     */
428    public MultiKey shuffle(IRNG rng)
429    {
430        if(keyCount > 0) {
431            int[] ordering = rng.randomOrdering(keys[0].size);
432            for (int i = 0; i < keyCount; i++) {
433                keys[i].reorder(ordering);
434            }
435        }
436        return this;
437    }
438
439    /**
440     * Creates a new iterator over the keys this holds in the keyset specified by which. This can be problematic for
441     * garbage collection if called very frequently; it may be better to access items by index (which also lets you
442     * access other keys associated with that index) using {@link #getAt(int, int)} in a for(int i=0...) loop.
443     * @return a newly-created iterator over this MultiKey's keys in the specified keyset
444     */
445    public Iterator iterator(int which)
446    {
447        if(which >= 0 && which < keyCount)
448            return keys[which].iterator();
449        return null;
450    }
451
452    /**
453     * Gets and caches the keys in the keyset specified by which as a Collection that implements SortedSet (and so also
454     * implements Set).
455     * @param which which keyset to get as a separate value
456     * @return the keys from the keyset specified by which, as a SortedSet
457     */
458    public SortedSet getSet(int which) {
459        if(which >= 0 && which < keyCount)
460            return keys[which].keySet();
461        return null;
462    }
463
464    /**
465     * To be called sparingly, since this allocates a new OrderedSet instead of reusing one.
466     * @param which which keyset to get as a separate value
467     * @return the keys from the keyset specified by which, as an OrderedSet
468     */
469    public OrderedSet getOrderedSet(int which) {
470        if(which >= 0 && which < keyCount)
471            return keys[which].keysAsOrderedSet();
472        return null;
473
474    }
475
476    public int keyCount() {
477        return keyCount;
478    }
479    public int valueCount() {
480        return 0;
481    }
482
483    public int size() {
484        return (keyCount > 0) ? keys[0].size : 0;
485    }
486
487    public boolean isEmpty()
488    {
489        return keyCount > 0 && keys[0].size > 0;
490    }
491
492}