/*
    Miscelaneous image manipulations
    Copyright (C) 2001 by Andrew Zabolotny
*/

#include "image.h"
#include <zlib.h>
#include <png.h>
#include <stdlib.h>
#include <unistd.h>
#include <math.h>

// A support of 3 gives an overall sharper looking image, but
// it is a) slower b) gives more sharpening artefacts
#define LANCZOS_SUPPORT    2
// Lanczos kernel is precomputed in a table with this resolution
// The value below seems to be enough for HQ upscaling up to eight times
#define LANCZOS_TABLE_RES  256

template<typename T> static inline T square (T x)
{
    return x * x;
}

float *Image::lanczos_func = NULL;
int Image::lanczos_func_use = 0;

Image::Image () :
    file (NULL), lanczos_func_in_use (false), image (NULL)
{
}

Image::~Image ()
{
    Close ();
    Free ();
}

bool Image::Open (const char *fName)
{
    file = fopen (fName, "rb");
    if (!file)
        return false;

    fseek (file, 0, SEEK_END);
    filesize = ftell (file);
    fseek (file, 0, SEEK_SET);

    return true;
}

void Image::Close ()
{
    if (file)
    {
        fclose (file);
        file = NULL;
    }
}

void Image::Free ()
{
    delete [] image;
    image = NULL;
    if (lanczos_func_in_use && !--lanczos_func_use)
    {
        delete [] lanczos_func;
        lanczos_func = NULL;
        lanczos_func_in_use = false;
    }
}

bool Image::LoadPNG ()
{
    png_structp png;
    png_infop info;

    size_t rowbytes, exp_rowbytes;
    png_bytep *row_pointers;

    Free ();

    png = png_create_read_struct (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
    if (!png)
        return false;

    info = png_create_info_struct (png);
    if (!info)
    {
        png_destroy_read_struct (&png, (png_infopp) NULL, (png_infopp) NULL);
        png = NULL;
        return false;
    }

    png_init_io (png, file);

    if (setjmp (png->jmpbuf))
        // If we get here, we had a problem reading the file
        goto nomem;

    png_read_info (png, info);

    // Get picture info
    png_uint_32 Width, Height;
    int bit_depth, color_type;

    png_get_IHDR (png, info, &Width, &Height, &bit_depth, &color_type,
                  NULL, NULL, NULL);

    if (bit_depth > 8)
        // tell libpng to strip 16 bit/color files down to 8 bits/color
        png_set_strip_16 (png);
    else if (bit_depth < 8)
        // Expand pictures with less than 8bpp to 8bpp
        png_set_packing (png);

    switch (color_type)
    {
        case PNG_COLOR_TYPE_GRAY:
        case PNG_COLOR_TYPE_GRAY_ALPHA:
            png_set_gray_to_rgb (png);
            break;
        case PNG_COLOR_TYPE_PALETTE:
            png_set_palette_to_rgb (png);
            break;
        case PNG_COLOR_TYPE_RGB:
        case PNG_COLOR_TYPE_RGB_ALPHA:
            break;
        default:
            goto nomem;
    }

    // If there is no alpha information, fill with 0xff
    if (!(color_type & PNG_COLOR_MASK_ALPHA))
    {
        // Expand paletted or RGB images with transparency to full alpha
        // channels so the data will be available as RGBA quartets.
        if (png_get_valid (png, info, PNG_INFO_tRNS))
            png_set_tRNS_to_alpha (png);
        else
            png_set_filler (png, 0xff, PNG_FILLER_AFTER);
    }

    // Update structure with the above settings
    png_read_update_info (png, info);

    // Allocate the memory to hold the image
    image = new RGBpixel [(width = Width) * (height = Height)];
    if (!image)
        goto nomem;
    exp_rowbytes = Width * sizeof (RGBpixel);

    rowbytes = png_get_rowbytes (png, info);
    if (rowbytes != exp_rowbytes)
        goto nomem;                         // Yuck! Something went wrong!

    row_pointers = new png_bytep [Height];

    if (!row_pointers
        || setjmp (png->jmpbuf))             // Set a new exception handler
    {
        delete [] row_pointers;
    nomem:
        png_destroy_read_struct (&png, &info, (png_infopp) NULL);
        Free ();
        return false;
    }

    for (png_uint_32 row = 0; row < Height; row++)
        row_pointers [row] = ((png_bytep)image) + row * rowbytes;

    // Read image data
    png_read_image (png, row_pointers);

    // read rest of file, and get additional chunks in info_ptr
    png_read_end (png, (png_infop)NULL);

    // Free the row pointers array that is not needed anymore
    delete [] row_pointers;

    png_destroy_read_struct (&png, &info, (png_infopp) NULL);

    return true;
}

static inline int isqr (int x)
{ return x * x; }

bool Image::SavePNG (const char *fName)
{
    /* Remove the file in the case it exists and it is a link */
    unlink (fName);
    /* open the file */
    FILE *fp = fopen (fName, "wb");
    if (fp == NULL)
        return false;

    png_structp png = png_create_write_struct (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);

    if (!png)
    {
    error1:
        fclose (fp);
        return false;
    }

    /* Allocate/initialize the image information data. */
    png_infop info = png_create_info_struct (png);
    if (info == NULL)
    {
    error2:
        png_destroy_write_struct (&png, &info);
        goto error1;
    }

    /* Catch processing errors */
    if (setjmp(png->jmpbuf))
        /* If we get here, we had a problem writing the file */
        goto error2;

    /* Set up the output function */
    png_init_io (png, fp);

    /* Set the image information here.  Width and height are up to 2^31,
     * bit_depth is one of 1, 2, 4, 8, or 16, but valid values also depend on
     * the color_type selected. color_type is one of PNG_COLOR_TYPE_GRAY,
     * PNG_COLOR_TYPE_GRAY_ALPHA, PNG_COLOR_TYPE_PALETTE, PNG_COLOR_TYPE_RGB,
     * or PNG_COLOR_TYPE_RGB_ALPHA.  interlace is either PNG_INTERLACE_NONE or
     * PNG_INTERLACE_ADAM7, and the compression_type and filter_type MUST
     * currently be PNG_COMPRESSION_TYPE_BASE and PNG_FILTER_TYPE_BASE. REQUIRED
     */
    int colortype, rowlen, bits;
    colortype = PNG_COLOR_TYPE_RGB_ALPHA;
    rowlen = width * sizeof (RGBpixel);
    bits = 8;

    png_set_IHDR (png, info, width, height, bits, colortype,
                  PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);

    /* if we are dealing with a color image then */
    png_color_8 sig_bit;
    memset (&sig_bit, 0, sizeof (sig_bit));
    sig_bit.red = 8;
    sig_bit.green = 8;
    sig_bit.blue = 8;

    /* if the image has an alpha channel then */
    if (colortype & PNG_COLOR_MASK_ALPHA)
        sig_bit.alpha = bits;
    png_set_sBIT (png, info, &sig_bit);

    /* Write the file header information. */
    png_write_info (png, info);

    /* Get rid of filler (OR ALPHA) bytes, pack XRGB/RGBX/ARGB/RGBA into
     * RGB (4 channels -> 3 channels). The second parameter is not used.
     */
    if (!(colortype & PNG_COLOR_MASK_ALPHA))
        png_set_filler (png, 0, PNG_FILLER_AFTER);

    /* The easiest way to write the image (you may have a different memory
     * layout, however, so choose what fits your needs best).  You need to
     * use the first method if you aren't handling interlacing yourself.
     */
    png_bytep *row_pointers = new png_bytep [height];
    unsigned char *ImageData = (unsigned char *)image;
    for (unsigned i = 0; i < height; i++)
        row_pointers [i] = ImageData + i * rowlen;

    /* One of the following output methods is REQUIRED */
    png_write_image (png, row_pointers);

    /* It is REQUIRED to call this to finish writing the rest of the file */
    png_write_end (png, info);

    /* if you malloced the palette, free it here */
    if (info->palette)
        free (info->palette);

    /* clean up after the write, and free any memory allocated */
    png_destroy_write_struct (&png, &info);

    /* Free the row pointers */
    delete [] row_pointers;

    /* close the file */
    fclose (fp);

    /* that's it */
    return true;
}

void Image::Resize (unsigned newwidth, unsigned newheight)
{
    Free ();
    image = new RGBpixel [(width = newwidth) * (height = newheight)];
}

void Image::InitInterpolation (InterpolationMethod method)
{
    switch (method)
    {
        case I_NEAREST:
            fGetR = GetR_n;
            fGetG = GetG_n;
            fGetB = GetB_n;
            fGet = Get_n;
            break;
        case I_BILINEAR:
            fGetR = GetR_b;
            fGetG = GetG_b;
            fGetB = GetB_b;
            fGet = Get_b;
            break;
        case I_LANCZOS:
            fGetR = GetR_l;
            fGetG = GetG_l;
            fGetB = GetB_l;
            fGet = Get_l;

            if (!lanczos_func)
            {
                // Precompute the function for faster interpolation
                lanczos_func = new float [LANCZOS_SUPPORT * LANCZOS_SUPPORT * LANCZOS_TABLE_RES];
                for (int i = 0; i < LANCZOS_SUPPORT * LANCZOS_SUPPORT * LANCZOS_TABLE_RES; i++)
                {
                    float d = sqrt (float (i) / LANCZOS_TABLE_RES);
                    if (d == 0.0)
                        lanczos_func [i] = 1.0;
                    else
                        lanczos_func [i] =
                            (LANCZOS_SUPPORT * sin (M_PI * d) *
                             sin ((M_PI / LANCZOS_SUPPORT) * d)) /
                            (M_PI * M_PI * d * d);
                }
            }
            if (!lanczos_func_in_use)
            {
                lanczos_func_in_use = true;
                lanczos_func_use++;
            }
            break;
    }
}

// --- // Nearest interpolation // --- //

unsigned char Image::GetR_n (Image *This, float x, float y)
{
    unsigned xi = unsigned (x + 0.5);
    unsigned yi = unsigned (y + 0.5);
    if (xi >= This->width || yi >= This->height)
        return 0;

    RGBpixel *p = This->image + yi * This->width + xi;
    return p->red;
}

unsigned char Image::GetG_n (Image *This, float x, float y)
{
    unsigned xi = unsigned (x + 0.5);
    unsigned yi = unsigned (y + 0.5);
    if (xi >= This->width || yi >= This->height)
        return 0;

    RGBpixel *p = This->image + yi * This->width + xi;
    return p->green;
}

unsigned char Image::GetB_n (Image *This, float x, float y)
{
    unsigned xi = unsigned (x + 0.5);
    unsigned yi = unsigned (y + 0.5);
    if (xi >= This->width || yi >= This->height)
        return 0;

    RGBpixel *p = This->image + yi * This->width + xi;
    return p->blue;
}

void Image::Get_n (Image *This, RGBpixel &out, float x, float y)
{
    unsigned xi = unsigned (x + 0.5);
    unsigned yi = unsigned (y + 0.5);
    if (xi >= This->width || yi >= This->height)
        return;

    RGBpixel *p = This->image + yi * This->width + xi;
    out = *p;
}


// --- // Bi-linear interpolation // --- //

unsigned char Image::GetR_b (Image *This, float x, float y)
{
    // linear interpolation
    unsigned xi = unsigned (x);
    unsigned yi = unsigned (y);
    if (xi >= This->width || yi >= This->height)
        return 0;

    unsigned dx = unsigned ((x - trunc (x)) * 256);
    unsigned dy = unsigned ((y - trunc (y)) * 256);

    RGBpixel *p0 = This->image + yi * This->width + xi;
    RGBpixel *p1 = p0 + This->width;

    unsigned k1, k2;
    k1 = 256 * p0 [0].red   + dx * (int (p0 [1].red  ) - int (p0 [0].red  ));
    k2 = 256 * p1 [0].red   + dx * (int (p1 [1].red  ) - int (p1 [0].red  ));
    return (256 * k1 + dy * (k2 - k1)) >> 16;
}

unsigned char Image::GetG_b (Image *This, float x, float y)
{
    // linear interpolation
    unsigned xi = int (x);
    unsigned yi = int (y);
    if (xi >= This->width || yi >= This->height)
        return 0;

    unsigned dx = unsigned ((x - trunc (x)) * 256);
    unsigned dy = unsigned ((y - trunc (y)) * 256);

    RGBpixel *p0 = This->image + yi * This->width + xi;
    RGBpixel *p1 = p0 + This->width;

    unsigned k1, k2;
    k1 = 256 * p0 [0].green + dx * (int (p0 [1].green) - int (p0 [0].green));
    k2 = 256 * p1 [0].green + dx * (int (p1 [1].green) - int (p1 [0].green));
    return (256 * k1 + dy * (k2 - k1)) >> 16;
}

unsigned char Image::GetB_b (Image *This, float x, float y)
{
    // linear interpolation
    unsigned xi = int (x);
    unsigned yi = int (y);
    if (xi >= This->width || yi >= This->height)
        return 0;

    unsigned dx = unsigned ((x - trunc (x)) * 256);
    unsigned dy = unsigned ((y - trunc (y)) * 256);

    RGBpixel *p0 = This->image + yi * This->width + xi;
    RGBpixel *p1 = p0 + This->width;

    unsigned k1, k2;
    k1 = 256 * p0 [0].blue  + dx * (int (p0 [1].blue ) - int (p0 [0].blue ));
    k2 = 256 * p1 [0].blue  + dx * (int (p1 [1].blue ) - int (p1 [0].blue ));
    return (256 * k1 + dy * (k2 - k1)) >> 16;
}

void Image::Get_b (Image *This, RGBpixel &out, float x, float y)
{
    // linear interpolation
    unsigned xi = unsigned (x);
    unsigned yi = unsigned (y);
    if (xi >= This->width || yi >= This->height)
    {
        out.red = out.green = out.blue = 0;
        return;
    }

    unsigned dx = unsigned ((x - trunc (x)) * 256);
    unsigned dy = unsigned ((y - trunc (y)) * 256);

    RGBpixel *p0 = This->image + yi * This->width + xi;
    RGBpixel *p1 = p0 + This->width;

    unsigned k1, k2;

    k1 = 256 * p0 [0].red   + dx * (int (p0 [1].red  ) - int (p0 [0].red  ));
    k2 = 256 * p1 [0].red   + dx * (int (p1 [1].red  ) - int (p1 [0].red  ));
    out.red = (256 * k1 + dy * (k2 - k1)) >> 16;

    k1 = 256 * p0 [0].green + dx * (int (p0 [1].green) - int (p0 [0].green));
    k2 = 256 * p1 [0].green + dx * (int (p1 [1].green) - int (p1 [0].green));
    out.green = (256 * k1 + dy * (k2 - k1)) >> 16;

    k1 = 256 * p0 [0].blue  + dx * (int (p0 [1].blue ) - int (p0 [0].blue ));
    k2 = 256 * p1 [0].blue  + dx * (int (p1 [1].blue ) - int (p1 [0].blue ));
    out.blue = (256 * k1 + dy * (k2 - k1)) >> 16;
}

// --- // Lanczos 2 interpolation // --- //

unsigned char Image::GetR_l (Image *This, float x, float y)
{
    float xs = rint (x) - LANCZOS_SUPPORT;
    float ys = rint (y) - LANCZOS_SUPPORT;
    float xe = xs + LANCZOS_SUPPORT * 2;
    float ye = ys + LANCZOS_SUPPORT * 2;

    float norm = 0.0;
    float sum = 0.0;
    RGBpixel *img = This->image + (long (ys) * This->width + long (xs));

    if (xs >= 0 && ys >= 0 && xe < This->width && ye < This->height)
        for (; ys <= ye; ys += 1.0)
        {
            for (float xc = xs; xc <= xe; xc += 1.0, img++)
            {
                float d = square (x - xc) + square (y - ys);
                if (d >= LANCZOS_SUPPORT * LANCZOS_SUPPORT)
                    continue;

                d = lanczos_func [int (d * LANCZOS_TABLE_RES)];
                norm += d;
                sum += d * img->red;
            }
            img += This->width - LANCZOS_SUPPORT * 2 - 1;
        }
    else
    {
        for (; ys <= ye; ys += 1.0)
        {
            if (ys < 0 || ys >= This->height)
            {
                img += This->width;
                continue;
            }

            for (float xc = xs; xc <= xe; xc += 1.0, img++)
            {
                if (xc < 0 || xc >= This->width)
                    continue;

                float d = square (x - xc) + square (y - ys);
                if (d >= LANCZOS_SUPPORT * LANCZOS_SUPPORT)
                    continue;

                d = lanczos_func [int (d * LANCZOS_TABLE_RES)];
                norm += d;
                sum += d * img->red;
            }
            img += This->width - LANCZOS_SUPPORT * 2 - 1;
        }
        if (norm == 0.0)
            return 0;
    }

    int r = int (sum / norm);
    return r > 255 ? 255 : r < 0 ? 0 : r;
}

unsigned char Image::GetG_l (Image *This, float x, float y)
{
    float xs = rint (x) - LANCZOS_SUPPORT;
    float ys = rint (y) - LANCZOS_SUPPORT;
    float xe = xs + LANCZOS_SUPPORT * 2;
    float ye = ys + LANCZOS_SUPPORT * 2;

    float norm = 0.0;
    float sum = 0.0;
    RGBpixel *img = This->image + (long (ys) * This->width + long (xs));

    if (xs >= 0 && ys >= 0 && xe < This->width && ye < This->height)
        for (; ys <= ye; ys += 1.0)
        {
            for (float xc = xs; xc <= xe; xc += 1.0, img++)
            {
                float d = square (x - xc) + square (y - ys);
                if (d >= LANCZOS_SUPPORT * LANCZOS_SUPPORT)
                    continue;

                d = lanczos_func [int (d * LANCZOS_TABLE_RES)];
                norm += d;
                sum += d * img->green;
            }
            img += This->width - LANCZOS_SUPPORT * 2 - 1;
        }
    else
    {
        for (; ys <= ye; ys += 1.0)
        {
            if (ys < 0 || ys >= This->height)
            {
                img += This->width;
                continue;
            }

            for (float xc = xs; xc <= xe; xc += 1.0, img++)
            {
                if (xc < 0 || xc >= This->width)
                    continue;

                float d = square (x - xc) + square (y - ys);
                if (d >= LANCZOS_SUPPORT * LANCZOS_SUPPORT)
                    continue;

                d = lanczos_func [int (d * LANCZOS_TABLE_RES)];
                norm += d;
                sum += d * img->green;
            }
            img += This->width - LANCZOS_SUPPORT * 2 - 1;
        }
        if (norm == 0.0)
            return 0;
    }

    int r = int (sum / norm);
    return r > 255 ? 255 : r < 0 ? 0 : r;
}

unsigned char Image::GetB_l (Image *This, float x, float y)
{
    float xs = rint (x) - LANCZOS_SUPPORT;
    float ys = rint (y) - LANCZOS_SUPPORT;
    float xe = xs + LANCZOS_SUPPORT * 2;
    float ye = ys + LANCZOS_SUPPORT * 2;

    float norm = 0.0;
    float sum = 0.0;
    RGBpixel *img = This->image + (long (ys) * This->width + long (xs));

    if (xs >= 0 && ys >= 0 && xe < This->width && ye < This->height)
        for (; ys <= ye; ys += 1.0)
        {
            for (float xc = xs; xc <= xe; xc += 1.0, img++)
            {
                float d = square (x - xc) + square (y - ys);
                if (d >= LANCZOS_SUPPORT * LANCZOS_SUPPORT)
                    continue;

                d = lanczos_func [int (d * LANCZOS_TABLE_RES)];
                norm += d;
                sum += d * img->blue;
            }
            img += This->width - LANCZOS_SUPPORT * 2 - 1;
        }
    else
    {
        for (; ys <= ye; ys += 1.0)
        {
            if (ys < 0 || ys >= This->height)
            {
                img += This->width;
                continue;
            }

            for (float xc = xs; xc <= xe; xc += 1.0, img++)
            {
                if (xc < 0 || xc >= This->width)
                    continue;

                float d = square (x - xc) + square (y - ys);
                if (d >= LANCZOS_SUPPORT * LANCZOS_SUPPORT)
                    continue;

                d = lanczos_func [int (d * LANCZOS_TABLE_RES)];
                norm += d;
                sum += d * img->blue;
            }
            img += This->width - LANCZOS_SUPPORT * 2 - 1;
        }
        if (norm == 0.0)
            return 0;
    }

    int r = int (sum / norm);
    return r > 255 ? 255 : r < 0 ? 0 : r;
}

void Image::Get_l (Image *This, RGBpixel &out, float x, float y)
{
    float xs = rint (x) - LANCZOS_SUPPORT;
    float ys = rint (y) - LANCZOS_SUPPORT;
    float xe = xs + LANCZOS_SUPPORT * 2;
    float ye = ys + LANCZOS_SUPPORT * 2;

    float norm = 0.0;
    float sumR = 0.0;
    float sumG = 0.0;
    float sumB = 0.0;
    RGBpixel *img = This->image + (long (ys) * This->width + long (xs));

    if (xs >= 0 && ys >= 0 && xe < This->width && ye < This->height)
        for (; ys <= ye; ys += 1.0)
        {
            for (float xc = xs; xc <= xe; xc += 1.0, img++)
            {
                float d = square (x - xc) + square (y - ys);
                if (d >= LANCZOS_SUPPORT * LANCZOS_SUPPORT)
                    continue;

                d = lanczos_func [int (d * LANCZOS_TABLE_RES)];
                norm += d;
                sumR += d * img->red;
                sumG += d * img->green;
                sumB += d * img->blue;
            }
            img += This->width - LANCZOS_SUPPORT * 2 - 1;
        }
    else
    {
        for (; ys <= ye; ys += 1.0)
        {
            if (ys < 0 || ys >= This->height)
            {
                img += This->width;
                continue;
            }

            for (float xc = xs; xc <= xe; xc += 1.0, img++)
            {
                if (xc < 0 || xc >= This->width)
                    continue;

                float d = square (x - xc) + square (y - ys);
                if (d >= LANCZOS_SUPPORT * LANCZOS_SUPPORT)
                    continue;

                d = lanczos_func [int (d * LANCZOS_TABLE_RES)];
                norm += d;
                sumR += d * img->red;
                sumG += d * img->green;
                sumB += d * img->blue;
            }
            img += This->width - LANCZOS_SUPPORT * 2 - 1;
        }
        if (norm == 0.0)
        {
            out.Set (0, 0, 0);
            return;
        }
    }

    int rR = int (sumR / norm);
    int rG = int (sumG / norm);
    int rB = int (sumB / norm);
    out.Set (rR > 255 ? 255 : rR < 0 ? 0 : rR,
             rG > 255 ? 255 : rG < 0 ? 0 : rG,
             rB > 255 ? 255 : rB < 0 ? 0 : rB);
}
