/*
 * luttool.c
 *
 * Utility to apply LUTs to images
 *
 * Author: Dario Rodriguez antartica@whereismybit.com
 * This program is licensed under the terms of the MIT license,
 * with an exception: the license text may be substituted for a
 * link to it. MIT license text: https://spdx.org/licenses/MIT.html
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <png.h>

#define DEFAULT_IDENTITYSIZE 12 /* Same as RawTherapee's HaldCLUTs, 1728x1728 AKA (12*12)*12x12*(12*12), equivalent to a cube with 144px wide sides*/
#define DEFAULT_QUALITY 85
/* #define JPEG_SUPPORTED */

int luttool_applylut(char *lutfilenamelist,char *infilename, char *outfilename,int quality,int identitysize);

int util_pnginfo(char *filename, int *width, int *height);
int util_pngload(char *filename, char **bufimg, int *sizebufimg, int *width, int *height);
int util_pngsave(char *filename, char *bufimg, int width, int height);
int util_identitygen(char **bufimg, int *sizebufimg, int identitysize, int *width, int *height);
int luttool_rgba2lut4096(char *rgba, int width, int height, char **lut4096, int *sizelut4096);
int luttool_rgba2lut4096_interpolated(char *rgba, int width, int height, char **lut4096, int *sizelut4096);
int luttool_applylut4096(char *lut4096,char *imgbuf,int imgwidth,int imgheight);

int
main(int argc, char *argv[])
{
        int firstarg,outfilenamearg;
        int identitysize;
        int quality;
        char *outfilename;
        char *lutlist;
        int i;
        int flagseveralinfile;
        char bufoutfilename[1024];
        char *currentoutfilename;
        if(argc<2 || strcmp(argv[argc-1],"--help")==0) {
#ifdef JPEG_SUPPORTED
                printf("Syntax: %s [-q jpegquality] [-n identitylevel] {identity | lutfile[,lutfile[,...]]} { identity | infile1 } [infile2 [...]] -o outfile\n",argv[0]);
                printf("Example: %s haldclut.png sprite.png -o result.png\n",argv[0]);
                printf("Example: %s -n 16 identity identity -o identity.png\n",argv[0]);
                printf("Example: %s -q 85 haldclut.png infile.png -o outfile.jpg\n",argv[0]);
#else
                printf("Syntax: %s [-n identitylevel] {identity | lutfile[,lutfile[,...]]} { identity | infile1 } [infile2 [...]] -o outfile\n",argv[0]);
                printf("Example: %s haldclut.png sprite.png -o result.png\n",argv[0]);
                printf("Example: %s -n 16 identity identity -o identity.png\n",argv[0]);
#endif
                return(1);
        }
        /* parse arguments */
        identitysize=DEFAULT_IDENTITYSIZE;
        quality=DEFAULT_QUALITY;
        for(firstarg=1;firstarg<(argc-1) && (strcmp(argv[firstarg],"-n")==0 || (strcmp(argv[firstarg],"-q")==0));) {
                if(strcmp(argv[firstarg],"-n")==0) {
                        identitysize=atoi(argv[firstarg+1]);
                        if(identitysize<2 || identitysize>16) {
                                fprintf(stderr,"%s: ERROR: the parameter to -n must be between 2 and 16 (default: %i)\n",argv[0],DEFAULT_IDENTITYSIZE);
                                return(1);
                        }
                } else if(strcmp(argv[firstarg],"-q")==0) {
                        quality=atoi(argv[firstarg+1]);
                        if(quality<1 || quality>100) {
                                fprintf(stderr,"%s: ERROR: the parameter to -q must be between 1 and 100 (default: %i)\n",argv[0],DEFAULT_QUALITY);
                                return(1);
                        }
                }
                firstarg+=2;
        }
        for(outfilenamearg=-1,outfilename=NULL,i=1;i<(argc-1);i++) {
                if(strcmp(argv[i],"-o")==0) {
                        outfilename=argv[i+1];
                        outfilenamearg=i;
                        break;
                }
        }
        if(outfilename==NULL) {
                fprintf(stderr,"%s: ERROR: no output file specified\n",argv[0]);
                return(1);
        }
        firstarg+=((firstarg==outfilenamearg)?2:0);
        lutlist=argv[firstarg];
        firstarg++;
        flagseveralinfile=((argc-firstarg-((outfilenamearg>=firstarg)?2:0))>1)?1:0;
        /* process files sequentially, skipping the "-o outputfilename" */
        for(i=firstarg;i<argc;i++) {
                if(i==outfilenamearg) {
                        i++;
                        continue;
                }
                currentoutfilename=outfilename;
                if(flagseveralinfile) {
                        char *ext;
                        int l;
                        strncpy(bufoutfilename,argv[i],sizeof(bufoutfilename));
                        bufoutfilename[sizeof(bufoutfilename)-1]='\0';
                        if((ext=strrchr(bufoutfilename,'.'))!=NULL)
                                *ext='\0';
                        l=strlen(bufoutfilename);
                        snprintf(bufoutfilename+l,sizeof(bufoutfilename)-l,"_%s",outfilename);
                        bufoutfilename[sizeof(bufoutfilename)-1]='\0';
                        currentoutfilename=bufoutfilename;
                }
                if(luttool_applylut(lutlist,argv[i],currentoutfilename,quality,identitysize)!=0) {
                        fprintf(stderr,"%s: ERROR: couldn't apply LUTs \"%s\" to \"%s\" or couldn't write \"%s\"\n",argv[0],lutlist,argv[i],currentoutfilename);
                        return(1);
                }
        }
        return(0);
}

int
luttool_applylut(char *lutlist,char *infilename, char *outfilename,int quality,int identitysize)
{
        char *curlut,*next;
        char filename[1024];
        int l;
        int in_error;
        char *lutbuf,*imgbuf,*rgblutbuf;
        int sizelutbuf,lutwidth,lutheight;
        int sizeimgbuf,imgwidth,imgheight;
        int sizergblutbuf;
        lutbuf=imgbuf=rgblutbuf=NULL;
        sizelutbuf=sizeimgbuf=sizergblutbuf=0;
        lutwidth=lutheight=imgwidth=imgheight=0;
        if(lutlist==NULL || infilename==NULL || outfilename==NULL)
                return(-1); /* sanity check error */
        in_error=0;
        /* load the image to process */
        if(strcmp(infilename,"identity")==0) {
                 if(util_identitygen(&imgbuf,&sizeimgbuf,identitysize,&imgwidth,&imgheight)==-1)
                         in_error=1;
        } else {
                if(util_pngload(infilename,&imgbuf,&sizeimgbuf,&imgwidth,&imgheight)!=0)
                        in_error=1;
        }
        /* apply all luts */
        for(curlut=lutlist,next=strchr(curlut,','),next=((next==NULL)?(curlut+strlen(curlut)):next)
          ;in_error==0 && *curlut!='\0'
          ;curlut=next+((*next==',')?1:0),next=strchr(curlut,','),next=((next==NULL)?(curlut+strlen(curlut)):next)) {
                l=next-curlut;
                l=(l>(sizeof(filename)-1))?(sizeof(filename)-1):l;
                memcpy(filename,curlut,l);
                filename[l]='\0';
                if(strcmp(filename,"identity")==0) {
                        if(util_identitygen(&lutbuf,&sizelutbuf,identitysize,&lutwidth,&lutheight)==-1) {
                                in_error=1;
                                break; /* couldn't generate identity lut */
                        }
                } else {
                        if(util_pngload(filename,&lutbuf,&sizelutbuf,&lutwidth,&lutheight)==-1) {
                                in_error=1;
                                break; /* couldn't load lut */
                        }
                }
                if(luttool_rgba2lut4096(lutbuf, lutwidth, lutheight, &rgblutbuf, &sizergblutbuf)!=0) {
                        in_error=1;
                        break; /* couldn't process lut */
                }
                if(luttool_applylut4096(rgblutbuf,imgbuf,imgwidth,imgheight)!=0) {
                        in_error=1;
                        break; /* couldn't apply lut */
                }
        }
        /* save the result */
        if(in_error==0) {
                if(util_pngsave(outfilename,imgbuf,imgwidth,imgheight)!=0)
                        in_error=1; /* couldn't write */
        }
        /* cleanup and exit */
        if(lutbuf!=NULL)
                free(lutbuf),lutbuf=NULL;
        if(imgbuf!=NULL)
                free(imgbuf),imgbuf=NULL;
        if(rgblutbuf!=NULL)
                free(rgblutbuf),rgblutbuf=NULL;
        return((in_error!=0)?-1:0);
}



int
util_pnginfo(char *filename, int *width, int *height)
{
        return(util_pngload(filename,NULL,NULL,width,height));
}

int
util_pngload(char *filename, char **bufimg, int *sizebufimg, int *width, int *height)
{
        FILE *f;
        png_structp png;
        png_infop info;
        png_byte color_type,bit_depth;
        png_bytep *rowpointers;
        int reqsize;
        int i,pitch;
        if(filename==NULL || (bufimg==NULL && sizebufimg!=NULL) || (bufimg!=NULL && sizebufimg==NULL) || width==NULL || height==NULL)
                return(-1); /* sanity check failed */
        if((f=fopen(filename,"rb"))==NULL)
                return(-1); /* couldn't open file */
        if((png=png_create_read_struct(PNG_LIBPNG_VER_STRING,NULL,NULL,NULL))==NULL
          || (info=png_create_info_struct(png))==NULL
          || setjmp(png_jmpbuf(png))) {
                if(png!=NULL)
                        png_destroy_read_struct(&png,&info,NULL),png=NULL,info=NULL;
                fclose(f),f=NULL;
                return(-1); /* couldn't init png library */
        }
        png_init_io(png,f);
        png_read_info(png,info);
        *width=png_get_image_width(png,info);
        *height=png_get_image_height(png,info);
        /* special case: if bufimg==NULL, we only want the width/height */
        if(bufimg==NULL) {
                png_destroy_read_struct(&png,&info,NULL),png=NULL,info=NULL;
                fclose(f),f=NULL;
                return(0); /* all done */
        }
        /* make sure we have enough space for the new image in 8-bit RGBA */
        reqsize=(*width)*(*height)*4;
        if(*sizebufimg<reqsize) {
                char *newbufimg;
                if((newbufimg=realloc(*bufimg,reqsize))==NULL) {
                      png_destroy_read_struct(&png,&info,NULL),png=NULL,info=NULL;
                      fclose(f),f=NULL;
                      return(-1); /* insuf. mem. for resulting image */
                }
                *bufimg=newbufimg;
                *sizebufimg=reqsize;
        }
        /* make sure we get 8-bit RGBA data, regardless of the file's bit_depth and color_type */
        if((bit_depth=png_get_bit_depth(png,info))==16)
                png_set_strip_16(png);
        color_type=png_get_color_type(png,info);
        if(color_type==PNG_COLOR_TYPE_PALETTE)
                png_set_palette_to_rgb(png);
        if(color_type==PNG_COLOR_TYPE_GRAY && bit_depth<8)
                png_set_expand_gray_1_2_4_to_8(png);
        if(png_get_valid(png,info,PNG_INFO_tRNS))
                png_set_tRNS_to_alpha(png);
        if(color_type==PNG_COLOR_TYPE_RGB
          || color_type==PNG_COLOR_TYPE_GRAY
          || color_type==PNG_COLOR_TYPE_PALETTE) {
                png_set_filler(png,0xff,PNG_FILLER_AFTER);
        }
        if(color_type==PNG_COLOR_TYPE_GRAY
          || color_type==PNG_COLOR_TYPE_GRAY_ALPHA) {
                png_set_gray_to_rgb(png);
        }
        /* read the image */
        png_read_update_info(png,info);         
        if((rowpointers=(png_bytep *)malloc(sizeof(png_bytep)*(*height)))==NULL) {
                png_destroy_read_struct(&png,&info,NULL),png=NULL,info=NULL;
                fclose(f),f=NULL;
                return(-1); /* insuf. mem. for temporary data */
        }
        for(i=0,pitch=(*width)*4;i<(*height);i++)
                rowpointers[i]=(png_byte *)((*bufimg)+i*pitch);
        png_read_image(png,rowpointers);
        /* cleanup */
        free(rowpointers),rowpointers=NULL;
        fclose(f),f=NULL;
        png_destroy_read_struct(&png,&info,NULL),png=NULL,info=NULL;
        return(0);
}

int
util_pngsave(char *filename, char *bufimg, int width, int height)
{
        FILE *f;
        png_structp png;
        png_infop info;
        int i,pitch;
        char *ptr;
        if((f=fopen(filename,"wb"))==NULL)
                return(-1); /* couldn't open file for writing */
        if((png=png_create_write_struct(PNG_LIBPNG_VER_STRING,NULL,NULL,NULL))==NULL
          || (info=png_create_info_struct(png))==NULL
          || setjmp(png_jmpbuf(png))) {
                if(png!=NULL)
                        png_destroy_write_struct(&png,&info),png=NULL,info=NULL;
                fclose(f),f=NULL;
                return(-1); /* couldn't init png library */
        }
        png_init_io(png,f);
        png_set_IHDR(png,info,width,height,8,PNG_COLOR_TYPE_RGBA,
          PNG_INTERLACE_NONE,PNG_COMPRESSION_TYPE_BASE,PNG_FILTER_TYPE_BASE);
        png_write_info(png,info);
        for(i=0,ptr=bufimg,pitch=width*4;i<height;i++,ptr+=pitch)
                png_write_row(png,(png_const_bytep)ptr);
        png_write_end(png,NULL);
        fclose(f),f=NULL;
        png_destroy_write_struct(&png,&info),png=NULL,info=NULL;
        return(0);
}

int
util_identitygen(char **bufimg, int *sizebufimg, int identitysize, int *width, int *height)
{
        int x,y,r,g,b;
        int isizesq,isizesqminusone;
        int osizesqminusone;
        int reqsize;
        int l;
        unsigned char *ptr;
        if(bufimg==NULL || sizebufimg==NULL || identitysize<=1 || identitysize>16 || width==NULL || height==NULL)
                return(-1);
        l=*width=*height=identitysize*identitysize*identitysize;
        reqsize=(*width)*(*height)*4;
        if(*sizebufimg<reqsize) {
                char *newbufimg;
                if((newbufimg=realloc(*bufimg,reqsize))==NULL)
                      return(-1); /* insuf. mem. for resulting image */
                *bufimg=newbufimg;
                *sizebufimg=reqsize;
        }
        isizesq=identitysize*identitysize;
        isizesqminusone=isizesq-1;
        osizesqminusone=256-1;
        for(y=0,ptr=(unsigned char *)(*bufimg);y<l;y++) {
                for(x=0;x<l;x++,ptr+=4) {
                        r=x%isizesq;
                        g=(y%identitysize)*identitysize+(x/isizesq);
                        b=y/identitysize;
                        ptr[0]=r*osizesqminusone/isizesqminusone;
                        ptr[1]=g*osizesqminusone/isizesqminusone;
                        ptr[2]=b*osizesqminusone/isizesqminusone;
                        ptr[3]=0xff;
                }
        }
        return(0);
}

int
luttool_rgba2lut4096(char *rgba, int width, int height, char **lut4096, int *sizelut4096)
{
        int n;
        int reqsize;
        int x,y;
        int r,g,b;
        int isize,isizesq,isizesqminusone;
        int osize,osizesq,osizesqminusone;
        unsigned char *orig,*dest;
        if(rgba==NULL || width!=height || lut4096==NULL || sizelut4096==NULL)
                return(-1); /* sanity check error */
        /* check if size is a supported size */
        for(n=2;n<=16 && width!=(n*n*n);n++)
                ;
        if(n>16)
                return(-1); /* invalid size */
        /* expand lut4096 as necessary */
        reqsize=4096*4096*3;
        if(*sizelut4096<reqsize) {
                char *newlut4096;
                if((newlut4096=realloc(*lut4096,reqsize))==NULL)
                      return(-1); /* insuf. mem. for resulting lut */
                *lut4096=newlut4096;
                *sizelut4096=reqsize;
        }
        /* do the conversion */
        isize=n;
        isizesq=n*n;
        isizesqminusone=isizesq-1;
        osize=16;
        osizesq=osize*osize;
        osizesqminusone=osizesq-1;
        for(r=0;r<osizesq;r++) {
                for(g=0;g<osizesq;g++) {
                        for(b=0;b<osizesq;b++) {
                                x=((isizesqminusone*r)/osizesqminusone)+(((isizesqminusone*g)/osizesqminusone)%isize)*isizesq;
                                y=((isizesqminusone*b)/osizesqminusone)*isize+(((isizesqminusone*g)/osizesqminusone)/isize);
                                orig=(unsigned char *) (rgba+((y*width+x)<<2));
                                dest=(unsigned char *) ((*lut4096)+((r<<16)+(g<<8)+b)*3);
                                dest[0]=orig[0];
                                dest[1]=orig[1];
                                dest[2]=orig[2];
                        }
                }
        }
        return(0);
}

int
luttool_applylut4096(char *lut4096,char *imgbuf,int imgwidth,int imgheight)
{
        int x,y;
        unsigned char *orig;
        unsigned char *lut;
        if(lut4096==NULL || imgbuf==NULL || imgwidth<=0 || imgheight<=0)
                return(-1);
        for(y=0,orig=(unsigned char *)imgbuf;y<imgheight ;y++) {
                for(x=0;x<imgwidth;x++,orig+=4) {
                        lut=((unsigned char *)lut4096)+((((int)(orig[0]))<<16)+(((int)(orig[1]))<<8)+((int)(orig[2])))*3;
                        orig[0]=lut[0];
                        orig[1]=lut[1];
                        orig[2]=lut[2];
                }
        }
        return(0);
}