#include "image.h"
#include "common/io.h"
#include "common/printing.h"
#include "common/processing.h"
#include "common/stringUtils.h"
#include "common/base64.h"
#include "detection/terminalsize/terminalsize.h"

#include <limits.h>
#include <math.h>

#ifdef __APPLE__
    #include <sys/syslimits.h>
#elif _WIN32
    #include <windows.h>
#elif __linux__
    #include <sys/sendfile.h>
#elif __sun
    #include <sys/termios.h>
#endif

static bool printImageIterm(bool printError)
{
    const FFOptionsLogo* options = &instance.config.logo;
    FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
    if(!ffAppendFileBuffer(options->source.chars, &buf))
    {
        if (printError)
            fputs("Logo (iterm): Failed to load image file\n", stderr);
        return false;
    }

    fflush(stdout);

    bool inTmux = false;
    {
        const char* term = getenv("TERM");
        inTmux = term && (ffStrStartsWith(term, "screen") || ffStrStartsWith(term, "tmux"));
    }

    FF_STRBUF_AUTO_DESTROY base64 = ffBase64EncodeStrbuf(&buf);
    ffStrbufClear(&buf);

    if (!options->width || !options->height)
    {
        if (options->position == FF_LOGO_POSITION_LEFT)
        {
            ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;%uH",
                (unsigned) options->paddingTop + 1,
                (unsigned) options->paddingLeft + 1
            );
        }
        else if (options->position == FF_LOGO_POSITION_TOP)
        {
            ffStrbufAppendNC(&buf, options->paddingTop, '\n');
            ffStrbufAppendNC(&buf, options->paddingLeft, ' ');
        }
        else if (options->position == FF_LOGO_POSITION_RIGHT)
        {
            if (!options->width)
            {
                if (printError)
                    fputs("Logo (iterm): Must set logo width when using position right\n", stderr);
                return false;
            }
            ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;9999999H\e[%uD", (unsigned) options->paddingTop + 1, (unsigned) options->paddingRight + options->width);
        }
        if (inTmux)
            ffStrbufAppendS(&buf, "\ePtmux;\e");

        if (options->width)
            ffStrbufAppendF(&buf, "\e]1337;File=inline=1;width=%u:%s\a", (unsigned) options->width, base64.chars);
        else
            ffStrbufAppendF(&buf, "\e]1337;File=inline=1:%s\a", base64.chars);
        if (inTmux)
            ffStrbufAppendS(&buf, "\e\\");
        ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf);

        if (options->position == FF_LOGO_POSITION_LEFT || options->position == FF_LOGO_POSITION_RIGHT)
        {
            uint16_t X = 0, Y = 0;
            const char* error = ffGetTerminalResponse("\e[6n", 2, "%*[^0-9]%hu;%huR", &Y, &X);
            if (error)
            {
                fprintf(stderr, "\nLogo (iterm): fail to query cursor position: %s\n", error);
                return true; // We already printed image logo, don't print ascii logo then
            }
            if (X < options->paddingLeft + options->width)
                X = (uint16_t) (options->paddingLeft + options->width);
            if (options->position == FF_LOGO_POSITION_LEFT)
                instance.state.logoWidth = X + options->paddingRight - 1;
            instance.state.logoHeight = Y;
            fputs("\e[H", stdout);
        }
        else if (options->position == FF_LOGO_POSITION_TOP)
        {
            instance.state.logoWidth = instance.state.logoHeight = 0;
            ffPrintCharTimes('\n', options->paddingRight);
        }
    }
    else
    {
        ffStrbufAppendNC(&buf, options->paddingTop, '\n');
        if (options->position == FF_LOGO_POSITION_RIGHT)
            ffStrbufAppendF(&buf, "\e[9999999C\e[%uD", (unsigned) options->paddingRight + options->width);
        else if (options->paddingLeft)
            ffStrbufAppendF(&buf, "\e[%uC", (unsigned) options->paddingLeft);
        if (inTmux)
            ffStrbufAppendS(&buf, "\ePtmux;\e");
        ffStrbufAppendF(&buf, "\e]1337;File=inline=1;width=%u;height=%u;preserveAspectRatio=%u:%s\a",
            (unsigned) options->width,
            (unsigned) options->height,
            (unsigned) options->preserveAspectRatio,
            base64.chars
        );
        if (inTmux)
            ffStrbufAppendS(&buf, "\e\\");
        ffStrbufAppendC(&buf, '\n');

        if (options->position == FF_LOGO_POSITION_LEFT)
        {
            instance.state.logoWidth = options->width + options->paddingLeft + options->paddingRight;
            instance.state.logoHeight = options->paddingTop + options->height;
            ffStrbufAppendF(&buf, "\e[%uA", (unsigned) instance.state.logoHeight);
        }
        else if (options->position == FF_LOGO_POSITION_TOP)
        {
            instance.state.logoWidth = instance.state.logoHeight = 0;
            ffStrbufAppendNC(&buf, options->paddingRight, '\n');
        }
        else if (options->position == FF_LOGO_POSITION_RIGHT)
        {
            instance.state.logoWidth = instance.state.logoHeight = 0;
            ffStrbufAppendF(&buf, "\e[1G\e[%uA", (unsigned) options->height);
        }
        ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf);
    }

    return true;
}

static bool printImageKittyIcat(bool printError)
{
    const FFOptionsLogo* options = &instance.config.logo;

    if (!ffPathExists(options->source.chars, FF_PATHTYPE_FILE))
    {
        if (printError)
            fputs("Logo (kitty-icat): Failed to load image file\n", stderr);
        return false;
    }

    fflush(stdout);

    FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();

    if (options->position == FF_LOGO_POSITION_LEFT)
    {
        ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;%uH",
            (unsigned) options->paddingTop + 1,
            (unsigned) options->paddingLeft + 1
        );
    }
    else if (options->position == FF_LOGO_POSITION_TOP)
    {
        if (!options->width)
        {
            ffStrbufAppendNC(&buf, options->paddingTop, '\n');
            ffStrbufAppendNC(&buf, options->paddingLeft, ' ');
        }
        else
        {
            if (printError)
                fputs("Logo (kitty-icat): position top is not supported when logo width is set\n", stderr);
            return false;
        }
    }
    else if (options->position == FF_LOGO_POSITION_RIGHT)
    {
        if (printError)
            fputs("Logo (kitty-icat): position right is not supported\n", stderr);
        return false;
    }

    uint32_t prevLength = buf.length;

    const char* error = NULL;

    if (options->width)
    {
        char place[64];
        snprintf(place,
            ARRAY_SIZE(place),
            "--place=%ux%u@%ux%u",
            options->width,
            options->height == 0 ? 9999 : options->height,
            options->paddingLeft + 1,
            options->paddingTop + 1);

        error = ffProcessAppendStdOut(&buf, (char* []) {
            "kitten",
            "icat",
            "-n",
            "--align=center",
            place,
            "--scale-up",
            options->source.chars,
            NULL,
        });
    }
    else
    {
        error = ffProcessAppendStdOut(&buf, (char* []) {
            "kitten",
            "icat",
            "-n",
            "--align=left",
            options->source.chars,
            NULL,
        });
    }
    if (error)
    {
        if (printError)
            fprintf(stderr, "Logo (kitty-icat): running `kitten icat` failed %s\n", error);
        return false;
    }

    if (buf.length == prevLength)
    {
        if (printError)
            fputs("Logo (kitty-icat): `kitten icat` returned empty output\n", stderr);
        return false;
    }

    ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf);

    if (options->position == FF_LOGO_POSITION_LEFT || options->position == FF_LOGO_POSITION_RIGHT)
    {
        uint16_t X = 0, Y = 0;
        const char* error = ffGetTerminalResponse("\e[6n", 2, "%*[^0-9]%hu;%huR", &Y, &X);
        if (error)
        {
            fprintf(stderr, "\nLogo (kitty-icat): fail to query cursor position: %s\n", error);
            return true; // We already printed image logo, don't print ascii logo then
        }
        if (X < options->paddingLeft + options->width)
            X = (uint16_t) (options->paddingLeft + options->width);
        if (options->position == FF_LOGO_POSITION_LEFT)
            instance.state.logoWidth = X + options->paddingRight - 1;
        instance.state.logoHeight = Y;
        fputs("\e[H", stdout);
    }
    else if (options->position == FF_LOGO_POSITION_TOP)
    {
        instance.state.logoWidth = instance.state.logoHeight = 0;
        ffPrintCharTimes('\n', options->paddingRight);
    }

    return true;
}

static bool printImageKittyDirect(bool printError)
{
    const FFOptionsLogo* options = &instance.config.logo;

    if (!ffPathExists(options->source.chars, FF_PATHTYPE_FILE))
    {
        if (printError)
            fputs("Logo (kitty-direct): Failed to load image file\n", stderr);
        return false;
    }

    fflush(stdout);

    bool inTmux = false;
    {
        const char* term = getenv("TERM");
        inTmux = term && (ffStrStartsWith(term, "screen") || ffStrStartsWith(term, "tmux"));
    }

    FF_STRBUF_AUTO_DESTROY base64 = ffBase64EncodeStrbuf(&options->source);
    FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();

    if (!options->width || !options->height)
    {
        if (options->position == FF_LOGO_POSITION_LEFT)
        {
            // We must clear the entre screen to make sure that terminal buffer won't scroll up
            ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;%uH",
                (unsigned) options->paddingTop + 1,
                (unsigned) options->paddingLeft + 1
            );
        }
        else if (options->position == FF_LOGO_POSITION_TOP)
        {
            ffStrbufAppendNC(&buf, options->paddingTop, '\n');
            ffStrbufAppendNC(&buf, options->paddingLeft, ' ');
        }
        else if (options->position == FF_LOGO_POSITION_RIGHT)
        {
            if (!options->width)
            {
                if (printError)
                    fputs("Logo (iterm): Must set logo width when using position right\n", stderr);
                return false;
            }
            ffStrbufAppendF(&buf, "\e[2J\e[3J\e[%u;9999999H\e[%uD", (unsigned) options->paddingTop + 1, (unsigned) options->paddingRight + options->width);
        }

        if (inTmux)
            ffStrbufAppendS(&buf, "\ePtmux;\e");
        if (options->width)
            ffStrbufAppendF(&buf, "\e_Ga=T,f=100,t=f,c=%u;%s", (unsigned) options->width, base64.chars);
        else
            ffStrbufAppendF(&buf, "\e_Ga=T,f=100,t=f;%s", base64.chars);
        if (inTmux)
            ffStrbufAppendC(&buf, '\e');
        ffStrbufAppendS(&buf, "\e\\");
        if (inTmux)
            ffStrbufAppendS(&buf, "\e\\");

        ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf);

        if (options->position == FF_LOGO_POSITION_LEFT || options->position == FF_LOGO_POSITION_RIGHT)
        {
            uint16_t X = 0, Y = 0;
            const char* error = ffGetTerminalResponse("\e[6n", 2, "%*[^0-9]%hu;%huR", &Y, &X);
            if (error)
            {
                if (printError)
                    fprintf(stderr, "\nLogo (kitty-direct): fail to query cursor position: %s\n", error);
                return true; // We already printed image logo, don't print ascii logo then
            }
            if (X < options->paddingLeft + options->width)
                X = (uint16_t) (options->paddingLeft + options->width);
            if (options->position == FF_LOGO_POSITION_LEFT)
                instance.state.logoWidth = X + options->paddingRight - 1;
            instance.state.logoHeight = Y;
            fputs("\e[H", stdout);
        }
        else if (options->position == FF_LOGO_POSITION_TOP)
        {
            instance.state.logoWidth = instance.state.logoHeight = 0;
            ffPrintCharTimes('\n', options->paddingRight);
        }
    }
    else
    {
        ffStrbufAppendNC(&buf, options->paddingTop, '\n');

        if (options->position == FF_LOGO_POSITION_RIGHT)
            ffStrbufAppendF(&buf, "\e[9999999C\e[%uD", (unsigned) options->paddingRight + options->width);
        else if (options->paddingLeft)
            ffStrbufAppendF(&buf, "\e[%uC", (unsigned) options->paddingLeft);

        if (inTmux)
            ffStrbufAppendS(&buf, "\ePtmux;\e");

        ffStrbufAppendF(&buf, "\e_Ga=T,f=100,t=f,c=%u,r=%u;%s\e\\",
            (unsigned) options->width,
            (unsigned) options->height,
            base64.chars
        );
        if (inTmux)
            ffStrbufAppendS(&buf, "\e\\");
        ffStrbufAppendC(&buf, '\n');
        if (options->position == FF_LOGO_POSITION_LEFT)
        {
            instance.state.logoWidth = options->width + options->paddingLeft + options->paddingRight;
            instance.state.logoHeight = options->paddingTop + options->height;
            ffStrbufAppendF(&buf, "\e[%uA", (unsigned) instance.state.logoHeight);
        }
        else if (options->position == FF_LOGO_POSITION_TOP)
        {
            instance.state.logoWidth = instance.state.logoHeight = 0;
            ffStrbufAppendNC(&buf, options->paddingRight, '\n');
        }
        else if (options->position == FF_LOGO_POSITION_RIGHT)
        {
            instance.state.logoWidth = instance.state.logoHeight = 0;
            ffStrbufAppendF(&buf, "\e[1G\e[%uA", (unsigned) options->height);
        }

        ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), &buf);
    }

    return true;
}

#if defined(FF_HAVE_IMAGEMAGICK7) || defined(FF_HAVE_IMAGEMAGICK6)

#define FF_KITTY_MAX_CHUNK_SIZE 4096

#define FF_CACHE_FILE_HEIGHT "height"
#define FF_CACHE_FILE_WIDTH "width"
#define FF_CACHE_FILE_SIXEL "sixel"
#define FF_CACHE_FILE_KITTY_COMPRESSED "kittyc"
#define FF_CACHE_FILE_KITTY_UNCOMPRESSED "kittyu"
#define FF_CACHE_FILE_CHAFA "chafa"

#include <string.h>
#include <unistd.h>
#include <fcntl.h>

#ifndef _WIN32
#include <sys/ioctl.h>
#else
#include <wincon.h>

static inline char* realpath(const char* restrict file_name, char* restrict resolved_name)
{
    char* result = _fullpath(resolved_name, file_name, _MAX_PATH);
    if(result)
    {
        resolved_name[1] = resolved_name[0]; // Drive Name
        resolved_name[0] = '/';
    }
    return result;
}
#endif

#ifdef FF_HAVE_ZLIB
#include "common/library.h"
#include <stdlib.h>
#include <zlib.h>

static bool compressBlob(void** blob, size_t* length)
{
    FF_LIBRARY_LOAD(zlib, false, "libz" FF_LIBRARY_EXTENSION, 2)
    FF_LIBRARY_LOAD_SYMBOL(zlib, compressBound, false)
    FF_LIBRARY_LOAD_SYMBOL(zlib, compress2, false)

    uLong compressedLength = ffcompressBound(*length);
    void* compressed = malloc(compressedLength);
    if(compressed == NULL)
        return false;

    if(ffcompress2(compressed, &compressedLength, *blob, *length, Z_BEST_COMPRESSION) != Z_OK)
    {
        free(compressed);
        return false;
    }

    free(*blob);

    *length = (size_t) compressedLength;
    *blob = compressed;
    return true;
}

#endif // FF_HAVE_ZLIB

//We use only the defines from here, that are exactly the same in both versions
#ifdef FF_HAVE_IMAGEMAGICK7
    #include <MagickCore/MagickCore.h>
#else
    #include <magick/MagickCore.h>
#endif

typedef struct ImageData
{
    FF_LIBRARY_SYMBOL(CopyMagickString)
    FF_LIBRARY_SYMBOL(ImageToBlob)
    FF_LIBRARY_SYMBOL(Base64Encode)

    ImageInfo* imageInfo;
    Image* image;
    ExceptionInfo* exceptionInfo;
} ImageData;

static inline bool checkAllocationResult(void* data, size_t length)
{
    if(data == NULL)
        return false;

    if(length == 0)
    {
        free(data);
        return false;
    }

    return true;
}

static void writeCacheStrbuf(FFLogoRequestData* requestData, const FFstrbuf* value, const char* cacheFileName)
{
    uint32_t cacheDirLength = requestData->cacheDir.length;
    ffStrbufAppendS(&requestData->cacheDir, cacheFileName);
    ffWriteFileBuffer(requestData->cacheDir.chars, value);
    ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength);
}

static void writeCacheUint32(FFLogoRequestData* requestData, uint32_t value, const char* cacheFileName)
{
    FFstrbuf content;
    content.chars = (char*) &value;
    content.length = sizeof(value);
    writeCacheStrbuf(requestData, &content, cacheFileName);
}

static void printImagePixels(FFLogoRequestData* requestData, const FFstrbuf* result, const char* cacheFileName)
{
    const FFOptionsLogo* options = &instance.config.logo;
    //Calculate character dimensions
    instance.state.logoWidth = requestData->logoCharacterWidth + options->paddingLeft + options->paddingRight;
    instance.state.logoHeight = requestData->logoCharacterHeight + options->paddingTop - 1;

    //Write cache files
    writeCacheStrbuf(requestData, result, cacheFileName);

    if(options->width == 0)
        writeCacheUint32(requestData, requestData->logoCharacterWidth, FF_CACHE_FILE_WIDTH);

    if(options->height == 0)
        writeCacheUint32(requestData, requestData->logoCharacterHeight, FF_CACHE_FILE_HEIGHT);

    //Write result to stdout
    ffPrintCharTimes('\n', options->paddingTop);
    if (options->position == FF_LOGO_POSITION_RIGHT)
        printf("\e[9999999C\e[%uD", (unsigned) options->paddingRight + requestData->logoCharacterWidth);
    else if (options->paddingLeft)
        printf("\e[%uC", (unsigned) options->paddingLeft);
    fflush(stdout);
    ffWriteFDBuffer(FFUnixFD2NativeFD(STDOUT_FILENO), result);

    if (options->position != FF_LOGO_POSITION_TOP)
    {
        //Go to upper left corner
        printf("\e[1G\e[%uA", instance.state.logoHeight);
    }

    if (options->position != FF_LOGO_POSITION_LEFT)
        instance.state.logoWidth = instance.state.logoHeight = 0;
}

static bool printImageSixel(FFLogoRequestData* requestData, const ImageData* imageData)
{
    imageData->ffCopyMagickString(imageData->imageInfo->magick, "SIXEL", 6);

    size_t length;
    void* blob = imageData->ffImageToBlob(imageData->imageInfo, imageData->image, &length, imageData->exceptionInfo);
    if(!checkAllocationResult(blob, length))
        return false;

    FFstrbuf result;
    result.chars = (char*) blob;
    result.length = (uint32_t) length;

    printImagePixels(requestData, &result, FF_CACHE_FILE_SIXEL);

    free(blob);
    return true;
}

static void appendKittyChunk(FFstrbuf* result, const char** blob, size_t* length, bool printEscapeCode)
{
    uint32_t chunkSize = *length > FF_KITTY_MAX_CHUNK_SIZE ? FF_KITTY_MAX_CHUNK_SIZE : (uint32_t) *length;

    if(printEscapeCode)
        ffStrbufAppendS(result, "\033_G");
    else
        ffStrbufAppendC(result, ',');

    ffStrbufAppendS(result, chunkSize != *length ? "m=1" : "m=0");
    ffStrbufAppendC(result, ';');
    ffStrbufAppendNS(result, chunkSize, *blob);
    ffStrbufAppendS(result, "\033\\");
    *length -= chunkSize;
    *blob += chunkSize;
}

static bool printImageKitty(FFLogoRequestData* requestData, const ImageData* imageData)
{
    imageData->ffCopyMagickString(imageData->imageInfo->magick, "RGBA", 5);

    size_t length;
    void* blob = imageData->ffImageToBlob(imageData->imageInfo, imageData->image, &length, imageData->exceptionInfo);
    if(!checkAllocationResult(blob, length))
        return false;

    #ifdef FF_HAVE_ZLIB
        bool isCompressed = compressBlob(&blob, &length);
    #else
        bool isCompressed = false;
    #endif

    char* chars = imageData->ffBase64Encode(blob, length, &length);
    free(blob);
    if(!checkAllocationResult(chars, length))
        return false;

    FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateA((uint32_t) (length + 1024));

    const char* currentPos = chars;
    size_t remainingLength = length;

    ffStrbufAppendF(&result, "\033_Ga=T,f=32,s=%u,v=%u", requestData->logoPixelWidth, requestData->logoPixelHeight);
    if(isCompressed)
        ffStrbufAppendS(&result, ",o=z");
    appendKittyChunk(&result, &currentPos, &remainingLength, false);
    while(remainingLength > 0)
        appendKittyChunk(&result, &currentPos, &remainingLength, true);

    printImagePixels(requestData, &result, isCompressed ? FF_CACHE_FILE_KITTY_COMPRESSED : FF_CACHE_FILE_KITTY_UNCOMPRESSED);

    free(chars);
    return true;
}

#ifdef FF_HAVE_CHAFA
#include <chafa.h>
static bool printImageChafa(FFLogoRequestData* requestData, const ImageData* imageData)
{
    FF_LIBRARY_LOAD(chafa, false,
        "libchafa" FF_LIBRARY_EXTENSION, 1,
        "libchafa-0" FF_LIBRARY_EXTENSION, -1 // Required for Windows
    )
    FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_symbol_map_new, false)
    FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_symbol_map_apply_selectors, false)
    FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_config_new, false)
    FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_config_set_geometry, false)
    FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_config_set_symbol_map, false)
    FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_new, false)
    FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_draw_all_pixels, false)
    FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_print, false)
    FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_unref, false)
    FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_canvas_config_unref, false)
    FF_LIBRARY_LOAD_SYMBOL(chafa, chafa_symbol_map_unref, false)

    imageData->ffCopyMagickString(imageData->imageInfo->magick, "RGBA", 5);
    size_t length;
    void* blob = imageData->ffImageToBlob(imageData->imageInfo, imageData->image, &length, imageData->exceptionInfo);
    if(!checkAllocationResult(blob, length))
        return false;

    ChafaSymbolMap* symbolMap = ffchafa_symbol_map_new();
    GError* error = NULL;
    if(!ffchafa_symbol_map_apply_selectors(symbolMap, instance.config.logo.chafaSymbols.chars, &error))
        fputs(error->message, stderr);

    ChafaCanvasConfig* canvasConfig = ffchafa_canvas_config_new();
    ffchafa_canvas_config_set_geometry(canvasConfig, (gint) requestData->logoCharacterWidth, (gint) requestData->logoCharacterHeight);
    ffchafa_canvas_config_set_symbol_map(canvasConfig, symbolMap);

    if(instance.config.logo.chafaFgOnly)
    {
        FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, chafa_canvas_config_set_fg_only_enabled);
        if(ffchafa_canvas_config_set_fg_only_enabled)
            ffchafa_canvas_config_set_fg_only_enabled(canvasConfig, true);
    }
    if(instance.config.logo.chafaCanvasMode < CHAFA_CANVAS_MODE_MAX)
    {
        FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, chafa_canvas_config_set_canvas_mode);
        if(ffchafa_canvas_config_set_canvas_mode)
            ffchafa_canvas_config_set_canvas_mode(canvasConfig, (ChafaCanvasMode) instance.config.logo.chafaCanvasMode);
    }
    if(instance.config.logo.chafaColorSpace < CHAFA_COLOR_SPACE_MAX)
    {
        FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, chafa_canvas_config_set_color_space)
        if(ffchafa_canvas_config_set_color_space)
            ffchafa_canvas_config_set_color_space(canvasConfig, (ChafaColorSpace) instance.config.logo.chafaColorSpace);
    }
    if(instance.config.logo.chafaDitherMode < CHAFA_DITHER_MODE_MAX)
    {
        FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, chafa_canvas_config_set_dither_mode)
        if(ffchafa_canvas_config_set_dither_mode)
            ffchafa_canvas_config_set_dither_mode(canvasConfig, (ChafaDitherMode) instance.config.logo.chafaDitherMode);
    }

    ChafaCanvas* canvas = ffchafa_canvas_new(canvasConfig);
    ffchafa_canvas_draw_all_pixels(
        canvas,
        CHAFA_PIXEL_RGBA8_UNASSOCIATED,
        blob,
        (gint) imageData->image->columns,
        (gint) imageData->image->rows,
        (gint) imageData->image->columns * 4
    );

    GString* str = ffchafa_canvas_print(canvas, NULL);
    FFstrbuf result;
    result.allocated = (uint32_t) str->allocated_len;
    result.length = (uint32_t) str->len;
    result.chars = str->str;

    ffLogoPrintChars(result.chars, false);
    writeCacheStrbuf(requestData, &result, FF_CACHE_FILE_CHAFA);

    // FIXME: These functions must be imported from `libglib` dlls on Windows
    FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, g_string_free);
    if(ffg_string_free)
        ffg_string_free(str, TRUE);
    if(error)
    {
        FF_LIBRARY_LOAD_SYMBOL_LAZY(chafa, g_error_free)
        if(ffg_error_free)
            ffg_error_free(error);
    }

    ffchafa_canvas_unref(canvas);
    ffchafa_canvas_config_unref(canvasConfig);
    ffchafa_symbol_map_unref(symbolMap);

    return true;
}
#endif

FFLogoImageResult ffLogoPrintImageImpl(FFLogoRequestData* requestData, const FFIMData* imData)
{
    FF_LIBRARY_LOAD_SYMBOL(imData->library, MagickCoreGenesis, FF_LOGO_IMAGE_RESULT_INIT_ERROR)
    FF_LIBRARY_LOAD_SYMBOL(imData->library, MagickCoreTerminus, FF_LOGO_IMAGE_RESULT_INIT_ERROR)
    FF_LIBRARY_LOAD_SYMBOL(imData->library, AcquireExceptionInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR)
    FF_LIBRARY_LOAD_SYMBOL(imData->library, DestroyExceptionInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR)
    FF_LIBRARY_LOAD_SYMBOL(imData->library, AcquireImageInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR)
    FF_LIBRARY_LOAD_SYMBOL(imData->library, DestroyImageInfo, FF_LOGO_IMAGE_RESULT_INIT_ERROR)
    FF_LIBRARY_LOAD_SYMBOL(imData->library, ReadImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR)
    FF_LIBRARY_LOAD_SYMBOL(imData->library, DestroyImage, FF_LOGO_IMAGE_RESULT_INIT_ERROR)

    ImageData imageData;

    FF_LIBRARY_LOAD_SYMBOL_VAR(imData->library, imageData, CopyMagickString, FF_LOGO_IMAGE_RESULT_INIT_ERROR)
    FF_LIBRARY_LOAD_SYMBOL_VAR(imData->library, imageData, ImageToBlob, FF_LOGO_IMAGE_RESULT_INIT_ERROR)
    FF_LIBRARY_LOAD_SYMBOL_VAR(imData->library, imageData, Base64Encode, FF_LOGO_IMAGE_RESULT_INIT_ERROR)

    ffMagickCoreGenesis(NULL, MagickFalse);

    imageData.exceptionInfo = ffAcquireExceptionInfo();
    if(imageData.exceptionInfo == NULL)
    {
        ffMagickCoreTerminus();
        return FF_LOGO_IMAGE_RESULT_RUN_ERROR;
    }

    ImageInfo* imageInfoIn = ffAcquireImageInfo();
    if(imageInfoIn == NULL)
    {
        ffDestroyExceptionInfo(imageData.exceptionInfo);
        ffMagickCoreTerminus();
        return FF_LOGO_IMAGE_RESULT_RUN_ERROR;
    }

    //+1, because we need to copy the null byte too
    imageData.ffCopyMagickString(imageInfoIn->filename, instance.config.logo.source.chars, instance.config.logo.source.length + 1);

    imageData.image = ffReadImage(imageInfoIn, imageData.exceptionInfo);
    ffDestroyImageInfo(imageInfoIn);
    if(imageData.image == NULL)
    {
        ffDestroyExceptionInfo(imageData.exceptionInfo);
        ffMagickCoreTerminus();
        return FF_LOGO_IMAGE_RESULT_RUN_ERROR;
    }

    if(requestData->logoPixelWidth == 0 && requestData->logoPixelHeight == 0)
    {
        requestData->logoPixelWidth = (uint32_t) imageData.image->columns;
        requestData->logoPixelHeight = (uint32_t) imageData.image->rows;
    }
    else if(requestData->logoPixelWidth == 0)
        requestData->logoPixelWidth = (uint32_t) ((double) imageData.image->columns / (double) imageData.image->rows * requestData->logoPixelHeight);
    else if(requestData->logoPixelHeight == 0)
        requestData->logoPixelHeight = (uint32_t) ((double) imageData.image->rows / (double) imageData.image->columns * requestData->logoPixelWidth);

    requestData->logoCharacterWidth = (uint32_t) ceil((double) requestData->logoPixelWidth / requestData->characterPixelWidth);
    requestData->logoCharacterHeight = (uint32_t) ceil((double) requestData->logoPixelHeight / requestData->characterPixelHeight);

    if(requestData->logoPixelWidth == 0 || requestData->logoPixelHeight == 0 || requestData->logoCharacterWidth == 0 || requestData->logoCharacterHeight == 0)
    {
        ffDestroyImage(imageData.image);
        ffDestroyExceptionInfo(imageData.exceptionInfo);
        ffMagickCoreTerminus();
        return FF_LOGO_IMAGE_RESULT_RUN_ERROR;
    }

    Image* resized = imData->resizeFunc(imageData.image, requestData->logoPixelWidth, requestData->logoPixelHeight, imageData.exceptionInfo);
    ffDestroyImage(imageData.image);
    if(resized == NULL)
    {
        ffDestroyExceptionInfo(imageData.exceptionInfo);
        ffMagickCoreTerminus();
        return FF_LOGO_IMAGE_RESULT_RUN_ERROR;
    }
    imageData.image = resized;

    imageData.imageInfo = ffAcquireImageInfo();
    if(imageData.imageInfo == NULL)
    {
        ffDestroyImage(imageData.image);
        ffDestroyExceptionInfo(imageData.exceptionInfo);
        ffMagickCoreTerminus();
        return FF_LOGO_IMAGE_RESULT_RUN_ERROR;
    }

    bool printSuccessful = false;
    if(requestData->type == FF_LOGO_TYPE_IMAGE_CHAFA)
    {
        #ifdef FF_HAVE_CHAFA
            printSuccessful = printImageChafa(requestData, &imageData);
        #endif
    }
    else if(requestData->type == FF_LOGO_TYPE_IMAGE_KITTY)
        printSuccessful = printImageKitty(requestData, &imageData);
    else if(requestData->type == FF_LOGO_TYPE_IMAGE_SIXEL)
        printSuccessful = printImageSixel(requestData, &imageData);

    ffDestroyImageInfo(imageData.imageInfo);
    ffDestroyImage(imageData.image);
    ffDestroyExceptionInfo(imageData.exceptionInfo);
    ffMagickCoreTerminus();

    return printSuccessful ? FF_LOGO_IMAGE_RESULT_SUCCESS : FF_LOGO_IMAGE_RESULT_RUN_ERROR;
}

static FFNativeFD getCacheFD(FFLogoRequestData* requestData, const char* fileName)
{
    uint32_t cacheDirLength = requestData->cacheDir.length;
    ffStrbufAppendS(&requestData->cacheDir, fileName);
    #ifndef _WIN32
    int fd = open(requestData->cacheDir.chars, O_RDONLY
        #ifdef O_CLOEXEC
            | O_CLOEXEC
        #endif
    );
    #else
    HANDLE fd = CreateFileA(requestData->cacheDir.chars, GENERIC_READ,
        FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    #endif
    ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength);
    return fd;
}

static void readCachedStrbuf(FFLogoRequestData* requestData, FFstrbuf* result, const char* cacheFileName)
{
    uint32_t cacheDirLength = requestData->cacheDir.length;
    ffStrbufAppendS(&requestData->cacheDir, cacheFileName);
    ffAppendFileBuffer(requestData->cacheDir.chars, result);
    ffStrbufSubstrBefore(&requestData->cacheDir, cacheDirLength);
}

static uint32_t readCachedUint32(FFLogoRequestData* requestData, const char* cacheFileName)
{
    FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate();
    readCachedStrbuf(requestData, &content, cacheFileName);

    uint32_t result = 0;

    if(content.length != sizeof(result))
        return 0;

    memcpy(&result, content.chars, sizeof(result));

    return result;
}

static bool printCachedChars(FFLogoRequestData* requestData)
{
    FF_STRBUF_AUTO_DESTROY content = ffStrbufCreateA(32768);

    if(requestData->type == FF_LOGO_TYPE_IMAGE_CHAFA)
        readCachedStrbuf(requestData, &content, FF_CACHE_FILE_CHAFA);

    if(content.length == 0)
        return false;

    ffLogoPrintChars(content.chars, false);
    return true;
}

static bool printCachedPixel(FFLogoRequestData* requestData)
{
    FFOptionsLogo* options = &instance.config.logo;

    requestData->logoCharacterWidth = options->width;
    if(requestData->logoCharacterWidth == 0)
    {
        requestData->logoCharacterWidth = readCachedUint32(requestData, FF_CACHE_FILE_WIDTH);
        if(requestData->logoCharacterWidth == 0)
            return false;
    }

    requestData->logoCharacterHeight = options->height;
    if(requestData->logoCharacterHeight == 0)
    {
        requestData->logoCharacterHeight = readCachedUint32(requestData, FF_CACHE_FILE_HEIGHT);
        if(requestData->logoCharacterHeight == 0)
            return false;
    }

    FF_AUTO_CLOSE_FD FFNativeFD fd = FF_INVALID_FD;
    if(requestData->type == FF_LOGO_TYPE_IMAGE_KITTY)
    {
        fd = getCacheFD(requestData, FF_CACHE_FILE_KITTY_COMPRESSED);
        if(!ffIsValidNativeFD(fd))
            fd = getCacheFD(requestData, FF_CACHE_FILE_KITTY_UNCOMPRESSED);
    }
    else if(requestData->type == FF_LOGO_TYPE_IMAGE_SIXEL)
        fd = getCacheFD(requestData, FF_CACHE_FILE_SIXEL);

    if(!ffIsValidNativeFD(fd))
        return false;

    ffPrintCharTimes('\n', options->paddingTop);
    if (options->position == FF_LOGO_POSITION_RIGHT)
        printf("\e[9999999C\e[%uD", (unsigned) options->paddingRight + requestData->logoCharacterWidth);
    else if (options->paddingLeft)
        printf("\e[%uC", (unsigned) options->paddingLeft);
    fflush(stdout);

    bool sent = false;
    #ifdef __linux__
    struct stat st;
    if (fstat(fd, &st) >= 0)
    {
        while (st.st_size > 0)
        {
            ssize_t bytes = sendfile(STDOUT_FILENO, fd, NULL, (size_t) st.st_size);
            if (bytes > 0)
            {
                sent = true;
                st.st_size -= bytes;
            }
            else
                break;
        }
    }
    #endif

    if (!sent)
    {
        char buffer[32768];
        ssize_t readBytes;
        while((readBytes = ffReadFDData(fd, sizeof(buffer), buffer)) > 0)
            ffWriteFDData(FFUnixFD2NativeFD(STDOUT_FILENO), (size_t) readBytes, buffer);
    }

    instance.state.logoWidth = requestData->logoCharacterWidth + options->paddingLeft + options->paddingRight;
    instance.state.logoHeight = requestData->logoCharacterHeight + options->paddingTop;

    if (options->position != FF_LOGO_POSITION_TOP)
    {
        //Go to upper left corner
        printf("\e[1G\e[%uA", instance.state.logoHeight);
    }

    if (options->position != FF_LOGO_POSITION_LEFT)
        instance.state.logoWidth = instance.state.logoHeight = 0;
    return true;
}

static bool printCached(FFLogoRequestData* requestData)
{
    if(requestData->type == FF_LOGO_TYPE_IMAGE_CHAFA)
        return printCachedChars(requestData);
    else
        return printCachedPixel(requestData);
}

static bool getCharacterPixelDimensions(FFLogoRequestData* requestData)
{
    #ifdef _WIN32

    CONSOLE_FONT_INFO cfi;
    if(GetCurrentConsoleFont(GetStdHandle(STD_OUTPUT_HANDLE), FALSE, &cfi)) // Only works for ConHost
    {
        requestData->characterPixelWidth = cfi.dwFontSize.X;
        requestData->characterPixelHeight = cfi.dwFontSize.Y;
    }
    if (requestData->characterPixelWidth > 1.0 && requestData->characterPixelHeight > 1.0)
        return true;
    #endif

    FFTerminalSizeResult termSize = {};
    if (ffDetectTerminalSize(&termSize))
    {
        requestData->characterPixelWidth = termSize.width / (double) termSize.columns;
        requestData->characterPixelHeight = termSize.height / (double) termSize.rows;
    }

    return requestData->characterPixelWidth > 1.0 && requestData->characterPixelHeight > 1.0;
}

static bool printImageIfExistsSlowPath(FFLogoType type, bool printError)
{
    FFLogoRequestData requestData;
    requestData.type = type;
    requestData.characterPixelWidth = 1;
    requestData.characterPixelHeight = 1;

    if(!getCharacterPixelDimensions(&requestData))
    {
        if(printError)
            fputs("Logo: getCharacterPixelDimensions() failed\n", stderr);
        return false;
    }

    requestData.logoPixelWidth = (uint32_t) ceil((double) instance.config.logo.width * requestData.characterPixelWidth);
    requestData.logoPixelHeight = (uint32_t) ceil((double) instance.config.logo.height * requestData.characterPixelHeight);

    ffStrbufInit(&requestData.cacheDir);
    ffStrbufAppend(&requestData.cacheDir, &instance.state.platform.cacheDir);
    ffStrbufAppendS(&requestData.cacheDir, "fastfetch/images");

    ffStrbufEnsureFree(&requestData.cacheDir, PATH_MAX);
    if(realpath(instance.config.logo.source.chars, requestData.cacheDir.chars + requestData.cacheDir.length) == NULL)
    {
        //We can safely return here, because if realpath failed, we surely won't be able to read the file
        ffStrbufDestroy(&requestData.cacheDir);
        if(printError)
            fputs("Logo: Querying realpath of the image source failed\n", stderr);
        return false;
    }
    ffStrbufRecalculateLength(&requestData.cacheDir);
    ffStrbufEnsureEndsWithC(&requestData.cacheDir, '/');
    ffStrbufAppendF(&requestData.cacheDir, "%u*%u/", requestData.logoPixelWidth, requestData.logoPixelHeight);

    if(!instance.config.logo.recache && printCached(&requestData))
    {
        ffStrbufDestroy(&requestData.cacheDir);
        return true;
    }

    FFLogoImageResult result = FF_LOGO_IMAGE_RESULT_INIT_ERROR;

    #ifdef FF_HAVE_IMAGEMAGICK7
        result = ffLogoPrintImageIM7(&requestData);
    #endif

    #ifdef FF_HAVE_IMAGEMAGICK6
        if(result == FF_LOGO_IMAGE_RESULT_INIT_ERROR)
            result = ffLogoPrintImageIM6(&requestData);
    #endif

    ffStrbufDestroy(&requestData.cacheDir);

    if(result == FF_LOGO_IMAGE_RESULT_SUCCESS)
        return true;

    if(printError)
    {
        if(result == FF_LOGO_IMAGE_RESULT_INIT_ERROR)
            fputs("Logo: Image Magick library not found\n", stderr);
        else
            fputs("Logo: Failed to load / convert the image source\n", stderr);
    }

    return false;
}

#endif //FF_HAVE_IMAGEMAGICK{6, 7}

bool ffLogoPrintImageIfExists(FFLogoType type, bool printError)
{
    if(instance.config.display.pipe)
    {
        if(printError)
            fputs("Logo: Image logo is not supported in pipe mode\n", stderr);
        return false;
    }

    if(!ffPathExists(instance.config.logo.source.chars, FF_PATHTYPE_FILE))
    {
        if(printError)
            fprintf(stderr, "Logo: Image source \"%s\" does not exist\n", instance.config.logo.source.chars);
        return false;
    }

    const char* term = getenv("TERM");
    if((term && ffStrEquals(term, "screen")) || getenv("ZELLIJ"))
    {
        if(printError)
            fputs("Logo: Image logo is not supported in terminal multiplexers\n", stderr);
        return false;
    }

    if(type == FF_LOGO_TYPE_IMAGE_ITERM)
        return printImageIterm(printError);

    if(type == FF_LOGO_TYPE_IMAGE_KITTY_DIRECT)
        return printImageKittyDirect(printError);

    if(type == FF_LOGO_TYPE_IMAGE_KITTY_ICAT)
        return printImageKittyIcat(printError);

    #if !defined(FF_HAVE_CHAFA)
        if(type == FF_LOGO_TYPE_IMAGE_CHAFA)
        {
            if(printError)
                fputs("Logo: Chafa support is not compiled in\n", stderr);
            return false;
        }
    #endif

    #if !defined(FF_HAVE_IMAGEMAGICK7) && !defined(FF_HAVE_IMAGEMAGICK6)
        if(printError)
            fputs("Logo: Image Magick support is not compiled in\n", stderr);
        return false;
    #else
        return printImageIfExistsSlowPath(type, printError);
    #endif
}
