/**
 * Photoshop Script to Update Text Layers
 * 
 * This script reads a JSON file containing text layer information
 * and updates the text content of matching layers in the open PSD.
 * 
 * Usage:
 * 1. Open the PSD file in Photoshop
 * 2. Run this script: File > Scripts > Browse... > updateTextLayers.jsx
 * 3. Select the JSON file with text layer data
 * 4. The script will update matching text layers
 */

// Enable double clicking from the Finder/Explorer
#target photoshop

// Function to read text file
function readTextFile(fileObj) {
    fileObj.open('r');
    var content = fileObj.read();
    fileObj.close();
    return content;
}

// Parse JSON function (safe for ExtendScript)
function parseJson(text) {
    try {
        // First try the native JSON parser if available
        if (typeof JSON !== 'undefined' && JSON.parse) {
            return JSON.parse(text);
        }
    } catch (e) {
        $.writeln("Native JSON parse failed: " + e);
    }
    
    // Skip safety checks and just try eval directly
    try {
        // Direct eval as fallback
        var data = eval('(' + text + ')');
        return data;
    } catch (e) {
        $.writeln("JSON eval failed: " + e);
        
        // Ultimate fallback: try to manually extract the layer data
        try {
            $.writeln("Attempting emergency JSON parsing...");
            
            // Try to extract the textLayers array with a basic regex
            var layersMatch = text.match(/"textLayers"\s*:\s*\[([\s\S]*?)\]\s*\}/);
            if (layersMatch && layersMatch[1]) {
                var layersText = layersMatch[1];
                
                // Parse the text layers
                var result = {
                    textLayers: []
                };
                
                // Find all layer objects
                var layerRegex = /\{\s*"id"[\s\S]*?styleInfo[\s\S]*?\}\s*\}/g;
                var layerMatch;
                while ((layerMatch = layerRegex.exec(layersText)) !== null) {
                    var layerText = layerMatch[0];
                    
                    // Extract the key fields
                    var nameMatch = layerText.match(/"name"\s*:\s*"([^"]*)"/);
                    var textMatch = layerText.match(/"text"\s*:\s*"([^"]*)"/);
                    var updatedTextMatch = layerText.match(/"updatedText"\s*:\s*"([^"]*)"/);
                    
                    if (nameMatch && textMatch && updatedTextMatch) {
                        result.textLayers.push({
                            name: nameMatch[1].replace(/\\n/g, "\n"),
                            text: textMatch[1].replace(/\\n/g, "\n"),
                            updatedText: updatedTextMatch[1].replace(/\\n/g, "\n"),
                            exists: false // Will be checked later
                        });
                    }
                }
                
                if (result.textLayers.length > 0) {
                    $.writeln("Successfully extracted " + result.textLayers.length + " layers with emergency parser");
                    return result;
                }
            }
            
            throw new Error("Emergency parsing failed");
        } catch (emergencyError) {
            $.writeln("Emergency parsing failed: " + emergencyError);
            throw new Error("Failed to parse JSON: " + e.message);
        }
    }
}

// Function to find a layer by name
function findLayerByName(doc, layerName) {
    $.writeln("Looking for layer: " + layerName);
    
    // Helper function to search in layer sets
    function searchLayers(layers, indent) {
        indent = indent || "";
        for (var i = 0; i < layers.length; i++) {
            var layer = layers[i];
            
            $.writeln(indent + "- Checking: " + layer.name + " (Type: " + layer.typename + ")");
            
            // Check if this is the layer we're looking for
            if (layer.name === layerName) {
                $.writeln(indent + "  ✓ FOUND MATCH!");
                return layer;
            }
            
            // If it's a group, search inside
            if (layer.typename === "LayerSet") {
                $.writeln(indent + "  > Searching inside group: " + layer.name);
                var found = searchInGroup(layer, indent + "    ");
                if (found) return found;
            }
        }
        return null;
    }
    
    // Helper function to search in a layer group
    function searchInGroup(group, indent) {
        indent = indent || "";
        for (var i = 0; i < group.layers.length; i++) {
            var layer = group.layers[i];
            
            $.writeln(indent + "- Checking: " + layer.name + " (Type: " + layer.typename + ")");
            
            // Check if this is the layer we're looking for
            if (layer.name === layerName) {
                $.writeln(indent + "  ✓ FOUND MATCH!");
                return layer;
            }
            
            // If it's a group, search inside
            if (layer.typename === "LayerSet") {
                $.writeln(indent + "  > Searching inside group: " + layer.name);
                var found = searchInGroup(layer, indent + "    ");
                if (found) return found;
            }
        }
        return null;
    }
    
    // Start search from the root layers
    var result = searchLayers(doc.layers);
    
    if (!result) {
        $.writeln("❌ Layer not found: " + layerName);
    }
    
    return result;
}

// Function to update text in a text layer
function updateTextLayer(layer, newText, styleData) {
    if (layer.kind === LayerKind.TEXT) {
        $.writeln("Updating text layer: " + layer.name);
        $.writeln("Original layer text: " + layer.textItem.contents);
        $.writeln("New text from JSON: " + newText);
        
        try {
            // Process line breaks - handle various escape sequences
            var processedText = newText;
            
            // Replace encoded newlines with actual newlines
            processedText = processedText.replace(/\\n/g, "\n");
            processedText = processedText.replace(/\\r/g, "\r");
            
            // Check if we have rich text formatting data for this layer
            var hasRichFormatting = styleData && 
                                    styleData.styleInfo && 
                                    styleData.styleInfo.styles && 
                                    styleData.styleInfo.styles.length > 1 && 
                                    styleData.hasRichTextFormatting;
            
            if (hasRichFormatting) {
                $.writeln("Layer has rich text formatting - attempting to preserve styles with the new text");
                
                try {
                    // First update the basic text content
                    $.writeln("Setting basic text to: " + processedText);
                    layer.textItem.contents = processedText;
                    
                    // Make this the active layer to work with it
                    app.activeDocument.activeLayer = layer;
                    
                    // For rich text, we need to use action descriptors to apply styling
                    $.writeln("Rich text update is experimental and may not work perfectly");
                    $.writeln("Original style ranges count: " + styleData.styleInfo.styles.length);
                    
                    // Now we'll attempt to recreate the style formatting on the new text
                    // This is a complex operation and may not work perfectly in all cases
                    
                    // Only apply this if we have the advanced formatting data
                    if (styleData.styleInfo.styles.length > 0) {
                        $.writeln("Attempting to apply rich text formatting to " + styleData.styleInfo.styles.length + " style ranges");
                        
                        // Get the current layer ID - needed for targeting our actions
                        var ref = new ActionReference();
                        ref.putEnumerated(charIDToTypeID("Lyr "), charIDToTypeID("Ordn"), charIDToTypeID("Trgt"));
                        var layerDesc = executeActionGet(ref);
                        
                        // For each style range in our data, we'll attempt to apply it to the corresponding text
                        for (var i = 0; i < styleData.styleInfo.styles.length; i++) {
                            var style = styleData.styleInfo.styles[i];
                            
                            // Get the corresponding text range in the new text
                            // For simplicity, we'll use the same character positions
                            // This is imperfect but should work for direct translations
                            var rangeStart = style.start;
                            var rangeEnd = style.end;
                            
                            // Safety check - make sure our range is within the text bounds
                            if (rangeEnd > processedText.length) {
                                rangeEnd = processedText.length;
                            }
                            
                            // Only apply if we have a valid range
                            if (rangeStart < rangeEnd && rangeStart >= 0 && rangeEnd <= processedText.length) {
                                $.writeln("Applying style to range [" + rangeStart + "-" + rangeEnd + "]: " + 
                                         style.font + " " + style.style + " " + style.size + "pt");
                                
                                // Select the text range
                                var rangeDesc = new ActionDescriptor();
                                var fromDesc = new ActionDescriptor();
                                fromDesc.putInteger(stringIDToTypeID("from"), rangeStart);
                                fromDesc.putInteger(stringIDToTypeID("to"), rangeEnd);
                                rangeDesc.putObject(stringIDToTypeID("range"), stringIDToTypeID("textRange"), fromDesc);
                                
                                // Create the font style descriptor
                                var styleDesc = new ActionDescriptor();
                                
                                // Set font family and style if available
                                if (style.font && style.font !== "Unknown") {
                                    styleDesc.putString(stringIDToTypeID("fontName"), style.font);
                                }
                                
                                if (style.style && style.style !== "Regular") {
                                    styleDesc.putString(stringIDToTypeID("fontStyleName"), style.style);
                                }
                                
                                // Set font size
                                if (style.size) {
                                    styleDesc.putUnitDouble(stringIDToTypeID("size"), charIDToTypeID("#Pnt"), style.size);
                                }
                                
                                // Set color if available
                                if (style.color && style.color.length > 0) {
                                    // Color handling is complex and would need to match the document's color mode
                                    // This is a simplified approach
                                    var colorDesc = new ActionDescriptor();
                                    var rgb = new ActionDescriptor();
                                    
                                    // Assuming RGB values in 0-255 range
                                    if (style.color.length >= 3) {
                                        rgb.putDouble(stringIDToTypeID("red"), style.color[0]);
                                        rgb.putDouble(stringIDToTypeID("green"), style.color[1]);
                                        rgb.putDouble(stringIDToTypeID("blue"), style.color[2]);
                                        colorDesc.putObject(stringIDToTypeID("color"), stringIDToTypeID("RGBColor"), rgb);
                                        styleDesc.putObject(stringIDToTypeID("color"), stringIDToTypeID("color"), colorDesc);
                                    }
                                }
                                
                                // Apply the styling to the range
                                rangeDesc.putObject(stringIDToTypeID("textStyle"), stringIDToTypeID("textStyle"), styleDesc);
                                executeAction(stringIDToTypeID("setd"), rangeDesc, DialogModes.NO);
                            } else {
                                $.writeln("Skipping invalid range [" + rangeStart + "-" + rangeEnd + "]");
                            }
                        }
                    }
                    
                    return true;
                } catch (richTextError) {
                    $.writeln("ERROR applying rich text formatting: " + richTextError);
                    $.writeln("Falling back to basic text update");
                    
                    // If rich text formatting fails, fall back to basic update
                    layer.textItem.contents = processedText;
                    return true;
                }
            } else {
                // Basic text update
                $.writeln("Setting text to: " + processedText);
                layer.textItem.contents = processedText;
                return true;
            }
        } catch (e) {
            $.writeln("ERROR updating text: " + e);
            return false;
        }
    }
    
    $.writeln("Not a text layer: " + layer.name + " (kind: " + layer.kind + ")");
    return false;
}

// Function to list all available text layers in the document
function listAvailableTextLayers(doc) {
    var allLayers = [];
    
    function collectLayers(layers, path) {
        path = path || "";
        
        for (var i = 0; i < layers.length; i++) {
            var layer = layers[i];
            var layerPath = path ? path + "/" + layer.name : layer.name;
            
            // Add this layer to our collection if it's a text layer
            if (layer.kind === LayerKind.TEXT) {
                allLayers.push({
                    name: layer.name,
                    path: layerPath,
                    text: layer.textItem.contents
                });
            }
            
            // If it's a group, collect layers from inside
            if (layer.typename === "LayerSet") {
                collectLayers(layer.layers, layerPath);
            }
        }
    }
    
    // Start collecting from the root
    collectLayers(doc.layers);
    return allLayers;
}

// Main function
function main() {
    try {
        // Check if a document is open
        if (!documents.length) {
            alert("Please open a PSD file before running this script.");
            return;
        }
        
        // Get the active document
        var doc = app.activeDocument;
        
        // Prompt user to select the JSON file
        var jsonFile = File.openDialog("Select the JSON file with text layer data", "*.json");
        if (!jsonFile) {
            return; // User cancelled
        }
        
        // Read and parse the JSON file
        var jsonContent = readTextFile(jsonFile);
        
        // Clean up common JSON errors
        jsonContent = jsonContent
            // Remove "px" or other units from numeric values
            .replace(/(\d+)\s*px/g, '$1')
            // Fix unescaped backslashes
            .replace(/([^\\])\\([^\\"])/g, '$1\\\\$2')
            // Remove trailing commas in arrays and objects
            .replace(/,\s*([}\]])/g, '$1')
            // Fix newline characters
            .replace(/\r\n/g, '\\n')
            .replace(/\r/g, '\\n');
        
        try {
            var textData = parseJson(jsonContent);
            
            if (!textData.textLayers || !textData.textLayers.length) {
                alert("No text layers found in the JSON file.");
                return;
            }
            
            // First, get all available text layers in the document
            $.writeln("\n=== SCANNING DOCUMENT FOR TEXT LAYERS ===");
            var availableLayers = listAvailableTextLayers(doc);
            $.writeln("Found " + availableLayers.length + " text layers in document");
            
            for (var i = 0; i < availableLayers.length; i++) {
                $.writeln((i+1) + ". " + availableLayers[i].name + ": \"" + 
                        availableLayers[i].text.substring(0, 30) + 
                        (availableLayers[i].text.length > 30 ? "..." : "") + "\"");
            }
            
            // Map layer names to their information for quick lookup
            var availableLayerMap = {};
            for (var i = 0; i < availableLayers.length; i++) {
                availableLayerMap[availableLayers[i].name] = availableLayers[i];
            }
            
            // Start undo group for the entire operation
            app.activeDocument.suspendHistory("Update Text Layers", "updateLayers()");
            
            function updateLayers() {
                // Counter for successfully updated layers
                var updatedCount = 0;
                var failedCount = 0;
                var skippedCount = 0;
                var failedLayers = [];
                
                // First, check which layers from JSON exist in the document
                $.writeln("\n=== MATCHING JSON LAYERS WITH DOCUMENT ===");
                for (var i = 0; i < textData.textLayers.length; i++) {
                    var layerName = textData.textLayers[i].name;
                    if (availableLayerMap[layerName]) {
                        $.writeln("✓ Found match for: " + layerName);
                        // Add a flag to indicate this layer exists in the document
                        textData.textLayers[i].exists = true;
                    } else {
                        $.writeln("❌ No match found for: " + layerName);
                        // Mark as not existing
                        textData.textLayers[i].exists = false;
                    }
                }
                
                // Process each text layer in the JSON file
                for (var i = 0; i < textData.textLayers.length; i++) {
                    var layerData = textData.textLayers[i];
                    
                    // Debug: Print layer data to console
                    $.writeln("\n=== PROCESSING LAYER: " + layerData.name + " ===");
                    
                    // Skip layers that don't exist in the document
                    if (!layerData.exists) {
                        $.writeln("⚠️ Skipping - layer doesn't exist in document");
                        skippedCount++;
                        continue;
                    }
                    
                    $.writeln("Original text: \"" + layerData.text + "\"");
                    $.writeln("Updated text: \"" + layerData.updatedText + "\"");
                    
                    // Skip if no updatedText
                    if (!layerData.updatedText) {
                        $.writeln("⚠️ Skipping - no updated text");
                        skippedCount++;
                        continue;
                    }
                    
                    // Simple comparison to see if text has changed at all
                    var hasExactSameText = layerData.text === layerData.updatedText;
                    
                    $.writeln("Exact text match? " + hasExactSameText);
                    
                    if (hasExactSameText) {
                        $.writeln("⚠️ Skipping - completely identical text");
                        skippedCount++;
                        continue;
                    }
                    
                    // Find the layer in the document using our direct search
                    $.writeln("Looking for layer: " + layerData.name);
                    var layer = findLayerByName(doc, layerData.name);
                    
                    if (layer) {
                        // Check if the layer has rich text formatting information
                        var hasRichTextInfo = layerData.hasRichTextFormatting || 
                                             (layerData.styleInfo && 
                                              layerData.styleInfo.styles && 
                                              layerData.styleInfo.styles.length > 1);
                        
                        if (hasRichTextInfo) {
                            $.writeln("Layer has rich text formatting information");
                        }
                        
                        // Update the text, passing the entire layer data for styling
                        var success = updateTextLayer(layer, layerData.updatedText, layerData);
                        if (success) {
                            updatedCount++;
                        } else {
                            failedCount++;
                            failedLayers.push(layerData.name + " (update failed)");
                        }
                    } else {
                        failedCount++;
                        failedLayers.push(layerData.name + " (not found)");
                    }
                }
                
                // Show results
                var resultMessage = "Text Layer Update Complete\n\n";
                resultMessage += "Successfully updated: " + updatedCount + " layers\n";
                resultMessage += "Skipped (no change): " + skippedCount + " layers\n";
                resultMessage += "Failed to update: " + failedCount + " layers\n";
                
                if (failedCount > 0) {
                    resultMessage += "\nFailed layers:\n" + failedLayers.join("\n");
                }
                
                alert(resultMessage);
            }
            
        } catch (e) {
            alert("Error parsing JSON file: " + e.message + "\n\nPlease check that the file is valid JSON.");
        }
    } catch (err) {
        alert("Error: " + err.message);
    }
}

// Run the script
main();