/*
 * xcursorgen.c
 *
 * Copyright (C) 2002 Manish Singh
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation, and that the name of Manish Singh not be used in
 * advertising or publicity pertaining to distribution of the software without
 * specific, written prior permission.  Manish Singh makes no
 * representations about the suitability of this software for any purpose.  It
 * is provided "as is" without express or implied warranty.
 *
 * MANISH SINGH DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
 * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
 * EVENT SHALL MANISH SINGH BE LIABLE FOR ANY SPECIAL, INDIRECT OR
 * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
 * DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
 * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 * PERFORMANCE OF THIS SOFTWARE.
 */

#include "config.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>

#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xcursor/Xcursor.h>

#include <png.h>

static const char *ProgramName;

struct flist {
    int size;
    int xhot, yhot;
    int delay;
    char *pngfile;
    struct flist *next;
};

static void
usage(const char *name)
{
    fprintf(stderr,
            "usage: %s [-V] [--version] [-?] [--help] [-p <dir>] [--prefix <dir>] [CONFIG [OUT]]\n%s",
            name,
            "Generate an Xcursor file from a series of PNG images\n"
            "\n"
            "  -V, --version      display the version number and exit\n"
            "  -?, --help         display this message and exit\n"
            "  -p, --prefix <dir> find cursor images in <dir>\n"
            "\n"
            "With no CONFIG, or when CONFIG is -, read standard input. "
            "Same with OUT and\n"
            "standard output.\n");
}

static int
read_config_file(const char *config, struct flist **list)
{
    FILE *fp;
    char line[4096], pngfile[4000];
    int size, xhot, yhot, delay;
    struct flist *start = NULL, *end = NULL, *curr;
    int count = 0;

    if (strcmp(config, "-") != 0) {
        fp = fopen(config, "r");
        if (!fp) {
            *list = NULL;
            return 0;
        }
    }
    else
        fp = stdin;

    while (fgets(line, sizeof(line), fp) != NULL) {
        if (line[0] == '#')
            continue;

        switch (sscanf (line, "%d %d %d %3999s %d", &size, &xhot, &yhot,
                        pngfile, &delay)) {
        case 4:
            delay = 50;
            break;
        case 5:
            break;
        default:
            {
                fprintf(stderr, "%s: Bad config file data on line %d of %s\n",
                        ProgramName, count + 1,
                        strcmp(config, "-") ? config : "stdin");
                fclose(fp);
                return 0;
            }
        }

        curr = malloc(sizeof(struct flist));
        if (curr == NULL) {
            fprintf(stderr, "%s: malloc() failed: %s\n",
                    ProgramName, strerror(errno));
            fclose(fp);
            return 0;
        }

        curr->size = size;
        curr->xhot = xhot;
        curr->yhot = yhot;

        curr->delay = delay;

        curr->pngfile = strdup(pngfile);
        if (curr->pngfile == NULL) {
            fprintf(stderr, "%s: strdup() failed: %s\n",
                    ProgramName, strerror(errno));
            fclose(fp);
            free(curr);
            return 0;
        }

        curr->next = NULL;

        if (start) {
            end->next = curr;
            end = curr;
        }
        else {
            start = curr;
            end = curr;
        }

        count++;
    }

    fclose(fp);

    *list = start;
    return count;
}

#define div_255(x) (((x) + 0x80 + (((x) + 0x80) >> 8)) >> 8)

static void
premultiply_data(png_structp png, png_row_infop row_info, png_bytep data)
{
    png_size_t i;

    for (i = 0; i < row_info->rowbytes; i += 4) {
        unsigned char *base  = &data[i];
        unsigned char  blue  = base[0];
        unsigned char  green = base[1];
        unsigned char  red   = base[2];
        unsigned char  alpha = base[3];
        XcursorPixel   p;

        red     = div_255((unsigned) red * (unsigned) alpha);
        green   = div_255((unsigned) green * (unsigned) alpha);
        blue    = div_255((unsigned) blue * (unsigned) alpha);
        p       = (alpha << 24) | (red << 16) | (green << 8) | (blue << 0);
        memcpy(base, &p, sizeof(XcursorPixel));
    }
}

static XcursorImage *
load_image(struct flist *list, const char *prefix)
{
    XcursorImage *image;
    png_structp png;
    png_infop info;
    png_bytepp rows;
    FILE *fp;
    png_uint_32 i;
    png_uint_32 width, height;
    int depth, color, interlace;
    char *file;

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

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

    if (setjmp(png_jmpbuf(png))) {
        png_destroy_read_struct(&png, &info, NULL);
        return NULL;
    }

    if (prefix) {
#ifdef HAVE_ASPRINTF
        if (asprintf(&file, "%s/%s", prefix, list->pngfile) == -1) {
            fprintf(stderr, "%s: asprintf() failed: %s\n",
                    ProgramName, strerror(errno));
            png_destroy_read_struct(&png, &info, NULL);
            return NULL;
        }
#else
        file = malloc(strlen(prefix) + 1 + strlen(list->pngfile) + 1);
        if (file == NULL) {
            fprintf(stderr, "%s: malloc() failed: %s\n",
                    ProgramName, strerror(errno));
            png_destroy_read_struct(&png, &info, NULL);
            return NULL;
        }
        strcpy(file, prefix);
        strcat(file, "/");
        strcat(file, list->pngfile);
#endif
    }
    else
        file = list->pngfile;
    fp = fopen(file, "rb");
    if (prefix)
        free(file);

    if (fp == NULL) {
        png_destroy_read_struct(&png, &info, NULL);
        return NULL;
    }

    png_init_io(png, fp);
    png_read_info(png, info);
    png_get_IHDR(png, info, &width, &height, &depth, &color, &interlace,
                 NULL, NULL);

    /* TODO: More needs to be done here maybe */

    if (color == PNG_COLOR_TYPE_PALETTE && depth <= 8)
        png_set_expand(png);

    if (color == PNG_COLOR_TYPE_GRAY && depth < 8)
        png_set_expand(png);

    if (png_get_valid(png, info, PNG_INFO_tRNS))
        png_set_expand(png);

    if (depth == 16)
        png_set_strip_16(png);

    if (depth < 8)
        png_set_packing(png);

    if (color == PNG_COLOR_TYPE_GRAY || color == PNG_COLOR_TYPE_GRAY_ALPHA)
        png_set_gray_to_rgb(png);

    if (interlace != PNG_INTERLACE_NONE)
        png_set_interlace_handling(png);

    png_set_bgr(png);
    png_set_filler(png, 255, PNG_FILLER_AFTER);

    png_set_read_user_transform_fn(png, premultiply_data);

    png_read_update_info(png, info);

    image = XcursorImageCreate(width, height);
    if (image == NULL) {
        fprintf (stderr,
                 "%s: XcursorImageCreate() failed to create %u x %u image\n"
                 " for file %s\n",
                 ProgramName, width, height, list->pngfile);
        fclose(fp);
        png_destroy_read_struct(&png, &info, NULL);
        return NULL;
    }

    image->size = list->size;
    image->xhot = list->xhot;
    image->yhot = list->yhot;
    image->delay = list->delay;

    rows = malloc(sizeof(png_bytep) * height);
    if (rows == NULL) {
        fclose(fp);
        png_destroy_read_struct(&png, &info, NULL);
        return NULL;
    }

    for (i = 0; i < height; i++)
        rows[i] = (png_bytep) (image->pixels + i * width);

    png_read_image(png, rows);
    png_read_end(png, info);

    free(rows);
    fclose(fp);
    png_destroy_read_struct(&png, &info, NULL);

    return image;
}

static int
write_cursors(int count, struct flist *list,
              const char *filename, const char *prefix)
{
    XcursorImages *cimages;
    XcursorImage *image;
    int i;
    FILE *fp;
    int ret;

    if (strcmp(filename, "-") != 0) {
        fp = fopen(filename, "wb");
        if (!fp)
            return 1;
    }
    else
        fp = stdout;

    cimages = XcursorImagesCreate(count);

    cimages->nimage = count;

    for (i = 0; i < count; i++, list = list->next) {
        image = load_image(list, prefix);
        if (image == NULL) {
            fprintf(stderr, "%s: PNG error while reading %s\n",
                    ProgramName, list->pngfile);
            fclose(fp);
            return 1;
        }

        cimages->images[i] = image;
    }

    ret = XcursorFileSaveImages(fp, cimages);

    fclose(fp);

    return ret ? 0 : 1;
}

static int
check_image(char *image)
{
    unsigned int width, height;
    unsigned char *data;
    int x_hot, y_hot;
    unsigned char hash[XCURSOR_BITMAP_HASH_SIZE];
    int i;

    if (XReadBitmapFileData(image, &width, &height, &data, &x_hot, &y_hot)
        != BitmapSuccess) {
        fprintf(stderr, "%s: Can't open bitmap file \"%s\"\n", ProgramName,
                image);
        return 1;
    }
    else {
        XImage ximage = {
            .width 		= width,
            .height             = height,
            .depth              = 1,
            .bits_per_pixel     = 1,
            .xoffset            = 0,
            .format             = XYPixmap,
            .data               = (char *) data,
            .byte_order         = LSBFirst,
            .bitmap_unit        = 8,
            .bitmap_bit_order   = LSBFirst,
            .bitmap_pad         = 8,
            .bytes_per_line     = (width + 7) / 8
        };
        XcursorImageHash(&ximage, hash);
    }
    printf("%s: ", image);
    for (i = 0; i < XCURSOR_BITMAP_HASH_SIZE; i++)
        printf("%02x", hash[i]);
    printf("\n");
    return 0;
}

int
main(int argc, char *argv[])
{
    struct flist *list;
    int count;
    const char *in = NULL, *out = NULL;
    const char *prefix = NULL;
    int i;

    ProgramName = argv[0];

    for (i = 1; i < argc; i++) {
        if (strcmp(argv[i], "-V") == 0 || strcmp(argv[i], "--version") == 0) {
            printf("xcursorgen version %s\n", PACKAGE_VERSION);
            return 0;
        }

        if (strcmp(argv[i], "-?") == 0 || strcmp(argv[i], "--help") == 0) {
            usage(argv[0]);
            return 0;
        }
        if (strcmp(argv[i], "-image") == 0) {
            int ret = 0;

            while (argv[++i]) {
                if (check_image(argv[i]))
                    ret = i;
            }
            return ret;
        }
        if (strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--prefix") == 0) {
            i++;
            if (argv[i] == NULL) {
                fprintf(stderr, "%s: %s requires an argument\n",
                        argv[0], argv[i - 1]);
                usage(argv[0]);
                return 1;
            }
            prefix = argv[i];
            continue;
        }

        if (!in)
            in = argv[i];
        else if (!out)
            out = argv[i];
        else {
            fprintf(stderr, "%s: unexpected argument '%s'\n", argv[0], argv[i]);
            usage(argv[0]);
            return 1;
        }
    }

    if (!in)
        in = "-";
    if (!out)
        out = "-";

    count = read_config_file(in, &list);
    if (count == 0) {
        fprintf(stderr, "%s: Error reading config file %s\n", argv[0],
                strcmp(in, "-") ? in : "from stdin");
        return 1;
    }

    return write_cursors(count, list, out, prefix);
}
