import { __assign } from "tslib";
import { Trie } from "@wry/trie";
import { canUseWeakMap, canUseWeakSet, isNonNullObject as isObjectOrArray, } from "../../utilities/index.js";
import { isArray } from "./helpers.js";
function shallowCopy(value) {
    if (isObjectOrArray(value)) {
        return isArray(value) ?
            value.slice(0)
            : __assign({ __proto__: Object.getPrototypeOf(value) }, value);
    }
    return value;
}
// When programmers talk about the "canonical form" of an object, they
// usually have the following meaning in mind, which I've copied from
// https://en.wiktionary.org/wiki/canonical_form:
//
// 1. A standard or normal presentation of a mathematical entity [or
//    object]. A canonical form is an element of a set of representatives
//    of equivalence classes of forms such that there is a function or
//    procedure which projects every element of each equivalence class
//    onto that one element, the canonical form of that equivalence
//    class. The canonical form is expected to be simpler than the rest of
//    the forms in some way.
//
// That's a long-winded way of saying any two objects that have the same
// canonical form may be considered equivalent, even if they are !==,
// which usually means the objects are structurally equivalent (deeply
// equal), but don't necessarily use the same memory.
//
// Like a literary or musical canon, this ObjectCanon class represents a
// collection of unique canonical items (JavaScript objects), with the
// important property that canon.admit(a) === canon.admit(b) if a and b
// are deeply equal to each other. In terms of the definition above, the
// canon.admit method is the "function or procedure which projects every"
// object "onto that one element, the canonical form."
//
// In the worst case, the canonicalization process may involve looking at
// every property in the provided object tree, so it takes the same order
// of time as deep equality checking. Fortunately, already-canonicalized
// objects are returned immediately from canon.admit, so the presence of
// canonical subtrees tends to speed up canonicalization.
//
// Since consumers of canonical objects can check for deep equality in
// constant time, canonicalizing cache results can massively improve the
// performance of application code that skips re-rendering unchanged
// results, such as "pure" UI components in a framework like React.
//
// Of course, since canonical objects may be shared widely between
// unrelated consumers, it's important to think of them as immutable, even
// though they are not actually frozen with Object.freeze in production,
// due to the extra performance overhead that comes with frozen objects.
//
// Custom scalar objects whose internal class name is neither Array nor
// Object can be included safely in the admitted tree, but they will not
// be replaced with a canonical version (to put it another way, they are
// assumed to be canonical already).
//
// If we ignore custom objects, no detection of cycles or repeated object
// references is currently required by the StoreReader class, since
// GraphQL result objects are JSON-serializable trees (and thus contain
// neither cycles nor repeated subtrees), so we can avoid the complexity
// of keeping track of objects we've already seen during the recursion of
// the admit method.
//
// In the future, we may consider adding additional cases to the switch
// statement to handle other common object types, such as "[object Date]"
// objects, as needed.
var ObjectCanon = /** @class */ (function () {
    function ObjectCanon() {
        // Set of all canonical objects this ObjectCanon has admitted, allowing
        // canon.admit to return previously-canonicalized objects immediately.
        this.known = new (canUseWeakSet ? WeakSet : Set)();
        // Efficient storage/lookup structure for canonical objects.
        this.pool = new Trie(canUseWeakMap);
        // Make the ObjectCanon assume this value has already been
        // canonicalized.
        this.passes = new WeakMap();
        // Arrays that contain the same elements in a different order can share
        // the same SortedKeysInfo object, to save memory.
        this.keysByJSON = new Map();
        // This has to come last because it depends on keysByJSON.
        this.empty = this.admit({});
    }
    ObjectCanon.prototype.isKnown = function (value) {
        return isObjectOrArray(value) && this.known.has(value);
    };
    ObjectCanon.prototype.pass = function (value) {
        if (isObjectOrArray(value)) {
            var copy = shallowCopy(value);
            this.passes.set(copy, value);
            return copy;
        }
        return value;
    };
    ObjectCanon.prototype.admit = function (value) {
        var _this = this;
        if (isObjectOrArray(value)) {
            var original = this.passes.get(value);
            if (original)
                return original;
            var proto = Object.getPrototypeOf(value);
            switch (proto) {
                case Array.prototype: {
                    if (this.known.has(value))
                        return value;
                    var array = value.map(this.admit, this);
                    // Arrays are looked up in the Trie using their recursively
                    // canonicalized elements, and the known version of the array is
                    // preserved as node.array.
                    var node = this.pool.lookupArray(array);
                    if (!node.array) {
                        this.known.add((node.array = array));
                        // Since canonical arrays may be shared widely between
                        // unrelated consumers, it's important to regard them as
                        // immutable, even if they are not frozen in production.
                        if (globalThis.__DEV__ !== false) {
                            Object.freeze(array);
                        }
                    }
                    return node.array;
                }
                case null:
                case Object.prototype: {
                    if (this.known.has(value))
                        return value;
                    var proto_1 = Object.getPrototypeOf(value);
                    var array_1 = [proto_1];
                    var keys = this.sortedKeys(value);
                    array_1.push(keys.json);
                    var firstValueIndex_1 = array_1.length;
                    keys.sorted.forEach(function (key) {
                        array_1.push(_this.admit(value[key]));
                    });
                    // Objects are looked up in the Trie by their prototype (which
                    // is *not* recursively canonicalized), followed by a JSON
                    // representation of their (sorted) keys, followed by the
                    // sequence of recursively canonicalized values corresponding to
                    // those keys. To keep the final results unambiguous with other
                    // sequences (such as arrays that just happen to contain [proto,
                    // keys.json, value1, value2, ...]), the known version of the
                    // object is stored as node.object.
                    var node = this.pool.lookupArray(array_1);
                    if (!node.object) {
                        var obj_1 = (node.object = Object.create(proto_1));
                        this.known.add(obj_1);
                        keys.sorted.forEach(function (key, i) {
                            obj_1[key] = array_1[firstValueIndex_1 + i];
                        });
                        // Since canonical objects may be shared widely between
                        // unrelated consumers, it's important to regard them as
                        // immutable, even if they are not frozen in production.
                        if (globalThis.__DEV__ !== false) {
                            Object.freeze(obj_1);
                        }
                    }
                    return node.object;
                }
            }
        }
        return value;
    };
    // It's worthwhile to cache the sorting of arrays of strings, since the
    // same initial unsorted arrays tend to be encountered many times.
    // Fortunately, we can reuse the Trie machinery to look up the sorted
    // arrays in linear time (which is faster than sorting large arrays).
    ObjectCanon.prototype.sortedKeys = function (obj) {
        var keys = Object.keys(obj);
        var node = this.pool.lookupArray(keys);
        if (!node.keys) {
            keys.sort();
            var json = JSON.stringify(keys);
            if (!(node.keys = this.keysByJSON.get(json))) {
                this.keysByJSON.set(json, (node.keys = { sorted: keys, json: json }));
            }
        }
        return node.keys;
    };
    return ObjectCanon;
}());
export { ObjectCanon };
//# sourceMappingURL=object-canon.js.map