/**
 * A hash map that associates a 'Massive'-object with a Vector3 (typically this is the force
 * exerted on the object). The number of key-value pairs is not limited.
 */
public class MassiveForceHashMap {
    private int size;
    private Massive[] keys;
    private Vector3[] values;

    /**
     * Initializes 'this' as an empty map.
     */
    public MassiveForceHashMap() {
        this(16);
    }

    public MassiveForceHashMap(int capacity) {
        this.size = 0;
        this.keys = new Massive[capacity];
        this.values = new Vector3[capacity];
    }

    /**
     * Adds a new key-value association to this map. If the key already exists in this map,
     * the value is replaced and the old value is returned. Otherwise 'null' is returned.
     * Precondition: key != null.
     */
    public Vector3 put(Massive key, Vector3 value) {
        if (size > keys.length / 2) doubleCapacity();

        int idx = ((key.hashCode() % keys.length) + keys.length) % keys.length;
        for (int i = 0; i < keys.length; i++) {
            int pos = (idx + i) % keys.length;
            if (values[pos] == null) {
                keys[pos] = key;
                values[pos] = value;
                size++;
                return null;
            } else if (keys[pos].equals(key)) {
                Vector3 old = values[pos];
                values[pos] = value;
                return old;
            }
        }
        throw new RuntimeException();
    }

    private void doubleCapacity() {
        Massive[] oldKeys = keys;
        Vector3[] oldValues = values;
        keys = new Massive[keys.length * 2];
        values = new Vector3[values.length * 2];
        size = 0;

        for (int i = 0; i < oldKeys.length; i++) {
            Massive k = oldKeys[i];
            Vector3 v = oldValues[i];
            if (v != null) put(k, v);
        }
    }

    /**
     * Returns the value associated with the specified key, i.e. the method returns the force vector
     * associated with the specified key. Returns 'null' if the key is not contained in this map.
     * Precondition: key != null.
     */
    public Vector3 get(Massive key) {
        int pos = find(key);
        return (pos == -1) ? null : values[pos];
    }

    private int find(Massive key) {
        int idx = ((key.hashCode() % keys.length) + keys.length) % keys.length;
        for (int i = 0; i < keys.length; i++) {
            int pos = (idx + i) % keys.length;
            if (keys[pos] == null) {
                break;
            } else if (keys[pos].equals(key)) {
                return pos;
            }
        }
        return -1;
    }

    /**
     * Deletes the mapping for the specified key from this map if present.
     * Returns the previous value associated with key, or null if there was
     * no mapping for key.
     * Precondition: key != null
     */
    public Vector3 delete(Massive key) {
        int pos = find(key);
        if (pos == -1) return null;

        Vector3 val = values[pos];
        values[pos] = null;
        return val;
    }

    /**
     * Returns 'true' if this map contains a mapping for the specified key.
     */
    public boolean containsKey(Massive key) {
        return this.get(key) != null;
    }

    /**
     * Returns a readable representation of this map, with all key-value pairs. Their order is not
     * defined.
     */
    @Override
    public String toString() {
        if (size == 0) return "{}";

        StringBuilder sb = new StringBuilder("{");
        for (int i = 0; i < keys.length; i++) {
            Massive k = keys[i];
            Vector3 v = values[i];
            if (k != null && v != null) {
                sb.append("\n  ");
                sb.append(k);
                sb.append(": ");
                sb.append(v);
                sb.append(",");
            }
        }
        sb.deleteCharAt(sb.length() - 1);
        sb.append("\n}");
        return sb.toString();
    }

    /**
     * Compares `this` with the specified object for equality. Returns `true` if the specified
     * `o` is not `null` and is of type `MassiveForceHashMap` and both `this` and `o` have equal
     * key-value pairs, i.e. the number of key-value pairs is the same in both maps and every
     * key-value pair in `this` equals one key-value pair in `o`. Two key-value pairs are
     * equal if the two keys are equal and the two values are equal. Otherwise, `false` is returned.
     */
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof MassiveForceHashMap map) || map.size != size)
            return false;

        for (int i = 0; i < keys.length; i++) {
            Massive k1 = keys[i];
            Vector3 v1 = values[i];
            if (k1 == null || v1 == null) continue;
            Vector3 v2 = map.get(k1);
            if (!v1.equals(v2)) return false;
        }
        return true;
    }

    /**
     * Returns the hashCode of `this`.
     */
    @Override
    public int hashCode() {
        int hash = 0;
        for (int i = 0; i < keys.length; i++) {
            Massive k = keys[i];
            Vector3 v = values[i];
            if (k == null || v == null) continue;
            hash ^= k.hashCode();
            hash ^= v.hashCode();
        }
        return hash;
    }

    /**
     * Returns a list of all the keys in no specified order.
     */
    public MassiveLinkedList keyList() {
        MassiveLinkedList list = new MassiveLinkedList();
        for (int i = 0; i < keys.length; i++) {
            if (keys[i] != null && values[i] != null)
                list.addLast(keys[i]);
        }
        return list;
    }
}
