/*
 * Copyright 2021 Olaf Wintermann
 *
 * Permission is hereby granted, free of charge, to any person obtaining a 
 * copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the 
 * Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in 
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
 * DEALINGS IN THE SOFTWARE.
 */

#include "colorchooser.h"

#include <Xm/XmAll.h>
#include <Xm/XmP.h>
#include <X11/Xft/Xft.h>

#include <inttypes.h>
#include <limits.h>

#define XNE_COLOR_DIALOG_TITLE "Select Color"

#define WIDGET_SPACING 5
#define WINDOW_SPACING 8

typedef struct {
    Widget dialog;
    
    Widget preview;
    Widget textfield;
    Widget selector;
    GC gc;
    GC selGC;
    XftDraw *d; // preview xft drawable
    
    uint8_t base_red;
    uint8_t base_green;
    uint8_t base_blue;
    
    int base_sel_y;
    float hue;
    float saturation;
    float value;
    
    uint8_t selected_red;
    uint8_t selected_green;
    uint8_t selected_blue;
    XftColor selected_color;
    
    XImage *image1;
    Dimension img1_width;
    Dimension img1_height;
    
    XImage *image2;
    Dimension img2_width;
    Dimension img2_height;
    
    int has_selection;
    Dimension img2_select_x;
    Dimension img2_select_y;
    
    int parse_textfield_input;
    
    int status;
    int end;
} cgData;

static XftDraw* create_xft_draw(Widget w);
static void selector_expose(Widget w, XtPointer u, XtPointer c);
static void selector_input(Widget w, XtPointer u, XtPointer c);
static void preview_color(Widget w, XtPointer u, XtPointer c);
static void textfield_changed(Widget w, XtPointer u, XtPointer c);
static void set_base_color(cgData *data, int r, int g, int b);
static void select_color(cgData *data, int r, int g, int b);

static void draw_img2(Display *dp, Window win, cgData *data);

static void okCB(Widget w, XtPointer u, XtPointer c);
static void cancelCB(Widget w, XtPointer u, XtPointer c);

int ColorChooser(Widget parent, int *red, int *green, int *blue) {
    Arg args[32];
    int n;
    XmString str;
    
    cgData data;
    memset(&data, 0, sizeof(data));
    
    data.base_sel_y = -1;
    
    n = 0;
    XtSetArg(args[n], XmNautoUnmanage, False); n++;
    Widget form = XmCreateFormDialog(parent, "ColorDialog", args, n);
    Widget dialog = XtParent(form);
    XtVaSetValues(dialog, XmNtitle, XNE_COLOR_DIALOG_TITLE, NULL);
    XtManageChild(form);
    
    // UI
    
    // color preview field
    n = 0;
    XtSetArg(args[n], XmNshadowType, XmSHADOW_IN); n++;
    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
    XtSetArg(args[n], XmNtopOffset, WINDOW_SPACING); n++;
    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
    XtSetArg(args[n], XmNleftOffset, WINDOW_SPACING); n++;
    XtSetArg(args[n], XmNrightAttachment, XmATTACH_POSITION); n++;
    XtSetArg(args[n], XmNrightPosition, 30); n++;
    //XtSetArg(args[n], XmNwidth, 50); n++;
    data.preview = XmCreateDrawingArea(form, "cgPreview", args, n);
    XtManageChild(data.preview);
    XtAddCallback(data.preview, XmNexposeCallback, preview_color, &data);
    
    // rgb textfield
    n = 0;
    XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
    XtSetArg(args[n], XmNtopOffset, WINDOW_SPACING); n++;
    XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
    XtSetArg(args[n], XmNrightOffset, WINDOW_SPACING); n++;
    XtSetArg(args[n], XmNleftAttachment, XmATTACH_POSITION); n++;
    XtSetArg(args[n], XmNleftPosition, 33); n++;
    data.textfield = XmCreateTextField(form, "cgText", args, n);
    XtManageChild(data.textfield);
    XtAddCallback(data.textfield, XmNvalueChangedCallback, textfield_changed, &data);
    
    XtVaSetValues(data.preview, XmNbottomAttachment, XmATTACH_OPPOSITE_WIDGET, XmNbottomWidget, data.textfield, NULL);
    
    
    // ok/cancel buttons
    n = 0;
    str = XmStringCreateSimple("OK");
    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
    XtSetArg(args[n], XmNbottomOffset, WINDOW_SPACING); n++;
    XtSetArg(args[n], XmNleftAttachment, XmATTACH_POSITION); n++;
    XtSetArg(args[n], XmNleftPosition, 12); n++;
    XtSetArg(args[n], XmNrightAttachment, XmATTACH_POSITION); n++;
    XtSetArg(args[n], XmNrightPosition, 37); n++;
    XtSetArg(args[n], XmNlabelString, str); n++;
    Widget ok = XmCreatePushButton(form, "cgOK", args, n);
    XtManageChild(ok);
    XmStringFree(str);
    XtAddCallback(ok, XmNactivateCallback, okCB, &data);
    
    n = 0;
    str = XmStringCreateSimple("Cancel");
    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
    XtSetArg(args[n], XmNbottomOffset, WINDOW_SPACING); n++;
    XtSetArg(args[n], XmNleftAttachment, XmATTACH_POSITION); n++;
    XtSetArg(args[n], XmNleftPosition, 63); n++;
    XtSetArg(args[n], XmNrightAttachment, XmATTACH_POSITION); n++;
    XtSetArg(args[n], XmNrightPosition, 88); n++;
    XtSetArg(args[n], XmNlabelString, str); n++;
    Widget cancel = XmCreatePushButton(form, "cgOK", args, n);
    XtManageChild(cancel);
    XmStringFree(str);
    XtAddCallback(cancel, XmNactivateCallback, cancelCB, &data);
    
    XtVaSetValues(form, XmNdefaultButton, ok, NULL);
    XtVaSetValues(form, XmNcancelButton, cancel, NULL);
    
    // color selector
    static XtTranslations selectorTranslations = NULL;
    if(!selectorTranslations) {
        selectorTranslations = XtParseTranslationTable("<Btn1Down>: DrawingAreaInput() ManagerGadgetArm()\n<Btn1Motion>: DrawingAreaInput() ManagerGadgetButtonMotion()");
    }
    
    n = 0;
    XtSetArg(args[n], XmNtopAttachment, XmATTACH_WIDGET); n++;
    XtSetArg(args[n], XmNtopWidget, data.textfield); n++;
    XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
    XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
    XtSetArg(args[n], XmNtopOffset, WINDOW_SPACING); n++;
    XtSetArg(args[n], XmNbottomAttachment, XmATTACH_WIDGET); n++;
    XtSetArg(args[n], XmNbottomWidget, ok); n++;
    XtSetArg(args[n], XmNbottomOffset, WINDOW_SPACING); n++;
    XtSetArg(args[n], XmNleftOffset, WINDOW_SPACING); n++;
    XtSetArg(args[n], XmNrightOffset, WINDOW_SPACING); n++;
    XtSetArg(args[n], XmNwidth, 500); n++;
    XtSetArg(args[n], XmNheight, 400); n++;
    XtSetArg(args[n], XmNtranslations, selectorTranslations); n++;
    data.selector = XmCreateDrawingArea(form, "cgSelector", args, n);    
    XtManageChild(data.selector);
    XtAddCallback(data.selector, XmNexposeCallback, selector_expose, &data);
    XtAddCallback(data.selector, XmNinputCallback, selector_input, &data);
    
    XGCValues gcValues;
    gcValues.background = data.selector->core.background_pixel;
    gcValues.foreground = BlackPixelOfScreen(DefaultScreenOfDisplay(XtDisplay(data.selector)));
    gcValues.graphics_exposures = 0;
    data.gc = XtAllocateGC(data.selector, 0, GCForeground | GCBackground, &gcValues, 0, 0);
    
    gcValues.background = data.selector->core.background_pixel;
    gcValues.foreground = WhitePixelOfScreen(DefaultScreenOfDisplay(XtDisplay(data.selector)));
    gcValues.graphics_exposures = 0;
    data.selGC = XtAllocateGC(data.selector, 0, GCForeground | GCBackground, &gcValues, 0, 0);
    
    // initial color
    select_color(&data, *red / 257, *green / 257, *blue / 257);
    
    // base color
    set_base_color(&data, (*red)/257, (*green)/257, (*blue)/257);
     
    // manage dialog
    XtManageChild(dialog);
    
    data.parse_textfield_input = 1;
    
    XtAppContext app = XtWidgetToApplicationContext(dialog);
    while(!data.end && !XtAppGetExitFlag(app)) {
        XEvent event;
        XtAppNextEvent(app, &event);
        XtDispatchEvent(&event);
    }
    
    XtReleaseGC(data.selector, data.gc);
    XtReleaseGC(data.selector, data.selGC);
    if(data.image1) XDestroyImage(data.image1);
    if(data.image2) XDestroyImage(data.image2);
    if(data.d) XftDrawDestroy(data.d);
    XtDestroyWidget(dialog);
    
    if(data.status == 1) {
        *red = data.selected_color.color.red;
        *green = data.selected_color.color.green;
        *blue = data.selected_color.color.blue;
    }
    
    return data.status;
}

static XftDraw* create_xft_draw(Widget w) {   
    XWindowAttributes attributes;
    XGetWindowAttributes(XtDisplay(w), XtWindow(w), &attributes); 
    
    Screen *screen = w->core.screen;
    Visual *visual = screen->root_visual;
    for(int i=0;i<screen->ndepths;i++) {
        Depth d = screen->depths[i];
        if(d.depth == w->core.depth) {
            visual = d.visuals;
            break;
        }
    }
    
    Display *dp = XtDisplay(w);
    return XftDrawCreate(
            dp,
            XtWindow(w),
            visual,
            w->core.colormap);
    
}

/*
 * get number of shifts for specific color mask
 */
static int get_shift(unsigned long mask) {
    if(mask == 0) {
        return 0;
    }
    
    int shift = 0;
    while((mask & 1L) == 0) {
        shift++;
        mask >>= 1;
    }
    return shift;
}

/*
 * get number of bits from color mask
 */
static int get_mask_len(unsigned long mask) {
    if(mask == 0) {
        return 0;
    }
    
    while((mask & 1L) == 0) {
        mask >>= 1;
    }
    int len = 0;
    while((mask & 1L) == 1) {
        len++;
        mask >>= 1;
    }
    return len;
}

static Visual* get_visual(Screen *screen, int depth) {
    // get the correct visual for current screen/depth
    Visual *visual = NULL;
    for(int i=0;i<screen->ndepths;i++) {
        Depth d = screen->depths[i];
        if(d.depth == depth) {
            for(int v=0;v<d.nvisuals;v++) {
                Visual *vs = &d.visuals[v];
                if(get_mask_len(vs->red_mask) == 8) {
                    visual = vs;
                    break;
                }
            }
        }
    }
    return visual;
}

#define IMG1_WIDTH    20
#define IMG1_X_OFFSET 20
#define IMG1_Y_OFFSET 10
#define IMG2_X_OFFSET 10
#define IMG2_Y_OFFSET 10

static void init_pix1(cgData *data, Widget w) {
    Display *dp = XtDisplay(w);
    Dimension width = IMG1_WIDTH;
    Dimension height = w->core.height - IMG1_Y_OFFSET*2;
    
    if(data->image1) {
        if(height == data->img2_height) return;
        XDestroyImage(data->image1);
    }
    
    float height6 = ((float)height)/6.f;
    float step = ((float)255.f)/height6;
    
    Visual *visual = get_visual(w->core.screen, w->core.depth);
    if(!visual) {
        return;
    }
    
    //get number of shifts required for each color bit mask
    int red_shift = get_shift(visual->red_mask);
    int green_shift = get_shift(visual->green_mask);
    int blue_shift = get_shift(visual->blue_mask);
    
    uint32_t *imgdata = malloc(width * height * 4); // will be freed by XDestroyImage
    uint32_t pixel_init = UINT32_MAX ^ (visual->red_mask ^ visual->green_mask ^ visual->blue_mask);

    
    int m = 0;
    float r = 0xFF;
    float g = 0;
    float b = 0;
    for(int i=0;i<height;i++) {
        if(r < 0) r = 0;
        if(r > 255) r = 255;
        if(g < 0) g = 0;
        if(g > 255) g = 255;
        if(b < 0) b = 0;
        if(b > 255) b = 255;
        
        uint8_t red = r;
        uint8_t green = g;
        uint8_t blue = b;
        
        switch(m) {
            case 0: {
                g += step;
                if(g >= 255) m++;
                break;
            }
            case 1: {
                r -= step;
                if(r <= 0) m++;
                break;
            }
            case 2: {
                b += step;
                if(b >= 255) m++;
                break;
            }
            case 3: {
                g -= step;
                if(g <= 0) m++;
                break;
            }
            case 4: {
                r += step;
                if(r >= 255) m++;
                break;
            }
            case 5: {
                b -= step;
                if(b <= 0) m = 0;
                break;
            }
        }
        
        uint32_t out = pixel_init;
        out ^= (red << red_shift);
        out ^= (green << green_shift);
        out ^= (blue << blue_shift);
        
        for(int j=0;j<width;j++) {
            imgdata[i*width + j] = out;
        }
    }
    
    data->image1 = XCreateImage(dp, visual, w->core.depth, ZPixmap, 0, (char*)imgdata, width, height, 32, 0);
    
    data->img1_width = width;
    data->img1_height = height;
}

static void init_pix2(cgData *data, Widget w) {
    Display *dp = XtDisplay(w);
    Dimension width = w->core.width - IMG1_X_OFFSET - IMG1_WIDTH - 2*IMG2_X_OFFSET;
    Dimension height = w->core.height - 2*IMG2_Y_OFFSET;
    
    if(data->image2) {
        if(width == data->img2_width && height == data->img2_height) return;
        XDestroyImage(data->image2);
        data->has_selection = 0;
    }
    
    if(!data->has_selection) {
        data->img2_select_x = (float)width * data->value - 1;
        data->img2_select_y = height - ((float)height * data->saturation);
        data->has_selection = 1;
    }
    
    Visual *visual = get_visual(w->core.screen, w->core.depth);
    if(!visual) {
        return;
    }
    
    //get number of shifts required for each color bit mask
    int red_shift = get_shift(visual->red_mask);
    int green_shift = get_shift(visual->green_mask);
    int blue_shift = get_shift(visual->blue_mask);
    
    uint32_t *imgdata = malloc(width * height * 4);
    uint32_t pixel_init = UINT32_MAX ^ (visual->red_mask ^ visual->green_mask ^ visual->blue_mask);
    
    float y_base_r = data->base_red;
    float y_base_g = data->base_green;
    float y_base_b = data->base_blue;
    
    float y_step_r = (255-data->base_red) / (float)(height-2*IMG2_Y_OFFSET);
    float y_step_g = (255-data->base_green) / (float)(height-2*IMG2_Y_OFFSET);
    float y_step_b = (255-data->base_blue) / (float)(height-2*IMG2_Y_OFFSET);
    
    for(int y=0;y<height;y++) { 
        float x_step_r = y_base_r / (float)width;
        float x_step_g = y_base_g / (float)width;
        float x_step_b = y_base_b / (float)width;
        
        float r = y_base_r;
        float g = y_base_g;
        float b = y_base_b;
        
        y_base_r += y_step_r;
        y_base_g += y_step_g;
        y_base_b += y_step_b;
        if(y_base_r >= 255) y_base_r = 255;
        if(y_base_g >= 255) y_base_g = 255;
        if(y_base_b >= 255) y_base_b = 255;
        
        for(int x=width-1;x>=0;x--) {
            if(r < 0) r = 0;
            if(g < 0) g = 0;
            if(b < 0) b = 0;
            
            uint8_t red = r;
            uint8_t green = g;
            uint8_t blue = b;
            
            r -= x_step_r;
            g -= x_step_g;
            b -= x_step_b;
            
            uint32_t out = pixel_init;
            out ^= (red << red_shift);
            out ^= (green << green_shift);
            out ^= (blue << blue_shift);
            imgdata[y*width + x] = out;
        }
    }
    
    data->image2 = XCreateImage(dp, visual, w->core.depth, ZPixmap, 0, (char*)imgdata, width, height, 32, 0);
    
    data->img2_width = width;
    data->img2_height = height;
}

static void draw_img2(Display *dp, Window win, cgData *data) {
    int img2_x = IMG1_X_OFFSET + data->img1_width + IMG2_X_OFFSET;
    int img2_y = IMG2_Y_OFFSET;
    XPutImage(dp, win, data->gc, data->image2, 0, 0, img2_x, img2_y, data->img2_width, data->img2_height);
    if(data->has_selection) {
        XDrawLine(dp, win, data->selGC,
                img2_x, img2_y + data->img2_select_y,
                img2_x + data->img2_width - 1, img2_y + data->img2_select_y);
        XDrawLine(dp, win, data->selGC,
                img2_x + data->img2_select_x, img2_y,
                img2_x + data->img2_select_x, img2_y + data->img2_height - 1);
    }
}

#define HUE_INDICATOR_SIZE 9

static void selector_expose(Widget w, XtPointer u, XtPointer c) {
    cgData *data = u;
    Dimension width = w->core.width;
    Dimension height = w->core.height;
    Display *dp = XtDisplay(w);
    Window win = XtWindow(w);
    
    init_pix1(data, w);
    init_pix2(data, w);
    
    if(data->base_sel_y < 0) {
        float img1h = data->img1_height;
        float deg2pix = img1h / 360.f;
        data->base_sel_y = data->hue * deg2pix;
    }
    
    // clear all
    
    // top
    XClearArea(XtDisplay(w), XtWindow(w), 1, 1, width-2, IMG1_Y_OFFSET-1, False);
    // bottom
    XClearArea(XtDisplay(w), XtWindow(w),
            1, IMG1_Y_OFFSET + data->img1_height,
            width-2, height - IMG1_Y_OFFSET - data->img1_height - 2, False);
    // left
    XClearArea(XtDisplay(w), XtWindow(w), 1, 1, IMG1_X_OFFSET-1, height-2, False);

    XPoint indicator[4];
    indicator[0].x = IMG1_X_OFFSET-1;
    indicator[0].y = IMG1_Y_OFFSET + data->base_sel_y;

    indicator[1].x = IMG1_X_OFFSET-HUE_INDICATOR_SIZE - 1;
    indicator[1].y = IMG1_Y_OFFSET + data->base_sel_y - HUE_INDICATOR_SIZE;

    indicator[2].x = IMG1_X_OFFSET-HUE_INDICATOR_SIZE - 1;
    indicator[2].y = IMG1_Y_OFFSET + data->base_sel_y + HUE_INDICATOR_SIZE;

    indicator[3].x = IMG1_X_OFFSET-1;
    indicator[3].y = IMG1_Y_OFFSET + data->base_sel_y;


    XFillPolygon(dp, win, data->gc, indicator, 4, Convex, CoordModeOrigin);
    
    // right
    XClearArea(XtDisplay(w), XtWindow(w),
            IMG1_X_OFFSET + data->img1_width + IMG2_X_OFFSET + data->img2_width, 1,
            width - 1 - IMG1_X_OFFSET - data->img1_width - IMG2_X_OFFSET - data->img2_width, height-2, False);
    // middle
    XClearArea(XtDisplay(w), XtWindow(w),
            IMG1_X_OFFSET + data->img1_width, 1,
            IMG2_X_OFFSET, height-2, False);
    
    
    if(data->image1) {
        XPutImage(dp, win, data->gc, data->image1, 0, 0, IMG1_X_OFFSET, IMG1_Y_OFFSET, data->img1_width, data->img1_height);
    }
    if(data->image2) {
        draw_img2(dp, win, data);
    }
    
    XDrawRectangle(dp, win, DefaultGC(dp, 0), 0, 0, width-1, height-1);
    
}

#define SELECT_MARGIN 5
static int translate_img1(cgData *data, int x, int y, int *trans_x, int *trans_y) {
    if(x < IMG1_X_OFFSET - HUE_INDICATOR_SIZE) return 0;
    if(y < IMG1_Y_OFFSET) return 0;
    
    int tx = x - IMG1_X_OFFSET;
    int ty = y - IMG1_Y_OFFSET;
    
    if(tx < data->img1_width && ty < data->img1_height) {
        *trans_x = tx;
        *trans_y = ty;
        return 1;
    }
    return 0;
}

static int translate_img2(cgData *data, int x, int y, int *trans_x, int *trans_y) {
    if(x < IMG1_X_OFFSET + IMG2_X_OFFSET + data->img1_width - SELECT_MARGIN) return 0;
    if(y < IMG1_Y_OFFSET) return 0;
    
    int tx = x - (IMG1_X_OFFSET + IMG2_X_OFFSET + data->img1_width);
    int ty = y - IMG2_Y_OFFSET;
    
    if(tx < data->img2_width + SELECT_MARGIN && ty < data->img2_height + SELECT_MARGIN) {
        if(tx < 0) tx = 0;
        if(ty < 0) ty = 0;
        if(tx >= data->img2_width) tx = data->img2_width-1;
        if(ty >= data->img2_height) ty = data->img2_height-1;
        *trans_x = tx;
        *trans_y = ty;
        return 1;
    }
    return 0;
}

static void selector_input(Widget w, XtPointer u, XtPointer c) {
    XmDrawingAreaCallbackStruct *cb = (XmDrawingAreaCallbackStruct*)c;
    cgData *data = u;
    Display *dp = XtDisplay(w);
    Window win = XtWindow(w);

    Dimension x, y;
    if(cb->event->type == MotionNotify) {
        x = cb->event->xmotion.x;
        y = cb->event->xmotion.y;
    } else {
        x = cb->event->xbutton.x;
        y = cb->event->xbutton.y;
    }
    
    int tx, ty;
    if(translate_img1(data, x, y, &tx, &ty)) {
        uint32_t color = XGetPixel(data->image1, tx, ty);
        int red_shift = get_shift(data->image1->red_mask);
        int green_shift = get_shift(data->image1->green_mask);
        int blue_shift = get_shift(data->image1->blue_mask);
        
        data->base_red = (color & data->image1->red_mask) >> red_shift;
        data->base_green = (color & data->image1->green_mask) >> green_shift;
        data->base_blue = (color & data->image1->blue_mask) >> blue_shift;
        
        data->base_sel_y = ty;
        
        XDestroyImage(data->image2);
        data->image2 = NULL;
        init_pix2(data, w);
        
        data->has_selection = 0;
        selector_expose(w, u, NULL);
    }
    if(translate_img2(data, x, y, &tx, &ty)) {
        uint32_t color = XGetPixel(data->image2, tx, ty);
        int red_shift = get_shift(data->image2->red_mask);
        int green_shift = get_shift(data->image2->green_mask);
        int blue_shift = get_shift(data->image2->blue_mask);
        
        data->img2_select_x = tx;
        data->img2_select_y = ty;
        data->has_selection = 1;
        
        select_color(data,
                (color & data->image2->red_mask) >> red_shift,
                (color & data->image2->green_mask) >> green_shift,
                (color & data->image2->blue_mask) >> blue_shift);
        
        preview_color(data->preview, data, NULL);
        
        draw_img2(dp, win, data);
    }
}

static float max3(float a, float b, float c) {
    if(a > b) {
        return a > c ? a : c;
    } else {
        return b > c ? b : c;
    }
}

static float min3(float a, float b, float c) {
    if(a < b) {
        return a < c ? a : c;
    } else {
        return b < c ? b : c;
    }
}

static void rgbToHsv(int red, int green, int blue, float *hue, float *saturation, float *value) {
    float r = red;
    float g = green;
    float b = blue;
    r /= 255;
    g /= 255;
    b /= 255;
    
    float rgbMax = max3(r, g, b);
    float rgbMin = min3(r, g, b);
    
    float mmDiff = rgbMax - rgbMin;
    
    float h;
    if(rgbMax == rgbMin) {
        h = 0;
    } else if(rgbMax == r) {
        h = 60.f * (g-b)/mmDiff;
    } else if(rgbMax == g) {
        h = 60.f * (2.f + (b-r)/mmDiff);
    } else {
        h = 60.f * (4.f + (r-g)/mmDiff);
    }
    if(h < 0) h += 360;
    
    float s = rgbMax == 0 ? 0 : mmDiff/rgbMax;
    float v = rgbMax;
    
    *hue = h;
    *saturation = s;
    *value = v;
}

static void hsvToRgb(float hue, float saturation, float value, int *red, int *green, int *blue) {
    int hi = hue/60;
    float f = (hue/60) - hi;
    float p = value * (1-saturation);
    float q = value * (1-saturation * f);
    float t = value * (1-saturation * (1-f));
    
    float r, g, b;
    switch(hi) {
        case 1: {
            r = q;
            g = value;
            b = p;
            break;
        }
        case 2: {
            r = p;
            g = value;
            b = t;
            break;
        }
        case 3: {
            r = p;
            g = q;
            b = value;
            break;
        }
        case 4: {
            r = t;
            g = p;
            b = value;
            break;
        }
        case 5: {
            r = value;
            g = p;
            b = q;
            break;  
        }
        default: {
            r = value;
            g = t;
            b = p;
        }
    }
    
    *red = r * 255;
    *green = g * 255;
    *blue = b * 255;
}

static void set_base_color(cgData *data, int r, int g, int b) {
    float h, s, v;
    rgbToHsv(r, g, b, &h, &s, &v);
       
    int red, green, blue;
    hsvToRgb(h, 1, 1, &red, &green, &blue);
    
    data->base_red = red;
    data->base_green = green;
    data->base_blue = blue;
    data->hue = h;
    data->saturation = s;
    data->value = v;
    data->base_sel_y = -1;
}

static void select_color(cgData *data, int r, int g, int b) {
    XftColor color;
    color.pixel = 0;
    color.color.alpha = 0xFFFF;
    color.color.red = r * 257;
    color.color.green = g * 257;
    color.color.blue = b * 257;
    data->selected_color = color;
    
    char buf[8];
    snprintf(buf, 8, "#%02x%02x%02x", r, g, b);
    data->parse_textfield_input = 0;
    XmTextFieldSetString(data->textfield, buf);
    data->parse_textfield_input = 1;
}

static void preview_color(Widget w, XtPointer u, XtPointer c) {
    cgData *data = u;
    if(!data->d) {
        data->d = create_xft_draw(w);
    }
    
    XftDrawRect(data->d, &data->selected_color, 0, 0, w->core.width, w->core.height);
    XDrawRectangle(XtDisplay(w), XtWindow(w), data->gc, 0, 0, w->core.width-1, w->core.height-1);
}

static void textfield_changed(Widget w, XtPointer u, XtPointer c) {
    cgData *data = u;
    if(!data->parse_textfield_input) return;
    
    char *colorStr = XmTextFieldGetString(w);
    if(strlen(colorStr) != 7) {
        XtFree(colorStr);
        return;
    }
    
    Display *dp = XtDisplay(w);
    Colormap cmap = w->core.colormap;
    XColor color;
    if(XParseColor(dp, cmap, colorStr, &color)) {
        XftColor c;
        c.pixel = 0;
        c.color.alpha = 0xFFFF;
        c.color.red = color.red;
        c.color.green = color.green;
        c.color.blue = color.blue;
        data->selected_color = c;
        
        set_base_color(data, color.red/257, color.green/257, color.blue/257);
        
        XDestroyImage(data->image2);
        data->image2 = NULL;
        init_pix2(data, w);
        
        data->has_selection = 0;
        
        selector_expose(data->selector, data, NULL);
        preview_color(data->preview, data, NULL);
    }
    XtFree(colorStr);
}

static void okCB(Widget w, XtPointer u, XtPointer c) {
    cgData *data = u;
    data->status = 1;
    data->end = 1;
}

static void cancelCB(Widget w, XtPointer u, XtPointer c) {
    cgData *data = u;
    data->status = 0;
    data->end = 1;
}
