/*
 * transparency.c --
 *
 *      This file implements the  
 *      ::imgtools::transparency command
 *
 * Copyright (c) 2016-2017 by Simon Bachmann <simonbachmann@bluewin.ch>
 * All rights reserved.
 *
 * Permission is hereby granted, without written agreement and without
 * license or royalty fees, to use, copy, modify, and distribute this
 * software and its documentation for any purpose, provided that the
 * above copyright notice and the following two paragraphs appear in
 * all copies of this software.
 * 
 */

#include <tcl.h>
#include <tk.h> 
#include <string.h>
#include <stdio.h>
#include <math.h>
#include <limits.h>

#ifdef _WINDOWS
   #include <windows.h>
#endif

//#include "config.h"
#include "imgtoolsDecl.h"
#include "transparency.h"
#include "debugmsg.h"

#include <assert.h>

/*
 * === Structs and enums ===
 */
 enum Subcommand {
    SET,
    INCREASE,
    DECREASE,
    MASK,
    GET
};
static const char *const subcommandNames[] = {
    "set",
    "increase",
    "decrease",
    "mask",
    "get",
    NULL
};

/*
 * === Prototypes ===
 */
static int Imgtools_TransparencyModify (Tcl_Interp *interp, int objc, 
    Tcl_Obj *const objv[], enum Subcommand subcommand);
static int Imgtools_TransparencyGet (Tcl_Interp *interp, int objc, 
    Tcl_Obj *const objv[], enum Subcommand subcommand);
static int Imgtools_TransparencyMask (Tcl_Interp *interp, int objc, 
    Tcl_Obj *const objv[], enum Subcommand subcommand);

/*
 *--------------------------------------------------------------
 *
 * Imgtools_Transparency --
 *
 *      Main Function for modifiying alpha channel. Called by the user via
 *      the corresponding command (::imgtools::transparency)
 *
 * Results:
 *      Standard Tcl result.
 *
 * Side effects:
 *      writes a modified image (according to options) to destImg. 
 *      destImg must exist, its contents will be partially or  
 *      completely overwritten. If the -width or -height options 
 *      for the dest image were set, these sizes are respected.
 *
 *--------------------------------------------------------------
 */
int Imgtools_Transparency(clientData, interp, objc, objv) 
ClientData clientData;  /* not used */
Tcl_Interp *interp;     /* current interpreter, used for error messages */
int objc;               /* argument count */
Tcl_Obj *const objv[];  /* argumetn values */
{
    int subcommand;
    
    /*
     * Parse subcommand
     */
    if (objc < 2) {
        Tcl_WrongNumArgs(interp, 1, objv, "subcommand ?options?");
        Tcl_SetErrorCode(interp, "IMGTOOLS", "OPTIONS", "WRONG_NUM_ARGS", NULL);
        return TCL_ERROR;
    }
    if (Tcl_GetIndexFromObj(interp, objv[1], subcommandNames, "subcommand", 
            TCL_EXACT, &subcommand) != TCL_OK) {
        Tcl_SetErrorCode(interp, "IMGTOOLS", "OPTIONS", "BAD_SUBCOMMAND", NULL);
        return TCL_ERROR;
    }
    
    /*
     * Call the appropriate functions
     */
    switch (subcommand) {
        case SET:
        case INCREASE:
        case DECREASE:
            return (Imgtools_TransparencyModify(interp, objc, objv, subcommand));
            break;
        case GET:
            return (Imgtools_TransparencyGet(interp, objc, objv, subcommand));
            break;
        case MASK:
            return (Imgtools_TransparencyMask(interp, objc, objv, subcommand));
        default:
            Tcl_Panic("Transparency: wrong subcommand index");
    }
        
    assert(0);
    
    return TCL_ERROR;
}

/*
 *--------------------------------------------------------------
 *
 * Imgtools_TransparencyModify --
 *
 *      Implement the decrease, increase and set subcommands
 *
 * Results:
 *      Standard Tcl result.
 *
 * Side effects:
 *      writes the modified image (according to options) to destImg.
 *      passed in objv[]. 
 *      On success, writes the result (image name) to interp and
 *      returns TCL_OK.
 *
 *--------------------------------------------------------------
 */
static int Imgtools_TransparencyModify (interp, objc, objv, subcommand)
Tcl_Interp *interp;         /* used for error messages */
int objc;                   /* number of arguments */
Tcl_Obj *const objv[];      /* arguments */
enum Subcommand subcommand; /* the name says it all */
{
    int result = TCL_OK;
    
    Tk_PhotoHandle srcimg, destimg;
    Imgtools_Image destData, srcData;
    srcData.memOwner = 0;   /* this is needed so we can safely call Imgtools_FreeImage on */
    destData.memOwner = 0;  /* these images before actual initialisation (early return on errors) */
    Tcl_Obj *destImgName = NULL;
    
    Imgtools_CommonOpts commonOpts;
    int allowedOpts;
    
    double alpha;
    
    /*
     * Get src and destImage
     */    
    if (objc < 5) {
        Tcl_WrongNumArgs(interp, 2, objv, "sourceImg alphaValue ?options? destImg");
        Tcl_SetErrorCode(interp, "IMGTOOLS", "OPTIONS", "WRONG_NUM_ARGS", NULL);
        result = TCL_ERROR;
        goto endOfModify;
    }
    if ((result = Imgtools_FindPhoto(interp, objv[2], &srcimg)) !=TCL_OK) {
        goto endOfModify;
    }
    if ((result = Imgtools_FindPhoto(interp, objv[objc-1], &destimg)) != TCL_OK) {
        goto endOfModify;
    }
    destImgName = objv[objc-1];
    
    debugmsg(interp, DEBUG_PROGRESS, "TransparencyModify: start\n");
    debugmsg(interp, DEBUG_PROGRESS, "\tsource image: %s\n", Tcl_GetString(objv[2]));
    debugmsg(interp, DEBUG_PROGRESS, "\tdest image: %s\n", Tcl_GetString(destImgName));
    
    if ((result = Imgtools_GetAlphaVal(interp, objv[3], "argument alphaValue", 
            &alpha)) != TCL_OK) {
        goto endOfModify;
    }    
    
    allowedOpts = OPT_COMPOSITE | OPT_SHRINK | OPT_TO | OPT_FROM ;
    if ((result = Imgtools_ParseCommonOpts(interp, objc-5, objv+4, allowedOpts, 
            &commonOpts)) != TCL_OK) {
        goto endOfModify;
    }
    
    /*
     * Get image data, apply -from option
     */
    if ((result = Imgtools_GetImage(interp, srcimg, &srcData)) != TCL_OK) {
        goto endOfModify;
    }
    
    if ((result = Imgtools_ApplySubregion(interp, commonOpts.fromCorners, &srcData)) 
            != TCL_OK) {
        goto endOfModify;
    }
    
    debugmsg(interp, DEBUG_PROGRESS, "TransparencyModify: subregion (-from) applied\n");
    
    /*
     * with an empty source image, we don't have much to do
     */
    if (srcData.width == 0 || srcData.height == 0) {
        destData = srcData;
        destData.memOwner = 0;  /* even if we own the memory of srcData, this one has to be */
                                /* false to avoid double free */
        goto putData;
    }
    
    /*
     * looks like we have some more work to do...
     * prepare destData.
     * We can't take the values from srcData: if a -from option was passed, pitch
     * might have an incorrect value -> memory corruption!
     */
    destData.width = srcData.width;
    destData.height = srcData.height;
    destData.pixelSize = 4;
    destData.pitch = destData.width * destData.pixelSize;
    int size = destData.width * destData.height * 4 * sizeof(unsigned char);
    destData.pixelPtr = (unsigned char *)Tcl_Alloc(size);
    destData.startPtr = destData.pixelPtr;
    destData.memOwner = 1;
    
    /*
     * copy image and apply changes
     */
    unsigned char *destPixelPtr, *srcPixelPtr;
    for (int y = destData.height; y--; ) {
        for (int x = destData.width; x--; ) {
            destPixelPtr = destData.startPtr + destData.pixelSize*x + destData.pitch*y;
            srcPixelPtr = srcData.startPtr + srcData.pixelSize*x + srcData.pitch*y;
            memcpy(destPixelPtr, srcPixelPtr, 3 * sizeof(unsigned char));
            switch (subcommand) {
                case SET:
                    destPixelPtr[ALPHA] = round(UCHAR_MAX * alpha);
                    break;
                case INCREASE:
                    destPixelPtr[ALPHA] = round(srcPixelPtr[ALPHA] * alpha);
                    break;
                case DECREASE:
                    destPixelPtr[ALPHA] = round(srcPixelPtr[ALPHA] * (1 - alpha) + alpha*UCHAR_MAX);
                    break;
                default:
                    Tcl_Panic("Error in Imgtools_Transparency : unknown subcommand");
            }
        }
    }
    
    /*
     * Put data to destImg
     */
    Imgtools_IntCoords start, end, putSize;
putData:
    putSize.x = destData.width;
    putSize.y = destData.height;
    Imgtools_GetRegion(interp, putSize, commonOpts.toCorners, ALLOW_LARGER, 
        SIZE_IS_INCREMENT, &start, &end);
    Imgtools_GetSizeFromCorners(start, end, &putSize);
    if ((result = Imgtools_PutImage(interp, commonOpts.composite, commonOpts.shrink, 
            &destData, start, putSize, Tcl_GetString(destImgName), destimg)) != TCL_OK) {
        Tcl_SetObjResult(interp, Tcl_NewStringObj("Failed to put data to destination image", -1));
        Tcl_SetErrorCode(interp, "IMGTOOLS", "DATA", "EXPORT_FAILED", NULL);
        goto endOfModify;
    }
    
    Tcl_SetObjResult(interp, destImgName);
    
endOfModify:
    Imgtools_FreeImage (&destData);
    Imgtools_FreeImage (&srcData);
        
    return result;
}

/*
 *--------------------------------------------------------------
 *
 * Imgtools_TransparencyGet --
 *
 *      Get the alpha val of a single pixel
 *
 * Results:
 *      Standard Tcl result
 *
 * Side effects:
 *      sets the alpha value of the specified pixelt as the
 *      current result in interp.
 * 
 *--------------------------------------------------------------
 */
static int Imgtools_TransparencyGet (interp, objc, objv, subcommand)
Tcl_Interp *interp;     /* used for error messages */
int objc;               /* number of arguments */
Tcl_Obj *const objv[];  /* arguments */
enum Subcommand subcommand;
{

    Tk_PhotoHandle srcimg;
    
    /* 
     * We don't use our custom image data stuct here: it's not needed
     * we can avoid to have a cleanup section this way 
     */
    Tk_PhotoImageBlock srcData;
    Imgtools_IntCoords getPixel;
    
    /*
     * Get src image and coords
     */    
    if (objc != 5) {
        Tcl_WrongNumArgs(interp, 2, objv, "sourceImg x y");
        Tcl_SetErrorCode(interp, "IMGTOOLS", "OPTIONS", "WRONG_NUM_ARGS", NULL);
        return TCL_ERROR;
    }
    
    if (Imgtools_FindPhoto(interp, objv[2], &srcimg) != TCL_OK) {
        return TCL_ERROR;
    }
    Tk_PhotoGetImage(srcimg, &srcData);
    
    if (srcData.pixelSize != 4) {
        Tcl_SetObjResult(interp, Tcl_ObjPrintf("looks like image has no alpha channel"));
        Tcl_SetErrorCode(interp, "IMGTOOLS", "IMAGE", "UNKNOWN_FORMAT", NULL);
        return TCL_ERROR;
    }
    if (Tcl_GetIntFromObj(interp, objv[3], &(getPixel.x)) != TCL_OK) {
        Tcl_SetErrorCode(interp, "IMGTOOLS", "OPTIONS", "BAD_VALUE", NULL);
        return TCL_ERROR;
    }
    if (Tcl_GetIntFromObj(interp, objv[4], &(getPixel.y)) != TCL_OK) {
        Tcl_SetErrorCode(interp, "IMGTOOLS", "OPTIONS", "BAD_VALUE", NULL);
        return TCL_ERROR;
    }
    if (getPixel.x < 0 || getPixel.y < 0 ||
        getPixel.x >= srcData.width || getPixel.y >= srcData.height) {
        Tcl_SetObjResult(interp, Tcl_ObjPrintf("coordinates out of range"));
        Tcl_SetErrorCode(interp, "IMGTOOLS", "OPTIONS", "BAD_VALUE", NULL);
        return TCL_ERROR;
    }
    
    /*
     * Get the desired alpha value, set the result and return
     */
    int offset;
    double alpha;
    Tcl_Obj *resultObj = NULL;
    
    offset = getPixel.x*srcData.pixelSize + getPixel.y*srcData.pitch + ALPHA;
    alpha = (double)srcData.pixelPtr[offset] / UCHAR_MAX;
    resultObj = Tcl_NewDoubleObj(alpha);
    
    Tcl_SetObjResult(interp, resultObj);
    
    return TCL_OK;
}

/*
 *--------------------------------------------------------------
 *
 * Imgtools_TransparencyMask --
 *
 *      Implements the mask subcommand: apply a mask to the
 *      alpha channel
 *
 * Results:
 *      Standard Tcl result.
 *
 * Side effects:
 *      writes a modified image (according to options) to destImg. 
 *      destImg must exist.
 *      on success sets the name of destImg as the result in interp
 *      and returns TCL_OK.
 *      On errors, sets appropriate error message and codes, and
 *      returns TCL_ERROR.
 *
 *--------------------------------------------------------------
 */
static int Imgtools_TransparencyMask (interp, objc, objv, subcommand)
Tcl_Interp *interp;     /* used for error messages */
int objc;               /* number of arguments */
Tcl_Obj *const objv[];  /* arguments */
enum Subcommand subcommand;
{
    int result = TCL_OK;
    
    Tk_PhotoHandle srcimg, destimg, maskimg;
    Imgtools_Image destData, srcData, maskData;
    destData.memOwner = 0;  /* this way we can sefely call Imgtools_FreeImage() */
    srcData.memOwner = 0;   /* on these structs on early return - before they */
    maskData.memOwner = 0;  /* get properly initialized */
    Tcl_Obj *destImgName = NULL;
    
    Imgtools_CommonOpts commonOpts;
    int allowedOpts;
    
    /*
     * Get src, mask and destImage
     */    
    if (objc < 5) {
        Tcl_WrongNumArgs(interp, 2, objv, "sourceImage maskImage ?options? destImage");
        Tcl_SetErrorCode(interp, "IMGTOOLS", "OPTIONS", "WRONG_NUM_ARGS", NULL);
        result = TCL_ERROR;
        goto endOfMask;
    }
    if ((result = Imgtools_FindPhoto(interp, objv[2], &srcimg)) !=TCL_OK) {
        goto endOfMask;
    }
    if ((result = Imgtools_FindPhoto(interp, objv[3], &maskimg)) !=TCL_OK) {
        goto endOfMask;
    }
    if ((result = Imgtools_FindPhoto(interp, objv[objc-1], &destimg)) != TCL_OK) {
        goto endOfMask;
    }
    destImgName = objv[objc-1];
    
    debugmsg(interp, DEBUG_PROGRESS, "TransparencyMask: start\n");
    debugmsg(interp, DEBUG_PROGRESS, "\tsource image: %s", Tcl_GetString(objv[2]));
    debugmsg(interp, DEBUG_PROGRESS, "\tmask image: %s", Tcl_GetString(objv[3]));
    debugmsg(interp, DEBUG_PROGRESS, "\tdest image: %s", Tcl_GetString(objv[objc-1]));
    
    /*
     * Parse options
     */
    allowedOpts = OPT_COMPOSITE | OPT_SHRINK | OPT_FROM | OPT_TO;
    if ((result = Imgtools_ParseCommonOpts(interp, objc-5, objv+4, allowedOpts, 
            &commonOpts)) != TCL_OK) {
        goto endOfMask;
    }

    /*
     * Get image data and apply -from option
     */
    if ((result = Imgtools_GetImage(interp, srcimg, &srcData)) != TCL_OK ||
        (result = Imgtools_GetImage(interp, maskimg, &maskData)) != TCL_OK ) {
        goto endOfMask;
    }
    if ((result = Imgtools_ApplySubregion(interp, commonOpts.fromCorners, &srcData)) != TCL_OK) {
        goto endOfMask;
    }
    
    /*
     * with an empty source image, we don't have much to do
     */
    if (srcData.width == 0 || srcData.height == 0) {
        destData = srcData;
        destData.memOwner = 0; /* avoid double free in cas we own source mem */
        goto putData;
    }
    
    /*
     * mask should not be empty
     */
    if (maskData.width == 0 || maskData.height == 0) {
        Tcl_SetObjResult(interp, Tcl_NewStringObj("mask image is empty", -1));
        Tcl_SetErrorCode(interp, "IMGTOOLS", "IMAGE", "EMPTY_IMAGE", NULL);
        result = TCL_ERROR;
        goto endOfMask;
    }
    
    /*
     * Prepare destData.
     * We can't take the values from srcData: if a -from option was passed, pitch
     * might have an incorrect value -> memory corruption!
     */
    destData.width = srcData.width;
    destData.height = srcData.height;
    destData.pixelSize = 4;
    destData.pitch = destData.width * destData.pixelSize;
    int size = destData.width * destData.height * 4 * sizeof(unsigned char);
    destData.pixelPtr = (unsigned char *)Tcl_Alloc(size);
    destData.startPtr = destData.pixelPtr;
    destData.memOwner = 1;
    
    debugmsg(interp, DEBUG_PROGRESS, "TransparencyMask: destData structure initialized.\n");
    
    /*
     * copy data and apply mask
     */
    unsigned char *srcPixelPtr, *destPixelPtr, *maskPixelPtr;
    Imgtools_IntCoords maskCoords = {0,0};
    for (int y = 0; y < destData.height; y++) {
        maskCoords.x = 0;
        for (int x = 0; x < destData.width; x++) {
            srcPixelPtr = srcData.startPtr + srcData.pixelSize*x + srcData.pitch*y;
            destPixelPtr = destData.startPtr + destData.pixelSize*x + destData.pitch*y;
            maskPixelPtr = maskData.startPtr + maskData.pixelSize*maskCoords.x 
                + maskData.pitch*maskCoords.y + ALPHA;

            memcpy(destPixelPtr, srcPixelPtr, 3 * sizeof(unsigned char));
            destPixelPtr[ALPHA] = *maskPixelPtr;
            
            if (++maskCoords.x >= maskData.width) {
                maskCoords.x = 0;
            }
        }
        if (++maskCoords.y >= maskData.height) {
            maskCoords.y = 0;
        }
    }
    
    /*
     * Put data to destImg
     */
    Imgtools_IntCoords start, end, putSize;
putData:
    putSize.x = destData.width;
    putSize.y = destData.height;
    Imgtools_GetRegion(interp, putSize, commonOpts.toCorners, ALLOW_LARGER,
        SIZE_IS_INCREMENT, &start, &end);
    Imgtools_GetSizeFromCorners(start, end, &putSize);
    if ((result = Imgtools_PutImage(interp, commonOpts.composite, commonOpts.shrink, &destData, 
            start, putSize, Tcl_GetString(destImgName), destimg)) != TCL_OK) {
        Tcl_SetObjResult(interp, Tcl_NewStringObj("Failed to put data to destination image", -1));
        Tcl_SetErrorCode(interp, "IMGTOOLS", "DATA", "EXPORT_FAILED", NULL);
        goto endOfMask;
    }
    
    Tcl_SetObjResult(interp, destImgName);
    
endOfMask:
    Imgtools_FreeImage(&srcData);
    Imgtools_FreeImage(&destData);
    Imgtools_FreeImage(&maskData);
    
    return result;
}

