import { Dot } from "./Dot.js";
import CrdtError from "../errors/CrdtError.js";
/**
 * Throws if the dot is not a well-formed {writerId: string, counter: integer}.
 */
function _assertValidDot(dot) {
    if (dot === null || dot === undefined || typeof dot.writerId !== 'string' || !Number.isInteger(dot.counter)) {
        throw new CrdtError(`ORSet.add: invalid dot -- expected {writerId: string, counter: integer}, got ${JSON.stringify(dot)}`, {
            code: 'E_CRDT_MALFORMED',
            context: { dot },
        });
    }
}
/**
 * ORSet (Observed-Remove Set) — a CRDT set supporting concurrent add
 * and remove operations with add-wins semantics.
 *
 * This is a GLOBAL OR-Set (one per category, not per element). It tracks:
 * - entries: Map<element, Set<encodedDot>> — elements and the dots that added them
 * - tombstones: Set<encodedDot> — global tombstones for removed dots
 *
 * An element is present if it has at least one non-tombstoned dot.
 *
 * Fields are public because JoinReducer (the merge engine) needs direct
 * access for performance-critical operations.
 */
export default class ORSet {
    /** Element to dots that added it. */
    entries;
    /** Global tombstones for removed dots. */
    tombstones;
    /**
     * Creates an ORSet from existing data structures.
     */
    constructor(entries, tombstones) {
        this.entries = entries;
        this.tombstones = tombstones;
    }
    /** Creates an empty ORSet. */
    static empty() {
        return new ORSet(new Map(), new Set());
    }
    /** Deserializes a plain object back to an ORSet. */
    static deserialize(obj) {
        const set = ORSet.empty();
        _deserializeEntriesInto(obj.entries, set.entries);
        _deserializeTombstonesInto(obj.tombstones, set.tombstones);
        return set;
    }
    // ---------------------------------------------------------------------------
    // Mutation operations
    // ---------------------------------------------------------------------------
    /**
     * Adds an element with the given dot.
     * Mutates the set.
     */
    add(element, dot) {
        _assertValidDot(dot);
        const encoded = Dot.encode(dot);
        let dots = this.entries.get(element);
        if (!dots) {
            dots = new Set();
            this.entries.set(element, dots);
        }
        dots.add(encoded);
    }
    /**
     * Removes an element by adding its observed dots to the tombstones.
     * Mutates the set.
     */
    remove(observedDots) {
        for (const encodedDot of observedDots) {
            this.tombstones.add(encodedDot);
        }
    }
    // ---------------------------------------------------------------------------
    // Query operations
    // ---------------------------------------------------------------------------
    /**
     * Checks if an element is present.
     * An element is present if it has at least one non-tombstoned dot.
     */
    contains(element) {
        const dots = this.entries.get(element);
        if (!dots) {
            return false;
        }
        for (const encodedDot of dots) {
            if (!this.tombstones.has(encodedDot)) {
                return true;
            }
        }
        return false;
    }
    /**
     * Returns all present elements.
     * Only returns elements that have at least one non-tombstoned dot.
     */
    elements() {
        const result = [];
        for (const element of this.entries.keys()) {
            if (this.contains(element)) {
                result.push(element);
            }
        }
        return result;
    }
    /**
     * Counts the total number of dot entries across all elements
     * (tombstoned and live alike).
     */
    countEntries() {
        let count = 0;
        for (const dots of this.entries.values()) {
            count += dots.size;
        }
        return count;
    }
    /**
     * Counts live (non-tombstoned) dots across all elements.
     */
    countLiveDots() {
        let count = 0;
        for (const dots of this.entries.values()) {
            for (const dot of dots) {
                if (!this.tombstones.has(dot)) {
                    count++;
                }
            }
        }
        return count;
    }
    /**
     * Counts tombstones that reference entry dots. Only counts tombstones
     * that actually correspond to dots in `entries` — floating tombstones
     * (for dots the replica has never observed as live) are ignored.
     */
    countTombstones() {
        let count = 0;
        for (const dots of this.entries.values()) {
            for (const dot of dots) {
                if (this.tombstones.has(dot)) {
                    count++;
                }
            }
        }
        return count;
    }
    /**
     * Returns the non-tombstoned dots for an element.
     */
    getDots(element) {
        const dots = this.entries.get(element);
        if (!dots) {
            return new Set();
        }
        const result = new Set();
        for (const encodedDot of dots) {
            if (!this.tombstones.has(encodedDot)) {
                result.add(encodedDot);
            }
        }
        return result;
    }
    /**
     * Returns true iff the element is tagged by the given encoded dot.
     * Tombstone status is ignored — this tests raw entry membership.
     */
    hasDot(element, encodedDot) {
        const dots = this.entries.get(element);
        return dots !== undefined && dots.has(encodedDot);
    }
    /**
     * Returns true iff the given encoded dot has been tombstoned.
     */
    isTombstoned(encodedDot) {
        return this.tombstones.has(encodedDot);
    }
    /**
     * Iterates `[element, dots]` pairs across all entries. The yielded
     * `dots` set includes tombstoned and live dots alike.
     */
    entriesIter() {
        return this.entries.entries();
    }
    /**
     * Iterates every encoded dot across all entries, tombstoned or not.
     */
    *entryDotsIter() {
        for (const dots of this.entries.values()) {
            for (const encodedDot of dots) {
                yield encodedDot;
            }
        }
    }
    /**
     * Iterates every tombstoned encoded dot, including floating
     * tombstones that have no corresponding entry.
     */
    tombstonesIter() {
        return this.tombstones.values();
    }
    // ---------------------------------------------------------------------------
    // CRDT operations
    // ---------------------------------------------------------------------------
    /**
     * Joins with another ORSet by taking the union of entries and tombstones.
     * Returns a new ORSet; does not mutate either input.
     *
     * Properties:
     * - Commutative: a.join(b) equals b.join(a)
     * - Associative: a.join(b).join(c) equals a.join(b.join(c))
     * - Idempotent: a.join(a) equals a
     */
    join(other) {
        const result = ORSet.empty();
        _copyEntries(this.entries, result.entries);
        _mergeEntries(other.entries, result.entries);
        _unionSets(this.tombstones, result.tombstones);
        _unionSets(other.tombstones, result.tombstones);
        return result;
    }
    /**
     * Compacts the ORSet by removing tombstoned dots that are <= includedVV.
     * Mutates the set.
     *
     * ## GC Safety Invariant
     *
     * Only compact dots that ALL replicas have observed. The `includedVV`
     * parameter represents the "stable frontier" — the version vector that
     * all known replicas have reached.
     */
    compact(includedVV) {
        const toDelete = _collectCompactableDots(this, includedVV);
        _applyCompaction(this, toDelete);
    }
    /** Creates a deep clone. */
    clone() {
        const result = ORSet.empty();
        for (const [element, dots] of this.entries) {
            result.entries.set(element, new Set(dots));
        }
        for (const dot of this.tombstones) {
            result.tombstones.add(dot);
        }
        return result;
    }
    /**
     * Returns a clone that retains only entries whose element matches
     * the predicate. All tombstones are copied verbatim, regardless of
     * whether their owning element survives the filter.
     */
    scopedClone(includeElement) {
        const result = ORSet.empty();
        for (const [element, dots] of this.entries) {
            if (includeElement(element)) {
                result.entries.set(element, new Set(dots));
            }
        }
        for (const dot of this.tombstones) {
            result.tombstones.add(dot);
        }
        return result;
    }
    /**
     * Serializes to a plain object for CBOR encoding.
     * Entries are sorted by element; dots within entries are sorted.
     * Tombstones are sorted.
     */
    serialize() {
        return {
            entries: _serializeEntries(this.entries),
            tombstones: _sortEncodedDots(this.tombstones),
        };
    }
}
// =============================================================================
// Internal helpers
// =============================================================================
/** Copies all entries by cloning each dot set into the target map. */
function _copyEntries(source, target) {
    for (const [element, dots] of source) {
        target.set(element, new Set(dots));
    }
}
/** Merges entries from source into target, unioning dot sets for existing elements. */
function _mergeEntries(source, target) {
    for (const [element, dots] of source) {
        const existing = target.get(element);
        if (existing !== undefined) {
            for (const dot of dots) {
                existing.add(dot);
            }
        }
        else {
            target.set(element, new Set(dots));
        }
    }
}
/** Adds all values from source into target set. */
function _unionSets(source, target) {
    for (const item of source) {
        target.add(item);
    }
}
/** Identifies dots eligible for compaction: tombstoned AND within the stable frontier. */
function _collectCompactableDots(set, includedVV) {
    const toDelete = [];
    for (const [element, dots] of set.entries) {
        for (const encodedDot of dots) {
            const dot = Dot.decode(encodedDot);
            if (set.tombstones.has(encodedDot) && includedVV.contains(dot)) {
                toDelete.push({ element, dot: encodedDot });
            }
        }
    }
    return toDelete;
}
/** Applies compaction by removing identified dots from entries and tombstones. */
function _applyCompaction(set, toDelete) {
    for (const { element, dot: encodedDot } of toDelete) {
        const dots = set.entries.get(element);
        if (dots !== undefined) {
            dots.delete(encodedDot);
            if (dots.size === 0) {
                set.entries.delete(element);
            }
        }
        set.tombstones.delete(encodedDot);
    }
}
/** Sorts encoded dots by their decoded (writerId, counter) order. */
function _sortEncodedDots(encodedDots) {
    const pairs = [];
    for (const encoded of encodedDots) {
        pairs.push({ encoded, decoded: Dot.decode(encoded) });
    }
    pairs.sort((a, b) => Dot.compare(a.decoded, b.decoded));
    return pairs.map((p) => p.encoded);
}
/** Serializes OR-Set entries as sorted [element, sortedDots[]] pairs. */
function _serializeEntries(entries) {
    const result = [];
    for (const [element, dots] of entries) {
        result.push([element, _sortEncodedDots(dots)]);
    }
    result.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
    return result;
}
/** Populates an entries map from a serialized entries array. */
function _deserializeEntriesInto(entries, target) {
    if (!Array.isArray(entries)) {
        return;
    }
    for (const [element, dots] of entries) {
        if (Array.isArray(dots)) {
            target.set(element, new Set(dots));
        }
    }
}
/** Populates a tombstone set from a serialized tombstones array. */
function _deserializeTombstonesInto(tombstones, target) {
    if (!Array.isArray(tombstones)) {
        return;
    }
    for (const dot of tombstones) {
        target.add(dot);
    }
}
