// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under    *
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0          *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************

#include "icon_loader.h"
#include <zen/thread.h> //includes <std/thread.hpp>

    #include <gtk/gtk.h>
    #include <sys/stat.h>
    #include <zen/sys_error.h>
    #include <zen/basic_math.h>
    #include <xBRZ/src/xbrz_tools.h>


using namespace zen;
using namespace fff;


namespace
{
ImageHolder copyToImageHolder(const GdkPixbuf& pixBuf, int maxSize) //throw SysError
{
    //see: https://developer.gnome.org/gdk-pixbuf/stable/gdk-pixbuf-The-GdkPixbuf-Structure.html
    if (const GdkColorspace cs = ::gdk_pixbuf_get_colorspace(&pixBuf);
        cs != GDK_COLORSPACE_RGB)
        throw SysError(formatSystemError("gdk_pixbuf_get_colorspace", L"", L"Unexpected color space: " + numberTo<std::wstring>(static_cast<int>(cs))));

    if (const int bitCount = ::gdk_pixbuf_get_bits_per_sample(&pixBuf);
        bitCount != 8)
        throw SysError(formatSystemError("gdk_pixbuf_get_bits_per_sample", L"", L"Unexpected bits per sample: " + numberTo<std::wstring>(bitCount)));

    const int channels = ::gdk_pixbuf_get_n_channels(&pixBuf);
    if (channels != 3 && channels != 4)
        throw SysError(formatSystemError("gdk_pixbuf_get_n_channels", L"", L"Unexpected number of channels: " + numberTo<std::wstring>(channels)));

    assert(::gdk_pixbuf_get_has_alpha(&pixBuf) == (channels == 4));

    const unsigned char* srcBytes = ::gdk_pixbuf_read_pixels(&pixBuf);
    const int srcWidth  = ::gdk_pixbuf_get_width (&pixBuf);
    const int srcHeight = ::gdk_pixbuf_get_height(&pixBuf);
    const int srcStride = ::gdk_pixbuf_get_rowstride(&pixBuf);

    //don't stretch small images, shrink large ones only!
    int targetWidth  = srcWidth;
    int targetHeight = srcHeight;

    const int maxExtent = std::max(targetWidth, targetHeight);
    if (maxSize < maxExtent)
    {
        targetWidth  = numeric::intDivRound(targetWidth  * maxSize, maxExtent);
        targetHeight = numeric::intDivRound(targetHeight * maxSize, maxExtent);
    }
    ImageHolder imgOut(targetWidth, targetHeight, true /*withAlpha*/);
    unsigned char* rgbOut   = imgOut.getRgb();
    unsigned char* alphaOut = imgOut.getAlpha();

    if (srcWidth  != targetWidth ||
        srcHeight != targetHeight)
    {
        const auto pixRead = [srcBytes, srcStride, channels](int x, int y)
        {
            const unsigned char* const ptr = srcBytes + y * srcStride + channels * x; //RGB(A) byte order

            const int a = channels == 4 ? ptr[3] : 255;

            return [a, ptr](int channel)
            {
                if (channel == 3)
                    return a;

                return ptr[channel] * a;
                //Limitation: alpha should be applied in gamma-decoded linear RGB space: https://ssp.impulsetrain.com/gamma-premult.html
            };
        };

        const auto pixWrite = [rgbOut, alphaOut](const auto& interpolate) mutable
        {
            const double a = interpolate(3);
            if (a <= 0.0)
            {
                *alphaOut++ = 0;
                rgbOut += 3; //don't care about color
            }
            else
            {
                *alphaOut++ = xbrz::byteRound(a);
                *rgbOut++   = xbrz::byteRound(interpolate(0) / a); //r
                *rgbOut++   = xbrz::byteRound(interpolate(1) / a); //g
                *rgbOut++   = xbrz::byteRound(interpolate(2) / a); //b
            }
        };
        xbrz::bilinearScale(pixRead,       //PixReader pixRead
                            srcWidth,      //int srcWidth
                            srcHeight,     //int srcHeight
                            pixWrite,      //PixWriter pixWrite
                            targetWidth,   //int trgWidth
                            targetHeight,  //int trgHeight
                            0,             //int yFirst
                            targetHeight); //int yLast
#if 0 //alternative: but does it support alpha-channel?
        GdkPixbuf* pixBufShrinked = ::gdk_pixbuf_scale_simple(pixBuf,               //const GdkPixbuf* src
                                                              targetWidth,          //int dest_width
                                                              targetHeight,         //int dest_height
                                                              GDK_INTERP_BILINEAR); //GdkInterpType interp_type
        if (!pixBufShrinked)
            throw SysError(formatSystemError("gdk_pixbuf_scale_simple", L"", L"Not enough memory."));
        ZEN_ON_SCOPE_EXIT(::g_object_unref(pixBufShrinked));
#endif
    }
    else //perf: going overboard?
        for (int y = 0; y < srcHeight; ++y)
            for (int x = 0; x < srcWidth; ++x)
            {
                const unsigned char* const ptr = srcBytes + y * srcStride + channels * x; //RGB(A) byte order

                *alphaOut++ = channels == 4 ? ptr[3] : 255;
                *rgbOut++   = ptr[0];
                *rgbOut++   = ptr[1];
                *rgbOut++   = ptr[2];
            }

    return imgOut;
}


ImageHolder imageHolderFromGicon(GIcon& gicon, int maxSize) //throw SysError
{
    assert(runningOnMainThread()); //GTK is NOT thread safe!!!
    assert(!G_IS_FILE_ICON(&gicon) && !G_IS_LOADABLE_ICON(&gicon)); //see comment in image_holder.h => icon loading must not block main thread

    GtkIconTheme* const defaultTheme = ::gtk_icon_theme_get_default(); //not owned!
    ASSERT_SYSERROR(defaultTheme); //no more error details

    GtkIconInfo* const iconInfo = ::gtk_icon_theme_lookup_by_gicon(defaultTheme,                 //GtkIconTheme* icon_theme
                                                                   &gicon,                       //GIcon* icon
                                                                   maxSize,                      //gint size
                                                                   GTK_ICON_LOOKUP_USE_BUILTIN); //GtkIconLookupFlags flags
    if (!iconInfo)
        throw SysError(formatSystemError("gtk_icon_theme_lookup_by_gicon", L"", L"Icon not available."));
    ZEN_ON_SCOPE_EXIT(::g_object_unref(iconInfo));

    GError* error = nullptr;
    ZEN_ON_SCOPE_EXIT(if (error) ::g_error_free(error));

    GdkPixbuf* const pixBuf = ::gtk_icon_info_load_icon(iconInfo, &error);
    if (!pixBuf)
        throw SysError(formatGlibError("gtk_icon_info_load_icon", error));
    ZEN_ON_SCOPE_EXIT(::g_object_unref(pixBuf));

    //we may have to shrink (e.g. openSUSE + GTK3): "an icon theme may have icons that differ slightly from their nominal sizes"
    return copyToImageHolder(*pixBuf, maxSize); //throw SysError
}
}


FileIconHolder fff::getIconByTemplatePath(const Zstring& templatePath, int maxSize) //throw SysError
{
    //uses full file name, e.g. "AUTHORS" has own mime type on Linux:
    gchar* const contentType = ::g_content_type_guess(templatePath.c_str(), //const gchar* filename
                                                      nullptr,              //const guchar* data
                                                      0,                    //gsize data_size
                                                      nullptr);             //gboolean* result_uncertain
    if (!contentType)
        throw SysError(formatSystemError("g_content_type_guess(" + copyStringTo<std::string>(templatePath) + ')', L"", L"Unknown content type."));
    ZEN_ON_SCOPE_EXIT(::g_free(contentType));

    GIcon* const fileIcon = ::g_content_type_get_icon(contentType);
    if (!fileIcon)
        throw SysError(formatSystemError("g_content_type_get_icon(" + std::string(contentType) + ')', L"", L"Icon not available."));

    return FileIconHolder(fileIcon /*pass ownership*/, maxSize);

}


FileIconHolder fff::genericFileIcon(int maxSize) //throw SysError
{
    //we're called by getDisplayIcon()! -> avoid endless recursion!
    GIcon* const fileIcon = ::g_content_type_get_icon("text/plain");
    if (!fileIcon)
        throw SysError(formatSystemError("g_content_type_get_icon(text/plain)", L"", L"Icon not available."));

    return FileIconHolder(fileIcon /*pass ownership*/, maxSize);

}


FileIconHolder fff::genericDirIcon(int maxSize) //throw SysError
{
    GIcon* const dirIcon = ::g_content_type_get_icon("inode/directory"); //should contain fallback to GTK_STOCK_DIRECTORY ("gtk-directory")
    if (!dirIcon)
        throw SysError(formatSystemError("g_content_type_get_icon(inode/directory)", L"", L"Icon not available."));

    return FileIconHolder(dirIcon /*pass ownership*/, maxSize);

}


FileIconHolder fff::getTrashIcon(int maxSize) //throw SysError
{
    GIcon* const trashIcon = ::g_themed_icon_new("user-trash-full"); //empty: "user-trash"
    if (!trashIcon)
        throw SysError(formatSystemError("g_themed_icon_new(user-trash-full)", L"", L"Icon not available."));

    return FileIconHolder(trashIcon /*pass ownership*/, maxSize);

}


FileIconHolder fff::getFileManagerIcon(int maxSize) //throw SysError
{
    GIcon* const trashIcon = ::g_themed_icon_new("system-file-manager");
    if (!trashIcon)
        throw SysError(formatSystemError("g_themed_icon_new(system-file-manager)", L"", L"Icon not available."));

    return FileIconHolder(trashIcon /*pass ownership*/, maxSize);

}


FileIconHolder fff::getFileIcon(const Zstring& filePath, int maxSize) //throw SysError
{
    GFile* file = ::g_file_new_for_path(filePath.c_str()); //documented to "never fail"
    ZEN_ON_SCOPE_EXIT(::g_object_unref(file));

    GError* error = nullptr;
    ZEN_ON_SCOPE_EXIT(if (error) ::g_error_free(error));

    GFileInfo* const fileInfo = ::g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_ICON, G_FILE_QUERY_INFO_NONE, nullptr /*cancellable*/, &error);
    if (!fileInfo)
        throw SysError(formatGlibError("g_file_query_info", error));
    ZEN_ON_SCOPE_EXIT(::g_object_unref(fileInfo));

    GIcon* const gicon = ::g_file_info_get_icon(fileInfo); //no ownership transfer!
    if (!gicon)
        throw SysError(formatSystemError("g_file_info_get_icon", L"", L"Icon not available."));

    //https://github.com/GNOME/gtk/blob/master/gtk/gtkicontheme.c#L4082
    if (G_IS_FILE_ICON(gicon) || G_IS_LOADABLE_ICON(gicon)) //see comment in image_holder.h
        throw SysError(L"Icon loading might block main thread.");
    //shouldn't be a problem for native file systems -> G_IS_THEMED_ICON(gicon)

    //the remaining icon types won't block!
    assert(GDK_IS_PIXBUF(gicon) || G_IS_THEMED_ICON(gicon) || G_IS_EMBLEMED_ICON(gicon));

    g_object_ref(gicon); /*macro!*/        //pass ownership
    return FileIconHolder(gicon, maxSize); //

}


ImageHolder fff::getThumbnailImage(const Zstring& filePath, int maxSize) //throw SysError
{
    struct stat fileInfo = {};
    if (::stat(filePath.c_str(), &fileInfo) != 0)
        THROW_LAST_SYS_ERROR("stat");

    if (!S_ISREG(fileInfo.st_mode)) //skip blocking file types, e.g. named pipes, see file_io.cpp
        throw SysError(_("Unsupported item type.") + L" [" + printNumber<std::wstring>(L"0%06o", fileInfo.st_mode & S_IFMT) + L']');

    GError* error = nullptr;
    ZEN_ON_SCOPE_EXIT(if (error) ::g_error_free(error));

    GdkPixbuf* const pixBuf = ::gdk_pixbuf_new_from_file(filePath.c_str(), &error);
    if (!pixBuf)
        throw SysError(formatGlibError("gdk_pixbuf_new_from_file", error));
    ZEN_ON_SCOPE_EXIT(::g_object_unref(pixBuf));

    return copyToImageHolder(*pixBuf, maxSize); //throw SysError

}


wxImage fff::extractWxImage(ImageHolder&& ih)
{
    assert(runningOnMainThread());
    if (!ih.getRgb())
        return wxNullImage;

    wxImage img(ih.getWidth(), ih.getHeight(), ih.releaseRgb(), false /*static_data*/); //pass ownership
    if (ih.getAlpha())
        img.SetAlpha(ih.releaseAlpha(), false /*static_data*/);
    else
    {
        assert(false);
        img.SetAlpha();
        ::memset(img.GetAlpha(), wxIMAGE_ALPHA_OPAQUE, ih.getWidth() * ih.getHeight());
    }
    return img;
}


wxImage fff::extractWxImage(zen::FileIconHolder&& fih)
{
    assert(runningOnMainThread());

    wxImage img;
    if (GIcon* gicon = fih.gicon.get())
        try
        {
            img = extractWxImage(imageHolderFromGicon(*gicon, fih.maxSize)); //throw SysError
        }
        catch (SysError&) {} //might fail if icon theme is missing a MIME type!

    fih.gicon.reset();
    return img;

}
