#ifndef APP_IMAGE_H
#define APP_IMAGE_H

#include <vector>
#include <string>
#include <map>
#include <optional> // Requires C++17
#include <memory>
#include <cstdint>
#include <cmath>
#include <fstream>
#include <stdexcept>
#include <algorithm>
#include <iostream> // For errors/warnings
#include <cstring>  // For strcmp, memcpy, etc.
#include <setjmp.h> // For libjpeg/libpng error handling
#define IMGUI_DEFINE_MATH_OPERATORS // Allows ImVec2 operators
#include "imgui_internal.h" // Need ImFloorSigned, ImClamp, ImMax, ImMin, ImAbs

// --- User Instructions ---
// 1. Place easyexif.h in your include path.
// 2. Ensure development libraries for LibRaw, libjpeg-turbo, libpng, and libtiff are installed.
// 3. In EXACTLY ONE .cpp file in your project, before including this header, define:
//    #define APP_IMAGE_IMPLEMENTATION
// 4. When compiling, LINK against the necessary libraries, e.g., using CMake or directly:
//    g++ your_app.cpp -o your_app -std=c++17 -lraw -ljpeg -lpng -ltiff -lm (order might matter)

// --- Forward declarations of external library types (optional, mostly for clarity) ---
// struct jpeg_decompress_struct;
// struct jpeg_compress_struct;
// struct jpeg_error_mgr;
// struct png_struct_def;
// struct png_info_def;
// typedef struct tiff TIFF; // From tiffio.h
// class LibRaw; // From libraw/libraw.h

// Include easyexif here as it's header-only anyway
#include "exif.h"

// Enum for specifying save formats
enum class ImageSaveFormat
{
    JPEG,    // Quality setting applies (1-100), saves as 8-bit sRGB.
    PNG_8,   // 8-bit PNG (sRGB assumption).
    PNG_16,  // 16-bit PNG (Linear or sRGB depends on future implementation details, currently linear).
    TIFF_8,  // 8-bit TIFF (Uncompressed, RGB).
    TIFF_16, // 16-bit TIFF (Uncompressed, RGB, Linear).
    // TIFF_LZW_16 // Example for compressed TIFF
    UNKNOWN
};

// Basic structure for image metadata (can hold EXIF tags)
using ImageMetadata = std::map<std::string, std::string>;

// --- App Internal Image Representation ---
class AppImage
{
public:
    // --- Constructors ---
    AppImage() = default;
    AppImage(uint32_t width, uint32_t height, uint32_t channels = 3);

    // --- Accessors ---
    uint32_t getWidth() const { return m_width; }
    uint32_t getHeight() const { return m_height; }
    uint32_t getChannels() const { return m_channels; }
    bool isEmpty() const { return m_pixelData.empty(); }

    // Pixel data: Linear floating point [0.0, 1.0+], interleaved RGB/RGBA/Gray.
    float *getData() { return m_pixelData.data(); }
    const float *getData() const { return m_pixelData.data(); }
    size_t getDataSize() const { return m_pixelData.size() * sizeof(float); }
    size_t getTotalFloats() const { return m_pixelData.size(); }

    std::vector<float> &getPixelVector() { return m_pixelData; }
    const std::vector<float> &getPixelVector() const { return m_pixelData; }

    // --- Metadata ---
    ImageMetadata &getMetadata() { return m_metadata; }
    const ImageMetadata &getMetadata() const { return m_metadata; }

    // --- Color Information ---
    std::vector<uint8_t> &getIccProfile() { return m_iccProfile; }
    const std::vector<uint8_t> &getIccProfile() const { return m_iccProfile; }

    std::string &getColorSpaceName() { return m_colorSpaceName; }
    const std::string &getColorSpaceName() const { return m_colorSpaceName; }
    bool isLinear() const { return m_isLinear; }

    // --- Modifiers ---
    void resize(uint32_t newWidth, uint32_t newHeight, uint32_t newChannels = 0);
    void clear_image();

    // --- Data members ---
    // Making them public for easier access in the implementation section below,
    // alternatively make loadImage/saveImage friends or add internal setters.
    // public:
    uint32_t m_width = 0;
    uint32_t m_height = 0;
    uint32_t m_channels = 0; // 1=Gray, 3=RGB, 4=RGBA

    std::vector<float> m_pixelData;
    ImageMetadata m_metadata;
    std::vector<uint8_t> m_iccProfile;
    std::string m_colorSpaceName = "Unknown";
    bool m_isLinear = true; // Default assumption for internal format
    GLuint m_textureId = 0;
    int m_textureWidth = 0;
    int m_textureHeight = 0;
};

// --- API Function Declarations ---

/**
 * @brief Loads an image file, attempting type detection (RAW, JPEG, PNG, TIFF).
 * Uses LibRaw, libjpeg-turbo, libpng, libtiff.
 * Uses EasyExif for EXIF metadata from JPEGs (only).
 * Converts loaded pixel data to internal linear float format.
 * Extracts ICC profile if available (primarily from RAW).
 *
 * @param filePath Path to the image file.
 * @return std::optional<AppImage> containing the loaded image on success, std::nullopt on failure.
 */
std::optional<AppImage> loadImage(const std::string &filePath);

/**
 * @brief Saves the AppImage to a file (JPEG, PNG, TIFF).
 * Uses libjpeg-turbo, libpng, libtiff.
 * Converts internal linear float data to target format (e.g., 8-bit sRGB for JPEG).
 * NOTE: Does NOT currently save EXIF or ICC metadata. This requires more complex handling
 *       (e.g., using Exiv2 library or manual file manipulation after saving pixels).
 *
 * @param image The AppImage to save. Assumed to be in linear float format.
 * @param filePath Path to save the image file.
 * @param format The desired output format.
 * @param quality JPEG quality (1-100), ignored otherwise.
 * @return True on success, false on failure.
 */
bool saveImage(const AppImage &image,
               const std::string &filePath,
               ImageSaveFormat format,
               int quality = 90);

bool loadImageTexture(const AppImage &appImage);

// ============================================================================
// =================== IMPLEMENTATION SECTION =================================
// ============================================================================
// Define APP_IMAGE_IMPLEMENTATION in exactly one .cpp file before including this header

#ifdef APP_IMAGE_IMPLEMENTATION

#include <libraw/libraw.h>
#include <jpeglib.h>
#include <png.h>
#include <tiffio.h>

// Internal helper namespace
namespace AppImageUtil
{

    // --- Error Handling ---
    // Basic error reporting (prints to stderr)
    inline void LogError(const std::string &msg)
    {
        std::cerr << "AppImage Error: " << msg << std::endl;
    }
    inline void LogWarning(const std::string &msg)
    {
        std::cerr << "AppImage Warning: " << msg << std::endl;
    }

    // --- Color Conversion Helpers (Approximate sRGB) ---
    // For critical work, use a color management library (LittleCMS) and proper piecewise functions
    inline float srgb_to_linear_approx(float srgbVal)
    {
        if (srgbVal <= 0.0f)
            return 0.0f;
        if (srgbVal <= 0.04045f)
        {
            return srgbVal / 12.92f;
        }
        else
        {
            return std::pow((srgbVal + 0.055f) / 1.055f, 2.4f);
        }
    }

    inline float linear_to_srgb_approx(float linearVal)
    {
        if (linearVal <= 0.0f)
            return 0.0f;
        // Simple clamp for typical display output
        linearVal = std::fmax(0.0f, std::fmin(1.0f, linearVal));
        if (linearVal <= 0.0031308f)
        {
            return linearVal * 12.92f;
        }
        else
        {
            return 1.055f * std::pow(linearVal, 1.0f / 2.4f) - 0.055f;
        }
    }

    // --- File Type Detection ---
    enum class DetectedFileType
    {
        RAW,
        JPEG,
        PNG,
        TIFF,
        UNKNOWN
    };

    inline DetectedFileType detectFileType(const std::string &filePath)
    {
        std::ifstream file(filePath, std::ios::binary);
        if (!file)
            return DetectedFileType::UNKNOWN;

        unsigned char magic[12]; // Read enough bytes for common signatures
        file.read(reinterpret_cast<char *>(magic), sizeof(magic));
        if (!file)
            return DetectedFileType::UNKNOWN;

        // Check common signatures
        if (magic[0] == 0xFF && magic[1] == 0xD8 && magic[2] == 0xFF)
            return DetectedFileType::JPEG;
        if (magic[0] == 0x89 && magic[1] == 'P' && magic[2] == 'N' && magic[3] == 'G')
            return DetectedFileType::PNG;
        if ((magic[0] == 'I' && magic[1] == 'I' && magic[2] == 0x2A && magic[3] == 0x00) || // Little-endian TIFF
            (magic[0] == 'M' && magic[1] == 'M' && magic[2] == 0x00 && magic[3] == 0x2A))   // Big-endian TIFF
        {

            size_t dotPos = filePath.rfind('.');
            if (dotPos != std::string::npos)
            {
                std::string ext = filePath.substr(dotPos);
                std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);

                // Common RAW formats that use TIFF structure
                const char *rawTiffExtensions[] = {
                    ".nef", // Nikon
                    ".cr2", // Canon
                    ".dng", // Adobe/Various
                    ".arw", // Sony
                    ".srw", // Samsung
                    ".orf", // Olympus
                    ".pef", // Pentax
                    ".raf", // Fuji
                    ".rw2"  // Panasonic
                };

                for (const char *rawExt : rawTiffExtensions)
                {
                    if (ext == rawExt)
                        return DetectedFileType::RAW;
                }
            }
            return DetectedFileType::TIFF;
        }

        // If no standard signature matches, check extension for RAW as a fallback
        // (LibRaw handles many internal variations)
        size_t dotPos = filePath.rfind('.');
        if (dotPos != std::string::npos)
        {
            std::string ext = filePath.substr(dotPos);
            std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
            const char *rawExtensions[] = {
                ".3fr", ".ari", ".arw", ".bay", ".braw", ".crw", ".cr2", ".cr3", ".cap",
                ".data", ".dcs", ".dcr", ".dng", ".drf", ".eip", ".erf", ".fff", ".gpr",
                ".iiq", ".k25", ".kdc", ".mdc", ".mef", ".mos", ".mrw", ".nef", ".nrw",
                ".obm", ".orf", ".pef", ".ptx", ".pxn", ".r3d", ".raf", ".raw", ".rwl",
                ".rw2", ".rwz", ".sr2", ".srf", ".srw", ".tif", ".x3f" // Note: .tif can be RAW or regular TIFF
            };
            for (const char *rawExt : rawExtensions)
            {
                if (ext == rawExt)
                    return DetectedFileType::RAW;
            }
            // Special case: Leica .dng can also be loaded by LibRaw
            if (ext == ".dng")
                return DetectedFileType::RAW;
        }

        return DetectedFileType::UNKNOWN;
    }

    // --- EXIF Loading Helper (using EasyExif) ---
    inline void loadExifData(const std::string &filePath, ImageMetadata &metadata)
    {
        std::ifstream file(filePath, std::ios::binary | std::ios::ate);
        if (!file)
            return;
        std::streamsize size = file.tellg();
        file.seekg(0, std::ios::beg);
        std::vector<unsigned char> buffer(size);
        if (!file.read(reinterpret_cast<char *>(buffer.data()), size))
            return;

        easyexif::EXIFInfo exifInfo;
        int code = exifInfo.parseFrom(buffer.data(), buffer.size());
        if (code == 0)
        {
            // Helper lambda to add if not empty
            auto addMeta = [&](const std::string &key, const std::string &value)
            {
                if (!value.empty())
                    metadata[key] = value;
            };
            auto addMetaInt = [&](const std::string &key, int value)
            {
                if (value > 0)
                    metadata[key] = std::to_string(value);
            };
            auto addMetaDouble = [&](const std::string &key, double value)
            {
                if (value > 0)
                    metadata[key] = std::to_string(value);
            };

            addMeta("Exif.Image.Make", exifInfo.Make);
            addMeta("Exif.Image.Model", exifInfo.Model);
            addMeta("Exif.Image.Software", exifInfo.Software);
            addMetaInt("Exif.Image.Orientation", exifInfo.Orientation);
            addMeta("Exif.Image.DateTime", exifInfo.DateTime);
            addMeta("Exif.Photo.DateTimeOriginal", exifInfo.DateTimeOriginal);
            addMeta("Exif.Photo.DateTimeDigitized", exifInfo.DateTimeDigitized);
            addMeta("Exif.Image.SubSecTimeOriginal", exifInfo.SubSecTimeOriginal); // Often empty
            addMeta("Exif.Image.Copyright", exifInfo.Copyright);
            addMetaDouble("Exif.Photo.ExposureTime", exifInfo.ExposureTime);
            addMetaDouble("Exif.Photo.FNumber", exifInfo.FNumber);
            addMetaInt("Exif.Photo.ISOSpeedRatings", exifInfo.ISOSpeedRatings);
            addMetaDouble("Exif.Photo.ShutterSpeedValue", exifInfo.ShutterSpeedValue); // APEX
            addMetaDouble("Exif.Photo.ApertureValue", exifInfo.FNumber);               // APEX
            addMetaDouble("Exif.Photo.ExposureBiasValue", exifInfo.ExposureBiasValue);
            addMetaDouble("Exif.Photo.FocalLength", exifInfo.FocalLength);
            addMeta("Exif.Photo.LensModel", exifInfo.LensInfo.Model);
            // GeoLocation
            if (exifInfo.GeoLocation.Latitude != 0 || exifInfo.GeoLocation.Longitude != 0)
            {
                metadata["Exif.GPSInfo.Latitude"] = std::to_string(exifInfo.GeoLocation.Latitude);
                metadata["Exif.GPSInfo.Longitude"] = std::to_string(exifInfo.GeoLocation.Longitude);
                metadata["Exif.GPSInfo.Altitude"] = std::to_string(exifInfo.GeoLocation.Altitude);
                metadata["Exif.GPSInfo.LatitudeRef"] = exifInfo.GeoLocation.LatComponents.direction;
                metadata["Exif.GPSInfo.LongitudeRef"] = exifInfo.GeoLocation.LonComponents.direction;
            }
        }
        else
        {
            // LogWarning("Could not parse EXIF data (Code " + std::to_string(code) + ") from " + filePath);
        }
    }

    // --- LibRaw Loading ---
    inline std::optional<AppImage> loadRaw(const std::string &filePath)
    {
        LibRaw rawProcessor;
        AppImage image;

        // Set parameters for desired output
        // Output 16-bit data
        rawProcessor.imgdata.params.output_bps = 16;
        // Disable automatic brightness adjustment (we want linear)
        rawProcessor.imgdata.params.no_auto_bright = 1;
        // Set output color space (e.g., 1 = sRGB, 3 = ProPhoto, 4 = AdobeRGB)
        // ProPhoto (3) or AdobeRGB (4) are good wide-gamut choices if editor supports them.
        // sRGB (1) is safest if unsure. We'll assume Linear sRGB for now.
        rawProcessor.imgdata.params.output_color = 1; // 1 = sRGB primaries
        // Set gamma (1.0 for linear) - use {1.0, 1.0} for linear output
        rawProcessor.imgdata.params.gamm[0] = 1.0; // Linear gamma
        rawProcessor.imgdata.params.gamm[1] = 1.0;
        // Use camera white balance if available, otherwise auto
        rawProcessor.imgdata.params.use_camera_wb = 1;
        rawProcessor.imgdata.params.use_auto_wb = (rawProcessor.imgdata.params.use_camera_wb == 0);
        // Consider other params: demosaic algorithm, highlight recovery, etc.

        int ret;
        if ((ret = rawProcessor.open_file(filePath.c_str())) != LIBRAW_SUCCESS)
        {
            LogError("LibRaw: Cannot open file " + filePath + " - " + libraw_strerror(ret));
            return std::nullopt;
        }

        if ((ret = rawProcessor.unpack()) != LIBRAW_SUCCESS)
        {
            LogError("LibRaw: Cannot unpack file " + filePath + " - " + libraw_strerror(ret));
            return std::nullopt;
        }

        // Process the image (demosaic, color conversion, etc.)
        if ((ret = rawProcessor.dcraw_process()) != LIBRAW_SUCCESS)
        {
            LogError("LibRaw: Cannot process file " + filePath + " - " + libraw_strerror(ret));
            // Try fallback processing if dcraw_process fails (might be non-RAW TIFF/JPEG)
            if (ret == LIBRAW_UNSUPPORTED_THUMBNAIL || ret == LIBRAW_REQUEST_FOR_NONEXISTENT_IMAGE)
            {
                LogWarning("LibRaw: File " + filePath + " might be non-RAW or only has thumbnail. Attempting fallback.");
                // You could try loading with libjpeg/libtiff here, but for simplicity we fail
            }
            return std::nullopt;
        }

        // Get the processed image data
        libraw_processed_image_t *processed_image = rawProcessor.dcraw_make_mem_image(&ret);
        if (!processed_image)
        {
            LogError("LibRaw: Cannot make memory image for " + filePath + " - " + libraw_strerror(ret));
            return std::nullopt;
        }

        // Copy data to AppImage format
        if (processed_image->type == LIBRAW_IMAGE_BITMAP && processed_image->bits == 16)
        {
            image.m_width = processed_image->width;
            image.m_height = processed_image->height;
            image.m_channels = processed_image->colors; // Should be 3 (RGB)
            image.m_isLinear = true;                    // We requested linear gamma

            if (image.m_channels != 3)
            {
                LogWarning("LibRaw: Expected 3 channels, got " + std::to_string(image.m_channels));
                // Handle grayscale or other cases if needed, for now assume RGB
                image.m_channels = 3;
            }

            size_t num_pixels = static_cast<size_t>(image.m_width) * image.m_height;
            size_t total_floats = num_pixels * image.m_channels;
            image.m_pixelData.resize(total_floats);

            uint16_t *raw_data = reinterpret_cast<uint16_t *>(processed_image->data);
            float *app_data = image.m_pixelData.data();

            // Convert 16-bit unsigned short [0, 65535] to float [0.0, 1.0+]
            for (size_t i = 0; i < total_floats; ++i)
            {
                app_data[i] = static_cast<float>(raw_data[i]) / 65535.0f;
            }

            // Get color space name based on output_color param
            switch (rawProcessor.imgdata.params.output_color)
            {
            case 1:
                image.m_colorSpaceName = "Linear sRGB";
                break;
            case 2:
                image.m_colorSpaceName = "Linear Adobe RGB (1998)";
                break; // Check LibRaw docs if this is correct mapping
            case 3:
                image.m_colorSpaceName = "Linear ProPhoto RGB";
                break;
            case 4:
                image.m_colorSpaceName = "Linear XYZ";
                break; // Check LibRaw docs
            default:
                image.m_colorSpaceName = "Linear Unknown";
                break;
            }

            // Extract Metadata (Example - add more fields as needed)
            image.m_metadata["LibRaw.Camera.Make"] = rawProcessor.imgdata.idata.make;
            image.m_metadata["LibRaw.Camera.Model"] = rawProcessor.imgdata.idata.model;
            image.m_metadata["LibRaw.Image.Timestamp"] = std::to_string(rawProcessor.imgdata.other.timestamp);
            image.m_metadata["LibRaw.Image.ShotOrder"] = std::to_string(rawProcessor.imgdata.other.shot_order);
            image.m_metadata["LibRaw.Photo.ExposureTime"] = std::to_string(rawProcessor.imgdata.other.shutter);
            image.m_metadata["LibRaw.Photo.Aperture"] = std::to_string(rawProcessor.imgdata.other.aperture);
            image.m_metadata["LibRaw.Photo.ISOSpeed"] = std::to_string(rawProcessor.imgdata.other.iso_speed);
            image.m_metadata["LibRaw.Photo.FocalLength"] = std::to_string(rawProcessor.imgdata.other.focal_len);
            // Copy EasyExif compatible fields if possible for consistency
            image.m_metadata["Exif.Image.Make"] = rawProcessor.imgdata.idata.make;
            image.m_metadata["Exif.Image.Model"] = rawProcessor.imgdata.idata.model;
            image.m_metadata["Exif.Photo.ExposureTime"] = std::to_string(rawProcessor.imgdata.other.shutter);
            image.m_metadata["Exif.Photo.FNumber"] = std::to_string(rawProcessor.imgdata.other.aperture); // Aperture == FNumber
            image.m_metadata["Exif.Photo.ISOSpeedRatings"] = std::to_string(rawProcessor.imgdata.other.iso_speed);
            image.m_metadata["Exif.Photo.FocalLength"] = std::to_string(rawProcessor.imgdata.other.focal_len);
            // LibRaw often provides DateTimeOriginal via timestamp
            // Convert timestamp to string if needed:
            // time_t ts = rawProcessor.imgdata.other.timestamp;
            // char buf[30];
            // strftime(buf, sizeof(buf), "%Y:%m:%d %H:%M:%S", localtime(&ts));
            // image.m_metadata["Exif.Photo.DateTimeOriginal"] = buf;

            // Extract ICC Profile
            unsigned int icc_size = 0;
            const void *icc_profile_ptr = nullptr;
            if (icc_profile_ptr && icc_size > 0)
            {
                image.m_iccProfile.resize(icc_size);
                std::memcpy(image.m_iccProfile.data(), icc_profile_ptr, icc_size);
                LogWarning("LibRaw: Successfully extracted ICC profile (" + std::to_string(icc_size) + " bytes).");
                // We could potentially parse the ICC profile name here, but it's complex.
                if (image.m_colorSpaceName == "Linear Unknown")
                    image.m_colorSpaceName = "Linear (Embedded ICC)";
            }
            else
            {
                LogWarning("LibRaw: No ICC profile found or extracted.");
            }
        }
        else
        {
            LogError("LibRaw: Processed image is not 16-bit bitmap (type=" + std::to_string(processed_image->type) + " bits=" + std::to_string(processed_image->bits) + ")");
            LibRaw::dcraw_clear_mem(processed_image);
            return std::nullopt;
        }

        // Clean up LibRaw resources
        LibRaw::dcraw_clear_mem(processed_image);
        // rawProcessor is automatically cleaned up by its destructor

        return image;
    }

    // --- libjpeg Loading ---
    // Custom error handler for libjpeg
    struct JpegErrorManager
    {
        jpeg_error_mgr pub;
        jmp_buf setjmp_buffer; // For returning control on error
    };

    void jpegErrorExit(j_common_ptr cinfo)
    {
        JpegErrorManager *myerr = reinterpret_cast<JpegErrorManager *>(cinfo->err);
        // Format the error message
        char buffer[JMSG_LENGTH_MAX];
        (*cinfo->err->format_message)(cinfo, buffer);
        LogError("libjpeg: " + std::string(buffer));
        // Return control to setjmp point
        longjmp(myerr->setjmp_buffer, 1);
    }

    inline std::optional<AppImage> loadJpeg(const std::string &filePath)
    {
        FILE *infile = fopen(filePath.c_str(), "rb");
        if (!infile)
        {
            LogError("Cannot open JPEG file: " + filePath);
            return std::nullopt;
        }

        AppImage image;
        jpeg_decompress_struct cinfo;
        JpegErrorManager jerr; // Custom error handler

        // Setup error handling
        cinfo.err = jpeg_std_error(&jerr.pub);
        jerr.pub.error_exit = jpegErrorExit;
        if (setjmp(jerr.setjmp_buffer))
        {
            // If we get here, a fatal error occurred
            jpeg_destroy_decompress(&cinfo);
            fclose(infile);
            return std::nullopt;
        }

        // Initialize decompression object
        jpeg_create_decompress(&cinfo);
        jpeg_stdio_src(&cinfo, infile);

        // Read header
        jpeg_read_header(&cinfo, TRUE);

        // Start decompressor - this guesses output parameters like color space
        // We usually get JCS_RGB for color JPEGs
        cinfo.out_color_space = JCS_RGB; // Request RGB output
        jpeg_start_decompress(&cinfo);

        image.m_width = cinfo.output_width;
        image.m_height = cinfo.output_height;
        image.m_channels = cinfo.output_components; // Should be 3 for JCS_RGB

        if (image.m_channels != 1 && image.m_channels != 3)
        {
            LogError("libjpeg: Unsupported number of channels: " + std::to_string(image.m_channels));
            jpeg_finish_decompress(&cinfo);
            jpeg_destroy_decompress(&cinfo);
            fclose(infile);
            return std::nullopt;
        }

        size_t num_pixels = static_cast<size_t>(image.m_width) * image.m_height;
        size_t total_floats = num_pixels * image.m_channels;
        image.m_pixelData.resize(total_floats);
        image.m_isLinear = true;                // We will convert to linear
        image.m_colorSpaceName = "Linear sRGB"; // Standard JPEG assumption

        // Allocate temporary buffer for one scanline
        int row_stride = cinfo.output_width * cinfo.output_components;
        std::vector<unsigned char> scanline_buffer(row_stride);
        JSAMPROW row_pointer[1];
        row_pointer[0] = scanline_buffer.data();

        float *app_data_ptr = image.m_pixelData.data();

        // Read scanlines
        while (cinfo.output_scanline < cinfo.output_height)
        {
            jpeg_read_scanlines(&cinfo, row_pointer, 1);
            // Convert scanline from 8-bit sRGB to linear float
            for (int i = 0; i < row_stride; ++i)
            {
                *app_data_ptr++ = srgb_to_linear_approx(static_cast<float>(scanline_buffer[i]) / 255.0f);
            }
        }

        // Finish decompression and clean up
        jpeg_finish_decompress(&cinfo);
        jpeg_destroy_decompress(&cinfo);
        fclose(infile);

        // Load EXIF data separately
        loadExifData(filePath, image.m_metadata);

        return image;
    }

    // --- libpng Loading ---
    // Custom error handler for libpng
    void pngErrorFunc(png_structp png_ptr, png_const_charp error_msg)
    {
        LogError("libpng: " + std::string(error_msg));
        jmp_buf *jmp_ptr = reinterpret_cast<jmp_buf *>(png_get_error_ptr(png_ptr));
        if (jmp_ptr)
        {
            longjmp(*jmp_ptr, 1);
        }
        // If no jmp_buf, just exit (shouldn't happen if setup correctly)
        exit(EXIT_FAILURE);
    }
    void pngWarningFunc(png_structp png_ptr, png_const_charp warning_msg)
    {
        LogWarning("libpng: " + std::string(warning_msg));
        // Don't longjmp on warnings
    }

    inline std::optional<AppImage> loadPng(const std::string &filePath)
    {
        FILE *fp = fopen(filePath.c_str(), "rb");
        if (!fp)
        {
            LogError("Cannot open PNG file: " + filePath);
            return std::nullopt;
        }

        // Check PNG signature
        unsigned char header[8];
        fread(header, 1, 8, fp);
        if (png_sig_cmp(header, 0, 8))
        {
            LogError("File is not a valid PNG: " + filePath);
            fclose(fp);
            return std::nullopt;
        }

        png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, pngErrorFunc, pngWarningFunc);
        if (!png_ptr)
        {
            LogError("libpng: png_create_read_struct failed");
            fclose(fp);
            return std::nullopt;
        }

        png_infop info_ptr = png_create_info_struct(png_ptr);
        if (!info_ptr)
        {
            LogError("libpng: png_create_info_struct failed");
            png_destroy_read_struct(&png_ptr, nullptr, nullptr);
            fclose(fp);
            return std::nullopt;
        }

        // Setup jump buffer for error handling
        jmp_buf jmpbuf;
        if (setjmp(jmpbuf))
        {
            LogError("libpng: Error during read");
            png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
            fclose(fp);
            return std::nullopt;
        }
        // Assign jump buffer to png error pointer
        // Note: The cast from jmp_buf* to png_voidp* might feel odd, but it's standard practice
        png_set_error_fn(png_ptr, reinterpret_cast<png_voidp>(&jmpbuf), pngErrorFunc, pngWarningFunc);

        png_init_io(png_ptr, fp);
        png_set_sig_bytes(png_ptr, 8); // We already read the 8 signature bytes

        // Read file info
        png_read_info(png_ptr, info_ptr);

        AppImage image;
        png_uint_32 png_width, png_height;
        int bit_depth, color_type, interlace_method, compression_method, filter_method;
        png_get_IHDR(png_ptr, info_ptr, &png_width, &png_height, &bit_depth, &color_type,
                     &interlace_method, &compression_method, &filter_method);

        image.m_width = png_width;
        image.m_height = png_height;

        // --- Transformations ---
        // We want linear float RGB or RGBA output

        // Handle palette -> RGB
        if (color_type == PNG_COLOR_TYPE_PALETTE)
        {
            png_set_palette_to_rgb(png_ptr);
        }
        // Handle low bit depth grayscale -> 8 bit
        if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
        {
            png_set_expand_gray_1_2_4_to_8(png_ptr);
            bit_depth = 8; // Update bit depth after expansion
        }
        // Handle transparency chunk -> Alpha channel
        if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
        {
            png_set_tRNS_to_alpha(png_ptr);
        }
        // Convert 16-bit -> 8-bit if needed (we handle 16 bit below, so maybe don't strip)
        // if (bit_depth == 16) {
        //     png_set_strip_16(png_ptr);
        //     bit_depth = 8;
        // }
        // Convert grayscale -> RGB
        if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
        {
            png_set_gray_to_rgb(png_ptr);
        }
        // Add alpha channel if missing but requested (we might always want RGBA internally)
        // if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY) {
        //     png_set_add_alpha(png_ptr, 0xFF, PNG_FILLER_AFTER); // Add opaque alpha
        // }

        // --- Gamma Handling ---
        double file_gamma = 0.0;
        bool is_srgb = (png_get_sRGB(png_ptr, info_ptr, nullptr) != 0);

        if (is_srgb)
        {
            // If sRGB chunk is present, libpng can convert to linear for us
            png_set_gamma(png_ptr, 1.0, 0.45455); // Request linear output (screen gamma 2.2)
            image.m_isLinear = true;
            image.m_colorSpaceName = "Linear sRGB";
        }
        else if (png_get_gAMA(png_ptr, info_ptr, &file_gamma))
        {
            // If gAMA chunk is present, convert to linear
            png_set_gamma(png_ptr, 1.0, file_gamma);
            image.m_isLinear = true;
            image.m_colorSpaceName = "Linear Unknown (Gamma Corrected)";
        }
        else
        {
            // No gamma info, assume sRGB and convert manually later
            image.m_isLinear = false; // Data read will be sRGB
            image.m_colorSpaceName = "sRGB (Assumed)";
        }

        // Apply transformations
        png_read_update_info(png_ptr, info_ptr);

        // Get updated info after transformations
        image.m_channels = png_get_channels(png_ptr, info_ptr);
        bit_depth = png_get_bit_depth(png_ptr, info_ptr); // Update bit_depth after transforms

        if (image.m_channels < 3)
        {
            LogWarning("libpng: Resulting image has < 3 channels after transforms. Handling as RGB.");
            // Force RGB if needed? Be careful here. For simplicity, assume RGB/RGBA works.
        }

        // Allocate memory for the image data
        size_t num_pixels = static_cast<size_t>(image.m_width) * image.m_height;
        size_t total_floats = num_pixels * image.m_channels;
        image.m_pixelData.resize(total_floats);
        float *app_data_ptr = image.m_pixelData.data();

        // Allocate row pointers
        png_bytep *row_pointers = new png_bytep[image.m_height];
        size_t row_bytes = png_get_rowbytes(png_ptr, info_ptr);
        std::vector<unsigned char> image_buffer(row_bytes * image.m_height); // Read whole image at once

        for (png_uint_32 i = 0; i < image.m_height; ++i)
        {
            row_pointers[i] = image_buffer.data() + i * row_bytes;
        }

        // Read the entire image
        png_read_image(png_ptr, row_pointers);

        // Convert the read data to linear float
        unsigned char *buffer_ptr = image_buffer.data();
        if (bit_depth == 8)
        {
            for (size_t i = 0; i < total_floats; ++i)
            {
                float val = static_cast<float>(buffer_ptr[i]) / 255.0f;
                // Convert to linear if libpng didn't do it (i.e., no sRGB/gAMA chunk found)
                app_data_ptr[i] = image.m_isLinear ? val : srgb_to_linear_approx(val);
            }
        }
        else if (bit_depth == 16)
        {
            uint16_t *buffer_ptr16 = reinterpret_cast<uint16_t *>(buffer_ptr);
            // PNG 16-bit uses network byte order (big-endian)
            bool needs_swap = (png_get_uint_16((png_bytep) "\x01\x02") != 0x0102); // Check system endianness

            for (size_t i = 0; i < total_floats; ++i)
            {
                uint16_t raw_val = buffer_ptr16[i];
                if (needs_swap)
                { // Swap bytes if system is little-endian
                    raw_val = (raw_val >> 8) | (raw_val << 8);
                }
                float val = static_cast<float>(raw_val) / 65535.0f;
                // Convert to linear if libpng didn't do it
                app_data_ptr[i] = image.m_isLinear ? val : srgb_to_linear_approx(val);
            }
        }
        else
        {
            LogError("libpng: Unsupported bit depth after transforms: " + std::to_string(bit_depth));
            delete[] row_pointers;
            png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
            fclose(fp);
            return std::nullopt;
        }

        // If we assumed sRGB and converted manually, update state
        if (!image.m_isLinear)
        {
            image.m_isLinear = true;
            image.m_colorSpaceName = "Linear sRGB (Assumed)";
        }

        // Clean up
        delete[] row_pointers;
        png_read_end(png_ptr, nullptr); // Finish reading remaining chunks
        png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
        fclose(fp);

        // Note: PNG typically doesn't store EXIF in the same way as JPEG/TIFF.
        // It can have text chunks (tEXt, zTXt, iTXt) which might hold metadata.
        // Reading these requires additional libpng calls (png_get_text). Not implemented here.

        return image;
    }

    // --- libtiff Loading ---
    // Suppress libtiff warnings/errors (optional, can be noisy)
    void tiffErrorHandler(const char *module, const char *fmt, va_list ap) { /* Do nothing */ }
    void tiffWarningHandler(const char *module, const char *fmt, va_list ap) { /* Do nothing */ }

    inline std::optional<AppImage> loadTiff(const std::string &filePath)
    {
        // Set custom handlers to suppress console output from libtiff
        // TIFFSetErrorHandler(tiffErrorHandler);
        // TIFFSetWarningHandler(tiffWarningHandler);

        TIFF *tif = TIFFOpen(filePath.c_str(), "r");
        if (!tif)
        {
            LogError("Cannot open TIFF file: " + filePath);
            return std::nullopt;
        }

        AppImage image;
        uint32_t w, h;
        uint16_t bitsPerSample, samplesPerPixel, photometric, planarConfig;

        TIFFGetFieldDefaulted(tif, TIFFTAG_IMAGEWIDTH, &w);
        TIFFGetFieldDefaulted(tif, TIFFTAG_IMAGELENGTH, &h);
        TIFFGetFieldDefaulted(tif, TIFFTAG_BITSPERSAMPLE, &bitsPerSample);
        TIFFGetFieldDefaulted(tif, TIFFTAG_SAMPLESPERPIXEL, &samplesPerPixel);
        TIFFGetFieldDefaulted(tif, TIFFTAG_PHOTOMETRIC, &photometric);
        TIFFGetFieldDefaulted(tif, TIFFTAG_PLANARCONFIG, &planarConfig);

        image.m_width = w;
        image.m_height = h;
        image.m_channels = samplesPerPixel; // Usually 1 (Gray) or 3 (RGB) or 4 (RGBA)

        // --- Sanity Checks ---
        if (w == 0 || h == 0 || samplesPerPixel == 0)
        {
            LogError("libtiff: Invalid dimensions or samples per pixel.");
            TIFFClose(tif);
            return std::nullopt;
        }
        if (bitsPerSample != 8 && bitsPerSample != 16 && bitsPerSample != 32)
        {
            // Note: 32-bit float TIFFs exist but require different handling
            LogError("libtiff: Unsupported bits per sample: " + std::to_string(bitsPerSample) + ". Only 8/16 supported currently.");
            TIFFClose(tif);
            return std::nullopt;
        }
        if (photometric != PHOTOMETRIC_MINISBLACK && photometric != PHOTOMETRIC_MINISWHITE &&
            photometric != PHOTOMETRIC_RGB && photometric != PHOTOMETRIC_PALETTE &&
            photometric != PHOTOMETRIC_MASK && photometric != PHOTOMETRIC_SEPARATED /*CMYK?*/ &&
            photometric != PHOTOMETRIC_LOGL && photometric != PHOTOMETRIC_LOGLUV)
        {
            LogWarning("libtiff: Unhandled photometric interpretation: " + std::to_string(photometric));
            // We will try to read as RGB/Gray anyway... might be wrong.
        }

        // --- Data Reading ---
        // Use TIFFReadRGBAImage for simplicity - converts many formats to RGBA uint32 internally
        // Advantage: Handles various photometric interpretations, planar configs, palettes etc.
        // Disadvantage: Always gives 8-bit RGBA, loses 16-bit precision. Less control.

        // Alternative: Read scanlines manually (more complex, preserves bit depth)
        // Let's try the manual scanline approach to preserve bit depth

        size_t num_pixels = static_cast<size_t>(w) * h;
        size_t total_values = num_pixels * samplesPerPixel; // Total uint8/uint16 values
        image.m_pixelData.resize(total_values);             // Resize for float output
        image.m_isLinear = true;                            // Assume linear, correct later if gamma info found
        image.m_colorSpaceName = "Linear Unknown (TIFF)";   // Default assumption

        tmsize_t scanline_size = TIFFScanlineSize(tif);
        std::vector<unsigned char> scanline_buffer(scanline_size);

        float *app_data_ptr = image.m_pixelData.data();
        float max_val = (bitsPerSample == 8) ? 255.0f : 65535.0f; // Normalization factor

        if (planarConfig == PLANARCONFIG_CONTIG)
        {
            for (uint32_t row = 0; row < h; ++row)
            {
                if (TIFFReadScanline(tif, scanline_buffer.data(), row) < 0)
                {
                    LogError("libtiff: Error reading scanline " + std::to_string(row));
                    TIFFClose(tif);
                    return std::nullopt;
                }
                // Process the contiguous scanline
                if (bitsPerSample == 8)
                {
                    unsigned char *buf_ptr = scanline_buffer.data();
                    for (size_t i = 0; i < w * samplesPerPixel; ++i)
                    {
                        *app_data_ptr++ = static_cast<float>(buf_ptr[i]) / max_val;
                    }
                }
                else
                { // bitsPerSample == 16
                    uint16_t *buf_ptr = reinterpret_cast<uint16_t *>(scanline_buffer.data());
                    for (size_t i = 0; i < w * samplesPerPixel; ++i)
                    {
                        *app_data_ptr++ = static_cast<float>(buf_ptr[i]) / max_val;
                    }
                }
            }
        }
        else if (planarConfig == PLANARCONFIG_SEPARATE)
        {
            // Read plane by plane - more complex, needs buffer per plane
            LogWarning("libtiff: Planar configuration PLANARCONFIG_SEPARATE reading not fully implemented, data might be incorrect.");
            // Basic attempt: Read all scanlines for each plane sequentially into the final buffer
            size_t plane_stride = w * h;
            for (uint16_t plane = 0; plane < samplesPerPixel; ++plane)
            {
                float *plane_start_ptr = image.m_pixelData.data() + plane; // Start at the channel offset
                for (uint32_t row = 0; row < h; ++row)
                {
                    if (TIFFReadScanline(tif, scanline_buffer.data(), row, plane) < 0)
                    {
                        LogError("libtiff: Error reading scanline " + std::to_string(row) + " plane " + std::to_string(plane));
                        TIFFClose(tif);
                        return std::nullopt;
                    }
                    // Process the separate scanline for this plane
                    if (bitsPerSample == 8)
                    {
                        unsigned char *buf_ptr = scanline_buffer.data();
                        float *current_pixel_in_plane = plane_start_ptr + row * w * samplesPerPixel;
                        for (uint32_t col = 0; col < w; ++col)
                        {
                            *current_pixel_in_plane = static_cast<float>(buf_ptr[col]) / max_val;
                            current_pixel_in_plane += samplesPerPixel; // Jump to next pixel's spot for this channel
                        }
                    }
                    else
                    { // 16 bit
                        uint16_t *buf_ptr = reinterpret_cast<uint16_t *>(scanline_buffer.data());
                        float *current_pixel_in_plane = plane_start_ptr + row * w * samplesPerPixel;
                        for (uint32_t col = 0; col < w; ++col)
                        {
                            *current_pixel_in_plane = static_cast<float>(buf_ptr[col]) / max_val;
                            current_pixel_in_plane += samplesPerPixel;
                        }
                    }
                }
            }
        }
        else
        {
            LogError("libtiff: Unknown planar configuration: " + std::to_string(planarConfig));
            TIFFClose(tif);
            return std::nullopt;
        }

        // --- Post-processing based on Photometric interpretation ---
        // Handle grayscale inversion
        if (photometric == PHOTOMETRIC_MINISWHITE)
        {
            LogWarning("libtiff: Inverting MINISWHITE image.");
            for (float &val : image.m_pixelData)
            {
                val = 1.0f - val; // Simple inversion
            }
        }

        // TODO: Handle Palette -> RGB (needs reading the colormap tag)
        if (photometric == PHOTOMETRIC_PALETTE)
        {
            LogWarning("libtiff: PHOTOMETRIC_PALETTE not fully handled. Image loaded as indexed.");
            // Requires reading TIFFTAG_COLORMAP and expanding pixels
        }

        // TODO: Check for gamma tags or ICC profile tag
        // uint16_t* icc_profile_count = nullptr;
        // void* icc_profile_data = nullptr;
        // if (TIFFGetField(tif, TIFFTAG_ICCPROFILE, &icc_profile_count, &icc_profile_data) && icc_profile_count && icc_profile_data) {
        //      image.m_iccProfile.resize(*icc_profile_count);
        //      std::memcpy(image.m_iccProfile.data(), icc_profile_data, *icc_profile_count);
        //      image.m_colorSpaceName = "Linear (Embedded ICC)"; // Or just "(Embedded ICC)"
        // } else {
        //     // Check for gamma? Not standard. Assume sRGB/linear for now.
        // }

        // If no specific color info found, assume sRGB and convert to linear
        // For TIFF, it's often safer to assume linear if 16-bit, sRGB if 8-bit without other info.
        if (bitsPerSample == 8)
        {
            LogWarning("libtiff: Assuming 8-bit TIFF is sRGB. Converting to linear.");
            for (float &val : image.m_pixelData)
            {
                val = srgb_to_linear_approx(val);
            }
            image.m_isLinear = true;
            image.m_colorSpaceName = "Linear sRGB (Assumed)";
        }
        else
        {
            LogWarning("libtiff: Assuming 16-bit TIFF is already linear.");
            image.m_isLinear = true;
            image.m_colorSpaceName = "Linear Unknown (TIFF)";
        }

        TIFFClose(tif);

        // Try loading EXIF using LibTiff directory reading or Exiv2 (not EasyExif)
        // This basic example doesn't load EXIF from TIFFs.
        // You could use Exiv2 here if integrated.
        LogWarning("EXIF loading from TIFF not implemented in this example.");

        return image;
    }

} // namespace AppImageUtil

// --- AppImage Constructor Implementation ---
AppImage::AppImage(uint32_t width, uint32_t height, uint32_t channels)
    : m_width(width), m_height(height), m_channels(channels), m_isLinear(true)
{
    if (width > 0 && height > 0 && channels > 0)
    {
        try
        {
            m_pixelData.resize(static_cast<size_t>(width) * height * channels);
        }
        catch (const std::bad_alloc &e)
        {
            AppImageUtil::LogError("Failed to allocate memory for image: " + std::string(e.what()));
            clear_image(); // Reset to empty state
            throw;         // Re-throw exception
        }
    }
    // Default assumption is linear data in our internal format
    m_colorSpaceName = "Linear Generic";
}

void AppImage::resize(uint32_t newWidth, uint32_t newHeight, uint32_t newChannels)
{
    if (newChannels == 0)
        newChannels = m_channels;
    if (newChannels == 0)
        newChannels = 3; // Default if was empty

    m_width = newWidth;
    m_height = newHeight;
    m_channels = newChannels;

    if (newWidth == 0 || newHeight == 0 || newChannels == 0)
    {
        m_pixelData.clear();
        // Keep metadata? Optional.
    }
    else
    {
        try
        {
            m_pixelData.resize(static_cast<size_t>(newWidth) * newHeight * newChannels);
            // Note: Resizing doesn't preserve pixel content intelligently.
            // Consider adding different resize modes (clear, copy existing, etc.)
        }
        catch (const std::bad_alloc &e)
        {
            AppImageUtil::LogError("Failed to allocate memory during resize: " + std::string(e.what()));
            clear_image();
            throw;
        }
    }
}

void AppImage::clear_image()
{
    m_width = 0;
    m_height = 0;
    m_channels = 0;
    m_pixelData.clear();
    m_metadata.clear();
    m_iccProfile.clear();
    m_colorSpaceName = "Unknown";
    m_isLinear = true;
}

// --- loadImage Implementation ---
std::optional<AppImage> loadImage(const std::string &filePath)
{
    using namespace AppImageUtil;

    DetectedFileType type = detectFileType(filePath);

    try
    {
        switch (type)
        {
        case DetectedFileType::RAW:
            LogWarning("Detected type: RAW (using LibRaw)");
            return loadRaw(filePath);
        case DetectedFileType::JPEG:
            LogWarning("Detected type: JPEG (using libjpeg)");
            return loadJpeg(filePath);
        case DetectedFileType::PNG:
            LogWarning("Detected type: PNG (using libpng)");
            return loadPng(filePath);
        case DetectedFileType::TIFF:
            LogWarning("Detected type: TIFF (using libtiff)");
            // LibRaw can sometimes open TIFFs that contain RAW data. Try it first?
            // For now, directly use libtiff.
            return loadTiff(filePath);
        case DetectedFileType::UNKNOWN:
        default:
            LogError("Unknown or unsupported file type: " + filePath);
            return std::nullopt;
        }
    }
    catch (const std::exception &e)
    {
        LogError("Exception caught during image loading: " + std::string(e.what()));
        return std::nullopt;
    }
    catch (...)
    {
        LogError("Unknown exception caught during image loading.");
        return std::nullopt;
    }
}

// --- saveImage Implementation ---

namespace AppImageUtil
{

    // --- libjpeg Saving ---
    inline bool saveJpeg(const AppImage &image, const std::string &filePath, int quality)
    {
        if (image.getChannels() != 1 && image.getChannels() != 3)
        {
            LogError("libjpeg save: Can only save 1 (Grayscale) or 3 (RGB) channels. Image has " + std::to_string(image.getChannels()));
            return false;
        }

        FILE *outfile = fopen(filePath.c_str(), "wb");
        if (!outfile)
        {
            LogError("Cannot open file for JPEG writing: " + filePath);
            return false;
        }

        jpeg_compress_struct cinfo;
        JpegErrorManager jerr; // Use the same error manager as loading

        // Setup error handling
        cinfo.err = jpeg_std_error(&jerr.pub);
        jerr.pub.error_exit = jpegErrorExit; // Use the same exit function
        if (setjmp(jerr.setjmp_buffer))
        {
            // Error occurred during compression
            jpeg_destroy_compress(&cinfo);
            fclose(outfile);
            return false;
        }

        // Initialize compression object
        jpeg_create_compress(&cinfo);
        jpeg_stdio_dest(&cinfo, outfile);

        // Set parameters
        cinfo.image_width = image.getWidth();
        cinfo.image_height = image.getHeight();
        cinfo.input_components = image.getChannels();
        cinfo.in_color_space = (image.getChannels() == 1) ? JCS_GRAYSCALE : JCS_RGB;

        jpeg_set_defaults(&cinfo);
        jpeg_set_quality(&cinfo, std::max(1, std::min(100, quality)), TRUE /* limit to baseline-JPEG */);
        // Could set density, comments, etc. here if needed using jpeg_set_... functions

        // Start compressor
        jpeg_start_compress(&cinfo, TRUE);

        // Prepare 8-bit sRGB scanline buffer
        int row_stride = cinfo.image_width * cinfo.input_components;
        std::vector<unsigned char> scanline_buffer(row_stride);
        const float *app_data = image.getData();

        // Process scanlines
        while (cinfo.next_scanline < cinfo.image_height)
        {
            unsigned char *buffer_ptr = scanline_buffer.data();
            size_t row_start_index = static_cast<size_t>(cinfo.next_scanline) * cinfo.image_width * cinfo.input_components;

            // Convert one row from linear float to 8-bit sRGB uchar
            for (int i = 0; i < row_stride; ++i)
            {
                float linear_val = app_data[row_start_index + i];
                float srgb_val = linear_to_srgb_approx(linear_val);
                int int_val = static_cast<int>(std::round(srgb_val * 255.0f));
                buffer_ptr[i] = static_cast<unsigned char>(std::max(0, std::min(255, int_val)));
            }

            JSAMPROW row_pointer[1];
            row_pointer[0] = scanline_buffer.data();
            jpeg_write_scanlines(&cinfo, row_pointer, 1);
        }

        // Finish compression and clean up
        jpeg_finish_compress(&cinfo);
        jpeg_destroy_compress(&cinfo);
        fclose(outfile);

        // --- Metadata Saving ---
        LogWarning("JPEG EXIF/ICC Metadata saving is NOT implemented.");
        // Saving metadata would typically involve:
        // 1. Using Exiv2 library.
        // 2. Opening the file *after* libjpeg saves the pixels.
        // 3. Writing the metadata from image.m_metadata and image.m_iccProfile into the file structure.

        return true;
    }

    // --- libpng Saving ---
    inline bool savePng(const AppImage &image, const std::string &filePath, int bit_depth_out)
    {
        if (bit_depth_out != 8 && bit_depth_out != 16)
        {
            LogError("libpng save: Only 8 or 16 bit output supported.");
            return false;
        }
        if (image.getChannels() < 1 || image.getChannels() > 4 || image.getChannels() == 2)
        {
            LogError("libpng save: Can only save 1 (Gray), 3 (RGB), or 4 (RGBA) channels. Image has " + std::to_string(image.getChannels()));
            return false;
        }

        FILE *fp = fopen(filePath.c_str(), "wb");
        if (!fp)
        {
            LogError("Cannot open file for PNG writing: " + filePath);
            return false;
        }

        png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, pngErrorFunc, pngWarningFunc);
        if (!png_ptr)
        {
            LogError("libpng: png_create_write_struct failed");
            fclose(fp);
            return false;
        }

        png_infop info_ptr = png_create_info_struct(png_ptr);
        if (!info_ptr)
        {
            LogError("libpng: png_create_info_struct failed");
            png_destroy_write_struct(&png_ptr, nullptr);
            fclose(fp);
            return false;
        }

        // Setup jump buffer for error handling
        jmp_buf jmpbuf;
        if (setjmp(jmpbuf))
        {
            LogError("libpng: Error during write");
            png_destroy_write_struct(&png_ptr, &info_ptr);
            fclose(fp);
            return false;
        }
        png_set_error_fn(png_ptr, reinterpret_cast<png_voidp>(&jmpbuf), pngErrorFunc, pngWarningFunc);

        png_init_io(png_ptr, fp);

        // Determine PNG color type
        int color_type;
        switch (image.getChannels())
        {
        case 1:
            color_type = PNG_COLOR_TYPE_GRAY;
            break;
        case 3:
            color_type = PNG_COLOR_TYPE_RGB;
            break;
        case 4:
            color_type = PNG_COLOR_TYPE_RGB_ALPHA;
            break;
        default: /* Should have been caught earlier */
            return false;
        }

        // Set IHDR chunk
        png_set_IHDR(png_ptr, info_ptr, image.getWidth(), image.getHeight(),
                     bit_depth_out, color_type,
                     PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);

        // Set Gamma/sRGB info
        bool save_as_srgb = (bit_depth_out == 8); // Convention: Save 8-bit as sRGB, 16-bit as linear
        if (save_as_srgb)
        {
            png_set_sRGB_gAMA_and_cHRM(png_ptr, info_ptr, PNG_sRGB_INTENT_PERCEPTUAL);
            LogWarning("libpng save: Saving 8-bit PNG with sRGB chunk.");
        }
        else
        {                                         // 16-bit linear
            png_set_gAMA(png_ptr, info_ptr, 1.0); // Explicitly linear gamma
            LogWarning("libpng save: Saving 16-bit PNG with gamma 1.0 (linear).");
        }

        // Write header info
        png_write_info(png_ptr, info_ptr);

        // --- Prepare Data ---
        std::vector<png_bytep> row_pointers(image.getHeight());
        size_t values_per_row = static_cast<size_t>(image.getWidth()) * image.getChannels();
        size_t bytes_per_value = (bit_depth_out == 8) ? 1 : 2;
        size_t row_bytes = values_per_row * bytes_per_value;
        std::vector<unsigned char> output_buffer(row_bytes * image.getHeight());

        const float *app_data = image.getData();
        bool needs_swap = (bit_depth_out == 16 && (png_get_uint_16((png_bytep) "\x01\x02") != 0x0102)); // Check endianness only for 16-bit

        // Convert internal float data to target format row by row
        for (uint32_t y = 0; y < image.getHeight(); ++y)
        {
            unsigned char *row_buf_ptr = output_buffer.data() + y * row_bytes;
            row_pointers[y] = row_buf_ptr;
            size_t row_start_index = static_cast<size_t>(y) * values_per_row;

            if (bit_depth_out == 8)
            {
                unsigned char *uchar_ptr = row_buf_ptr;
                for (size_t i = 0; i < values_per_row; ++i)
                {
                    float linear_val = app_data[row_start_index + i];
                    float srgb_val = linear_to_srgb_approx(linear_val); // Convert to sRGB for 8-bit output
                    int int_val = static_cast<int>(std::round(srgb_val * 255.0f));
                    uchar_ptr[i] = static_cast<unsigned char>(std::max(0, std::min(255, int_val)));
                }
            }
            else
            { // 16-bit
                uint16_t *ushort_ptr = reinterpret_cast<uint16_t *>(row_buf_ptr);
                for (size_t i = 0; i < values_per_row; ++i)
                {
                    float linear_val = app_data[row_start_index + i];
                    // Clamp linear value before scaling for 16-bit output (0.0 to 1.0 range typical for linear PNG)
                    linear_val = std::fmax(0.0f, std::fmin(1.0f, linear_val));
                    int int_val = static_cast<int>(std::round(linear_val * 65535.0f));
                    uint16_t val16 = static_cast<uint16_t>(std::max(0, std::min(65535, int_val)));

                    if (needs_swap)
                    { // Swap bytes for big-endian PNG format
                        ushort_ptr[i] = (val16 >> 8) | (val16 << 8);
                    }
                    else
                    {
                        ushort_ptr[i] = val16;
                    }
                }
            }
        }

        // Write image data
        png_write_image(png_ptr, row_pointers.data());

        // End writing
        png_write_end(png_ptr, nullptr);

        // Clean up
        png_destroy_write_struct(&png_ptr, &info_ptr);
        fclose(fp);

        LogWarning("PNG Metadata saving (text chunks, ICC) is NOT implemented.");

        return true;
    }

    // --- libtiff Saving ---
    inline bool saveTiff(const AppImage &image, const std::string &filePath, int bit_depth_out)
    {
        if (bit_depth_out != 8 && bit_depth_out != 16)
        {
            LogError("libtiff save: Only 8 or 16 bit output supported.");
            return false;
        }
        if (image.getChannels() < 1 || image.getChannels() > 4 || image.getChannels() == 2)
        {
            LogError("libtiff save: Can only save 1 (Gray), 3 (RGB), or 4 (RGBA) channels. Image has " + std::to_string(image.getChannels()));
            return false;
        }

        TIFF *tif = TIFFOpen(filePath.c_str(), "w");
        if (!tif)
        {
            LogError("Cannot open file for TIFF writing: " + filePath);
            return false;
        }

        // --- Set Core TIFF Tags ---
        TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, image.getWidth());
        TIFFSetField(tif, TIFFTAG_IMAGELENGTH, image.getHeight());
        TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, static_cast<uint16_t>(image.getChannels()));
        TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, static_cast<uint16_t>(bit_depth_out));
        TIFFSetField(tif, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT);
        TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); // Interleaved is simpler

        // Set Photometric Interpretation
        uint16_t photometric;
        if (image.getChannels() == 1)
        {
            photometric = PHOTOMETRIC_MINISBLACK; // Grayscale
        }
        else if (image.getChannels() >= 3)
        {
            photometric = PHOTOMETRIC_RGB; // RGB or RGBA
            if (image.getChannels() == 4)
            {
                // Need to specify that the extra channel is Alpha
                uint16_t extra_samples = 1;
                uint16_t sample_info[] = {EXTRASAMPLE_ASSOCALPHA}; // Associated alpha
                TIFFSetField(tif, TIFFTAG_EXTRASAMPLES, extra_samples, sample_info);
            }
        }
        else
        {
            LogError("libtiff save: Unexpected channel count: " + std::to_string(image.getChannels()));
            TIFFClose(tif);
            return false;
        }
        TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, photometric);

        // Compression (optional, default is none)
        TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE);
        // Examples: COMPRESSION_LZW, COMPRESSION_ADOBE_DEFLATE

        // Rows per strip (can affect performance/compatibility)
        // A sensible default is often related to scanline buffer size.
        TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, TIFFDefaultStripSize(tif, (uint32_t)-1));

        // Software Tag (optional)
        TIFFSetField(tif, TIFFTAG_SOFTWARE, "AppImage Saver");

        // --- Prepare and Write Data ---
        size_t values_per_row = static_cast<size_t>(image.getWidth()) * image.getChannels();
        size_t bytes_per_value = (bit_depth_out == 8) ? 1 : 2;
        tmsize_t row_bytes = values_per_row * bytes_per_value;
        std::vector<unsigned char> output_buffer(row_bytes); // Buffer for one row

        const float *app_data = image.getData();
        bool save_as_srgb = (bit_depth_out == 8); // Convention: 8-bit=sRGB, 16-bit=Linear

        for (uint32_t y = 0; y < image.getHeight(); ++y)
        {
            unsigned char *row_buf_ptr = output_buffer.data();
            size_t row_start_index = static_cast<size_t>(y) * values_per_row;

            if (bit_depth_out == 8)
            {
                unsigned char *uchar_ptr = row_buf_ptr;
                for (size_t i = 0; i < values_per_row; ++i)
                {
                    float linear_val = app_data[row_start_index + i];
                    float srgb_val = linear_to_srgb_approx(linear_val); // Convert to sRGB
                    int int_val = static_cast<int>(std::round(srgb_val * 255.0f));
                    uchar_ptr[i] = static_cast<unsigned char>(std::max(0, std::min(255, int_val)));
                }
            }
            else
            { // 16-bit
                uint16_t *ushort_ptr = reinterpret_cast<uint16_t *>(row_buf_ptr);
                for (size_t i = 0; i < values_per_row; ++i)
                {
                    float linear_val = app_data[row_start_index + i];
                    // Clamp linear [0,1] before scaling
                    linear_val = std::fmax(0.0f, std::fmin(1.0f, linear_val));
                    int int_val = static_cast<int>(std::round(linear_val * 65535.0f));
                    ushort_ptr[i] = static_cast<uint16_t>(std::max(0, std::min(65535, int_val)));
                    // Note: TIFF uses native byte order by default, no swapping needed usually.
                }
            }

            // Write the scanline
            if (TIFFWriteScanline(tif, row_buf_ptr, y, 0) < 0)
            {
                LogError("libtiff save: Error writing scanline " + std::to_string(y));
                TIFFClose(tif);
                return false;
            }
        }

        // Clean up
        TIFFClose(tif);

        LogWarning("TIFF EXIF/ICC Metadata saving is NOT implemented.");
        // Saving metadata requires:
        // 1. Using Exiv2 or LibTiff's directory writing functions *before* closing the file.
        // 2. For ICC: TIFFSetField(tif, TIFFTAG_ICCPROFILE, count, data_ptr);

        return true;
    }

} // namespace AppImageUtil

namespace ImGuiImageViewerUtil {
    // Linear float [0,1+] -> sRGB approx [0,1]
    inline float linear_to_srgb_approx(float linearVal) {
        if (linearVal <= 0.0f) return 0.0f;
        linearVal = std::fmax(0.0f, std::fmin(1.0f, linearVal)); // Clamp for display
        if (linearVal <= 0.0031308f) { return linearVal * 12.92f; }
        else { return 1.055f * std::pow(linearVal, 1.0f / 2.4f) - 0.055f; }
    }

    // Round float to nearest integer
    inline float Round(float f) { return ImFloor(f + 0.5f); }
} // namespace ImGuiImageViewerUtil


bool loadImageTexture(AppImage &appImage)
{
    if (appImage.isEmpty() || appImage.getWidth() == 0 || appImage.getHeight() == 0)
    {
        AppImageUtil::LogError("loadImageTexture: Image is empty.");
        return false;
    }
    if (!appImage.isLinear())
    {
        // This shouldn't happen if loadImage converts correctly, but good practice to check.
        AppImageUtil::LogWarning("loadImageTexture: Warning - Image data is not linear. Pipeline expects linear input.");
        // Ideally, convert to linear here if not already done. For now, proceed with caution.
    }

    const int width = static_cast<int>(appImage.getWidth());
    const int height = static_cast<int>(appImage.getHeight());
    const int channels = static_cast<int>(appImage.getChannels());
    const float *linearData = appImage.getData();
    size_t numFloats = static_cast<size_t>(width) * height * channels;

    if (!linearData || numFloats == 0) {
         AppImageUtil::LogError("loadImageTexture: Image data pointer is null or size is zero.");
         return false;
    }

    // --- Determine OpenGL texture format ---
    GLenum internalFormat;
    GLenum dataFormat;
    GLenum dataType = GL_FLOAT;
    std::vector<float> textureDataBuffer; // Temporary buffer if we need to convert format (e.g., RGB -> RGBA)

    const float* dataPtr = linearData;

    if (channels == 1) {
        internalFormat = GL_R16F; // Single channel, 16-bit float
        dataFormat = GL_RED;
        // Expand Grayscale to RGBA for easier shader handling (optional, shaders could handle GL_RED)
        // Example: Expand to RGBA float buffer
        textureDataBuffer.resize(static_cast<size_t>(width) * height * 4);
        float* outPtr = textureDataBuffer.data();
        for(int i = 0; i < width * height; ++i) {
             float val = linearData[i];
             *outPtr++ = val;
             *outPtr++ = val;
             *outPtr++ = val;
             *outPtr++ = 1.0f; // Alpha
        }
        internalFormat = GL_RGBA16F; // Use RGBA16F if expanding
        dataFormat = GL_RGBA;
        dataPtr = textureDataBuffer.data(); // Point to the new buffer
        AppImageUtil::LogWarning("loadImageTexture: Expanding 1-channel to RGBA16F for texture.");

    } else if (channels == 3) {
        internalFormat = GL_RGBA16F; // Store as RGBA, easier for FBOs/blending
        dataFormat = GL_RGBA;
        // Need to convert RGB float -> RGBA float
        textureDataBuffer.resize(static_cast<size_t>(width) * height * 4);
        float* outPtr = textureDataBuffer.data();
        const float* inPtr = linearData;
        for(int i = 0; i < width * height; ++i) {
            *outPtr++ = *inPtr++; // R
            *outPtr++ = *inPtr++; // G
            *outPtr++ = *inPtr++; // B
            *outPtr++ = 1.0f;     // A
        }
        dataPtr = textureDataBuffer.data(); // Point to the new buffer
        AppImageUtil::LogWarning("loadImageTexture: Expanding 3-channel RGB to RGBA16F for texture.");
    } else if (channels == 4) {
        internalFormat = GL_RGBA16F; // Native RGBA
        dataFormat = GL_RGBA;
        dataPtr = linearData; // Use original data directly
    } else {
        AppImageUtil::LogError("loadImageTexture: Unsupported number of channels: " + std::to_string(channels));
        return false;
    }

    // --- Upload to OpenGL Texture ---
    GLint lastTexture;
    glGetIntegerv(GL_TEXTURE_BINDING_2D, &lastTexture);

    if (appImage.m_textureId == 0) {
        glGenTextures(1, &appImage.m_textureId);
         AppImageUtil::LogWarning("loadImageTexture: Generated new texture ID: " + std::to_string(appImage.m_textureId));
    } else {
         AppImageUtil::LogWarning("loadImageTexture: Reusing texture ID: " + std::to_string(appImage.m_textureId));
    }


    glBindTexture(GL_TEXTURE_2D, appImage.m_textureId);
    // Use GL_LINEAR for smoother results when zooming/scaling in the viewer, even if processing is nearest neighbor.
    // The processing pipeline itself uses FBOs, textures don't need mipmaps typically.
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

    glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // Ensure correct alignment, especially for RGB data
    glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); // Data is contiguous

    // Check if texture dimensions/format need updating
    bool needsTexImage = true;
    if (appImage.m_textureWidth == width && appImage.m_textureHeight == height) {
         // Could potentially use glTexSubImage2D if format matches, but glTexImage2D is safer
         // if the internal format might change or if it's the first load.
         // For simplicity, we'll just recreate with glTexImage2D.
         AppImageUtil::LogWarning("loadImageTexture: Texture dimensions match, overwriting with glTexImage2D.");
    } else {
        AppImageUtil::LogWarning("loadImageTexture: Texture dimensions or format mismatch, recreating with glTexImage2D.");
    }


    glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, dataFormat, dataType, dataPtr);
    GLenum err = glGetError();
     if (err != GL_NO_ERROR) {
        AppImageUtil::LogError("loadImageTexture: OpenGL Error after glTexImage2D: " + std::to_string(err));
        glBindTexture(GL_TEXTURE_2D, lastTexture); // Restore previous binding
        // Consider deleting the texture ID if creation failed badly?
        if (appImage.m_textureId != 0) {
            glDeleteTextures(1, &appImage.m_textureId);
            appImage.m_textureId = 0;
        }
        return false;
    } else {
         AppImageUtil::LogWarning("loadImageTexture: glTexImage2D successful.");
    }


    // Optional: Generate mipmaps if you want smoother downscaling *in the final view*
    // glGenerateMipmap(GL_TEXTURE_2D);
    // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);


    glBindTexture(GL_TEXTURE_2D, lastTexture); // Restore previous binding

    appImage.m_textureWidth = width;
    appImage.m_textureHeight = height;

    AppImageUtil::LogWarning("loadImageTexture: Successfully loaded linear data into texture ID " + std::to_string(appImage.m_textureId));

    return true;
}

// --- saveImage Implementation ---
bool saveImage(const AppImage &image,
               const std::string &filePath,
               ImageSaveFormat format,
               int quality)
{
    using namespace AppImageUtil;

    if (image.isEmpty())
    {
        LogError("Cannot save an empty image.");
        return false;
    }

    // Ensure internal data is linear before saving (or handle conversion if needed)
    if (!image.isLinear())
    {
        LogWarning("Attempting to save non-linear internal data. Results may be incorrect if conversion to target space isn't handled properly.");
        // Ideally, convert to linear here if required by the saving functions.
        // For this implementation, we assume the saving functions expect linear input
        // and perform the linear -> target space conversion (e.g., linear -> sRGB).
    }

    try
    {
        switch (format)
        {
        case ImageSaveFormat::JPEG:
            return saveJpeg(image, filePath, quality);
        case ImageSaveFormat::PNG_8:
            return savePng(image, filePath, 8);
        case ImageSaveFormat::PNG_16:
            return savePng(image, filePath, 16);
        case ImageSaveFormat::TIFF_8:
            return saveTiff(image, filePath, 8);
        case ImageSaveFormat::TIFF_16:
            return saveTiff(image, filePath, 16);
        case ImageSaveFormat::UNKNOWN:
        default:
            LogError("Unknown or unsupported save format specified.");
            return false;
        }
    }
    catch (const std::exception &e)
    {
        LogError("Exception caught during image saving: " + std::string(e.what()));
        return false;
    }
    catch (...)
    {
        LogError("Unknown exception caught during image saving.");
        return false;
    }
}

#endif // APP_IMAGE_IMPLEMENTATION

#endif // APP_IMAGE_H