#include "icon.h"

#include <assert.h>
#include <cairo.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <stdbool.h>
#include <string.h>
#include <math.h>

#include "log.h"
#include "notification.h"
#include "settings.h"
#include "utils.h"
#include "icon-lookup.h"

/**
 * Reassemble the data parts of a GdkPixbuf into a cairo_surface_t's data field.
 *
 * Requires to call on the surface flush before and mark_dirty after the execution.
 */
static void pixbuf_data_to_cairo_data(
                const unsigned char *pixels_p,
                      unsigned char *pixels_c,
                size_t rowstride_p,
                size_t rowstride_c,
                int width,
                int height,
                int n_channels)
{
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
        static const size_t CAIRO_B = 0;
        static const size_t CAIRO_G = 1;
        static const size_t CAIRO_R = 2;
        static const size_t CAIRO_A = 3;
#elif G_BYTE_ORDER == G_BIG_ENDIAN
        static const size_t CAIRO_A = 0;
        static const size_t CAIRO_R = 1;
        static const size_t CAIRO_G = 2;
        static const size_t CAIRO_B = 3;
#elif G_BYTE_ORDER == G_PDP_ENDIAN
        static const size_t CAIRO_R = 0;
        static const size_t CAIRO_A = 1;
        static const size_t CAIRO_B = 2;
        static const size_t CAIRO_G = 3;
#else
// GLib doesn't support any other endiannesses
#error Unsupported Endianness
#endif

        assert(pixels_p);
        assert(pixels_c);
        assert(width > 0);
        assert(height > 0);

        if (n_channels == 3) {
                for (int h = 0; h < height; h++) {
                              unsigned char *iter_c = pixels_c + h * rowstride_c;
                        const unsigned char *iter_p = pixels_p + h * rowstride_p;
                        for (int w = 0; w < width; w++) {
                                iter_c[CAIRO_R] = iter_p[0];
                                iter_c[CAIRO_G] = iter_p[1];
                                iter_c[CAIRO_B] = iter_p[2];
                                iter_c[CAIRO_A] = 0xff;
                                iter_c += 4;
                                iter_p += n_channels;
                        }
                }
        } else {
                for (int h = 0; h < height; h++) {
                              unsigned char *iter_c = pixels_c + h * rowstride_c;
                        const unsigned char *iter_p = pixels_p + h * rowstride_p;
                        for (int w = 0; w < width; w++) {
                                double alpha_factor = iter_p[3] / (double)0xff;
                                iter_c[CAIRO_R] = (unsigned char)(iter_p[0] * alpha_factor + .5);
                                iter_c[CAIRO_G] = (unsigned char)(iter_p[1] * alpha_factor + .5);
                                iter_c[CAIRO_B] = (unsigned char)(iter_p[2] * alpha_factor + .5);
                                iter_c[CAIRO_A] =                 iter_p[3];
                                iter_c += 4;
                                iter_p += n_channels;
                        }
                }

        }
}

int get_icon_width(cairo_surface_t *icon, double scale) {
        return round(cairo_image_surface_get_width(icon) / scale);
}

int get_icon_height(cairo_surface_t *icon, double scale) {
        return round(cairo_image_surface_get_height(icon) / scale);
}

cairo_surface_t *gdk_pixbuf_to_cairo_surface(GdkPixbuf *pixbuf)
{
        if (!pixbuf) {
                return NULL;
        }

        int width  = gdk_pixbuf_get_width(pixbuf);
        int height = gdk_pixbuf_get_height(pixbuf);

        cairo_format_t fmt = gdk_pixbuf_get_has_alpha(pixbuf) ? CAIRO_FORMAT_ARGB32 : CAIRO_FORMAT_RGB24;
        cairo_surface_t *icon_surface = cairo_image_surface_create(fmt, width, height);

        /* Copy pixel data from pixbuf to surface */
        cairo_surface_flush(icon_surface);
        pixbuf_data_to_cairo_data(gdk_pixbuf_read_pixels(pixbuf),
                                  cairo_image_surface_get_data(icon_surface),
                                  gdk_pixbuf_get_rowstride(pixbuf),
                                  cairo_format_stride_for_width(fmt, width),
                                  gdk_pixbuf_get_width(pixbuf),
                                  gdk_pixbuf_get_height(pixbuf),
                                  gdk_pixbuf_get_n_channels(pixbuf));
        cairo_surface_mark_dirty(icon_surface);

        return icon_surface;
}

/**
 * Scales the given image dimensions if necessary according to the settings.
 *
 * @param w a pointer to the image width, to be modified in-place
 * @param h a pointer to the image height, to be modified in-place
 * @return TRUE if the dimensions were updated, FALSE if they were left unchanged
 */
static bool icon_size_clamp(int *w, int *h) {
        int _w = *w, _h = *h;
        int landscape = _w > _h;
        int orig_larger = landscape ? _w : _h;
        double larger = orig_larger;
        double smaller = landscape ? _h : _w;
        if (settings.min_icon_size && smaller < settings.min_icon_size) {
                larger = larger / smaller * settings.min_icon_size;
                smaller = settings.min_icon_size;
        }
        if (settings.max_icon_size && larger > settings.max_icon_size) {
                smaller = smaller / larger * settings.max_icon_size;
                larger = settings.max_icon_size;
        }
        if ((int) larger != orig_larger) {
                *w = (int) (landscape ? larger : smaller);
                *h = (int) (landscape ? smaller : larger);
                return TRUE;
        }
        return FALSE;
}

static bool icon_size_clamp2(int *w, int *h, int desired_size, double scale) {
        int largest_size = MAX(*w, *h);
        int new_size = desired_size * scale;
        if (largest_size != new_size) {
                double scale = (double) new_size / (double) largest_size;
                *w = round(*w * scale);
                *h = round(*h * scale);
                return true;
        }
        return false;
}
/**
 * Scales the given GdkPixbuf to a given size.. If the image is not square, the
 * largest size will be scaled up to the given size.
 *
 * The icon is scaled to a size of icon_size * dpi_scale.
 *
 * @param pixbuf (nullable) The pixbuf, which may be too big.
 *                          Takes ownership of the reference.
 * @param icon_size An integer the unscaled icon size.
 * @param dpi_scale A double for the dpi scaling.
 * @return the scaled version of the pixbuf. If scaling wasn't
 *         necessary, it returns the same pixbuf. Transfers full
 *         ownership of the reference.
 */
static GdkPixbuf *icon_pixbuf_scale_to_size(GdkPixbuf *pixbuf, int icon_size, double dpi_scale)
{
        ASSERT_OR_RET(pixbuf, NULL);

        int w = gdk_pixbuf_get_width(pixbuf);
        int h = gdk_pixbuf_get_height(pixbuf);
        bool needs_change = false;

        if (settings.enable_recursive_icon_lookup)
        {
                needs_change = icon_size_clamp2(&w, &h, icon_size, dpi_scale);
                if (needs_change) {
                        LOG_D("Scaling to a size of %ix%i", w, h);
                        LOG_D("While the icon size and scale are %ix%f", icon_size, dpi_scale);
                }
        } else {
                // TODO immediately rescale icon upon scale changes
                if(icon_size_clamp(&w, &h)) {
                        w = round(w * dpi_scale);
                        h = round(h * dpi_scale);
                        needs_change = true;
                }
        }
        if (needs_change) {
                GdkPixbuf *scaled = gdk_pixbuf_scale_simple(
                                pixbuf,
                                w,
                                h,
                                GDK_INTERP_BILINEAR);
                g_object_unref(pixbuf);
                pixbuf = scaled;
        }
        return pixbuf;
}

GdkPixbuf *get_pixbuf_from_file(const char *filename, int icon_size, double scale)
{
        char *path = string_to_path(g_strdup(filename));
        GError *error = NULL;
        gint w, h;

        if (!gdk_pixbuf_get_file_info (path, &w, &h)) {
                LOG_W("Failed to load image info for %s", filename);
                g_free(path);
                return NULL;
        }
        GdkPixbuf *pixbuf = NULL;
        if (settings.enable_recursive_icon_lookup)
        {
                icon_size_clamp2(&w, &h, icon_size, scale);
                pixbuf = gdk_pixbuf_new_from_file_at_scale(path,
                                w,
                                h,
                                TRUE,
                                &error);
        } else {
                // TODO immediately rescale icon upon scale changes
                icon_size_clamp(&w, &h);
                pixbuf = gdk_pixbuf_new_from_file_at_scale(path,
                                round(w * scale),
                                round(h * scale),
                                TRUE,
                                &error);
        }

        if (error) {
                LOG_W("%s", error->message);
                g_error_free(error);
        }

        g_free(path);
        return pixbuf;
}

char *get_path_from_icon_name(const char *iconname, int size)
{
        if (settings.enable_recursive_icon_lookup) {
                char *path = find_icon_path(iconname, size);
                LOG_I("Found icon at %s", path);
                return path;
        }
        if (STR_EMPTY(iconname))
                return NULL;

        const char *suffixes[] = { ".svg", ".svgz", ".png", ".xpm", NULL };
        gchar *uri_path = NULL;
        char *new_name = NULL;

        if (g_str_has_prefix(iconname, "file://")) {
                uri_path = g_filename_from_uri(iconname, NULL, NULL);
                if (uri_path)
                        iconname = uri_path;
        }

        /* absolute path? */
        if (iconname[0] == '/' || iconname[0] == '~') {
                new_name = g_strdup(iconname);
        } else {
        /* search in icon_path */
                char *start = settings.icon_path,
                     *end, *current_folder, *maybe_icon_path;
                do {
                        end = strchr(start, ':');
                        if (!end) end = strchr(settings.icon_path, '\0'); /* end = end of string */

                        current_folder = g_strndup(start, end - start);

                        for (const char **suf = suffixes; *suf; suf++) {
                                gchar *name_with_extension = g_strconcat(iconname, *suf, NULL);
                                maybe_icon_path = g_build_filename(current_folder, name_with_extension, NULL);
                                if (is_readable_file(maybe_icon_path)) {
                                        new_name = g_strdup(maybe_icon_path);
                                }
                                g_free(name_with_extension);
                                g_free(maybe_icon_path);

                                if (new_name)
                                        break;
                        }

                        g_free(current_folder);
                        if (new_name)
                                break;

                        start = end + 1;
                } while (STR_FULL(end));
                if (!new_name)
                        LOG_W("No icon found in path: '%s'", iconname);
        }

        g_free(uri_path);
        return new_name;
}

GdkPixbuf *icon_get_for_data(GVariant *data, char **id, double dpi_scale, int icon_size)
{
        ASSERT_OR_RET(data, NULL);
        ASSERT_OR_RET(id, NULL);

        if (!STR_EQ("(iiibiiay)", g_variant_get_type_string(data))) {
                LOG_W("Invalid data for pixbuf given.");
                return NULL;
        }

        /* The raw image is a big array of char data.
         *
         * The image is serialised rowwise pixel by pixel. The rows are aligned
         * by a spacer full of garbage. The overall data length of data + garbage
         * is called the rowstride.
         *
         * Mind the missing spacer at the last row.
         *
         * len:     |<--------------rowstride---------------->|
         * len:     |<-width*pixelstride->|
         * row 1:   |   data for row 1    | spacer of garbage |
         * row 2:   |   data for row 2    | spacer of garbage |
         *          |         .           | spacer of garbage |
         *          |         .           | spacer of garbage |
         *          |         .           | spacer of garbage |
         * row n-1: |   data for row n-1  | spacer of garbage |
         * row n:   |   data for row n    |
         */

        GdkPixbuf *pixbuf = NULL;
        GVariant *data_variant = NULL;
        unsigned char *data_pb;

        gsize len_expected;
        gsize len_actual;
        gsize pixelstride;

        int width;
        int height;
        int rowstride;
        int has_alpha;
        int bits_per_sample;
        int n_channels;

        g_variant_get(data,
                      "(iiibii@ay)",
                      &width,
                      &height,
                      &rowstride,
                      &has_alpha,
                      &bits_per_sample,
                      &n_channels,
                      &data_variant);

        // note: (A+7)/8 rounds up A to the next byte boundary
        pixelstride = (n_channels * bits_per_sample + 7)/8;
        len_expected = (height - 1) * rowstride + width * pixelstride;
        len_actual = g_variant_get_size(data_variant);

        if (len_actual != len_expected) {
                LOG_W("Expected image data to be of length %" G_GSIZE_FORMAT
                      " but got a length of %" G_GSIZE_FORMAT,
                      len_expected,
                      len_actual);
                g_variant_unref(data_variant);
                return NULL;
        }

        // g_memdup is deprecated in glib 2.67.4 and higher.
        // g_memdup2 is a safer alternative
#if GLIB_CHECK_VERSION(2,67,3)
        data_pb = (guchar *) g_memdup2(g_variant_get_data(data_variant), len_actual);
#else
        data_pb = (guchar *) g_memdup(g_variant_get_data(data_variant), len_actual);
#endif

        pixbuf = gdk_pixbuf_new_from_data(data_pb,
                                          GDK_COLORSPACE_RGB,
                                          has_alpha,
                                          bits_per_sample,
                                          width,
                                          height,
                                          rowstride,
                                          (GdkPixbufDestroyNotify) g_free,
                                          data_pb);
        if (!pixbuf) {
                /* Dear user, I'm sorry, I'd like to give you a more specific
                 * error message. But sadly, I can't */
                LOG_W("Cannot serialise raw icon data into pixbuf.");
                return NULL;
        }

        /* To calculate a checksum of the current image, we have to remove
         * all excess spacers, so that our checksummed memory only contains
         * real data. */
        size_t data_chk_len = pixelstride * width * height;
        unsigned char *data_chk = g_malloc(data_chk_len);
        size_t rowstride_short = pixelstride * width;

        for (int i = 0; i < height; i++) {
                memcpy(data_chk + (i*rowstride_short),
                       data_pb  + (i*rowstride),
                       rowstride_short);
        }

        *id = g_compute_checksum_for_data(G_CHECKSUM_MD5, data_chk, data_chk_len);

        g_free(data_chk);
        g_variant_unref(data_variant);

        pixbuf = icon_pixbuf_scale_to_size(pixbuf, icon_size, dpi_scale);

        return pixbuf;
}

/* vim: set ft=c tabstop=8 shiftwidth=8 expandtab textwidth=0: */
