/*
 * zoom.c - resize an image to an arbitrary size with filtering
 * Based on "Filtered Image Rescaling" by Dale Schumacher (public domain).
 *
 * This file is part of transcode, a video stream processing tool.
 * transcode is free software, distributable under the terms of the GNU
 * General Public License (version 2 or later).  See the file COPYING
 * for details.
 */

#include "tcvideo.h"
#include "zoom.h"

#include "src/transcode.h"
#include <math.h>

/*************************************************************************/

/* Data structures for holding resizing data (used internally). */

/* A contributor to a single pixel */
struct contrib {
    int pixel;
    double weight;
};

/* List of all contributors to a single pixel */
struct clist {
    int n;                      /* Number of contributors */
    struct contrib *list;       /* Pointer to list of contributors */
};

/* Data for a resize operation */
struct zoominfo {
    int old_w, old_h;           /* Original width and height */
    int new_w, new_h;           /* New width and height */
    int Bpp;                    /* Bytes per pixel */
    int old_stride;             /* Bytes per line (original image) */
    int new_stride;             /* Bytes per line (new image) */
    double (*filter)(double);   /* Filter function */
    double fwidth;              /* Filter width */
    int32_t *x_contrib;         /* Contributors in the horizontal direction */
    int32_t *y_contrib;         /* Contributors in the vertical direction */
    uint8_t *tmpimage;          /* Temporary buffer */
};

/* Convert a double to a 16.16 fixed-point value */
#define DOUBLE_TO_FIXED(v) ((int32_t)((v)*65536))

/* Convert a 16.16 fixed-point value to an integer */
#define FIXED_TO_INT(v) ((v)>>16)

/*************************************************************************/

/* FIXME: use a static table for every data related to a filter,
 *        accessed by filter_id;
 * typedef struct tcvzoomdata_ TCVZoomData;
 * struct tcvzoom_data {
 *     const char   *name;
 *     double       support;
 *     double       (*filter)(double t);
 * };
 */

/**
 * tcv_zoom_filter_to_string:
 *     return the human-readable name of a given filter, as string.
 *
 * Parameters:
 *     filter: Filter identifier (TCV_ZOOM_*).
 * Return value:
 *     name of the given filter. DO NOT free() it.
 *     NULL if the filter is unknown/unsupported.
 */
const char *tcv_zoom_filter_to_string(TCVZoomFilter filter)
{
    const char *name = NULL;
    if (filter == TCV_ZOOM_BELL) {
        name = "Bell";
    } else if (filter == TCV_ZOOM_BOX) {
        name = "Box";
    } else if (filter == TCV_ZOOM_B_SPLINE) {
        name = "B_spline";
    } else if (filter == TCV_ZOOM_HERMITE) {
        name = "Hermite";
    } else if (filter == TCV_ZOOM_LANCZOS3) {
        name = "Lanczos3";
    } else if (filter == TCV_ZOOM_MITCHELL) {
        name = "Mitchell";
    } else if (filter == TCV_ZOOM_TRIANGLE) {
        name = "Triangle";
    } else if (filter == TCV_ZOOM_CUBIC_KEYS4) {
        name = "Cubic_Keys4";
    } else if (filter == TCV_ZOOM_SINC8) {
        name = "Sinc8";
    } else if (filter == TCV_ZOOM_DEFAULT) {
        name = "Lanczos3";
    } else {
        name = NULL;
    }
    return name;
}

/**
 * tcv_zoom_filter_from_string:
 *     return the filter id given its human-readable name, as string.
 *     case insensitive.
 *
 * Parameters:
 *     name: name fo the given filter.
 * Return value:
 *     the corrisponding identifier of the given name.
 *     TCV_ZOOM_NULL if the filter is unknown/unsupported.
 */
TCVZoomFilter tcv_zoom_filter_from_string(const char *name)
{
    TCVZoomFilter filter = TCV_ZOOM_NULL;
    if (strcasecmp(name, "bell") == 0) {
        filter = TCV_ZOOM_BELL;
    } else if (strcasecmp(name, "box") == 0) {
        filter = TCV_ZOOM_BOX;
    } else if (strcasecmp(name, "b_spline") == 0) {
        filter = TCV_ZOOM_B_SPLINE;
    } else if (strcasecmp(name, "hermite") == 0) {
        filter = TCV_ZOOM_HERMITE;
    } else if (strcasecmp(name, "lanczos3") == 0) {
        filter = TCV_ZOOM_LANCZOS3;
    } else if (strcasecmp(name, "mitchell") == 0) {
        filter = TCV_ZOOM_MITCHELL;
    } else if (strcasecmp(name, "triangle") == 0) {
        filter = TCV_ZOOM_TRIANGLE;
    } else if (strcasecmp(name, "cubic_keys4") == 0) {
        filter = TCV_ZOOM_CUBIC_KEYS4;
    } else if (strcasecmp(name, "sinc8") == 0) {
        filter = TCV_ZOOM_SINC8;
    } else if (strcasecmp(name, "default") == 0) {
        filter = TCV_ZOOM_LANCZOS3;
    } else {
        filter = TCV_ZOOM_NULL;
    }
    return filter;
}

/*************************************************************************/
/*************************************************************************/

/**
 * *_filter:  Filter functions for resizing.
 *
 * Parameters:
 *     t: Filter parameter.
 * Return value:
 *     Filter result.
 * Postconditions: 0 <= t && t <= 1
 */

/************************************/

static const double hermite_support = 1.0;

static double hermite_filter(double t)
{
    /* f(t) = 2|t|^3 - 3|t|^2 + 1, -1 <= t <= 1 */
    if (t < 0.0)
        t = -t;
    if (t < 1.0)
        return (2.0 * t - 3.0) * t * t + 1.0;
    return 0.0;
}

/************************************/

static const double box_support = 0.5;

static double box_filter(double t)
{
    if ((t > -0.5) && (t <= 0.5))
        return 1.0;
    return 0.0;
}

/************************************/

static const double triangle_support = 1.0;

static double triangle_filter(double t)
{
    if (t < 0.0)
        t = -t;
    if (t < 1.0)
        return 1.0 - t;
    return 0.0;
}

/************************************/

static const double bell_support = 1.5;

static double bell_filter(double t)
{
    if (t < 0)
        t = -t;
    if  (t < .5)
        return .75 - (t * t);
    if (t < 1.5) {
        t = (t - 1.5);
        return .5 * (t * t);
    }
    return 0.0;
}

/************************************/

static const double B_spline_support = 2.0;

static double B_spline_filter(double t)
{
    double tt;

    if (t < 0)
        t = -t;
    if (t < 1) {
        tt = t * t;
        return (.5 * tt * t) - tt + (2.0 / 3.0);
    } else if (t < 2) {
        t = 2 - t;
        return (1.0 / 6.0) * (t * t * t);
    }
    return 0.0;
}

/************************************/

static const double lanczos3_support = 3.0;

#ifndef PI
# define PI 3.14159265358979323846264338327950
#endif
#define SINC(x) ((x) != 0 ? sin((x)*PI) / ((x)*PI) : 1.0)

static double lanczos3_filter(double t)
{
    if (t < 0)
        t = -t;
    if (t < 3.0)
        return SINC(t) * SINC(t/3.0);
    return 0.0;
}

#undef SINC

/************************************/

static const double mitchell_support = 2.0;

#define B       (1.0 / 3.0)
#define C       (1.0 / 3.0)

static double mitchell_filter(double t)
{
    double tt;

    tt = t * t;
    if (t < 0)
        t = -t;
    if (t < 1.0) {
        t = (((12.0 - 9.0 * B - 6.0 * C) * (t * tt))
             + ((-18.0 + 12.0 * B + 6.0 * C) * tt)
             + (6.0 - 2 * B));
        return t / 6.0;
    } else if (t < 2.0) {
        t = (((-1.0 * B - 6.0 * C) * (t * tt))
             + ((6.0 * B + 30.0 * C) * tt)
             + ((-12.0 * B - 48.0 * C) * t)
             + (8.0 * B + 24 * C));
        return t / 6.0;
    }
    return 0.0;
}

#undef B
#undef C

/************************************/
/* Keys 4th-order Cubic */

static const double cubic_keys4_support = 3.0;

static double cubic_keys4_filter(double t)
{
    if (t < 0.0)
        t = -t;
    if (t < 1.0) 
        return (3.0 + (t * t * (-7.0 + (t * 4.0)))) / 3.0;
    if (t < 2.0) 
        return (30.0 + (t * (-59.0 + (t * (36.0 + (t * -7.0)))))) / 12.0;
    if (t < 3.0)
        return (-18.0 + (t * (21.0 + (t * (-8.0 + t))))) / 12.0;
    return 0.0;
}

/************************************/
/* Sinc with Lanczos window, 8 cycles */

static const double sinc8_support = 8.0;

static double sinc8_filter(double t)
{
    if (t < 0.0)
        t = -t;
    if (t == 0.0) {
        return 1.0;
    } else if (t < 8.0) {
        double w = sin(PI*t / 8.0) / (PI*t / 8.0);
        return w * sin(t*PI) / (t*PI);
    } else {
        return 0.0;
    }
}

/*************************************************************************/

/**
 * gen_contrib:  Helper function to generate the list of contributors to
 * each resized pixel in one direction (horizontal or vertical).
 *
 * Parameters:
 *     oldsize: Size of original image in the direction for which
 *              contributors are being generated.
 *     newsize: Size of resized image in the direction for which
 *              contributors are being generated.
 *      stride: Number of bytes between adjacent pixels in the direction
 *              for which contributors are being generated.
 *      filter: As for zoom_process().
 *      fwidth: As for zoom_process().
 * Return value:
 *     A pointer to a `newsize'-element array of `struct clist' elements,
 *     or NULL on error (out of memory).
 * Preconditions:
 *     oldsize > 0
 *     newsize > 0
 *     stride > 0
 *     filter != NULL
 *     fwidth > 0
 */

static struct clist *gen_contrib(int oldsize, int newsize, int stride,
                                 double (*filter)(double), double fwidth)
{
    struct clist *contrib;
    double scale = (double)newsize / (double)oldsize;
    double new_fwidth, fscale;
    int i, j;

    contrib = tc_zalloc(newsize * sizeof(struct clist));

    if (scale < 1.0) {
        fscale = 1.0 / scale;
    } else {
        fscale = 1.0;
    }
    new_fwidth = fwidth * fscale;
    for (i = 0; i < newsize; ++i) {
        double center = (double) i / scale;
        int left = ceil(center - new_fwidth);
        int right = floor(center + new_fwidth);
        contrib[i].n = 0;
        contrib[i].list = tc_zalloc((right-left+1) * sizeof(struct contrib));
        for (j = left; j <= right; ++j) {
            int k, n;
            double weight = center - (double) j;
            weight = (*filter)(weight / fscale) / fscale;
            if (j < 0) {
                n = -j;
            } else if (j >= oldsize) {
                n = (oldsize - j) + oldsize - 1;
            } else {
                n = j;
            }
            k = contrib[i].n++;
            contrib[i].list[k].pixel = n*stride;
            contrib[i].list[k].weight = weight;
        }
    }

    return contrib;
}

/*************************************************************************/
/*************************************************************************/

/* External interface. */

/*************************************************************************/

/**
 * zoom_init:  Allocate, initialize, and return a ZoomInfo structure for
 * use in subsequent zoom_process() calls.  The structure should be freed
 * with zoom_free() when no longer needed.
 *
 * Parameters:
 *          old_w: Width of original image.
 *          old_h: Height of original image.
 *          new_w: Width of resized image.
 *          new_h: Height of resized image.
 *            Bpp: Bytes (not bits!) per pixel.
 *     old_stride: Bytes per line of original image.
 *     new_stride: Bytes per line of resized image.
 *         filter: Filter identifier (TCV_ZOOM_*).
 * Return value:
 *     A pointer to a newly allocated ZoomInfo structure, or NULL on error
 *     (invalid parameters or out of memory).
 */

ZoomInfo *zoom_init(int old_w, int old_h, int new_w, int new_h, int Bpp,
                    int old_stride, int new_stride, TCVZoomFilter filter)
{
    ZoomInfo *zi;
    struct clist *x_contrib = NULL, *y_contrib = NULL;

    /* Sanity check */
    if (old_w <= 0 || old_h <= 0 || new_w <= 0 || new_h <= 0 || Bpp <= 0
     || old_stride <= 0 || new_stride <= 0)
        return NULL;

    /* Allocate structure */
    zi = tc_malloc(sizeof(*zi));
    if (!zi)
        return NULL;

    /* Set up scalar members, and check filter value */
    zi->old_w = old_w;
    zi->old_h = old_h;
    zi->new_w = new_w;
    zi->new_h = new_h;
    zi->Bpp = Bpp;
    zi->old_stride = old_stride;
    zi->new_stride = new_stride;
    switch (filter) {
      case TCV_ZOOM_BOX:
        zi->filter = box_filter;
        zi->fwidth = box_support;
        break;
      case TCV_ZOOM_TRIANGLE:
        zi->filter = triangle_filter;
        zi->fwidth = triangle_support;
        break;
      case TCV_ZOOM_HERMITE:
        zi->filter = hermite_filter;
        zi->fwidth = hermite_support;
        break;
      case TCV_ZOOM_BELL:
        zi->filter = bell_filter;
        zi->fwidth = bell_support;
        break;
      case TCV_ZOOM_B_SPLINE:
        zi->filter = B_spline_filter;
        zi->fwidth = B_spline_support;
        break;
      case TCV_ZOOM_MITCHELL:
        zi->filter = mitchell_filter;
        zi->fwidth = mitchell_support;
        break;
      case TCV_ZOOM_LANCZOS3:
        zi->filter = lanczos3_filter;
        zi->fwidth = lanczos3_support;
        break;
      case TCV_ZOOM_CUBIC_KEYS4:
        zi->filter = cubic_keys4_filter;
        zi->fwidth = cubic_keys4_support;
        break;
      case TCV_ZOOM_SINC8:
        zi->filter = sinc8_filter;
        zi->fwidth = sinc8_support;
        break;
      default:
        free(zi);
        return NULL;
    }

    /* Generate contributor lists and allocate temporary image buffer */
    zi->x_contrib = NULL;
    zi->y_contrib = NULL;
    zi->tmpimage = tc_malloc(new_w * old_h * Bpp);
    if (!zi->tmpimage)
        goto error_out;
    if (old_w != new_w) {
        x_contrib = gen_contrib(old_w, new_w, Bpp, zi->filter, zi->fwidth);
        if (!x_contrib)
            goto error_out;
    }
    if (old_h != new_h) {
        /* Calculate the correct stride--if the width isn't changing,
         * this will just be old_stride */
        int stride = (old_w==new_w) ? old_stride : Bpp*new_w;
        y_contrib = gen_contrib(old_h, new_h, stride, zi->filter,
                                zi->fwidth);
        if (!y_contrib)
            goto error_out;
    }

    /* Convert contributor lists into flat arrays and fixed-point values.
     * The flat array consists of a contributor count plus two values per
     * contributor (index and fixed-point weight) for each output pixel.
     * Note that for the horizontal direction, we make `Bpp' copies of the
     * contributors, adjusting the offset for each byte of the pixel. */

    if (x_contrib) {
        int count = 0, i;
        int32_t *ptr;

        for (i = 0; i < new_w; i++)
            count += 1 + 2 * x_contrib[i].n;
        zi->x_contrib = tc_malloc(sizeof(int32_t) * count * Bpp);
        if (!zi->x_contrib)
            goto error_out;
        for (ptr = zi->x_contrib, i = 0; i < new_w * Bpp; i++) {
            int j;
            *ptr++ = x_contrib[i/Bpp].n;
            for (j = 0; j < x_contrib[i/Bpp].n; j++) {
                *ptr++ = x_contrib[i/Bpp].list[j].pixel + i%Bpp;
                *ptr++ = DOUBLE_TO_FIXED(x_contrib[i/Bpp].list[j].weight);
            }
        }
        /* Free original contributor list */
        for (i = 0; i < new_w; i++)
            free(x_contrib[i].list);
        free(x_contrib);
        x_contrib = NULL;
    }

    if (y_contrib) {
        int count = 0, i;
        int32_t *ptr;

        for (i = 0; i < new_h; i++)
            count += 1 + 2 * y_contrib[i].n;
        zi->y_contrib = tc_malloc(sizeof(int32_t) * count);
        if (!zi->y_contrib)
            goto error_out;
        for (ptr = zi->y_contrib, i = 0; i < new_h; i++) {
            int j;
            *ptr++ = y_contrib[i].n;
            for (j = 0; j < y_contrib[i].n; j++) {
                *ptr++ = y_contrib[i].list[j].pixel;
                *ptr++ = DOUBLE_TO_FIXED(y_contrib[i].list[j].weight);
            }
        }
        for (i = 0; i < new_h; i++)
            free(y_contrib[i].list);
        free(y_contrib);
        y_contrib = NULL;
    }

    /* Done */
    return zi;

  error_out:
    {
        if (x_contrib) {
            int i;
            for (i = 0; i < new_w; i++)
                free(x_contrib[i].list);
            free(x_contrib);
        }
        if (y_contrib) {
            int i;
            for (i = 0; i < new_w; i++)
                free(x_contrib[i].list);
            free(x_contrib);
        }
        zoom_free(zi);
        return NULL;
    }
}

/*************************************************************************/

/**
 * zoom_process:  Image resizing core.
 *
 * Parameters:
 *       zi: ZoomInfo structure allocated by zoom_init().
 *      src: Source data plane.
 *     dest: Destination data plane.
 * Return value: None.
 * Preconditions:
 *     zi was allocated by zoom_init()
 *     src != NULL
 *     dest != NULL
 *     src and dest do not overlap
 */

/* clamp the input to the specified range */
#define CLAMP(v,l,h)    ((v)<(l) ? (l) : (v) > (h) ? (h) : (v))

void zoom_process(const ZoomInfo *zi, const uint8_t *src, uint8_t *dest)
{
    int from_stride, to_stride;
    const uint8_t *from;
    uint8_t *to;

    from = src;
    from_stride = zi->old_stride;

    /* Apply filter to zoom horizontally from src to tmp (if necessary) */
    if (zi->x_contrib) {
        int y;
        to = zi->tmpimage;
        to_stride = zi->new_w * zi->Bpp;
        for (y = 0; y < zi->old_h; y++, from += from_stride, to += to_stride) {
            int32_t *contrib = zi->x_contrib;
            int x;
            for (x = 0; x < zi->new_w * zi->Bpp; x++) {
                int32_t weight = DOUBLE_TO_FIXED(0.5);
                int n = *contrib++, i;
                for (i = 0; i < n; i++) {
                    int pixel = *contrib++;
                    weight += from[pixel] * (*contrib++);
                }
                to[x] = CLAMP(FIXED_TO_INT(weight), 0, 255);
            }
        }
        from = zi->tmpimage;
        from_stride = to_stride;
    }

    /* Apply filter to zoom vertically from tmp (or src) to dest */
    /* Use Y as the outside loop to avoid cache thrashing on output buffer */
    to = dest;
    to_stride = zi->new_stride;
    if (zi->y_contrib) {
        int32_t *contrib = zi->y_contrib;
        int y;
        for (y = 0; y < zi->new_h; y++, to += to_stride) {
            int n = *contrib++, x;
            for (x = 0; x < zi->new_w * zi->Bpp; x++) {
                int32_t weight = DOUBLE_TO_FIXED(0.5);
                int i;
                for (i = 0; i < n; i++) {
                    int pixel = contrib[i*2];
                    weight += from[x+pixel] * contrib[i*2+1];
                }
                to[x] = CLAMP(FIXED_TO_INT(weight), 0, 255);
            }
            contrib += 2*n;
        }
    } else {
        /* No zooming necessary, just copy */
        if (from_stride == zi->new_w*zi->Bpp
         && to_stride == zi->new_w*zi->Bpp
        ) {
            /* We can copy the whole frame at once */
            ac_memcpy(to, from, to_stride * zi->new_h);
        } else {
            /* Copy one row at a time */
            int y;
            for (y = 0; y < zi->new_h; y++) {
                ac_memcpy(to + y*to_stride, from + y*from_stride,
                          zi->new_w * zi->Bpp);
            }
        }
    }
}

/*************************************************************************/

/**
 * zoom_free():  Free a ZoomInfo structure.
 *
 * Parameters:
 *     zi: ZoomInfo structure allocated by zoom_init().
 * Return value:
 *     None.
 * Preconditions:
 *     zi was allocated by zoom_init()
 * Postconditions:
 *     zi is freed
 */
void zoom_free(ZoomInfo *zi)
{
    free(zi->x_contrib);
    free(zi->y_contrib);
    free(zi->tmpimage);
    free(zi);
}

/*************************************************************************/

/*
 * Local variables:
 *   c-file-style: "stroustrup"
 *   c-file-offsets: ((case-label . *) (statement-case-intro . *))
 *   indent-tabs-mode: nil
 * End:
 *
 * vim: expandtab shiftwidth=4:
 */
