/*
Copyright (C) 1996-2001 Id Software, Inc.
Copyright (C) 2002-2009 John Fitzgibbons and others
Copyright (C) 2010-2014 QuakeSpasm developers

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

*/
//image.c -- image loading

#include "quakedef.hpp"

#define STB_IMAGE_WRITE_IMPLEMENTATION
#define STB_IMAGE_WRITE_STATIC
#include "stb_image_write.hpp"

#define LODEPNG_NO_COMPILE_DECODER
#define LODEPNG_NO_COMPILE_CPP
#define LODEPNG_NO_COMPILE_ANCILLARY_CHUNKS
#define LODEPNG_NO_COMPILE_ERROR_TEXT
#include <cstring>

#include "lodepng.hpp"
#include "lodepng.cpp"

static char loadfilename[MAX_OSPATH]; //file scope so that error messages can use it

typedef struct stdio_buffer_s {
    FILE *f;
    unsigned char buffer[1024];
    int size;
    int pos;
} stdio_buffer_t;

static stdio_buffer_t *Buf_Alloc(FILE *f) {
    stdio_buffer_t *buf = (stdio_buffer_t *) calloc(1, sizeof(stdio_buffer_t));
    buf->f = f;
    return buf;
}

static void Buf_Free(stdio_buffer_t *buf) {
    free(buf);
}

static inline int Buf_GetC(stdio_buffer_t *buf) {
    if (buf->pos >= buf->size) {
        buf->size = fread(buf->buffer, 1, sizeof(buf->buffer), buf->f);
        buf->pos = 0;

        if (buf->size == 0)
            return EOF;
    }

    return buf->buffer[buf->pos++];
}

/*
============
Image_LoadImage

returns a pointer to hunk allocated RGBA data

TODO: search order: tga png jpg pcx lmp
============
*/
byte *Image_LoadImage(const char *name, int *width, int *height) {
    FILE *f;

    q_snprintf(loadfilename, sizeof(loadfilename), "%s.tga", name);
    COM_FOpenFile(loadfilename, &f, NULL);
    if (f)
        return Image_LoadTGA(f, width, height);

    q_snprintf(loadfilename, sizeof(loadfilename), "%s.pcx", name);
    COM_FOpenFile(loadfilename, &f, NULL);
    if (f)
        return Image_LoadPCX(f, width, height);

    return NULL;
}

//==============================================================================
//
//  TGA
//
//==============================================================================

typedef struct targaheader_s {
    unsigned char id_length, colormap_type, image_type;
    unsigned short colormap_index, colormap_length;
    unsigned char colormap_size;
    unsigned short x_origin, y_origin, width, height;
    unsigned char pixel_size, attributes;
} targaheader_t;

#define TARGAHEADERSIZE 18		/* size on disk */

int fgetLittleShort(FILE *f) {
    byte b1, b2;

    b1 = fgetc(f);
    b2 = fgetc(f);

    return (short) (b1 + b2 * 256);
}

int fgetLittleLong(FILE *f) {
    byte b1, b2, b3, b4;

    b1 = fgetc(f);
    b2 = fgetc(f);
    b3 = fgetc(f);
    b4 = fgetc(f);

    return b1 + (b2 << 8) + (b3 << 16) + (b4 << 24);
}

/*
============
Image_WriteTGA -- writes RGB or RGBA data to a TGA file

returns true if successful

TODO: support BGRA and BGR formats (since opengl can return them, and we don't have to swap)
============
*/
bool Image_WriteTGA(const char *name, byte *data, int width, int height, int bpp, bool upsidedown) {
    int handle, i, size, temp, bytes;
    char pathname[MAX_OSPATH];
    byte header[TARGAHEADERSIZE];

    Sys_mkdir(com_gamedir); //if we've switched to a nonexistant gamedir, create it now so we don't crash
    q_snprintf(pathname, sizeof(pathname), "%s/%s", com_gamedir, name);
    handle = Sys_FileOpenWrite(pathname);
    if (handle == -1)
        return false;

    std::memset(header, 0, TARGAHEADERSIZE);
    header[2] = 2; // uncompressed type
    header[12] = width & 255;
    header[13] = width >> 8;
    header[14] = height & 255;
    header[15] = height >> 8;
    header[16] = bpp; // pixel size
    if (upsidedown)
        header[17] = 0x20; //upside-down attribute

    // swap red and blue bytes
    bytes = bpp / 8;
    size = width * height * bytes;
    for (i = 0; i < size; i += bytes) {
        temp = data[i];
        data[i] = data[i + 2];
        data[i + 2] = temp;
    }

    Sys_FileWrite(handle, header, TARGAHEADERSIZE);
    Sys_FileWrite(handle, data, size);
    Sys_FileClose(handle);

    return true;
}

/*
=============
Image_LoadTGA
=============
*/
byte *Image_LoadTGA(FILE *fin, int *width, int *height) {
    int columns, rows, numPixels;
    byte *pixbuf;
    int row, column;
    byte *targa_rgba;
    int realrow; //johnfitz -- fix for upside-down targas
    bool upside_down; //johnfitz -- fix for upside-down targas
    stdio_buffer_t *buf;
    targaheader_t targa_header;

    targa_header.id_length = fgetc(fin);
    targa_header.colormap_type = fgetc(fin);
    targa_header.image_type = fgetc(fin);

    targa_header.colormap_index = fgetLittleShort(fin);
    targa_header.colormap_length = fgetLittleShort(fin);
    targa_header.colormap_size = fgetc(fin);
    targa_header.x_origin = fgetLittleShort(fin);
    targa_header.y_origin = fgetLittleShort(fin);
    targa_header.width = fgetLittleShort(fin);
    targa_header.height = fgetLittleShort(fin);
    targa_header.pixel_size = fgetc(fin);
    targa_header.attributes = fgetc(fin);

    if (targa_header.image_type == 1) {
        if (targa_header.pixel_size != 8 || targa_header.colormap_size != 24 || targa_header.colormap_length > 256)
            Sys_Error("Image_LoadTGA: %s has an %ibit palette", loadfilename, targa_header.colormap_type);
    } else {
        if (targa_header.image_type != 2 && targa_header.image_type != 10)
            Sys_Error("Image_LoadTGA: %s is not a type 2 or type 10 targa (%i)", loadfilename, targa_header.image_type);

        if (targa_header.colormap_type != 0 || (targa_header.pixel_size != 32 && targa_header.pixel_size != 24))
            Sys_Error("Image_LoadTGA: %s is not a 24bit or 32bit targa", loadfilename);
    }

    columns = targa_header.width;
    rows = targa_header.height;
    numPixels = columns * rows;
    upside_down = !(targa_header.attributes & 0x20); //johnfitz -- fix for upside-down targas

    targa_rgba = (byte *) Hunk_Alloc(numPixels * 4);

    if (targa_header.id_length != 0)
        fseek(fin, targa_header.id_length, SEEK_CUR); // skip TARGA image comment

    buf = Buf_Alloc(fin);

    if (targa_header.image_type == 1) // Uncompressed, paletted images
    {
        byte palette[256 * 4];
        int i;
        //palette data comes first
        for (i = 0; i < targa_header.colormap_length; i++) {
            //this palette data is bgr.
            palette[i * 3 + 2] = Buf_GetC(buf);
            palette[i * 3 + 1] = Buf_GetC(buf);
            palette[i * 3 + 0] = Buf_GetC(buf);
            palette[i * 3 + 3] = 255;
        }
        for (i = targa_header.colormap_length * 4; i < sizeof(palette); i++)
            palette[i] = 0;
        for (row = rows - 1; row >= 0; row--) {
            realrow = upside_down ? row : rows - 1 - row;
            pixbuf = targa_rgba + realrow * columns * 4;

            for (column = 0; column < columns; column++) {
                i = Buf_GetC(buf);
                *pixbuf++ = palette[i * 3 + 0];
                *pixbuf++ = palette[i * 3 + 1];
                *pixbuf++ = palette[i * 3 + 2];
                *pixbuf++ = palette[i * 3 + 3];
            }
        }
    } else if (targa_header.image_type == 2) // Uncompressed, RGB images
    {
        for (row = rows - 1; row >= 0; row--) {
            //johnfitz -- fix for upside-down targas
            realrow = upside_down ? row : rows - 1 - row;
            pixbuf = targa_rgba + realrow * columns * 4;
            //johnfitz
            for (column = 0; column < columns; column++) {
                unsigned char red, green, blue, alphabyte;
                switch (targa_header.pixel_size) {
                    case 24:
                        blue = Buf_GetC(buf);
                        green = Buf_GetC(buf);
                        red = Buf_GetC(buf);
                        *pixbuf++ = red;
                        *pixbuf++ = green;
                        *pixbuf++ = blue;
                        *pixbuf++ = 255;
                        break;
                    case 32:
                        blue = Buf_GetC(buf);
                        green = Buf_GetC(buf);
                        red = Buf_GetC(buf);
                        alphabyte = Buf_GetC(buf);
                        *pixbuf++ = red;
                        *pixbuf++ = green;
                        *pixbuf++ = blue;
                        *pixbuf++ = alphabyte;
                        break;
                }
            }
        }
    } else if (targa_header.image_type == 10) // Runlength encoded RGB images
    {
        unsigned char red, green, blue, alphabyte, packetHeader, packetSize, j;
        for (row = rows - 1; row >= 0; row--) {
            //johnfitz -- fix for upside-down targas
            realrow = upside_down ? row : rows - 1 - row;
            pixbuf = targa_rgba + realrow * columns * 4;
            //johnfitz
            for (column = 0; column < columns;) {
                packetHeader = Buf_GetC(buf);
                packetSize = 1 + (packetHeader & 0x7f);
                if (packetHeader & 0x80) // run-length packet
                {
                    switch (targa_header.pixel_size) {
                        case 24:
                            blue = Buf_GetC(buf);
                            green = Buf_GetC(buf);
                            red = Buf_GetC(buf);
                            alphabyte = 255;
                            break;
                        case 32:
                            blue = Buf_GetC(buf);
                            green = Buf_GetC(buf);
                            red = Buf_GetC(buf);
                            alphabyte = Buf_GetC(buf);
                            break;
                        default: /* avoid compiler warnings */
                            blue = red = green = alphabyte = 0;
                    }

                    for (j = 0; j < packetSize; j++) {
                        *pixbuf++ = red;
                        *pixbuf++ = green;
                        *pixbuf++ = blue;
                        *pixbuf++ = alphabyte;
                        column++;
                        if (column == columns) // run spans across rows
                        {
                            column = 0;
                            if (row > 0)
                                row--;
                            else
                                goto breakOut;
                            //johnfitz -- fix for upside-down targas
                            realrow = upside_down ? row : rows - 1 - row;
                            pixbuf = targa_rgba + realrow * columns * 4;
                            //johnfitz
                        }
                    }
                } else // non run-length packet
                {
                    for (j = 0; j < packetSize; j++) {
                        switch (targa_header.pixel_size) {
                            case 24:
                                blue = Buf_GetC(buf);
                                green = Buf_GetC(buf);
                                red = Buf_GetC(buf);
                                *pixbuf++ = red;
                                *pixbuf++ = green;
                                *pixbuf++ = blue;
                                *pixbuf++ = 255;
                                break;
                            case 32:
                                blue = Buf_GetC(buf);
                                green = Buf_GetC(buf);
                                red = Buf_GetC(buf);
                                alphabyte = Buf_GetC(buf);
                                *pixbuf++ = red;
                                *pixbuf++ = green;
                                *pixbuf++ = blue;
                                *pixbuf++ = alphabyte;
                                break;
                            default: /* avoid compiler warnings */
                                blue = red = green = alphabyte = 0;
                        }
                        column++;
                        if (column == columns) // pixel packet run spans across rows
                        {
                            column = 0;
                            if (row > 0)
                                row--;
                            else
                                goto breakOut;
                            //johnfitz -- fix for upside-down targas
                            realrow = upside_down ? row : rows - 1 - row;
                            pixbuf = targa_rgba + realrow * columns * 4;
                            //johnfitz
                        }
                    }
                }
            }
        breakOut:;
        }
    }

    Buf_Free(buf);
    fclose(fin);

    *width = (int) (targa_header.width);
    *height = (int) (targa_header.height);
    return targa_rgba;
}

//==============================================================================
//
//  PCX
//
//==============================================================================

typedef struct {
    char signature;
    char version;
    char encoding;
    char bits_per_pixel;
    unsigned short xmin, ymin, xmax, ymax;
    unsigned short hdpi, vdpi;
    byte colortable[48];
    char reserved;
    char color_planes;
    unsigned short bytes_per_line;
    unsigned short palette_type;
    char filler[58];
} pcxheader_t;

/*
============
Image_LoadPCX
============
*/
byte *Image_LoadPCX(FILE *f, int *width, int *height) {
    pcxheader_t pcx;
    int x, y, w, h, readbyte, runlength, start;
    byte *p, *data;
    byte palette[768];
    stdio_buffer_t *buf;

    start = ftell(f);
    //save start of file (since we might be inside a pak file, SEEK_SET might not be the start of the pcx)

    if (!fread(&pcx, sizeof(pcx), 1, f))
        Sys_Error("Failed reading header from '%s'", loadfilename);
    pcx.xmin = (unsigned short) LittleShort(pcx.xmin);
    pcx.ymin = (unsigned short) LittleShort(pcx.ymin);
    pcx.xmax = (unsigned short) LittleShort(pcx.xmax);
    pcx.ymax = (unsigned short) LittleShort(pcx.ymax);
    pcx.bytes_per_line = (unsigned short) LittleShort(pcx.bytes_per_line);

    if (pcx.signature != 0x0A)
        Sys_Error("'%s' is not a valid PCX file", loadfilename);

    if (pcx.version != 5)
        Sys_Error("'%s' is version %i, should be 5", loadfilename, pcx.version);

    if (pcx.encoding != 1 || pcx.bits_per_pixel != 8 || pcx.color_planes != 1)
        Sys_Error("'%s' has wrong encoding or bit depth", loadfilename);

    w = pcx.xmax - pcx.xmin + 1;
    h = pcx.ymax - pcx.ymin + 1;

    data = (byte *) Hunk_Alloc((w * h + 1) * 4); //+1 to allow reading padding byte on last line

    //load palette
    fseek(f, start + com_filesize - 768, SEEK_SET);
    if (!fread(palette, 768, 1, f))
        Sys_Error("Failed reading palette from '%s'", loadfilename);

    //back to start of image data
    fseek(f, start + sizeof(pcx), SEEK_SET);

    buf = Buf_Alloc(f);

    for (y = 0; y < h; y++) {
        p = data + y * w * 4;

        for (x = 0; x < (pcx.bytes_per_line);) //read the extra padding byte if necessary
        {
            readbyte = Buf_GetC(buf);

            if (readbyte >= 0xC0) {
                runlength = readbyte & 0x3F;
                readbyte = Buf_GetC(buf);
            } else
                runlength = 1;

            while (runlength--) {
                p[0] = palette[readbyte * 3];
                p[1] = palette[readbyte * 3 + 1];
                p[2] = palette[readbyte * 3 + 2];
                p[3] = 255;
                p += 4;
                x++;
            }
        }
    }

    Buf_Free(buf);
    fclose(f);

    *width = w;
    *height = h;
    return data;
}

//==============================================================================
//
//  STB_IMAGE_WRITE
//
//==============================================================================

static byte *CopyFlipped(const byte *data, int width, int height, int bpp) {
    int y, rowsize;
    byte *flipped;

    rowsize = width * (bpp / 8);
    flipped = (byte *) malloc(height * rowsize);
    if (!flipped)
        return NULL;

    for (y = 0; y < height; y++) {
        memcpy(&flipped[y * rowsize], &data[(height - 1 - y) * rowsize], rowsize);
    }
    return flipped;
}

/*
============
Image_WriteJPG -- writes using stb_image_write

returns true if successful
============
*/
bool Image_WriteJPG(const char *name, byte *data, int width, int height, int bpp, int quality, bool upsidedown) {
    unsigned error;
    char pathname[MAX_OSPATH];
    byte *flipped;
    int bytes_per_pixel;

    if (!(bpp == 32 || bpp == 24))
        Sys_Error("bpp not 24 or 32");

    bytes_per_pixel = bpp / 8;

    Sys_mkdir(com_gamedir); //if we've switched to a nonexistant gamedir, create it now so we don't crash
    q_snprintf(pathname, sizeof(pathname), "%s/%s", com_gamedir, name);

    if (!upsidedown) {
        flipped = CopyFlipped(data, width, height, bpp);
        if (!flipped)
            return false;
    } else
        flipped = data;

    error = stbi_write_jpg(pathname, width, height, bytes_per_pixel, flipped, quality);
    if (!upsidedown)
        free(flipped);

    return (error != 0);
}

bool Image_WritePNG(const char *name, byte *data, int width, int height, int bpp, bool upsidedown) {
    unsigned error;
    char pathname[MAX_OSPATH];
    byte *flipped;
    unsigned char *filters;
    unsigned char *png;
    size_t pngsize;
    LodePNGState state;

    if (!(bpp == 32 || bpp == 24))
        Sys_Error("bpp not 24 or 32");

    Sys_mkdir(com_gamedir); //if we've switched to a nonexistant gamedir, create it now so we don't crash
    q_snprintf(pathname, sizeof(pathname), "%s/%s", com_gamedir, name);

    flipped = (!upsidedown) ? CopyFlipped(data, width, height, bpp) : data;
    filters = (unsigned char *) malloc(height);
    if (!filters || !flipped) {
        if (!upsidedown)
            free(flipped);
        free(filters);
        return false;
    }

    // set some options for faster compression
    lodepng_state_init(&state);
    state.encoder.zlibsettings.use_lz77 = 0;
    state.encoder.auto_convert = 0;
    state.encoder.filter_strategy = LFS_PREDEFINED;
    memset(filters, 1, height); //use filter 1; see https://www.w3.org/TR/PNG-Filters.html
    state.encoder.predefined_filters = filters;

    if (bpp == 24) {
        state.info_raw.colortype = LCT_RGB;
        state.info_png.color.colortype = LCT_RGB;
    } else {
        state.info_raw.colortype = LCT_RGBA;
        state.info_png.color.colortype = LCT_RGBA;
    }

    error = lodepng_encode(&png, &pngsize, flipped, width, height, &state);
    if (error == 0) lodepng_save_file(png, pngsize, pathname);
#ifdef LODEPNG_COMPILE_ERROR_TEXT
	else console::info("WritePNG: %s\n", lodepng_error_text(error));
#endif

    lodepng_state_cleanup(&state);
    lodepng_free(png); /* png was allocated by lodepng */
    free(filters);
    if (!upsidedown) {
        free(flipped);
    }

    return (error == 0);
}
