(function duplicateCompDeep() {

    var DEFAULT_SUFFIX = "_copy";
    var suffix = DEFAULT_SUFFIX;

    // ---------- Recursive duplication ----------
    // original comp id -> { orig: CompItem, copy: CompItem }
    var cache = {};
    // old layer name -> new layer name (AE renames a layer when its source is
    // swapped and the layer name was following the source name)
    var layerNameMap = {};

    function swapSource(layer, newSrc) {
        var oldName = layer.name;
        layer.replaceSource(newSrc, true);
        if (layer.name !== oldName) layerNameMap[oldName] = layer.name;
    }

    function deepDuplicate(comp) {
        if (cache[comp.id]) return cache[comp.id].copy;

        var newComp = comp.duplicate();
        newComp.name = comp.name + suffix;
        cache[comp.id] = { orig: comp, copy: newComp };

        for (var i = 1; i <= newComp.numLayers; i++) {
            var layer = newComp.layer(i);
            if (!(layer instanceof AVLayer)) continue; // lights, cameras...
            if (layer.source instanceof CompItem) {
                var newSrc = deepDuplicate(layer.source);
                swapSource(layer, newSrc);
            }
        }
        return newComp;
    }

    // ---------- Expression rewriting ----------
    function walkProps(group, fn) {
        for (var i = 1; i <= group.numProperties; i++) {
            var p = group.property(i);
            if (p.propertyType === PropertyType.PROPERTY) {
                fn(p);
            } else {
                walkProps(p, fn);
            }
        }
    }

    // Rewrites, in every expression of the duplicated tree:
    //   comp("Old")  -> comp("Old_suffix")   for every duplicated comp
    //   layer("Old") -> layer("Old_suffix")  for every layer that got renamed
    //                                        because its source was swapped
    // Returns the number of expressions changed.
    function fixExpressions() {
        var nameMap = {};
        for (var id in cache) {
            if (!cache.hasOwnProperty(id)) continue;
            nameMap[cache[id].orig.name] = cache[id].copy.name;
        }

        var reComp  = /comp\(\s*(["'])(.*?)\1\s*\)/g;
        var reLayer = /layer\(\s*(["'])(.*?)\1\s*\)/g;
        var changed = 0;

        for (var id2 in cache) {
            if (!cache.hasOwnProperty(id2)) continue;
            var newComp = cache[id2].copy;

            for (var l = 1; l <= newComp.numLayers; l++) {
                var layer = newComp.layer(l);
                var wasLocked = layer.locked;
                if (wasLocked) layer.locked = false; // locked layers refuse edits

                walkProps(layer, function (prop) {
                    if (!prop.canSetExpression || prop.expression === "") return;
                    var src = prop.expression;
                    var out = src.replace(reComp, function (match, quote, name) {
                        if (nameMap.hasOwnProperty(name)) {
                            return 'comp(' + quote + nameMap[name] + quote + ')';
                        }
                        return match;
                    });
                    out = out.replace(reLayer, function (match, quote, name) {
                        if (layerNameMap.hasOwnProperty(name)) {
                            return 'layer(' + quote + layerNameMap[name] + quote + ')';
                        }
                        return match;
                    });
                    if (out !== src) {
                        try {
                            prop.expression = out;
                            changed++;
                        } catch (e) {
                            // leave the original expression untouched on failure
                        }
                    }
                });

                if (wasLocked) layer.locked = true;
            }
        }
        return changed;
    }

    // ---------- Figure out what to duplicate ----------
    var activeComp = (app.project.activeItem instanceof CompItem) ? app.project.activeItem : null;

    // 1) Pre-comp layers selected in the Timeline
    var precompLayers = [];
    if (activeComp) {
        var selLayers = activeComp.selectedLayers;
        for (var i = 0; i < selLayers.length; i++) {
            var l = selLayers[i];
            if (l instanceof AVLayer && l.source instanceof CompItem) precompLayers.push(l);
        }
    }

    // 2) Fallback: comps selected in the Project panel, then the active comp
    var projectComps = [];
    if (precompLayers.length === 0) {
        var sel = app.project.selection;
        for (var j = 0; j < sel.length; j++) {
            if (sel[j] instanceof CompItem) projectComps.push(sel[j]);
        }
        if (projectComps.length === 0 && activeComp) projectComps.push(activeComp);
    }

    if (precompLayers.length === 0 && projectComps.length === 0) {
        alert("Select a pre-comp layer in the Timeline (or open a comp) and run the script again.");
        return;
    }

    var answer = prompt("Suffix for the duplicated comp names:", DEFAULT_SUFFIX, "Duplicate Comp Deep");
    if (answer === null) return; // cancelled
    if (answer !== "") suffix = answer;

    // ---------- Run ----------
    app.beginUndoGroup("Duplicate Comp Deep");
    try {
        var created = [];

        if (precompLayers.length > 0) {
            // Timeline mode: duplicate each selected layer's comp and swap the layer to it
            for (var p = 0; p < precompLayers.length; p++) {
                var lay = precompLayers[p];
                var newComp = deepDuplicate(lay.source);
                swapSource(lay, newComp);
                created.push(newComp);
            }
        } else {
            // Project mode: duplicate the comp itself
            for (var t = 0; t < projectComps.length; t++) {
                created.push(deepDuplicate(projectComps[t]));
            }
            for (var s = 1; s <= app.project.numItems; s++) app.project.item(s).selected = false;
            for (var c = 0; c < created.length; c++) created[c].selected = true;
        }

        var exprCount = fixExpressions();

        var total = 0;
        for (var k in cache) if (cache.hasOwnProperty(k)) total++;
        alert("Done.\n" + created.length + " comp(s) duplicated, " + total +
              " composition(s) created in total (including nested pre-comps).\n" +
              exprCount + " expression(s) re-linked to the new comps.");
    } catch (err) {
        alert("Duplicate Comp Deep - error:\n" + err.toString());
    } finally {
        app.endUndoGroup();
    }

})();