#target photoshop

(function () {
    var oldDialogs = app.displayDialogs;
    app.displayDialogs = DialogModes.NO;
    var scriptFile = null;

    try {
        if (app.documents.length < 1) {
            alert("Please open a Photoshop document first.");
            return;
        }

        scriptFile = new File($.fileName);
        var exeFile = new File(scriptFile.parent.fsName + "/DotClean.exe");
        if (!exeFile.exists) {
            alert("DotClean.exe was not found:\n" + exeFile.fsName + "\n\nPut this JSX file in the same folder as DotClean.exe.");
            return;
        }

        var originalDoc = app.activeDocument;
        var originalLayer = originalDoc.activeLayer;
        var layerName = originalLayer.name;
        var originalDocWidth = px(originalDoc.width);
        var originalDocHeight = px(originalDoc.height);
        var bounds = originalLayer.bounds;
        var left = px(bounds[0]);
        var top = px(bounds[1]);
        var right = px(bounds[2]);
        var bottom = px(bounds[3]);
        var width = right - left;
        var height = bottom - top;

        if (width <= 0 || height <= 0) {
            alert("The selected layer has no processable pixel content.");
            return;
        }

        var tempFolder = new Folder(Folder.temp.fsName + "/qwt_remove_floating_dots");
        if (!tempFolder.exists) {
            tempFolder.create();
        }

        var stamp = String(new Date().getTime());
        var safeLayerName = safeFilePart(layerName);
        var inputFile = new File(tempFolder.fsName + "/" + safeLayerName + "_" + stamp + ".png");
        var outputFile = new File(tempFolder.fsName + "/" + safeLayerName + "_" + stamp + "_done.png");

        exportSelectedLayerDocument(originalDoc, originalLayer, inputFile);
        writePhotoshopLog(exeFile.parent, inputFile, outputFile);
        writeCallbackFile(exeFile.parent, originalDoc.name, outputFile, layerName + "_processed", originalDocWidth, originalDocHeight, inputFile);

        if (!exeFile.execute()) {
            throw new Error("Failed to start processor:\n" + exeFile.fsName);
        }
    } catch (err) {
        writeJsxError(scriptFile, err);
    } finally {
        app.displayDialogs = oldDialogs;
    }

    function px(value) {
        return Math.round(value.as("px"));
    }

    function exportSelectedLayerDocument(originalDoc, originalLayer, inputFile) {
        app.activeDocument = originalDoc;
        originalDoc.activeLayer = originalLayer;

        var tempDoc = originalDoc.duplicate("qwt_export_layer", false);
        app.activeDocument = tempDoc;
        removeOtherLayers(tempDoc, tempDoc.activeLayer);
        makeLayerTreeVisible(tempDoc.activeLayer);

        savePng(tempDoc, inputFile);
        tempDoc.close(SaveOptions.DONOTSAVECHANGES);
        app.activeDocument = originalDoc;
    }

    function removeOtherLayers(container, keepLayer) {
        for (var isoo = container.layers.length - 1; isoo >= 0; isoo--) {
            var layer = container.layers[isoo];
            if (layer == keepLayer) {
                continue;
            }

            if (layer.typename == "LayerSet" && containsLayer(layer, keepLayer)) {
                removeOtherLayers(layer, keepLayer);
            } else {
                layer.remove();
            }
        }
    }

    function containsLayer(container, keepLayer) {
        for (var isoo = 0; isoo < container.layers.length; isoo++) {
            var layer = container.layers[isoo];
            if (layer == keepLayer) {
                return true;
            }

            if (layer.typename == "LayerSet" && containsLayer(layer, keepLayer)) {
                return true;
            }
        }

        return false;
    }

    function makeLayerTreeVisible(layer) {
        try {
            layer.visible = true;
        } catch (ignore) {
        }

        var parent = layer.parent;
        while (parent && parent.typename == "LayerSet") {
            try {
                parent.visible = true;
            } catch (ignoreParent) {
            }
            parent = parent.parent;
        }
    }

    function writePhotoshopLog(runtimeFolder, inputFile, outputFile) {
        var logFile = new File(runtimeFolder.fsName + "/ps.log");
        var resultFile = new File(runtimeFolder.fsName + "/ps_result.log");

        tryRemove(logFile);
        tryRemove(resultFile);

        logFile.encoding = "UTF8";
        logFile.open("w");
        logFile.writeln(inputFile.fsName);
        logFile.writeln(outputFile.fsName);
        logFile.close();
    }

    function writeCallbackFile(runtimeFolder, docName, outputFile, resultName, expectedDocWidth, expectedDocHeight, inputFile) {
        var callbackFile = new File(runtimeFolder.fsName + "/ps_callback.jsx");
        tryRemove(callbackFile);

        callbackFile.encoding = "UTF8";
        callbackFile.open("w");
        callbackFile.writeln("#target photoshop");
        callbackFile.writeln("(function () {");
        callbackFile.writeln("    var oldDialogs = app.displayDialogs;");
        callbackFile.writeln("    app.displayDialogs = DialogModes.NO;");
        callbackFile.writeln("    try {");
        callbackFile.writeln("        var runtimeFolder = new Folder(" + jsString(runtimeFolder.fsName) + ");");
        callbackFile.writeln("        var resultFile = new File(runtimeFolder.fsName + '/ps_result.log');");
        callbackFile.writeln("        var inputFile = new File(" + jsString(inputFile.fsName) + ");");
        callbackFile.writeln("        var outputFile = new File(" + jsString(outputFile.fsName) + ");");
        callbackFile.writeln("        var psLogFile = new File(runtimeFolder.fsName + '/ps.log');");
        callbackFile.writeln("        var callbackFile = new File(runtimeFolder.fsName + '/ps_callback.jsx');");
        callbackFile.writeln("        var result = readResult(resultFile);");
        callbackFile.writeln("        if (result.status == 'cancelled') { writeCallbackLog(runtimeFolder, 'Photoshop processing cancelled.'); cleanup(inputFile, null, psLogFile, resultFile, callbackFile); return; }");
        callbackFile.writeln("        if (result.success != '1') { writeCallbackLog(runtimeFolder, 'Processing failed: ' + result.message); cleanup(inputFile, null, psLogFile, resultFile, callbackFile); return; }");
        callbackFile.writeln("        if (!outputFile.exists) { writeCallbackLog(runtimeFolder, 'Output file missing: ' + outputFile.fsName); cleanup(inputFile, null, psLogFile, resultFile, callbackFile); return; }");
        callbackFile.writeln("        var originalDoc = findDocument(" + jsString(docName) + ");");
        callbackFile.writeln("        app.activeDocument = originalDoc;");
        callbackFile.writeln("        importResultLayer(originalDoc, outputFile, " + jsString(resultName) + ", " + expectedDocWidth + ", " + expectedDocHeight + ", runtimeFolder);");
        callbackFile.writeln("        writeCallbackLog(runtimeFolder, 'Current layer processed. Regions: ' + result.removed_regions + '; Pixels: ' + result.removed_pixels + '; Time: ' + result.elapsed_ms + ' ms');");
        callbackFile.writeln("        cleanup(inputFile, outputFile, psLogFile, resultFile, callbackFile);");
        callbackFile.writeln("    } catch (err) {");
        callbackFile.writeln("        writeCallbackLog(runtimeFolder, 'Photoshop callback failed: ' + err);");
        callbackFile.writeln("    } finally {");
        callbackFile.writeln("        app.displayDialogs = oldDialogs;");
        callbackFile.writeln("    }");
        callbackFile.writeln("");
        callbackFile.writeln("    function readResult(file) {");
        callbackFile.writeln("        var data = {};");
        callbackFile.writeln("        if (!file.exists) { data.status = 'failed'; data.success = '0'; data.message = 'Result log not found'; return data; }");
        callbackFile.writeln("        file.encoding = 'UTF8';");
        callbackFile.writeln("        file.open('r');");
        callbackFile.writeln("        var text = file.read();");
        callbackFile.writeln("        file.close();");
        callbackFile.writeln("        var lines = text.split(/\\r\\n|\\n|\\r/);");
        callbackFile.writeln("        for (var isoo = 0; isoo < lines.length; isoo++) {");
        callbackFile.writeln("            var match = lines[isoo].match(/^([^=]+)=(.*)$/);");
        callbackFile.writeln("            if (match) { data[match[1]] = match[2]; }");
        callbackFile.writeln("        }");
        callbackFile.writeln("        return data;");
        callbackFile.writeln("    }");
        callbackFile.writeln("");
        callbackFile.writeln("    function findDocument(docName) {");
        callbackFile.writeln("        for (var isoo = 0; isoo < app.documents.length; isoo++) {");
        callbackFile.writeln("            if (app.documents[isoo].name == docName) { return app.documents[isoo]; }");
        callbackFile.writeln("        }");
        callbackFile.writeln("        throw new Error('Original Photoshop document not found: ' + docName);");
        callbackFile.writeln("    }");
        callbackFile.writeln("");
        callbackFile.writeln("    function importResultLayer(originalDoc, outputFile, resultName, expectedDocWidth, expectedDocHeight, runtimeFolder) {");
        callbackFile.writeln("        var resultSize = getPngSize(outputFile, originalDoc);");
        callbackFile.writeln("        var currentDocWidth = Math.round(originalDoc.width.as('px'));");
        callbackFile.writeln("        var currentDocHeight = Math.round(originalDoc.height.as('px'));");
        callbackFile.writeln("        if (currentDocWidth != expectedDocWidth || currentDocHeight != expectedDocHeight || resultSize.width != expectedDocWidth || resultSize.height != expectedDocHeight) {");
        callbackFile.writeln("            enableSnapIfOff(runtimeFolder);");
        callbackFile.writeln("        }");
        callbackFile.writeln("        app.activeDocument = originalDoc;");
        callbackFile.writeln("        placeFile(outputFile);");
        callbackFile.writeln("        originalDoc.activeLayer.name = resultName;");
        callbackFile.writeln("    }");
        callbackFile.writeln("");
        callbackFile.writeln("    function placeFile(file) {");
        callbackFile.writeln("        var desc = new ActionDescriptor();");
        callbackFile.writeln("        desc.putPath(charIDToTypeID('null'), file);");
        callbackFile.writeln("        desc.putEnumerated(charIDToTypeID('FTcs'), charIDToTypeID('QCSt'), charIDToTypeID('Qcsa'));");
        callbackFile.writeln("        var offsetDesc = new ActionDescriptor();");
        callbackFile.writeln("        offsetDesc.putUnitDouble(charIDToTypeID('Hrzn'), charIDToTypeID('#Pxl'), 0);");
        callbackFile.writeln("        offsetDesc.putUnitDouble(charIDToTypeID('Vrtc'), charIDToTypeID('#Pxl'), 0);");
        callbackFile.writeln("        desc.putObject(charIDToTypeID('Ofst'), charIDToTypeID('Ofst'), offsetDesc);");
        callbackFile.writeln("        executeAction(charIDToTypeID('Plc '), desc, DialogModes.NO);");
        callbackFile.writeln("    }");
        callbackFile.writeln("");
        callbackFile.writeln("    function getPngSize(file, originalDoc) {");
        callbackFile.writeln("        var size = { width: 0, height: 0 };");
        callbackFile.writeln("        var resultDoc = app.open(file);");
        callbackFile.writeln("        size.width = Math.round(resultDoc.width.as('px'));");
        callbackFile.writeln("        size.height = Math.round(resultDoc.height.as('px'));");
        callbackFile.writeln("        resultDoc.close(SaveOptions.DONOTSAVECHANGES);");
        callbackFile.writeln("        app.activeDocument = originalDoc;");
        callbackFile.writeln("        return size;");
        callbackFile.writeln("    }");
        callbackFile.writeln("");
        callbackFile.writeln("    function enableSnapIfOff(runtimeFolder) {");
        callbackFile.writeln("        try {");
        callbackFile.writeln("            var snapEnabled = getSnapEnabled();");
        callbackFile.writeln("            if (snapEnabled === true) {");
        callbackFile.writeln("                writeCallbackLog(runtimeFolder, 'Size changed; Photoshop Snap was already enabled.');");
        callbackFile.writeln("                return;");
        callbackFile.writeln("            }");
        callbackFile.writeln("            if (snapEnabled === false) {");
        callbackFile.writeln("                selectSnapMenuItem();");
        callbackFile.writeln("                writeCallbackLog(runtimeFolder, 'Size changed; enabled Photoshop View > Snap.');");
        callbackFile.writeln("                return;");
        callbackFile.writeln("            }");
        callbackFile.writeln("            writeCallbackLog(runtimeFolder, 'Size changed, but Snap state could not be read reliably.');");
        callbackFile.writeln("        } catch (err) {");
        callbackFile.writeln("            writeCallbackLog(runtimeFolder, 'Failed to enable Photoshop Snap: ' + err);");
        callbackFile.writeln("        }");
        callbackFile.writeln("    }");
        callbackFile.writeln("");
        callbackFile.writeln("    function getSnapEnabled() {");
        callbackFile.writeln("        try {");
        callbackFile.writeln("            var ref = new ActionReference();");
        callbackFile.writeln("            ref.putProperty(stringIDToTypeID('property'), stringIDToTypeID('menuBarInfo'));");
        callbackFile.writeln("            ref.putEnumerated(stringIDToTypeID('application'), stringIDToTypeID('ordinal'), stringIDToTypeID('targetEnum'));");
        callbackFile.writeln("            var viewMenus = executeActionGet(ref).getObjectValue(stringIDToTypeID('menuBarInfo')).getList(stringIDToTypeID('submenu')).getObjectValue(9).getList(stringIDToTypeID('submenu'));");
        callbackFile.writeln("            return viewMenus.getObjectValue(20).getBoolean(stringIDToTypeID('checked'));");
        callbackFile.writeln("        } catch (err) {");
        callbackFile.writeln("            return null;");
        callbackFile.writeln("        }");
        callbackFile.writeln("    }");
        callbackFile.writeln("");
        callbackFile.writeln("    function selectSnapMenuItem() {");
        callbackFile.writeln("        var d = new ActionDescriptor();");
        callbackFile.writeln("        var r = new ActionReference();");
        callbackFile.writeln("        r.putEnumerated(charIDToTypeID('Mn  '), charIDToTypeID('MnIt'), stringIDToTypeID('snap'));");
        callbackFile.writeln("        d.putReference(charIDToTypeID('null'), r);");
        callbackFile.writeln("        executeAction(charIDToTypeID('slct'), d, DialogModes.NO);");
        callbackFile.writeln("    }");
        callbackFile.writeln("");
        callbackFile.writeln("    function cleanup(inputFile, outputFile, psLogFile, resultFile, callbackFile) {");
        callbackFile.writeln("        tryRemove(inputFile);");
        callbackFile.writeln("        if (outputFile != null) { tryRemove(outputFile); }");
        callbackFile.writeln("        tryRemove(psLogFile);");
        callbackFile.writeln("        tryRemove(resultFile);");
        callbackFile.writeln("        tryRemove(callbackFile);");
        callbackFile.writeln("    }");
        callbackFile.writeln("");
        callbackFile.writeln("    function tryRemove(file) {");
        callbackFile.writeln("        try { if (file.exists) { file.remove(); } } catch (ignore) {}");
        callbackFile.writeln("    }");
        callbackFile.writeln("");
        callbackFile.writeln("    function writeCallbackLog(runtimeFolder, message) {");
        callbackFile.writeln("        try {");
        callbackFile.writeln("            var logFile = new File(runtimeFolder.fsName + '/ps_callback_status.log');");
        callbackFile.writeln("            logFile.encoding = 'UTF8';");
        callbackFile.writeln("            logFile.open('w');");
        callbackFile.writeln("            logFile.writeln(message);");
        callbackFile.writeln("            logFile.close();");
        callbackFile.writeln("        } catch (ignore) {}");
        callbackFile.writeln("    }");
        callbackFile.writeln("})();");
        callbackFile.close();
    }

    function savePng(doc, file) {
        var options = new PNGSaveOptions();
        options.interlaced = false;
        doc.saveAs(file, options, true, Extension.LOWERCASE);
    }

    function safeFilePart(value) {
        var text = String(value);
        text = text.replace(/[\\\/:\*\?"<>\|]/g, "_");
        text = text.replace(/^\s+|\s+$/g, "");
        if (text == "") {
            text = "selected_layer";
        }
        return text;
    }

    function jsString(value) {
        var text = String(value);
        text = text.replace(/\\/g, "\\\\");
        text = text.replace(/'/g, "\\'");
        text = text.replace(/\r/g, "\\r");
        text = text.replace(/\n/g, "\\n");
        return "'" + text + "'";
    }

    function writeJsxError(scriptFile, err) {
        try {
            var folder = Folder.temp;
            if (scriptFile != null) {
                folder = scriptFile.parent;
            }
            var logFile = new File(folder.fsName + "/ps_jsx_error.log");
            logFile.encoding = "UTF8";
            logFile.open("w");
            logFile.writeln("Current layer processing failed:");
            logFile.writeln(String(err));
            logFile.close();
        } catch (ignore) {
        }
    }

    function tryRemove(file) {
        try {
            var freshFile = new File(file.fsName);
            if (freshFile.exists) {
                freshFile.remove();
            }
        } catch (ignore) {
        }
    }
})();
