"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.mergeMutationEvents = void 0;
var tslib_1 = require("tslib");
var rrweb_types_1 = require("@amplitude/rrweb-types");
function isMergeableMutation(event) {
    if (event.type !== rrweb_types_1.EventType.IncrementalSnapshot)
        return false;
    var data = event.data;
    return data.source === rrweb_types_1.IncrementalSource.Mutation && !data.isAttachIframe;
}
function isStyleObject(value) {
    return typeof value === 'object' && value !== null;
}
/**
 * Deep-merges a run of object-form (`styleOMValue`) style diffs into a single
 * object. Later writes win on a per-property basis, exactly mirroring how the
 * rrweb replayer applies object-form styles cumulatively. A `false` value
 * (delete) is preserved so the merged write still removes the property.
 */
function mergeStyleDiffs(diffs) {
    var e_1, _a, e_2, _b;
    var merged = {};
    try {
        for (var diffs_1 = tslib_1.__values(diffs), diffs_1_1 = diffs_1.next(); !diffs_1_1.done; diffs_1_1 = diffs_1.next()) {
            var diff = diffs_1_1.value;
            try {
                for (var _c = (e_2 = void 0, tslib_1.__values(Object.keys(diff))), _d = _c.next(); !_d.done; _d = _c.next()) {
                    var prop = _d.value;
                    merged[prop] = diff[prop];
                }
            }
            catch (e_2_1) { e_2 = { error: e_2_1 }; }
            finally {
                try {
                    if (_d && !_d.done && (_b = _c.return)) _b.call(_c);
                }
                finally { if (e_2) throw e_2.error; }
            }
        }
    }
    catch (e_1_1) { e_1 = { error: e_1_1 }; }
    finally {
        try {
            if (diffs_1_1 && !diffs_1_1.done && (_a = diffs_1.return)) _a.call(diffs_1);
        }
        finally { if (e_1) throw e_1.error; }
    }
    return merged;
}
/**
 * Coalesces `style` attribute mutations within a merged group in a way that is
 * faithful to how the rrweb replayer applies them, so the merged payload stays
 * small without dropping any property that real cumulative replay would keep.
 *
 * Per node id, the surviving `style` writes are computed as follows:
 *   - A `string`/`null` style write is a FULL reset: it supersedes every earlier
 *     style write for that node. Only the last such reset is kept.
 *   - Object-form (`styleOMValue`) writes after the last reset are PARTIAL diffs
 *     applied cumulatively; they are deep-merged (later property wins, `false`
 *     deletes preserved) into a single object placed at the last write's
 *     position. Earlier superseded object writes are dropped.
 *   - When all of a node's style writes are object-form, they all merge into one.
 *
 * This means a pure burst of string-form updates collapses to last-write-wins
 * (the common inline-style/opacity ticker case), while object-form partial diffs
 * such as `{color:'red'}` then `{background:'blue'}` are combined into
 * `{color:'red', background:'blue'}` rather than silently losing `color`.
 *
 * Only the `style` key is coalesced. Any other attribute keys on a superseded
 * entry are preserved in place, and non-style entries are left untouched.
 */
function coalesceStyleAttributes(attributes) {
    var e_3, _a;
    // Per node id, the array indices of attribute mutations carrying a `style` key.
    var styleIndicesById = new Map();
    attributes.forEach(function (attr, i) {
        if (attr.attributes && 'style' in attr.attributes) {
            var list = styleIndicesById.get(attr.id);
            if (list)
                list.push(i);
            else
                styleIndicesById.set(attr.id, [i]);
        }
    });
    if (styleIndicesById.size === 0)
        return attributes;
    var styleValueAt = function (i) { return attributes[i].attributes.style; };
    // Per attributes-array index: 'drop' removes the (superseded) style key.
    // mergedStyleByIndex replaces the style value with a deep-merged object.
    // Indices not present in either map keep their style value untouched.
    var dropStyleAt = new Set();
    var mergedStyleByIndex = new Map();
    var _loop_1 = function (indices) {
        if (indices.length === 1)
            return "continue"; // single style write — keep as-is
        // Position (within `indices`) of the last full-reset string/null write.
        var lastResetPos = -1;
        indices.forEach(function (idx, pos) {
            if (!isStyleObject(styleValueAt(idx)))
                lastResetPos = pos;
        });
        // Object-form writes that survive: those after the last reset (or all of
        // them when there is no reset). Everything before the last reset is dropped.
        var survivingObjectIndices = [];
        indices.forEach(function (idx, pos) {
            if (pos === lastResetPos)
                return; // the surviving full-reset write — keep
            dropStyleAt.add(idx);
            if (pos > lastResetPos)
                survivingObjectIndices.push(idx);
        });
        if (survivingObjectIndices.length > 0) {
            var lastObjectIdx = survivingObjectIndices[survivingObjectIndices.length - 1];
            var merged = mergeStyleDiffs(survivingObjectIndices.map(function (idx) { return styleValueAt(idx); }));
            dropStyleAt.delete(lastObjectIdx);
            mergedStyleByIndex.set(lastObjectIdx, merged);
        }
    };
    try {
        for (var _b = tslib_1.__values(styleIndicesById.values()), _c = _b.next(); !_c.done; _c = _b.next()) {
            var indices = _c.value;
            _loop_1(indices);
        }
    }
    catch (e_3_1) { e_3 = { error: e_3_1 }; }
    finally {
        try {
            if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
        }
        finally { if (e_3) throw e_3.error; }
    }
    if (dropStyleAt.size === 0 && mergedStyleByIndex.size === 0)
        return attributes;
    var result = [];
    attributes.forEach(function (attr, i) {
        var mergedStyle = mergedStyleByIndex.get(i);
        if (mergedStyle !== undefined) {
            result.push(tslib_1.__assign(tslib_1.__assign({}, attr), { attributes: tslib_1.__assign(tslib_1.__assign({}, attr.attributes), { style: mergedStyle }) }));
            return;
        }
        if (!dropStyleAt.has(i)) {
            result.push(attr);
            return;
        }
        // Superseded style write: drop only the `style` key, preserving any other
        // attribute keys. If the entry was style-only, it is dropped entirely.
        var rest = tslib_1.__assign({}, attr.attributes);
        delete rest.style;
        if (Object.keys(rest).length > 0) {
            result.push(tslib_1.__assign(tslib_1.__assign({}, attr), { attributes: rest }));
        }
    });
    return result;
}
function mergeGroup(events) {
    var e_4, _a, e_5, _b;
    var first = events[0];
    // Track first/last event index for each node's adds and removes.
    // lastParentById: final parent from most recent add (last-write-wins).
    var firstAddEventIndex = new Map();
    var lastAddEventIndex = new Map();
    var firstRemoveEventIndex = new Map();
    var lastRemoveEventIndex = new Map();
    var lastParentById = new Map();
    events.forEach(function (e, i) {
        var e_6, _a, e_7, _b;
        var data = e.data;
        try {
            for (var _c = tslib_1.__values(data.adds), _d = _c.next(); !_d.done; _d = _c.next()) {
                var add = _d.value;
                if (!firstAddEventIndex.has(add.node.id))
                    firstAddEventIndex.set(add.node.id, i);
                lastAddEventIndex.set(add.node.id, i);
                lastParentById.set(add.node.id, add.parentId);
            }
        }
        catch (e_6_1) { e_6 = { error: e_6_1 }; }
        finally {
            try {
                if (_d && !_d.done && (_a = _c.return)) _a.call(_c);
            }
            finally { if (e_6) throw e_6.error; }
        }
        try {
            for (var _e = tslib_1.__values(data.removes), _f = _e.next(); !_f.done; _f = _e.next()) {
                var remove = _f.value;
                if (!firstRemoveEventIndex.has(remove.id))
                    firstRemoveEventIndex.set(remove.id, i);
                lastRemoveEventIndex.set(remove.id, i);
            }
        }
        catch (e_7_1) { e_7 = { error: e_7_1 }; }
        finally {
            try {
                if (_f && !_f.done && (_b = _e.return)) _b.call(_e);
            }
            finally { if (e_7) throw e_7.error; }
        }
    });
    // Classify nodes that appear in both adds and removes within this window:
    //
    // Pure transient:          created here (firstAddIdx < firstRemoveIdx) and ultimately removed
    //                          (lastAddIdx < lastRemoveIdx). Cancel all adds + all removes.
    //
    // Pre-existing transient:  pre-existed in DOM (firstRemoveIdx < firstAddIdx — removed before
    //                          first add), then re-added, then removed again (lastAddIdx < lastRemoveIdx).
    //                          The rrweb replayer processes all removes first: the re-add would still
    //                          execute after both removes, leaving the node present when it should be
    //                          absent. Fix: cancel the re-add and all post-add removes; keep only the
    //                          pre-add removes (they represent the legitimate removal from the original
    //                          location).
    var transientIds = new Set();
    var preExistingTransientIds = new Set();
    try {
        for (var firstAddEventIndex_1 = tslib_1.__values(firstAddEventIndex), firstAddEventIndex_1_1 = firstAddEventIndex_1.next(); !firstAddEventIndex_1_1.done; firstAddEventIndex_1_1 = firstAddEventIndex_1.next()) {
            var _c = tslib_1.__read(firstAddEventIndex_1_1.value, 2), id = _c[0], firstAddIdx = _c[1];
            var firstRemoveIdx = firstRemoveEventIndex.get(id);
            if (firstRemoveIdx === undefined)
                continue;
            var lastAddIdx = lastAddEventIndex.get(id);
            var lastRemoveIdx = lastRemoveEventIndex.get(id);
            if (lastAddIdx >= lastRemoveIdx)
                continue; // ultimately present — keep as-is
            if (firstAddIdx < firstRemoveIdx) {
                transientIds.add(id);
            }
            else if (firstRemoveIdx < firstAddIdx) {
                // firstRemoveIdx < firstAddIdx: pre-existing node removed, re-added, then removed again
                preExistingTransientIds.add(id);
            }
            // firstAddIdx === firstRemoveIdx: same-event move (remove+add in one rrweb event) followed
            // by a later remove — keep all operations so the move and final removal survive
        }
    }
    catch (e_4_1) { e_4 = { error: e_4_1 }; }
    finally {
        try {
            if (firstAddEventIndex_1_1 && !firstAddEventIndex_1_1.done && (_a = firstAddEventIndex_1.return)) _a.call(firstAddEventIndex_1);
        }
        finally { if (e_4) throw e_4.error; }
    }
    // Cascade: nodes whose FINAL parent is effectively cancelled (transient or pre-existing-transient)
    // would be orphaned, so treat them as cancelled too.
    // Use lastParentById so a node moved away from a cancelled parent to a live one is not wrongly elided.
    //
    // Three cascade outcomes mirror the main-loop classification:
    //   transientIds:            no pre-existing removes (node created in window), or no remove/add overlap
    //   preExistingTransientIds: nodeFirstRemoveIdx < nodeFirstAddIdx (pre-existing, removed before re-add)
    //   cascadeDropAddsOnlyIds:  nodeFirstRemoveIdx === nodeFirstAddIdx (same-event move to cancelled parent)
    //                            → drop adds but preserve removes from non-cancelled parents
    var cascadeDropAddsOnlyIds = new Set();
    if (transientIds.size > 0 || preExistingTransientIds.size > 0) {
        var changed = true;
        while (changed) {
            changed = false;
            try {
                for (var lastParentById_1 = (e_5 = void 0, tslib_1.__values(lastParentById)), lastParentById_1_1 = lastParentById_1.next(); !lastParentById_1_1.done; lastParentById_1_1 = lastParentById_1.next()) {
                    var _d = tslib_1.__read(lastParentById_1_1.value, 2), nodeId = _d[0], parentId = _d[1];
                    if (!transientIds.has(nodeId) &&
                        !preExistingTransientIds.has(nodeId) &&
                        !cascadeDropAddsOnlyIds.has(nodeId) &&
                        (transientIds.has(parentId) || preExistingTransientIds.has(parentId) || cascadeDropAddsOnlyIds.has(parentId))) {
                        var nodeFirstRemoveIdx = firstRemoveEventIndex.get(nodeId);
                        var nodeFirstAddIdx = firstAddEventIndex.get(nodeId);
                        if (nodeFirstRemoveIdx !== undefined &&
                            nodeFirstAddIdx !== undefined &&
                            nodeFirstRemoveIdx < nodeFirstAddIdx) {
                            preExistingTransientIds.add(nodeId);
                        }
                        else if (nodeFirstRemoveIdx !== undefined &&
                            nodeFirstAddIdx !== undefined &&
                            nodeFirstRemoveIdx === nodeFirstAddIdx) {
                            cascadeDropAddsOnlyIds.add(nodeId);
                        }
                        else {
                            transientIds.add(nodeId);
                        }
                        changed = true;
                    }
                }
            }
            catch (e_5_1) { e_5 = { error: e_5_1 }; }
            finally {
                try {
                    if (lastParentById_1_1 && !lastParentById_1_1.done && (_b = lastParentById_1.return)) _b.call(lastParentById_1);
                }
                finally { if (e_5) throw e_5.error; }
            }
        }
    }
    var needsFilter = transientIds.size > 0 || preExistingTransientIds.size > 0 || cascadeDropAddsOnlyIds.size > 0;
    // Build filtered removes by iterating per event so we know each remove's event index.
    // Pure transients:          drop all removes.
    // Pre-existing transients:  drop removes at eventIdx >= firstAddIdx (the cancelled re-add cycle);
    //                           keep removes at eventIdx < firstAddIdx (legitimate pre-window removal).
    // Cascade drop-adds-only:   keep removes from non-cancelled parents; drop removes from cancelled parents
    //                           (the cancelled parent is never added in the replay, so a remove from it
    //                           would reference a non-existent node in the replayer).
    var filteredRemoves = [];
    events.forEach(function (e, eventIdx) {
        var e_8, _a;
        try {
            for (var _b = tslib_1.__values(e.data.removes), _c = _b.next(); !_c.done; _c = _b.next()) {
                var r = _c.value;
                if (transientIds.has(r.id))
                    continue;
                if (preExistingTransientIds.has(r.id) && eventIdx >= firstAddEventIndex.get(r.id))
                    continue;
                if (cascadeDropAddsOnlyIds.has(r.id) &&
                    (transientIds.has(r.parentId) ||
                        preExistingTransientIds.has(r.parentId) ||
                        cascadeDropAddsOnlyIds.has(r.parentId)))
                    continue;
                filteredRemoves.push(r);
            }
        }
        catch (e_8_1) { e_8 = { error: e_8_1 }; }
        finally {
            try {
                if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
            }
            finally { if (e_8) throw e_8.error; }
        }
    });
    var allAdds = events.flatMap(function (e) { return e.data.adds; });
    var allTexts = events.flatMap(function (e) { return e.data.texts; });
    var allAttributes = events.flatMap(function (e) { return e.data.attributes; });
    var mergedAttributes = needsFilter
        ? allAttributes.filter(function (a) { return !transientIds.has(a.id) && !preExistingTransientIds.has(a.id) && !cascadeDropAddsOnlyIds.has(a.id); })
        : allAttributes;
    var merged = {
        source: rrweb_types_1.IncrementalSource.Mutation,
        removes: filteredRemoves,
        adds: needsFilter
            ? allAdds.filter(function (a) {
                return !transientIds.has(a.node.id) &&
                    !preExistingTransientIds.has(a.node.id) &&
                    !cascadeDropAddsOnlyIds.has(a.node.id);
            })
            : allAdds,
        texts: needsFilter
            ? allTexts.filter(function (t) { return !transientIds.has(t.id) && !preExistingTransientIds.has(t.id) && !cascadeDropAddsOnlyIds.has(t.id); })
            : allTexts,
        attributes: coalesceStyleAttributes(mergedAttributes),
    };
    return tslib_1.__assign(tslib_1.__assign({}, first), { data: merged });
}
/**
 * Merges consecutive IncrementalSnapshot mutation events into a single event,
 * reducing overall event count without changing replay semantics.
 *
 * isAttachIframe events are never merged — they carry a full iframe document
 * tree and must remain isolated.
 */
function mergeMutationEvents(events) {
    if (events.length <= 1)
        return events;
    var result = [];
    var i = 0;
    while (i < events.length) {
        if (!isMergeableMutation(events[i])) {
            result.push(events[i]);
            i++;
            continue;
        }
        var j = i + 1;
        while (j < events.length && isMergeableMutation(events[j])) {
            j++;
        }
        result.push(j > i + 1 ? mergeGroup(events.slice(i, j)) : events[i]);
        i = j;
    }
    return result;
}
exports.mergeMutationEvents = mergeMutationEvents;
//# sourceMappingURL=merge-mutation-events.js.map