/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import * as fs from 'node:fs/promises';
import { createWriteStream, existsSync, statSync } from 'node:fs';
import { execSync, spawn } from 'node:child_process';
import * as path from 'node:path';
import { debugLogger, spawnAsync, escapePath, Storage, } from '@google/gemini-cli-core';
/**
 * Supported image file extensions based on Gemini API.
 * See: https://ai.google.dev/gemini-api/docs/image-understanding
 */
export const IMAGE_EXTENSIONS = [
    '.png',
    '.jpg',
    '.jpeg',
    '.webp',
    '.heic',
    '.heif',
];
/** Matches strings that start with a path prefix (/, ~, ., Windows drive letter, or UNC path) */
const PATH_PREFIX_PATTERN = /^([/~.]|[a-zA-Z]:|\\\\)/;
// Track which tool works on Linux to avoid redundant checks/failures
let linuxClipboardTool = null;
// Helper to check the user's display server and whether they have a compatible clipboard tool installed
function getUserLinuxClipboardTool() {
    if (linuxClipboardTool !== null) {
        return linuxClipboardTool;
    }
    let toolName = null;
    const displayServer = process.env['XDG_SESSION_TYPE'];
    if (displayServer === 'wayland')
        toolName = 'wl-paste';
    else if (displayServer === 'x11')
        toolName = 'xclip';
    else
        return null;
    try {
        // output is piped to stdio: 'ignore' to suppress the path printing to console
        execSync(`command -v ${toolName}`, { stdio: 'ignore' });
        linuxClipboardTool = toolName;
        return toolName;
    }
    catch (e) {
        debugLogger.warn(`${toolName} not found. Please install it: ${e}`);
        return null;
    }
}
/**
 * Helper to save command stdout to a file while preventing shell injections and race conditions
 */
async function saveFromCommand(command, args, destination) {
    return new Promise((resolve) => {
        const child = spawn(command, args);
        const fileStream = createWriteStream(destination);
        let resolved = false;
        const safeResolve = (value) => {
            if (!resolved) {
                resolved = true;
                resolve(value);
            }
        };
        child.stdout.pipe(fileStream);
        child.on('error', (err) => {
            debugLogger.debug(`Failed to spawn ${command}:`, err);
            safeResolve(false);
        });
        fileStream.on('error', (err) => {
            debugLogger.debug(`File stream error for ${destination}:`, err);
            safeResolve(false);
        });
        child.on('close', async (code) => {
            if (resolved)
                return;
            if (code !== 0) {
                debugLogger.debug(`${command} exited with code ${code}. Args: ${args.join(' ')}`);
                safeResolve(false);
                return;
            }
            // Helper to check file size
            const checkFile = async () => {
                try {
                    const stats = await fs.stat(destination);
                    safeResolve(stats.size > 0);
                }
                catch (e) {
                    debugLogger.debug(`Failed to stat output file ${destination}:`, e);
                    safeResolve(false);
                }
            };
            if (fileStream.writableFinished) {
                await checkFile();
            }
            else {
                fileStream.on('finish', checkFile);
                // In case finish never fires due to error (though error handler should catch it)
                fileStream.on('close', async () => {
                    if (!resolved)
                        await checkFile();
                });
            }
        });
    });
}
/**
 * Checks if the Wayland clipboard contains an image using wl-paste.
 */
async function checkWlPasteForImage() {
    try {
        const { stdout } = await spawnAsync('wl-paste', ['--list-types']);
        return stdout.includes('image/');
    }
    catch (e) {
        debugLogger.warn('Error checking wl-clipboard for image:', e);
    }
    return false;
}
/**
 * Checks if the X11 clipboard contains an image using xclip.
 */
async function checkXclipForImage() {
    try {
        const { stdout } = await spawnAsync('xclip', [
            '-selection',
            'clipboard',
            '-t',
            'TARGETS',
            '-o',
        ]);
        return stdout.includes('image/');
    }
    catch (e) {
        debugLogger.warn('Error checking xclip for image:', e);
    }
    return false;
}
/**
 * Checks if the system clipboard contains an image (macOS, Windows, and Linux)
 * @returns true if clipboard contains an image
 */
export async function clipboardHasImage() {
    if (process.platform === 'linux') {
        const tool = getUserLinuxClipboardTool();
        if (tool === 'wl-paste') {
            if (await checkWlPasteForImage())
                return true;
        }
        else if (tool === 'xclip') {
            if (await checkXclipForImage())
                return true;
        }
        return false;
    }
    if (process.platform === 'win32') {
        try {
            const { stdout } = await spawnAsync('powershell', [
                '-NoProfile',
                '-Command',
                'Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Clipboard]::ContainsImage()',
            ]);
            return stdout.trim() === 'True';
        }
        catch (error) {
            debugLogger.warn('Error checking clipboard for image:', error);
            return false;
        }
    }
    if (process.platform !== 'darwin') {
        return false;
    }
    try {
        // Use osascript to check clipboard type
        const { stdout } = await spawnAsync('osascript', ['-e', 'clipboard info']);
        const imageRegex = /«class PNGf»|TIFF picture|JPEG picture|GIF picture|«class JPEG»|«class TIFF»/;
        return imageRegex.test(stdout);
    }
    catch (error) {
        debugLogger.warn('Error checking clipboard for image:', error);
        return false;
    }
}
/**
 * Saves clipboard content to a file using wl-paste (Wayland).
 */
async function saveFileWithWlPaste(tempFilePath) {
    const success = await saveFromCommand('wl-paste', ['--no-newline', '--type', 'image/png'], tempFilePath);
    if (success) {
        return true;
    }
    // Cleanup on failure
    try {
        await fs.unlink(tempFilePath);
    }
    catch {
        /* ignore */
    }
    return false;
}
/**
 * Saves clipboard content to a file using xclip (X11).
 */
const saveFileWithXclip = async (tempFilePath) => {
    const success = await saveFromCommand('xclip', ['-selection', 'clipboard', '-t', 'image/png', '-o'], tempFilePath);
    if (success) {
        return true;
    }
    // Cleanup on failure
    try {
        await fs.unlink(tempFilePath);
    }
    catch {
        /* ignore */
    }
    return false;
};
/**
 * Gets the directory where clipboard images should be stored for a specific project.
 *
 * This uses the global temporary directory but creates a project-specific subdirectory
 * based on the hash of the project path (via `Storage.getProjectTempDir()`).
 * This prevents path conflicts between different projects while keeping the images
 * outside of the user's project directory.
 *
 * @param targetDir The root directory of the current project.
 * @returns The absolute path to the images directory.
 */
async function getProjectClipboardImagesDir(targetDir) {
    const storage = new Storage(targetDir);
    await storage.initialize();
    const baseDir = storage.getProjectTempDir();
    return path.join(baseDir, 'images');
}
/**
 * Saves the image from clipboard to a temporary file (macOS, Windows, and Linux)
 * @param targetDir The target directory to create temp files within
 * @returns The path to the saved image file, or null if no image or error
 */
export async function saveClipboardImage(targetDir) {
    try {
        const tempDir = await getProjectClipboardImagesDir(targetDir);
        await fs.mkdir(tempDir, { recursive: true });
        // Generate a unique filename with timestamp
        const timestamp = new Date().getTime();
        if (process.platform === 'linux') {
            const tempFilePath = path.join(tempDir, `clipboard-${timestamp}.png`);
            const tool = getUserLinuxClipboardTool();
            if (tool === 'wl-paste') {
                if (await saveFileWithWlPaste(tempFilePath))
                    return tempFilePath;
                return null;
            }
            if (tool === 'xclip') {
                if (await saveFileWithXclip(tempFilePath))
                    return tempFilePath;
                return null;
            }
            return null;
        }
        if (process.platform === 'win32') {
            const tempFilePath = path.join(tempDir, `clipboard-${timestamp}.png`);
            // The path is used directly in the PowerShell script.
            const psPath = tempFilePath.replace(/'/g, "''");
            const script = `
        Add-Type -AssemblyName System.Windows.Forms
        Add-Type -AssemblyName System.Drawing
        if ([System.Windows.Forms.Clipboard]::ContainsImage()) {
          $image = [System.Windows.Forms.Clipboard]::GetImage()
          $image.Save('${psPath}', [System.Drawing.Imaging.ImageFormat]::Png)
          Write-Output "success"
        }
      `;
            const { stdout } = await spawnAsync('powershell', [
                '-NoProfile',
                '-Command',
                script,
            ]);
            if (stdout.trim() === 'success') {
                try {
                    const stats = await fs.stat(tempFilePath);
                    if (stats.size > 0) {
                        return tempFilePath;
                    }
                }
                catch {
                    // File doesn't exist
                }
            }
            return null;
        }
        // AppleScript clipboard classes to try, in order of preference.
        // macOS converts clipboard images to these formats (WEBP/HEIC/HEIF not supported by osascript).
        const formats = [
            { class: 'PNGf', extension: 'png' },
            { class: 'JPEG', extension: 'jpg' },
        ];
        for (const format of formats) {
            const tempFilePath = path.join(tempDir, `clipboard-${timestamp}.${format.extension}`);
            // Try to save clipboard as this format
            const script = `
        try
          set imageData to the clipboard as «class ${format.class}»
          set fileRef to open for access POSIX file "${tempFilePath}" with write permission
          write imageData to fileRef
          close access fileRef
          return "success"
        on error errMsg
          try
            close access POSIX file "${tempFilePath}"
          end try
          return "error"
        end try
      `;
            const { stdout } = await spawnAsync('osascript', ['-e', script]);
            if (stdout.trim() === 'success') {
                // Verify the file was created and has content
                try {
                    const stats = await fs.stat(tempFilePath);
                    if (stats.size > 0) {
                        return tempFilePath;
                    }
                }
                catch (e) {
                    // File doesn't exist, continue to next format
                    debugLogger.debug('Clipboard image file not found:', tempFilePath, e);
                }
            }
            // Clean up failed attempt
            try {
                await fs.unlink(tempFilePath);
            }
            catch (e) {
                // Ignore cleanup errors
                debugLogger.debug('Failed to clean up temp file:', tempFilePath, e);
            }
        }
        // No format worked
        return null;
    }
    catch (error) {
        debugLogger.warn('Error saving clipboard image:', error);
        return null;
    }
}
/**
 * Cleans up old temporary clipboard image files
 * Removes files older than 1 hour
 * @param targetDir The target directory where temp files are stored
 */
export async function cleanupOldClipboardImages(targetDir) {
    try {
        const tempDir = await getProjectClipboardImagesDir(targetDir);
        const files = await fs.readdir(tempDir);
        const oneHourAgo = Date.now() - 60 * 60 * 1000;
        for (const file of files) {
            const ext = path.extname(file).toLowerCase();
            if (file.startsWith('clipboard-') && IMAGE_EXTENSIONS.includes(ext)) {
                const filePath = path.join(tempDir, file);
                const stats = await fs.stat(filePath);
                if (stats.mtimeMs < oneHourAgo) {
                    await fs.unlink(filePath);
                }
            }
        }
    }
    catch (e) {
        // Ignore errors in cleanup
        debugLogger.debug('Failed to clean up old clipboard images:', e);
    }
}
/**
 * Splits a pasted text block up into escaped path segements if it's a legal
 * drag-and-drop string.
 *
 * There are multiple ways drag-and-drop paths might be escaped:
 *  - Bare (only if there are no special chars): /path/to/myfile.png
 *  - Wrapped in double quotes (Windows only): "/path/to/my file~!.png"
 *  - Escaped with backslashes (POSIX only): /path/to/my\ file~!.png
 *  - Wrapped in single quotes: '/path/to/my file~!.png'
 *
 * When wrapped in single quotes, actual single quotes in the filename are
 * escaped with "'\''". For example: '/path/to/my '\''fancy file'\''.png'
 *
 * When wrapped in double quotes, actual double quotes are not an issue becuase
 * windows doesn't allow them in filenames.
 *
 * On all systems, a single drag-and-drop may include both wrapped and bare
 * paths, so we need to handle both simultaneously.
 *
 * @param text
 * @returns An iterable of escaped paths
 */
export function* splitDragAndDropPaths(text) {
    let current = '';
    let mode = 'NORMAL';
    const isWindows = process.platform === 'win32';
    let i = 0;
    while (i < text.length) {
        const char = text[i];
        if (mode === 'NORMAL') {
            if (char === ' ') {
                if (current.length > 0) {
                    yield current;
                    current = '';
                }
            }
            else if (char === '"') {
                mode = 'DOUBLE';
            }
            else if (char === "'") {
                mode = 'SINGLE';
            }
            else if (char === '\\' && !isWindows) {
                // POSIX escape in normal mode
                if (i + 1 < text.length) {
                    const next = text[i + 1];
                    current += next;
                    i++;
                }
            }
            else {
                current += char;
            }
        }
        else if (mode === 'DOUBLE') {
            if (char === '"') {
                mode = 'NORMAL';
            }
            else {
                current += char;
            }
        }
        else if (mode === 'SINGLE') {
            if (char === "'") {
                mode = 'NORMAL';
            }
            else {
                current += char;
            }
        }
        i++;
    }
    if (current.length > 0) {
        yield current;
    }
}
/**
 * Helper to validate if a path exists and is a file.
 */
function isValidFilePath(p) {
    try {
        return PATH_PREFIX_PATTERN.test(p) && existsSync(p) && statSync(p).isFile();
    }
    catch {
        return false;
    }
}
/**
 * Processes pasted text containing file paths (like those from drag and drop),
 * adding @ prefix to valid paths and escaping them in a standard way.
 *
 * @param text The pasted text
 * @returns Processed string with @ prefixes or null if any paths are invalid
 */
export function parsePastedPaths(text) {
    // First, check if the entire text is a single valid path
    if (isValidFilePath(text)) {
        return `@${escapePath(text)} `;
    }
    const validPaths = [];
    for (const segment of splitDragAndDropPaths(text)) {
        if (isValidFilePath(segment)) {
            validPaths.push(`@${escapePath(segment)}`);
        }
        else {
            return null; // If any segment is invalid, return null for the whole string
        }
    }
    if (validPaths.length === 0) {
        return null;
    }
    return validPaths.join(' ') + ' ';
}
//# sourceMappingURL=clipboardUtils.js.map