#include <stdio.h>
#include <gdk-pixbuf/gdk-pixbuf.h>

//maximum number of frames in animation(before repeating)
#define MAX_NUMBER_FRAMES 1000

#define IMAGE_TYPE "gif"
#define IMAGE_FILENAME "aero.gif"

/*
 * Auxiliary function, returns numeric representation of pixel.
 * For RGB format it is rrggbb(in hex), for RGBA - rrggbbaa.
 */
guint32 get_pixel(GdkPixbuf* pixbuf, int x, int y)
{
    guchar* colors = ((guchar*)gdk_pixbuf_get_pixels(pixbuf)
                + gdk_pixbuf_get_n_channels(pixbuf) * x
                + gdk_pixbuf_get_rowstride(pixbuf) * y);
    
    guint32 pixel = (colors[0] << 16) | (colors[1] << 8) | colors[2];
    if(gdk_pixbuf_get_n_channels(pixbuf) == 4) 
        pixel = (pixel << 8) | colors[3];
                 
    return pixel;
}
/*
 * Verify, whether all pixels outside area, given via x, y, width, height,
 * have same colors in pixbuf_old and pixbuf_new.
 */
gboolean pixbuf_not_changed_outside_area(
    GdkPixbuf* pixbuf_new, GdkPixbuf* pixbuf_old,
    gint x, gint y, gint width, gint height)
{

    gint pixbuf_width = gdk_pixbuf_get_width(pixbuf_new);
    gint pixbuf_height = gdk_pixbuf_get_height(pixbuf_new);

    int x_curr, y_curr;    
    for(x_curr = 0; x_curr < pixbuf_width; x_curr++)
        for(y_curr = 0; y_curr < pixbuf_height; y_curr++)
    {
        if((x_curr >= x)
            && (x_curr < x+width)
            && (y_curr >= y)
            && (y_curr < y+height))
            continue;//inside area - doesn't compare pixels.
        if(get_pixel(pixbuf_new, x_curr, y_curr)
             != get_pixel(pixbuf_old, x_curr, y_curr))
        {
            printf("Pixel at (%d, %d) changed from %x to %x,\n",
                x_curr, y_curr,
                get_pixel(pixbuf_old, x_curr, y_curr),
                get_pixel(pixbuf_new, x_curr, y_curr));
            printf("But it is outside of area with ");
            printf("x=%d, y=%d, width=%d, height=%d\n",
                x, y, width, height);

            return FALSE;
        }
    }
    return TRUE;
}
/*
 * Information about currently loading frame of animation.
 * 
 * pixbuf==NULL means that animation is fully loaded
 * (no currently loading frames exist).
 */
typedef struct
{
    GdkPixbufAnimationIter* iter;//iterator pointed to the frame
    GTimeVal time;//time when this frame appears
    GdkPixbuf* pixbuf;//current content of the frame
}FrameData;

//Auxiliary function - look for frame is currently loading.
void update_currently_loaded_frame(FrameData* frame)
{
    if(gdk_pixbuf_animation_iter_on_currently_loading_frame(frame->iter))
        return;//frame is currently being loaded
    //clear old content of pixbuf
    if(frame->pixbuf) g_object_unref(frame->pixbuf);
    frame->pixbuf = NULL;

    int tmp_count = 0;
    do
    {
        if(++tmp_count > MAX_NUMBER_FRAMES)
        {
            //protection against frames repeating
            return;
        }
        int delay_time = gdk_pixbuf_animation_iter_get_delay_time(frame->iter);
        if(delay_time < 0)
        {
            //this is last frame in the animation
            return;
        }
        g_time_val_add(&frame->time, delay_time * 1000);
        gdk_pixbuf_animation_iter_advance(frame->iter, &frame->time);
    }while(!gdk_pixbuf_animation_iter_on_currently_loading_frame(frame->iter));
    //store current content of the frame
    frame->pixbuf = 
        gdk_pixbuf_copy(gdk_pixbuf_animation_iter_get_pixbuf(frame->iter));
}

void callback_area_updated(GdkPixbufLoader* loader, int x, int y, int width, int height,
    FrameData* frame_old)
{
    static gboolean signal_correct = TRUE;    
    //if(y + height < gdk_pixbuf_get_height(frame_old->pixbuf) - 1) height++;    
    if(!signal_correct)return;//error already found
    if(frame_old->pixbuf == NULL)
    {
        printf("\"area-updated\" signal was emitted after animation had fully loaded.\n");
        return;
    }
    
    GdkPixbuf* pixbuf_new = gdk_pixbuf_animation_iter_get_pixbuf(frame_old->iter);
    if(!pixbuf_not_changed_outside_area(pixbuf_new, frame_old->pixbuf, x, y, width, height))
    {
        signal_correct = FALSE;
        //correct area parameters(for cover full pixbuf) for copy frame content
        x = 0;
        y = 0;
        width = gdk_pixbuf_get_width(pixbuf_new);
        height = gdk_pixbuf_get_height(pixbuf_new);
    }
    gdk_pixbuf_copy_area(pixbuf_new, x, y, width, height, frame_old->pixbuf, x, y);

    update_currently_loaded_frame(frame_old);
}

//free resourses used in callback_area_updated
void callback_closed(GdkPixbufLoader* loader, FrameData* frame_copy)
{
    g_object_unref(frame_copy->iter);
    if(frame_copy->pixbuf != NULL) g_object_unref(frame_copy->pixbuf);
    g_free(frame_copy);
}
//prepare frame information and register other callbacks
void callback_area_prepared(GdkPixbufLoader* loader)
{
    FrameData* frame_copy = g_new(FrameData, 1);

    g_signal_connect(loader, "area-prepared",
            (GCallback) callback_area_prepared, (gpointer)frame_copy);
    g_signal_connect(loader, "area-updated",
            (GCallback) callback_area_updated, (gpointer)frame_copy);

    frame_copy->time.tv_sec = frame_copy->time.tv_usec = 0;//some time
    
    GdkPixbufAnimation* anim = gdk_pixbuf_loader_get_animation(loader);
    frame_copy->iter = gdk_pixbuf_animation_get_iter(anim, &frame_copy->time);
    frame_copy->pixbuf = 
        gdk_pixbuf_copy(gdk_pixbuf_animation_iter_get_pixbuf(frame_copy->iter));
    update_currently_loaded_frame(frame_copy);
}

/*
 * Reads count_bytes from the channel and write them to the loader.
 * Returns number of bytes written.
 * count_bytes = G_MAXSIZE means read as many bytes as possible.  
 */
gsize loader_write_from_channel(GdkPixbufLoader* loader, GIOChannel *channel, gsize count_bytes)
{
    guchar* buffer = NULL;
    gsize bytes_read = 0;
    GIOStatus read_status;
    if(count_bytes < G_MAXSIZE)
    { 
        //read no more than 'count_bytes' bytes
        buffer = g_malloc(count_bytes);
        read_status = 
            g_io_channel_read_chars(channel, (gchar*)buffer, count_bytes, &bytes_read, NULL);
    }
    else
    {
        //read up to end
        read_status = 
            g_io_channel_read_to_end(channel, (gchar**)&buffer, &bytes_read, NULL);
    }
    if((read_status != G_IO_STATUS_NORMAL) && (read_status != G_IO_STATUS_EOF))
    {
        printf("Cannot read bytes from channel.\n");
        g_free(buffer);
        return 0;    
    }
    
    if(!gdk_pixbuf_loader_write(loader, buffer, bytes_read, NULL))
    {
        printf("gdk_pixbuf_loader_write() returned FALSE.\n");
        g_free(buffer);
        return 0;
    }
    g_free(buffer);
    return bytes_read;
}


int main()
{
    g_type_init();
    //create channel from IMAGE_FILENAME
    GIOChannel* channel = g_io_channel_new_file(IMAGE_FILENAME, "r", NULL);
    if(channel == NULL)
    {
        printf("Cannot open file '%s' for read.\n", IMAGE_FILENAME);
        return 1;
    }
    g_io_channel_set_encoding(channel, NULL, NULL);
    //create loader
    //GdkPixbufLoader* loader = gdk_pixbuf_loader_new();    
    GdkPixbufLoader* loader = gdk_pixbuf_loader_new_with_type(IMAGE_TYPE, NULL);
    if(loader == NULL)
    {
        printf("Cannot create loader of type '%s'.\n", IMAGE_TYPE);
        g_io_channel_unref(channel);
        return 1;
    }

    g_signal_connect(loader, "area-prepared",
            (GCallback) callback_area_prepared, NULL);
    //other callbacks will be registered inside callback_area_prepared

    //read animation by portions of bytes    
    //while(loader_write_from_channel(loader, channel, 20) == 20);
    //or read it at once
    loader_write_from_channel(loader, channel, G_MAXSIZE);

    //free resourses
    g_io_channel_unref(channel);

    gdk_pixbuf_loader_close(loader, NULL);
    g_object_unref(loader);

    return 0;
}
