/* $Id: rmfill.c,v 1.17.2.1 2007/02/04 13:16:22 rmagick Exp $ */
/*============================================================================\
|                Copyright (C) 2007 by Timothy P. Hunter
| Name:     rmfill.c
| Author:   Tim Hunter
| Purpose:  GradientFill, TextureFill class definitions for RMagick
\============================================================================*/

#include "rmagick.h"

typedef struct
{
    double x1, y1, x2, y2;
    PixelPacket start_color;
    PixelPacket stop_color;
} rm_GradientFill;

typedef struct
{
    Image *texture;
} rm_TextureFill;

/*
    Static:     free_Fill
    Purpose:    free Fill or Fill subclass object (except for
                TextureFill)
*/
static void free_Fill(void *fill)
{
    xfree(fill);
}

/*
    Extern:     GradientFill.new(x1, y1, x2, y2, start_color, stop_color)
    Purpose:    Create new GradientFill object
*/
#if !defined(HAVE_RB_DEFINE_ALLOC_FUNC)
VALUE
GradientFill_new(
    VALUE class,
    VALUE x1,
    VALUE y1,
    VALUE x2,
    VALUE y2,
    VALUE start_color,
    VALUE stop_color)
{
    rm_GradientFill *fill;
    volatile VALUE new_fill;
    VALUE argv[6];

    argv[0] = x1;
    argv[1] = y1;
    argv[2] = x2;
    argv[3] = y2;
    argv[4] = start_color;
    argv[5] = stop_color;

    new_fill = Data_Make_Struct(class
                              , rm_GradientFill
                              , NULL
                              , free_Fill
                              , fill);

    rb_obj_call_init((VALUE)new_fill, 6, argv);
    return new_fill;
}
#else
VALUE
GradientFill_alloc(VALUE class)
{
    rm_GradientFill *fill;

    return Data_Make_Struct(class, rm_GradientFill, NULL, free_Fill, fill);
}
#endif

/*
    Extern:     GradientFill#initialize(start_color, stop_color)
    Purpose:    store the vector points and the start and stop colors
*/
VALUE
GradientFill_initialize(
    VALUE self,
    VALUE x1,
    VALUE y1,
    VALUE x2,
    VALUE y2,
    VALUE start_color,
    VALUE stop_color)
{
    rm_GradientFill *fill;

    Data_Get_Struct(self, rm_GradientFill, fill);

    fill->x1 = NUM2DBL(x1);
    fill->y1 = NUM2DBL(y1);
    fill->x2 = NUM2DBL(x2);
    fill->y2 = NUM2DBL(y2);
    Color_to_PixelPacket(&fill->start_color, start_color);
    Color_to_PixelPacket(&fill->stop_color, stop_color);

    return self;
}

/*
    Static:     point_fill
    Purpose:    do a gradient that radiates from a point
*/
static void
point_fill(
    Image *image,
    double x0,
    double y0,
    PixelPacket *start_color,
    PixelPacket *stop_color)
{
    double steps, distance;
    unsigned long x, y;
    double red_step, green_step, blue_step;

    steps = sqrt((double)((image->columns-x0)*(image->columns-x0)
                + (image->rows-y0)*(image->rows-y0)));
    red_step = ((double)(stop_color->red - start_color->red)) / steps;
    green_step = ((double)(stop_color->green - start_color->green)) / steps;
    blue_step = ((double)(stop_color->blue - start_color->blue)) / steps;

    for (y = 0; y < image->rows; y++)
    {
        PixelPacket *row_pixels;

        if (!(row_pixels = SetImagePixels(image, 0, (long int)y, image->columns, 1)))
        {
            rm_check_image_exception(image, RetainOnError);
        }
        for (x = 0; x < image->columns; x++)
        {
            distance = sqrt((double)((x-x0)*(x-x0)+(y-y0)*(y-y0)));
            row_pixels[x].red     = ROUND_TO_QUANTUM(start_color->red   + (distance * red_step));
            row_pixels[x].green   = ROUND_TO_QUANTUM(start_color->green + (distance * green_step));
            row_pixels[x].blue    = ROUND_TO_QUANTUM(start_color->blue  + (distance * blue_step));
            row_pixels[x].opacity = OpaqueOpacity;
        }
        if (!SyncImagePixels(image))
        {
            rm_check_image_exception(image, RetainOnError);
        }
    }
}

/*
    Static:     vertical_fill
    Purpose:    do a gradient fill that proceeds from a vertical line to the
                right and left sides of the image
*/
static void
vertical_fill(
    Image *image,
    double x1,
    PixelPacket *start_color,
    PixelPacket *stop_color)
{
    double steps;
    unsigned long x, y;
    volatile PixelPacket *master;
    double red_step, green_step, blue_step;

    // Keep in mind that x1 could be < 0 or > image->columns. If steps
    // is negative, swap the start and end colors and use the absolute
    // value.
    steps = FMAX(x1, ((long)image->columns)-x1);
    if (steps < 0)
    {
        PixelPacket t = *start_color;
        *start_color = *stop_color;
        *stop_color = t;
        steps = -steps;
    }

    // If x is to the left of the x-axis, add that many steps so that
    // the color at the right side will be that many steps away from
    // the stop color.
    if (x1 < 0)
    {
        steps -= x1;
    }

    red_step = (stop_color->red - start_color->red) / steps;
    green_step = (stop_color->green - start_color->green) / steps;
    blue_step = (stop_color->blue - start_color->blue) / steps;

    // All the rows are the same. Make a "master row" and simply copy
    // it to each actual row.
    master = ALLOC_N(PixelPacket, image->columns);

    for (x = 0; x < image->columns; x++)
    {
        double distance   = fabs(x1 - x);
        master[x].red     = ROUND_TO_QUANTUM(start_color->red   + (red_step * distance));
        master[x].green   = ROUND_TO_QUANTUM(start_color->green + (green_step * distance));
        master[x].blue    = ROUND_TO_QUANTUM(start_color->blue  + (blue_step * distance));
        master[x].opacity = OpaqueOpacity;
    }

    // Now copy the master row to each actual row.
    for (y = 0; y < image->rows; y++)
    {
        PixelPacket *row_pixels;

        if (!(row_pixels = SetImagePixels(image, 0, (long int)y, image->columns, 1)))
        {
            rm_check_image_exception(image, RetainOnError);
        }

        memcpy(row_pixels, (PixelPacket *)master, image->columns * sizeof(PixelPacket));
        if (!SyncImagePixels(image))
        {
            rm_check_image_exception(image, RetainOnError);
        }
    }

    xfree((PixelPacket *)master);
}

/*
    Static:     horizontal_fill
    Purpose:    do a gradient fill that starts from a horizontal line
*/
static void
horizontal_fill(
    Image *image,
    double y1,
    PixelPacket *start_color,
    PixelPacket *stop_color)
{
    double steps;
    unsigned long x, y;
    volatile PixelPacket *master;
    double red_step, green_step, blue_step;

    // Bear in mind that y1 could be < 0 or > image->rows. If steps is
    // negative, swap the start and end colors and use the absolute value.
    steps = FMAX(y1, ((long)image->rows)-y1);
    if (steps < 0)
    {
        PixelPacket t = *start_color;
        *start_color = *stop_color;
        *stop_color = t;
        steps = -steps;
    }

    // If the line is below the y-axis, add that many steps so the color
    // at the bottom of the image is that many steps away from the stop color
    if (y1 < 0)
    {
        steps -= y1;
    }

    red_step = (stop_color->red - start_color->red) / steps;
    green_step = (stop_color->green - start_color->green) / steps;
    blue_step = (stop_color->blue - start_color->blue) / steps;

    // All the columns are the same, so make a master column and copy it to
    // each of the "real" columns.
    master = ALLOC_N(volatile PixelPacket, image->rows);

    for (y = 0; y < image->rows; y++)
    {
        double distance   = fabs(y1 - y);
        master[y].red     = ROUND_TO_QUANTUM(start_color->red   + (distance * red_step));
        master[y].green   = ROUND_TO_QUANTUM(start_color->green + (distance * green_step));
        master[y].blue    = ROUND_TO_QUANTUM(start_color->blue  + (distance * blue_step));
        master[y].opacity = OpaqueOpacity;
    }

    for (x = 0; x < image->columns; x++)
    {
        PixelPacket *col_pixels;

        if (!(col_pixels = SetImagePixels(image, (long int)x, 0, 1, image->rows)))
        {
            rm_check_image_exception(image, RetainOnError);
        }
        memcpy(col_pixels, (PixelPacket *)master, image->rows * sizeof(PixelPacket));
        if (!SyncImagePixels(image))
        {
            rm_check_image_exception(image, RetainOnError);
        }
    }

    xfree((PixelPacket *)master);
}

/*
    Static:     v_diagonal_fill
    Purpose:    do a gradient fill that starts from a diagonal line and
                ends at the top and bottom of the image
*/
static void
v_diagonal_fill(
    Image *image,
    double x1,
    double y1,
    double x2,
    double y2,
    PixelPacket *start_color,
    PixelPacket *stop_color)
{
    unsigned long x, y;
    double red_step, green_step, blue_step;
    double m, b, steps = 0.0;
    double d1, d2;

    // Compute the equation of the line: y=mx+b
    m = ((double)(y2 - y1))/((double)(x2 - x1));
    b = y1 - (m * x1);

    // The number of steps is the greatest distance between the line and
    // the top or bottom of the image between x=0 and x=image->columns
    // When x=0, y=b. When x=image->columns, y = m*image->columns+b
    d1 = b;
    d2 = m * image->columns + b;

    if (d1 < 0 && d2 < 0)
    {
        steps += FMAX(fabs(d1),fabs(d2));
    }
    else if (d1 > (double)image->rows && d2 > (double)image->rows)
    {
        steps += FMAX(d1-image->rows, d2-image->rows);
    }

    d1 = FMAX(b, image->rows-b);
    d2 = FMAX(d2, image->rows-d2);
    steps += FMAX(d1, d2);

    // If the line is entirely > image->rows, swap the start & end color
    if (steps < 0)
    {
        PixelPacket t = *stop_color;
        *stop_color = *start_color;
        *start_color = t;
        steps = -steps;
    }

    red_step = (stop_color->red - start_color->red) / steps;
    green_step = (stop_color->green - start_color->green) / steps;
    blue_step = (stop_color->blue - start_color->blue) / steps;

    for (y = 0; y < image->rows; y++)
    {
        PixelPacket *row_pixels;

        if (!(row_pixels = SetImagePixels(image, 0, (long int)y, image->columns, 1)))
        {
            rm_check_image_exception(image, RetainOnError);
        }
        for (x = 0; x < image->columns; x++)
        {
            double distance = (double) abs((int)(y-(m * x + b)));
            row_pixels[x].red     = ROUND_TO_QUANTUM(start_color->red   + (distance * red_step));
            row_pixels[x].green   = ROUND_TO_QUANTUM(start_color->green + (distance * green_step));
            row_pixels[x].blue    = ROUND_TO_QUANTUM(start_color->blue  + (distance * blue_step));
            row_pixels[x].opacity = OpaqueOpacity;
        }
        if (!SyncImagePixels(image))
        {
            rm_check_image_exception(image, RetainOnError);
        }
    }
}

/*
    Static:     h_diagonal_fill
    Purpose:    do a gradient fill that starts from a diagonal line and
                ends at the sides of the image
*/
static void
h_diagonal_fill(
    Image *image,
    double x1,
    double y1,
    double x2,
    double y2,
    PixelPacket *start_color,
    PixelPacket *stop_color)
{
    unsigned long x, y;
    double m, b, steps = 0.0;
    double red_step, green_step, blue_step;
    double d1, d2;

    // Compute the equation of the line: y=mx+b
    m = ((double)(y2 - y1))/((double)(x2 - x1));
    b = y1 - (m * x1);

    // The number of steps is the greatest distance between the line and
    // the left or right side of the image between y=0 and y=image->rows.
    // When y=0, x=-b/m. When y=image->rows, x = (image->rows-b)/m.
    d1 = -b/m;
    d2 = (double) ((image->rows-b) / m);

    // If the line is entirely to the right or left of the image, increase
    // the number of steps.
    if (d1 < 0 && d2 < 0)
    {
        steps += FMAX(fabs(d1),fabs(d2));
    }
    else if (d1 > (double)image->columns && d2 > (double)image->columns)
    {
        steps += FMAX(abs((int)(image->columns-d1)),abs((int)(image->columns-d2)));
    }

    d1 = FMAX(d1, image->columns-d1);
    d2 = FMAX(d2, image->columns-d2);
    steps += FMAX(d1, d2);

    // If the line is entirely > image->columns, swap the start & end color
    if (steps < 0)
    {
        PixelPacket t = *stop_color;
        *stop_color = *start_color;
        *start_color = t;
        steps = -steps;
    }

    red_step = (stop_color->red - start_color->red) / steps;
    green_step = (stop_color->green - start_color->green) / steps;
    blue_step = (stop_color->blue - start_color->blue) / steps;

    for (y = 0; y < image->rows; y++)
    {
        PixelPacket *row_pixels;

        if (!(row_pixels = SetImagePixels(image, 0, (long int)y, image->columns, 1)))
        {
            rm_check_image_exception(image, RetainOnError);
        }
        for (x = 0; x < image->columns; x++)
        {
            double distance = (double) abs((int)(x-((y-b)/m)));
            row_pixels[x].red     = ROUND_TO_QUANTUM(start_color->red   + (distance * red_step));
            row_pixels[x].green   = ROUND_TO_QUANTUM(start_color->green + (distance * green_step));
            row_pixels[x].blue    = ROUND_TO_QUANTUM(start_color->blue  + (distance * blue_step));
            row_pixels[x].opacity = OpaqueOpacity;
        }
        if (!SyncImagePixels(image))
        {
            rm_check_image_exception(image, RetainOnError);
        }
    }
}

/*
    Extern:     GradientFill_fill(image_obj)
    Purpose:    the GradientFill#fill method - call GradientFill with the
                start and stop colors specified when this fill object
                was created.
*/
VALUE
GradientFill_fill(VALUE self, VALUE image_obj)
{
    rm_GradientFill *fill;
    Image *image;
    PixelPacket start_color, stop_color;
    double x1, y1, x2, y2;          // points on the line

    Data_Get_Struct(self, rm_GradientFill, fill);
    Data_Get_Struct(image_obj, Image, image);

    x1 = fill->x1;
    y1 = fill->y1;
    x2 = fill->x2;
    y2 = fill->y2;
    start_color = fill->start_color;
    stop_color  = fill->stop_color;

    if (fabs(x2-x1) < 0.5)       // vertical?
    {
        // If the x1,y1 and x2,y2 points are essentially the same
        if (fabs(y2-y1) < 0.5)
        {
            point_fill(image, x1, y1, &start_color, &stop_color);
        }

        // A vertical line is a special case. (Yes, really do pass x1
        // as both the 2nd and 4th arguments!)
        else
        {
            vertical_fill(image, x1, &start_color, &stop_color);
        }
    }

    // A horizontal line is a special case.
    else if (fabs(y2-y1) < 0.5)
    {
        // Pass y1 as both the 3rd and 5th arguments!
        horizontal_fill(image, y1, &start_color, &stop_color);
    }

    // This is the general case - a diagonal line. If the line is more horizontal
    // than vertical, use the top and bottom of the image as the ends of the
    // gradient, otherwise use the sides of the image.
    else
    {
        double m = ((double)(y2 - y1))/((double)(x2 - x1));
        double diagonal = ((double)image->rows)/image->columns;
        if (fabs(m) <= diagonal)
        {
            v_diagonal_fill(image, x1, y1, x2, y2, &start_color, &stop_color);
        }
        else
        {
            h_diagonal_fill(image, x1, y1, x2, y2, &start_color, &stop_color);
        }
    }

    return self;
}


/*
    Static:     free_TextureFill
    Purpose:    free the TextureFill struct and the texture image it points to
    Notes:      called from GC
*/
static void
free_TextureFill(void *fill_obj)
{
    rm_TextureFill *fill = (rm_TextureFill *)fill_obj;

    (void) DestroyImage(fill->texture);
    xfree(fill);
}

/*
    Extern:     TextureFill.new(texture)
    Purpose:    Create new TextureFill object
    Notes:      the texture is an Image or Image *object
*/
#if !defined(HAVE_RB_DEFINE_ALLOC_FUNC)
VALUE
TextureFill_new(VALUE class, VALUE texture)
{
    rm_TextureFill *fill;
    VALUE argv[1];
    volatile VALUE new_fill;

    new_fill = Data_Make_Struct(class
                              , rm_TextureFill
                              , NULL
                              , free_TextureFill
                              , fill);
    argv[0] = texture;
    rb_obj_call_init((VALUE)new_fill, 1, argv);
    return new_fill;
}
#else
VALUE
TextureFill_alloc(VALUE class)
{
    rm_TextureFill *fill;
    return Data_Make_Struct(class
                        , rm_TextureFill
                        , NULL
                        , free_TextureFill
                        , fill);
}
#endif

/*
    Extern:     TextureFill#initialize
    Purpose:    Store the texture image
*/
VALUE
TextureFill_initialize(VALUE self, VALUE texture_arg)
{
    rm_TextureFill *fill;
    Image *texture;
    volatile VALUE texture_image;

    Data_Get_Struct(self, rm_TextureFill, fill);

    texture_image = ImageList_cur_image(texture_arg);

    // Bump the reference count on the texture image.
    Data_Get_Struct(texture_image, Image, texture);
    (void) ReferenceImage(texture);

    fill->texture = texture;
    return self;
}

/*
    Extern:     TextureFill_fill(image_obj)
    Purpose:    the TextureFill#fill method
*/
VALUE
TextureFill_fill(VALUE self, VALUE image_obj)
{
    rm_TextureFill *fill;
    Image *image;

    Data_Get_Struct(image_obj, Image, image);
    Data_Get_Struct(self, rm_TextureFill, fill);

    (void) TextureImage(image, fill->texture);
    rm_check_image_exception(image, RetainOnError);

    return self;
}