/* a cmap is a mapping from double [0.0,1.0] (#index) -> color */

#include "cmap.h"

#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <math.h>

#include <new>

rgba_t black = {0,0,0,255};

#define EPSILON 1.0e-10

//#define DEBUG_CREATION

ColorMap::ColorMap()
{
    canary = 0xfeeefeee;
    ncolors = 0;
    solids[0] = solids[1] = black;
    transfers[0] = TRANSFER_LINEAR; // outer
    transfers[1] = TRANSFER_LINEAR; // inner

#ifdef DEBUG_CREATION
    fprintf(stderr,"%p : CM : CTOR\n", this);
#endif
}

void 
ColorMap::set_transfer(int which, e_transferType type)
{
    if(which >= 0 && which < 2)
    {
	if(type < TRANSFER_SIZE && type >= 0)
	{
	    transfers[which] = type;
	}
	else
	{
	    assert("bad transfer type" && 0);
	}
    }
    else
    {
	assert("bad transfer index" && 0);
    }
}

void
ColorMap::set_solid(int which, int r, int g, int b, int a)
{
    rgba_t color;
    color.r = (unsigned char)r;
    color.g = (unsigned char)g;
    color.b = (unsigned char)b;
    color.a = (unsigned char)a;

    if(which >= 0 && which < 2)
    {
	solids[which] = color;
    }
    else
    {
	assert("set bad color" && 0);
    }
}

rgba_t 
ColorMap::get_solid(int which) const
{
    rgba_t color = {0,0,0,1};
    if(which >= 0 && which < 2)
    {
	color = solids[which];
    }
    else
    {
	assert("get bad color" && 0);
    }
    return color;
} 

void
cmap_delete(ColorMap *cmap)
{
    assert(cmap->canary == 0xfeeefeee);
    delete cmap;
}

ColorMap::~ColorMap()
{
#ifdef DEBUG_CREATION
    fprintf(stderr,"%p : CM : DTOR\n", this);
#endif 

    canary = 0xbaadf00d;
    // NO OP
}

/* finds the indices in t of the largest item which is <= key 
   and the next item above it.
   If there are multiple identical items, returns one at random
 */

/* binary search algorithm from Programming Pearls.
   sadly C stdlib's bsearch is no good because it won't tell us the position
   of nearest match if there's no exact one */

int 
find(double key, list_item_t *array, int n)
{
    int left=0,right=n-1;
    do
    {
	int middle;
	if(left > right)
	{
	    return left-1 < 0 ? 0 : left-1 ;
	}
	middle = (left + right) / 2;
	if(array[middle].index < key)
	{ 
	    left = middle+1;
	}
	else if(array[middle].index == key)
	{
	    return middle;
	}
	else
	{
	    right = middle-1;
	}
    }while(1);
}

rgba_t
ColorMap::lookup_with_dca(int solid, int inside, double *colors) const
{
    rgba_t new_color;

    if(solid)
    {
	return solids[inside];
    }
    

    e_transferType t = transfers[inside];
    switch(t)
    {
    case TRANSFER_NONE:
	return solids[inside];
    case TRANSFER_LINEAR:
	new_color.r = (unsigned char)(255.0 * colors[0]);
	new_color.g = (unsigned char)(255.0 * colors[1]);
	new_color.b = (unsigned char)(255.0 * colors[2]);
	new_color.a = (unsigned char)(255.0 * colors[3]);
	return new_color;
    default:
	assert("bad transfer type" && 0);
	return black;
    }
}
 
rgba_t
ColorMap::lookup_with_transfer(double index, int solid, int inside) const
{
    if(solid)
    {
	return solids[inside];
    }
    
    e_transferType t = transfers[inside];
    switch(t)
    {
    case TRANSFER_NONE:
	return solids[inside];
    case TRANSFER_LINEAR:
	return lookup(index);
    default:
	assert("bad transfer type" && 0);
	return black;
    }
}
 
GradientColorMap::GradientColorMap() : ColorMap()
{
    items = NULL;
}

GradientColorMap::~GradientColorMap()
{
    delete[] items;
}

bool
GradientColorMap::init(int ncolors_)
{
    if(ncolors_ == 0)
    {
	return false;
    }

    ncolors = ncolors_; 

    items = new(std::nothrow) gradient_item_t[ncolors];
    if(!items)
    {
	return false;
    }

    for(int i = 0; i < ncolors; ++i)
    {
	gradient_item_t *p = &items[i];
	p->left = p->right = 0;
	p->bmode = BLEND_LINEAR;
	p->cmode = RGB;
    }
    return true;
}

void 
GradientColorMap::set(
    int i,
    double left, double right, double mid,
    double *left_col,
    double *right_col,
    e_blendType bmode, e_colorType cmode)
{
    items[i].left = left;
    items[i].right = right;
    items[i].mid = mid;
    for(int j = 0; j < 4 ; ++j)
    {
	items[i].left_color[j] = left_col[j];
	items[i].right_color[j] = right_col[j];
    }
    items[i].bmode = bmode;
    items[i].cmode = cmode;
	
/*
    fprintf(stderr,"left: %g [%g,%g,%g,%g]\nright: %g [%g,%g,%g,%g]\n%d %d\n",
	   left, left_col[0], left_col[1], left_col[2], left_col[3],
	   right, right_col[0], right_col[1], right_col[2], right_col[3], 
	   (int)bmode, (int)cmode);
*/

}

static void
grad_dump(gradient_item_t *items, int ncolors)
{
    fprintf(stderr,"gradient dump: %d\n", ncolors);
    for(int i = 0; i < ncolors; ++i)
    {
	fprintf(stderr,"%d: %g\n", i, items[i].right);
    }
}

int 
grad_find(double index, gradient_item_t *items, int ncolors)
{
    for(int i = 0; i < ncolors; ++i)
    {
	if(index <= items[i].right)
	{
	    return i;
	} 
    }
    fprintf(stderr,"No gradient for %g\n", index);
    grad_dump(items, ncolors);
    assert(0 && "Didn't find an entry");
    return -1;
}

static double
calc_linear_factor (double middle, double pos)
{
  if (pos <= middle)
    {
      if (middle < EPSILON)
	return 0.0;
      else
	return 0.5 * pos / middle;
    }
  else
    {
      pos -= middle;
      middle = 1.0 - middle;

      if (middle < EPSILON)
	return 1.0;
      else
	return 0.5 + 0.5 * pos / middle;
    }
}

static double
calc_curved_factor (double middle,double pos)
{
  if (middle < EPSILON)
    middle = EPSILON;

  return pow (pos, log (0.5) / log (middle));
}

static double
calc_sine_factor (double middle, double pos)
{
    pos = calc_linear_factor (middle, pos);
    return (sin ((-M_PI / 2.0) + M_PI * pos) + 1.0) / 2.0;
}

static double
calc_sphere_increasing_factor (double middle,
			       double pos)
{
    pos = calc_linear_factor (middle, pos) - 1.0;
    return sqrt (1.0 - pos * pos); 
}

static double
calc_sphere_decreasing_factor (double middle,
			       double pos)
{
    pos = calc_linear_factor (middle, pos);
    return 1.0 - sqrt(1.0 - pos * pos);
}

rgba_t 
GradientColorMap::lookup(double input_index) const
{
    assert(canary == 0xfeeefeee);
    double index = input_index == 1.0 ? 1.0 : fmod(input_index,1.0);
    if(index < 0.0 || index > 1.0 || index != index)
    {
	// must be infinite or NaN
	return black;
    }
    int i = grad_find(index, items, ncolors); 
    assert(i >= 0 && i < ncolors);

    gradient_item_t *seg = &items[i];

    double seg_len = seg->right - seg->left;
    
    double middle;
    double pos;
    if (seg_len < EPSILON)
    {
        middle = 0.5;
        pos    = 0.5;
    }
    else
    {
        middle = (seg->mid - seg->left) / seg_len;
        pos    = (index - seg->left) / seg_len;
    }
    
    double factor;
    switch (seg->bmode)
    {
    case BLEND_LINEAR:
        factor = calc_linear_factor (middle, pos);
        break;
    
    case BLEND_CURVED:
        factor = calc_curved_factor (middle, pos);
        break;
      
    case BLEND_SINE:
        factor = calc_sine_factor (middle, pos);
        break;
    
    case BLEND_SPHERE_INCREASING:
        factor = calc_sphere_increasing_factor (middle, pos);
        break;
    
    case BLEND_SPHERE_DECREASING:
        factor = calc_sphere_decreasing_factor (middle, pos);
        break;
    
    default:
        assert(0 && "Unknown gradient type");
        return black;
    }

    /* Calculate color components */
    rgba_t result;
    double *lc = seg->left_color;
    double *rc = seg->right_color;
    switch (seg->cmode) {
    case RGB:
        result.r = (unsigned char)(255.0 * (lc[0] + (rc[0] - lc[0]) * factor));
        result.g = (unsigned char)(255.0 * (lc[1] + (rc[1] - lc[1]) * factor));
        result.b = (unsigned char)(255.0 * (lc[2] + (rc[2] - lc[2]) * factor));
        break;
    case HSV_CCW:
    case HSV_CW:
        double lh,ls,lv;
        double rh,rs,rv;

        gimp_rgb_to_hsv (lc[0], lc[1], lc[2], &lh, &ls, &lv);
        gimp_rgb_to_hsv (rc[0], rc[1], rc[2], &rh, &rs, &rv);

        if (seg->cmode == HSV_CCW && lh >= rh)
        {
            rh += 1.0;
        } 
        if (seg->cmode == HSV_CW && lh <= rh )
        { 
            lh += 1.0;
        }
                
                
        double h,s,v;
        h = lh + (rh - lh) * factor;
        s = ls + (rs - ls) * factor;
        v = lv + (rv - lv) * factor;
                
        if ( h > 1.0 ) { h -= 1.0; }
        //fprintf(stderr,"HSV  %f %f %f FACTOR %f POS %f\n", h,s,v,factor,pos);
                
        double r,g,b;
        gimp_hsv_to_rgb(h, s, v, &r, &g, &b);
        result.r = (unsigned char)(255.0 * r);
        result.g = (unsigned char)(255.0 * g);
        result.b = (unsigned char)(255.0 * b);
      
        break;
    default:
        assert(0 && "Unknown color type");
        result = black;
        break;
    }

    /* Calculate alpha */
    result.a = (unsigned char)(255.0 * (lc[3] + (rc[3] - lc[3]) * factor));
    return result;
}


ListColorMap::ListColorMap() : ColorMap()
{
    items = NULL;
}

ListColorMap::~ListColorMap()
{
    delete[] items;
}

bool
ListColorMap::init(int ncolors_)
{
    if(ncolors_ == 0)
    {
	return false;
    }

    ncolors = ncolors_; 

    items = new(std::nothrow) list_item_t[ncolors];
    if(!items)
    {
	return false;
    }

    for(int i = 0; i < ncolors; ++i)
    {
	items[i].color = black;
	items[i].index = 0;
    }
    return true;
}

void 
ListColorMap::set(int i, double d, int r, int g, int b, int a)
{
    rgba_t color;
    color.r = (unsigned char)r;
    color.g = (unsigned char)g;
    color.b = (unsigned char)b;
    color.a = (unsigned char)a;

    items[i].color = color;
    items[i].index = d;
} 

rgba_t 
ListColorMap::lookup(double index) const
{
    int i,j;
    rgba_t mix, left, right;
    double dist, r;

    assert(canary == 0xfeeefeee);

    index = index == 1.0 ? 1.0 : fmod(index,1.0);
    i = find(index, items, ncolors); 
    assert(i >= 0 && i < ncolors);

    /* fprintf(stderr,"%g->%d\n",index,i); */
    if(index <= items[i].index || i == ncolors-1) 
    {
	return items[i].color;
    }

    j = i+1;

    /* mix colors i & j in proportion to the distance between them */
    dist = items[j].index - items[i].index;

    /* fprintf(stderr,"dist: %g\n",dist); */
    if(dist == 0.0)
    {
	return items[i].color;
    }
    
    r = (index - items[i].index)/dist;
    /* fprintf(stderr,"r:%g\n",r); */

    left = items[i].color;
    right = items[j].color;

    mix.r = (unsigned char)((left.r * (1.0-r) + right.r * r));
    mix.g = (unsigned char)((left.g * (1.0-r) + right.g * r));
    mix.b = (unsigned char)((left.b * (1.0-r) + right.b * r));
    mix.a = (unsigned char)((left.a * (1.0-r) + right.a * r));

    return mix;
}

/* Convert from rgb colorspace to hsv and hsl
   all components in [0,1] except hue in [0,6]

   Taken from Foley, van Dam, Feiner & Hughes
*/

#define MAX3(a,b,c) ((a) > (b) ? \
                       ((a) > (c) ? (a) : (c)) : \
                       ((b) > (c) ? (b) : (c)))

#define MIN3(a,b,c) ((a) < (b) ? \
                       ((a) < (c) ? (a) : (c)) : \
                       ((b) < (c) ? (b) : (c)))
  
void
rgb_to_hsv(
    double r, double g, double b,
    double *h, double *s, double *v)
{
    double min = MIN3( r, g, b );
    double max = MAX3( r, g, b );
    *v = max;			

    double delta = max - min;

    *s = (max == 0.0) ? 0.0 : (delta/max);

    if(*s == 0.0)
    {
        // achromatic
        *h = 0; // strictly, undefined. we choose 0
        return;
    }

    
    if( r == max )
    {
        *h = ( g - b ) / delta;                // between yellow & magenta
    }
    else if( g == max )
    {
        *h = 2 + ( b - r ) / delta;        // between cyan & yellow
    }    
    else
    {
        *h = 4 + ( r - g ) / delta;        // between magenta & cyan
    }

    if( *h < 0 )
    {
        *h += 6.0;
    }
}

void gimp_rgb_to_hsv(double r, double g, double b, double *h, double *s, double *v) 
{
    rgb_to_hsv(r,g,b,h,s,v);
    *h /= 6.0;
}

void
rgb_to_hsl(
    double r, double g, double b,
    double *h, double *s, double *l)
{
    double min = MIN3( r, g, b );
    double max = MAX3( r, g, b );

    *l = (max+min)/2.0;                        

    if(max == min)
    {
        // achromatic
        *s = 0;
        *h = 0;
    }
    else
    {
        double delta = max - min;

        *s = (*l <= 0.5) ? (delta / (max + min)) : (delta / (2.0 - (max+min)));

        if( r == max )
        {
            *h = ( g - b ) / delta;                // between yellow & magenta
        }
        else if( g == max )
        {
            *h = 2 + ( b - r ) / delta;        // between cyan & yellow
        }    
        else
        {
            *h = 4 + ( r - g ) / delta;        // between magenta & cyan
        }
        
        if( *h < 0 )
        {
            *h += 6.0;
        }
    }
}

// hue is assumed to be in degrees
double rgb_component(double n1, double n2, double hue)
{
    hue = (hue > 6.0) ? (hue - 6.0) : (hue < 0.0) ? hue + 6.0 : hue;
    if (hue < 1.0)
    {
	return n1 + (n2 - n1)*hue;
    }
    if (hue < 3.0)
    {
	return n2;
    }
    if (hue < 4.0)
    {
	return n1 + (n2 - n1)*(4.0 - hue);
    }
    return n1;
}

void 
hsl_to_rgb(
    double h, double s, double l,
    double *r, double *g, double *b)
{
    if(s == 0.0)
    {
        // achromatic
        *r = *g = *b = l;
    }
    else
    {
	// chromatic
        double n2;
        if(l <= 0.5)
        {
            n2 = l * (1.0 + s);
        }
        else
        {
            n2 = l + s - l*s;
        }

        double n1 = 2.0 * l - n2;

        *r = rgb_component(n1, n2, h + 2.0);
        *g = rgb_component(n1, n2, h);
        *b = rgb_component(n1, n2, h - 2.0);
    }

    //fprintf(stderr,"hsl(%g,%g,%g) -> rgb(%g,%g,%g)\n", h,s,l,*r,*g,*b);
}

void 
hsv_to_rgb(
    double h, double s, double v,
    double *r, double *g, double *b)
{
    /* 0 <= h < 6 */
    if(s == 0)
    {
        *r = *g = *b = v;
        return;
    }

    h = fmod(h,6.0);
    if(h < 0)
    {
        h += 6.0;
    }

    int i = int(h);
    double f = h - i; //Decimal bit of hue
    double p = v * (1 - s);
    double q = v * (1 - s * f);
    double t = v * (1 - s * (1 - f));
    
    switch(i)
    {
    case 0:
        *r = v;
        *g = t;
        *b = p;
        break;
    case 1:
        *r = q;
        *g = v;
        *b = p;
        break;
    case 2:
        *r = p;
        *g = v;
        *b = t;
        break;
    case 3:
        *r = p;
        *g = q;
        *b = v;
        break;
    case 4:
        *r = t;
        *g = p;
        *b = v;
        break;
    case 5:
        *r = v;
        *g = p;
        *b = q;
    }
}

void gimp_hsv_to_rgb(double h, double s, double v, double *r, double *g, double *b)
{
    h *= 6.0; // h*360/60
    hsv_to_rgb(h,s,v,r,g,b);
}

// accessors for hsl components
double hue(double r, double g, double b)
{
    double h,s,l;
    rgb_to_hsl(r,g,b,&h,&s,&l);
    return h;
}

double sat(double r, double g, double b)
{
    double h,s,l;
    rgb_to_hsl(r,g,b,&h,&s,&l);
    return s;

}

double lum(double r, double g, double b)
{
    double h,s,l;
    rgb_to_hsl(r,g,b,&h,&s,&l);
    return l;
}

void gradient(void *grad_object, double index, double *r, double *g, double *b)
{
    ColorMap *pmap = (ColorMap *)grad_object;

    //fprintf(stderr,"gradient %p\n", grad_object);
    rgba_t col = pmap->lookup(index);
    *r = ((double)col.r)/255.0;
    *g = ((double)col.g)/255.0;
    *b = ((double)col.b)/255.0;
}

