/* $Id: rmilist.c,v 1.43 2006/08/29 22:33:25 rmagick Exp $ */
/*============================================================================\
|                Copyright (C) 2006 by Timothy P. Hunter
| Name:     rmilist.c
| Author:   Tim Hunter
| Purpose:  ImageList class method definitions for RMagick
\============================================================================*/

#include "rmagick.h"


/*
    Method:     ImageList#animate(<delay>)
    Purpose:    repeatedly display the images in the images array to an XWindow
                screen. The "delay" argument is the number of 1/100ths of a
                second (0 to 65535) to delay between images.
*/

VALUE
ImageList_animate(int argc, VALUE *argv, VALUE self)
{
    Image *images;
    Info *info;
    volatile VALUE info_obj;

    if (argc > 1)
    {
        rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 or 1)", argc);
    }

    // Convert the images array to an images sequence.
    images = rm_images_from_imagelist(self);

    if (argc == 1)
    {
        Image *img;
        unsigned int delay;

        delay = NUM2UINT(argv[0]);
        for (img = images; img; img = GetNextImageInList(img))
        {
            img->delay = delay;
        }
    }


    // Create a new Info object to use with this call
    info_obj = rm_info_new();
    Data_Get_Struct(info_obj, Info, info);

    (void) AnimateImages(info, images);
    rm_check_image_exception(images, RetainOnError);
    rm_split(images);

    return self;
}

/*
    Method:     ImageList#append(stack)
    Purpose:    Append all the images by calling ImageAppend
    Returns:    an Frame object for the result
*/
VALUE
ImageList_append(VALUE self, VALUE stack_arg)
{
    Image *images, *new_image;
    unsigned int stack;
    ExceptionInfo exception;

    // Convert the image array to an image sequence.
    images = rm_images_from_imagelist(self);

    // If stack == true, stack rectangular images top-to-bottom,
    // otherwise left-to-right.
    stack = RTEST(stack_arg);

    GetExceptionInfo(&exception);
    new_image = AppendImages(images, stack, &exception);
    rm_split(images);
    rm_check_exception(&exception, new_image, DestroyOnError);
    DestroyExceptionInfo(&exception);

    rm_ensure_result(new_image);

    return rm_image_new(new_image);
}

/*
    Method:     ImageList#average
    Purpose:    Average all images together by calling AverageImages
    Returns:    an Frame object for the averaged image
*/
VALUE
ImageList_average(VALUE self)
{
    Image *images, *new_image;
    ExceptionInfo exception;

    // Convert the images array to an images sequence.
    images = rm_images_from_imagelist(self);

    GetExceptionInfo(&exception);
    new_image = AverageImages(images, &exception);
    rm_split(images);
    rm_check_exception(&exception, new_image, DestroyOnError);
    DestroyExceptionInfo(&exception);

    rm_ensure_result(new_image);

    return rm_image_new(new_image);
}

/*
    Method:     ImageList#coalesce
    Purpose:    call CoalesceImages
    Returns:    a new Image with the coalesced image sequence
                stored in the images array
    Notes:      respects the delay, matte, and start_loop fields
                in each image.
*/
VALUE
ImageList_coalesce(VALUE self)
{
    Image *images, *new_images;
    ExceptionInfo exception;

    // Convert the image array to an image sequence.
    images = rm_images_from_imagelist(self);

    GetExceptionInfo(&exception);
    new_images = CoalesceImages(images, &exception);
    rm_split(images);
    rm_check_exception(&exception, new_images, DestroyOnError);
    DestroyExceptionInfo(&exception);

    rm_ensure_result(new_images);

    return rm_imagelist_from_images(new_images);
}


/*
    Method:     ImageList#deconstruct
    Purpose:    compares each image with the next in a sequence and returns
                the maximum bounding region of any pixel differences it
                discovers.
    Returns:    a new imagelist
*/
VALUE
ImageList_deconstruct(VALUE self)
{
    Image *new_images, *images;
    ExceptionInfo exception;

    images = rm_images_from_imagelist(self);
    GetExceptionInfo(&exception);
    new_images = DeconstructImages(images, &exception);
    rm_split(images);
    rm_check_exception(&exception, new_images, DestroyOnError);
    DestroyExceptionInfo(&exception);

    rm_ensure_result(new_images);

    return rm_imagelist_from_images(new_images);
}

/*
    Method:     ImageList#display
    Purpose:    Display all the images to an X window screen.
*/
VALUE
ImageList_display(VALUE self)
{
    Image *images;
    Info *info;
    volatile VALUE info_obj;

    // Create a new Info object to use with this call
    info_obj = rm_info_new();
    Data_Get_Struct(info_obj, Info, info);

    // Convert the images array to an images sequence.
    images = rm_images_from_imagelist(self);

    (void) DisplayImages(info, images);
    rm_split(images);
    rm_check_image_exception(images, RetainOnError);

    return self;
}

/*
    Method:     ImageList#flatten_images
    Purpose:    merge all the images into a single image
    Returns:    the new image
    Notes:      Can't use "flatten" because that's an Array method
*/
VALUE
ImageList_flatten_images(VALUE self)
{
    Image *images, *new_image;
    ExceptionInfo exception;

    images = rm_images_from_imagelist(self);
    GetExceptionInfo(&exception);
    new_image = FlattenImages(images, &exception);
    rm_split(images);
    rm_check_exception(&exception, new_image, DestroyOnError);
    DestroyExceptionInfo(&exception);

    rm_ensure_result(new_image);

    return rm_image_new(new_image);
}



/*
    Method:     ImageList#map(reference, dither=false)
    Purpose:    Call MapImages
    Returns:    a new ImageList with mapped images. @scene is set to self.scene
*/
VALUE
ImageList_map(int argc, VALUE *argv, VALUE self)
{
    Image *images, *new_images = NULL;
    Image *map;
    unsigned int dither = False;
    volatile VALUE scene, new_imagelist;
    ExceptionInfo exception;

    switch (argc)
    {
        case 2:
            dither = RTEST(argv[1]);
        case 1:
            Data_Get_Struct(ImageList_cur_image(argv[0]), Image, map);
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 1 or 2)", argc);
            break;
    }


    if (rm_imagelist_length(self) == 0)
    {
        rb_raise(rb_eArgError, "no images in this image list");
    }

    // Convert image array to image sequence, clone image sequence.
    GetExceptionInfo(&exception);

    images = rm_images_from_imagelist(self);
    new_images = CloneImageList(images, &exception);
    rm_split(images);
    rm_check_exception(&exception, new_images, DestroyOnError);
    DestroyExceptionInfo(&exception);

    rm_ensure_result(new_images);

    // Call ImageMagick
    (void) MapImages(new_images, map, dither);
    rm_check_image_exception(new_images, DestroyOnError);

    // Set @scene in new ImageList object to same value as in self.
    new_imagelist = rm_imagelist_from_images(new_images);
    scene = rb_iv_get(self, "@scene");
    (void) rm_imagelist_scene_eq(new_imagelist, scene);

    return new_imagelist;
}

/*
    Method:     ImageList#montage <{parm block}>
    Purpose:    Call MontageImages
    Notes:      Creates Montage object, yields to block if present
                in Montage object's scope.
*/
VALUE
ImageList_montage(VALUE self)
{
    volatile VALUE montage_obj;
    Montage *montage;
    Image *new_images, *images;
    ExceptionInfo exception;

    // Create a new instance of the Magick::Montage class
    montage_obj = rm_montage_new();
    if (rb_block_given_p())
    {
        // Run the block in the instance's context, allowing the app to modify the
        // object's attributes.
        rb_obj_instance_eval(0, NULL, montage_obj);
    }

    Data_Get_Struct(montage_obj, Montage, montage);

    images = rm_images_from_imagelist(self);

    // If app specified a non-default composition operator, use it for all images.
    if (montage->compose != UndefinedCompositeOp)
    {
        Image *i;
        for (i = images; i; i = GetNextImageInList(i))
        {
            i->compose = montage->compose;
        }
    }

    GetExceptionInfo(&exception);

    // MontageImage can return more than one image.
    new_images = MontageImages(images, montage->info, &exception);
    rm_split(images);
    rm_check_exception(&exception, new_images, DestroyOnError);
    DestroyExceptionInfo(&exception);

    rm_ensure_result(new_images);

    return rm_imagelist_from_images(new_images);
}

/*
    Method:     ImageList#morph(number_images)
    Purpose:    requires a minimum of two images. The first image is
                transformed into the second by a number of intervening images
                as specified by "number_images".
    Returns:    a new Image with the images array set to the morph sequence.
                @scenes = 0
*/
VALUE
ImageList_morph(VALUE self, VALUE nimages)
{
    Image *images, *new_images;
    ExceptionInfo exception;
    long number_images;

    if (rm_imagelist_length(self) < 1)
    {
        rb_raise(rb_eArgError, "no images in this image list");
    }

    // Use a signed long so we can test for a negative argument.
    number_images = NUM2LONG(nimages);
    if (number_images <= 0)
    {
        rb_raise(rb_eArgError, "number of intervening images must be > 0");
    }

    GetExceptionInfo(&exception);
    images = rm_images_from_imagelist(self);
    new_images = MorphImages(images, (unsigned long)number_images, &exception);
    rm_split(images);
    rm_check_exception(&exception, new_images, DestroyOnError);
    DestroyExceptionInfo(&exception);

    rm_ensure_result(new_images);

    return rm_imagelist_from_images(new_images);
}

/*
    Method:     ImageList#mosaic
    Purpose:    merge all the images into a single image
    Returns:    the new image
*/
VALUE
ImageList_mosaic(VALUE self)
{
    Image *images, *new_image;
    ExceptionInfo exception;

    GetExceptionInfo(&exception);
    images = rm_images_from_imagelist(self);
    new_image = MosaicImages(images, &exception);
    rm_split(images);
    rm_check_exception(&exception, new_image, DestroyOnError);
    DestroyExceptionInfo(&exception);

    rm_ensure_result(new_image);

    return rm_image_new(new_image);
}


/*
    Method:     ImageList#optimize_layers
    Purpose:    Equivalent to -layers option in 6.2.6
    Returns:    a new imagelist
*/
VALUE
ImageList_optimize_layers(VALUE self, VALUE method)
{
#if defined(HAVE_COMPAREIMAGELAYERS)
    Image *images, *new_images;
    MagickLayerMethod mthd;
    ExceptionInfo exception;

    GetExceptionInfo(&exception);
    VALUE_TO_ENUM(method, mthd, MagickLayerMethod);
    images = rm_images_from_imagelist(self);

    switch (mthd)
    {
#if defined(HAVE_COALESCELAYER)
        case CoalesceLayer:
            new_images = CoalesceImages(images, &exception);
            break;
        case DisposeLayer:
            new_images = DisposeImages(images, &exception);
            break;
#endif
        case OptimizeLayer:
            new_images = OptimizeImageLayers(images, &exception);
            break;
        case OptimizePlusLayer:
            new_images = OptimizePlusImageLayers(images, &exception);
            break;
        case CompareAnyLayer:
        case CompareClearLayer:
        case CompareOverlayLayer:
            new_images = CompareImageLayers(images, mthd, &exception);
            break;
        default:
            rb_raise(rb_eArgError, "undefined layer method");
            break;
    }

    rm_split(images);
    rm_check_exception(&exception, new_images, DestroyOnError);
    DestroyExceptionInfo(&exception);

    rm_ensure_result(new_images);

    return rm_imagelist_from_images(new_images);

#else
    rm_not_implemented();
    return (VALUE)0;
#endif
}


/*
    External:   rm_imagelist_new
    Purpose:    create a new ImageList object with no images
    Notes:      this simply calls ImageList.new() in RMagick.rb
*/
VALUE
rm_imagelist_new()
{
    return rb_funcall(Class_ImageList, ID_new, 0);
}



/*
     Extern:   rm_imagelist_from_images
     Purpose:  Construct a new imagelist object from a list of images
     Notes:    Sets @scene to 0.
*/
VALUE
rm_imagelist_from_images(Image *images)
{
     volatile VALUE new_imagelist;
     Image *image;

     if (!images)
     {
         rb_bug("rm_imagelist_from_images called with NULL argument");
     }

     new_imagelist = rm_imagelist_new();

     while (images)
     {
          image = RemoveFirstImageFromList(&images);
          rm_imagelist_push(new_imagelist, rm_image_new(image));
     }

     rb_iv_set(new_imagelist, "@scene", INT2FIX(0));
     return new_imagelist;
}


/*
    Extern:     rm_images_from_imagelist
    Purpose:    Convert an array of Image *s to an ImageMagick scene
                sequence (i.e. a doubly-linked list of Images)
    Returns:    a pointer to the head of the scene sequence list
*/
Image *
rm_images_from_imagelist(VALUE imagelist)
{
    long x, len;
    Image *head = NULL;

    Check_Type(imagelist, T_ARRAY);
    len = rm_imagelist_length(imagelist);
    if (len == 0)
    {
        rb_raise(rb_eArgError, "no images in this image list");
    }

    for (x = 0; x < len; x++)
    {
        Image *image;

        Data_Get_Struct(rb_ary_entry(imagelist, x), Image, image);
        AppendImageToList(&head, image);
    }

    return head;
}


/*
 *   Extern:   rm_imagelist_scene_eq(imagelist, scene)
 *   Purpose:  @scene attribute writer
*/
VALUE
rm_imagelist_scene_eq(VALUE imagelist, VALUE scene)
{
     rm_check_frozen(imagelist);
     rb_iv_set(imagelist, "@scene", scene);
     return scene;
}

/*
    External:   rm_imagelist_length
    Purpose:    return the # of images in an imagelist
*/
int
rm_imagelist_length(VALUE imagelist)
{
    volatile VALUE len;

    len = rb_funcall(imagelist, ID_length, 0);
    return FIX2INT(len);
}

/*
    External:   rm_imagelist_push
    Purpose:    push an image onto the end of the imagelist
*/
void
rm_imagelist_push(VALUE imagelist, VALUE image)
{
    rm_check_frozen(imagelist);
    (void) rb_funcall(imagelist, ID_push, 1, image);
}

/*
    Method:     ImageList#quantize(<number_colors<, colorspace<, dither<, tree_depth<, measure_error>>>>>)
                     defaults: 256, Magick::RGBColorspace, true, 0, false
    Purpose:    call QuantizeImages
    Returns:    a new ImageList with quantized images. 'scene' is set to the same
                value as self.scene
*/
VALUE
ImageList_quantize(int argc, VALUE *argv, VALUE self)
{
    Image *images, *new_images;
    Image *new_image;
    QuantizeInfo quantize_info;
    ExceptionInfo exception;
    volatile VALUE new_imagelist, scene;

    GetQuantizeInfo(&quantize_info);

    switch (argc)
    {
        case 5:
            quantize_info.measure_error = RTEST(argv[4]);
        case 4:
            quantize_info.tree_depth = NUM2INT(argv[3]);
        case 3:
            quantize_info.dither = RTEST(argv[2]);
        case 2:
            VALUE_TO_ENUM(argv[1], quantize_info.colorspace, ColorspaceType);
        case 1:
            quantize_info.number_colors = NUM2INT(argv[0]);
        case 0:
            break;
        default:
            rb_raise(rb_eArgError, "wrong number of arguments (%d for 0 to 5)", argc);
            break;
    }

    if (rm_imagelist_length(self) == 0)
    {
        rb_raise(rb_eArgError, "no images in this image list");
    }

    // Convert image array to image sequence, clone image sequence.
    GetExceptionInfo(&exception);
    images = rm_images_from_imagelist(self);
    new_images = CloneImageList(images, &exception);
    rm_split(images);
    rm_check_exception(&exception, new_images, DestroyOnError);
    DestroyExceptionInfo(&exception);

    rm_ensure_result(new_images);


    QuantizeImages(&quantize_info, new_images);
    rm_check_exception(&exception, new_images, DestroyOnError);

    // Create new ImageList object, convert mapped image sequence to images,
    // append to images array.
    new_imagelist = rm_imagelist_new();
    while ((new_image = ShiftImageList(&new_images)))
    {
        rm_imagelist_push(new_imagelist, rm_image_new(new_image));
    }

    // Set @scene in new ImageList object to same value as in self.
    scene = rb_iv_get(self, "@scene");
    rb_iv_set(new_imagelist, "@scene", scene);

    return new_imagelist;
}

/*
    Method:     ImageList#to_blob
    Purpose:    returns the imagelist as a blob (a String)
    Notes:      runs an info parm block if present - the user can
                specify the image format and depth
*/
VALUE
ImageList_to_blob(VALUE self)
{
    Image *images;
    Info *info;
    volatile VALUE info_obj;
    volatile VALUE blob_str;
    void *blob = NULL;
    size_t length = 0;
    ExceptionInfo exception;

    info_obj = rm_info_new();
    Data_Get_Struct(info_obj, Info, info);

    // Convert the images array to an images sequence.
    images = rm_images_from_imagelist(self);

    GetExceptionInfo(&exception);
    (void) SetImageInfo(info, True, &exception);
    rm_check_exception(&exception, images, RetainOnError);

    if (*info->magick != '\0')
    {
        Image *img;
        for (img = images; img; img = GetNextImageInList(img))
        {
            strncpy(img->magick, info->magick, sizeof(info->magick)-1);
        }
    }

    // Unconditionally request multi-images support. The worst that
    // can happen is that there's only one image or the format
    // doesn't support multi-image files.
    info->adjoin = True;
#if defined(HAVE_IMAGESTOBLOB)
    blob = ImagesToBlob(info, images, &length, &exception);
#else
    blob = ImageToBlob(info, images, &length, &exception);
#endif
    if (exception.severity != UndefinedException)
    {
        magick_free((void*)blob);
    }
    rm_split(images);
    CHECK_EXCEPTION()
    DestroyExceptionInfo(&exception);


    if (length == 0 || !blob)
    {
        return Qnil;
    }

    blob_str = rb_str_new(blob, length);
    magick_free((void*)blob);

    return blob_str;
}


/*
 *  Static:     file_arg_rescue
 *  Purpose:    called when `arg_to_str' raised an exception
*/
static VALUE file_arg_rescue(VALUE arg)
{
    rb_raise(rb_eTypeError, "argument must be path name or open file (%s given)",
            rb_class2name(CLASS_OF(arg)));
}


/*
  Method:   ImageList#write(file)
  Purpose:  Write all the images to the specified file. If the file format
            supports multi-image files, and the @images array contains more
            than one image, then the images will be written as a single
            multi-image file. Otherwise each image will be written to a
            separate file. Returns self.
*/
VALUE
ImageList_write(VALUE self, VALUE file)
{
    Image *images, *img;
    Info *info;
    const MagickInfo *m;
    volatile VALUE info_obj;
    char *filename;
    long filenameL;
    int scene;
    ExceptionInfo exception;

    info_obj = rm_info_new();
    Data_Get_Struct(info_obj, Info, info);


    if (TYPE(file) == T_FILE)
    {
        OpenFile *fptr;

        // Ensure file is open - raise error if not
        GetOpenFile(file, fptr);
        SetImageInfoFile(info, GetReadFile(fptr));
    }
    else
    {
        // Convert arg to string. Catch exceptions.
        file = rb_rescue(rb_String, file, file_arg_rescue, file);

        // Copy the filename to the Info and to the Image.
        filename = STRING_PTR_LEN(file, filenameL);
        filenameL = min(filenameL, MaxTextExtent-1);
        memcpy(info->filename, filename, (size_t)filenameL);
        info->filename[filenameL] = '\0';
        SetImageInfoFile(info, NULL);
    }

    // Convert the images array to an images sequence.
    images = rm_images_from_imagelist(self);

    // Copy the filename into each images. Set a scene number to be used if
    // writing multiple files. (Ref: ImageMagick's utilities/convert.c
    for (scene = 0, img = images; img; img = GetNextImageInList(img))
    {
        img->scene = scene++;
        strcpy(img->filename, info->filename);
    }

    GetExceptionInfo(&exception);
    (void) SetImageInfo(info, True, &exception);
    rm_check_exception(&exception, images, RetainOnError);
    DestroyExceptionInfo(&exception);

    // Find out if the format supports multi-images files.
    GetExceptionInfo(&exception);
    m = GetMagickInfo(info->magick, &exception);
    rm_check_exception(&exception, images, RetainOnError);
    DestroyExceptionInfo(&exception);

    // Tell WriteImage if we want a multi-images file.
    if (rm_imagelist_length(self) > 1 && m->adjoin)
    {
        info->adjoin = True;
    }

    for (img = images; img; img = GetNextImageInList(img))
    {
        (void) WriteImage(info, img);
        // images will be split before raising an exception
        rm_check_image_exception(images, RetainOnError);
        if (info->adjoin)
        {
            break;
        }
    }

    rm_split(images);
    return self;
}
