package controlP5;

/**
 * controlP5 is a processing gui library.
 * 
 * 2006-2015 by Andreas Schlegel
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1
 * of the License, or (at your option) any later version.
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General
 * Public License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
 * Boston, MA 02111-1307 USA
 * 
 * @author Andreas Schlegel (http://www.sojamo.de)
 * @modified ##date##
 * @version ##version##
 * 
 */

import java.util.HashMap;
import java.util.Map;

import processing.core.PApplet;
import processing.core.PGraphics;

public class ColorWheel extends Controller< ColorWheel > {

	/* TODO _myColorValue should only be used internally,
	 * when broadcasting, a composed value based on the hsl
	 * and alpha value should be distributed, same goes for
	 * getValue. */

	private int _myColorValue = 0xffffffff;
	private final Map< String , PGraphics > _myColorResources;
	private final float[] _myCursor;
	private float scalar = 0.8f;
	private int yoff = 10;
	private boolean isInfo = false;
	private Label _myInfoLabel;
	private int drag = NONE;
	private final static int NONE = -1;
	private final static int SATURATION = 0;
	private final static int COLOR = 1;
	private final static int ALPHA = 2;
	int _sideHandleHeight = 8;
	private double[] hsl = new double[] { 1.0 , 1.0 , 1.0 };
	private boolean isUpdateColor = false;

	// argb = int ( 0-255 , 0-255 , 0-255 , 0-255 )
	// hue = double ( 0.0-1.0 ) 0-360
	// saturation = double ( 0.0-1.0 ) 0-100%
	// lightness = double ( 0.0-1.0 ) 0-100%
	// brightness = double ( 0.0-1.0 ) 0-100%

	public ColorWheel( ControlP5 theControlP5 , String theName ) {
		this( theControlP5 , theControlP5.getDefaultTab( ) , theName , 0 , 0 , autoWidth , autoHeight );
		theControlP5.register( theControlP5.papplet , theName , this );
	}

	public ColorWheel( ControlP5 theControlP5 , ControllerGroup< ? > theParent , String theName , int theX , int theY , int theWidth , int theHeight ) {
		super( theControlP5 , theParent , theName , theX , theY , theWidth , theHeight );

		_myColorResources = new HashMap< String , PGraphics >( );
		_myColorResources.put( "default" , cp5.papplet.createGraphics( theWidth , theHeight ) );
		_myCursor = new float[] { getWidth( ) / 2 , getHeight( ) / 2 };
		_myCaptionLabel.align( LEFT , BOTTOM_OUTSIDE );
		_myCaptionLabel.setPaddingX( 0 );
		_myInfoLabel = new Label( cp5 , theName + "-info" );
		_myInfoLabel.setPaddingX( 4 ).getStyle( ).marginTop = 4;
		yoff = ( int ) ( getWidth( ) * 0.05 );

		getColor().setBackground(cp5.papplet.color(0,100));

		setColorResources( );
	}

	@Override public void onStartDrag( ) {
		checkDrag( );
	}

	private void checkDrag( ) {
		double x = getPointer( ).x( );
		double y = getPointer( ).y( ) + yoff;
		double xcenter = getWidth( ) / 2;
		double ycenter = getHeight( ) / 2;
		double d1 = ( ( getWidth( ) / 2 ) * scalar ) + 1;
		double d = Math.sqrt( Math.pow( x - xcenter , 2 ) + Math.pow( y - ycenter , 2 ) );
		double w = ( getWidth( ) - ( d1 * 2 ) ) / 2;
		drag = NONE;
		if ( d <= d1 ) {
			drag = COLOR;
		} else if ( x >= 0 && x <= w ) {
			drag = SATURATION;
		} else if ( x >= getWidth( ) - w && x <= getWidth( ) ) {
			drag = ALPHA;
		}
	}

	public void onEndDrag( ) {
		drag = NONE;
	}

	@Override public void onDrag( ) {
		switch ( drag ) {
		case ( COLOR ):
			double x = getPointer( ).x( );
			double y = getPointer( ).y( ) + yoff;
			double xcenter = getWidth( ) / 2;
			double ycenter = getHeight( ) / 2;
			double a = Math.atan2( y - ycenter , x - xcenter );
			double d0 = getWidth( ) * 0.1;
			double d1 = ( ( getWidth( ) / 2 ) * scalar ) + 1;
			double d = Math.sqrt( Math.pow( x - xcenter , 2 ) + Math.pow( y - ycenter , 2 ) );
			if ( d >= d1 - 1 ) {
				x = ( xcenter + Math.cos( a ) * d1 );
				y = ( ycenter + Math.sin( a ) * d1 );
			} else if ( d <= d0 ) {
				x = ( xcenter + Math.cos( a ) * d0 );
				y = ( ycenter + Math.sin( a ) * d0 );
			}
			set( _myCursor , ( float ) x , ( float ) y );

			int xx = ( int ) x;
			int yy = ( int ) y;

			double[] t = RGBtoHSL( _myColorResources.get( "default" ).get( xx , yy ) );
			hsl[ 0 ] = t[ 0 ];
			hsl[ 2 ] = t[ 2 ];
			_myColorValue = HSLtoRGB( hsl );
			setValue( _myColorValue );
			break;
		case ( SATURATION ):
			float s1 = ( getHeight( ) - ( yoff * 2 ) - _sideHandleHeight );
			setSaturation( map( getPointer( ).y( ) , 0 , s1 , 1.0 , 0.0 ) );
			_myColorValue = HSLtoRGB( hsl );
			setValue( _myColorValue );
			break;
		case ( ALPHA ):
			float a1 = ( getHeight( ) - ( yoff * 2 ) - _sideHandleHeight );
			setAlpha( ( int ) map( getPointer( ).y( ) , 0 , a1 , 255 , 0 ) );
			_myColorValue = HSLtoRGB( hsl );
			setValue( _myColorValue );
			break;
		}
	}

	@Override public void onPress( ) {
		checkDrag( );
	}

	@Override public void onRelease( ) {
		onDrag( );
	}

	public ColorWheel scrolled( int theRotationValue ) {
		if ( isVisible ) {
			double x = getPointer( ).x( );
			double d1 = ( ( getWidth( ) / 2 ) * scalar ) + 1;
			double w = ( getWidth( ) - ( d1 * 2 ) ) / 2;
			if ( x >= 0 && x <= w ) {
				setSaturation( hsl[ 1 ] + theRotationValue * 0.01 );
				_myColorValue = HSLtoRGB( hsl );
				setValue( _myColorValue );
			} else if ( x >= getWidth( ) - w && x <= getWidth( ) ) {
				setAlpha( a( ) + theRotationValue );
			}
		}
		return this;
	}

	private void setColorResources( ) {
		/* for now there is only a default resource but this
		 * can be extended to support other color models in
		 * the future. */

		PGraphics buffer = _myColorResources.get( "default" );

		buffer.beginDraw( );

		buffer.background( 0 , 0 );

		int w = buffer.width;

		int h = buffer.height;

		float[] center = new float[] { w / 2 , h / 2 };

		int inner_radius = ( int ) ( buffer.width * 0.1 );

		int outer_radius = ( int ) ( buffer.width * scalar / 2 );
		buffer.fill( 0 );
		buffer.ellipseMode( CENTER );
		buffer.ellipse( buffer.width / 2 , buffer.height / 2 , buffer.width * scalar + 4 , buffer.width * scalar + 4 );
		buffer.fill( 255 );
		buffer.ellipse( buffer.width / 2 , buffer.height / 2 , ( inner_radius + 1 ) * 2 , ( inner_radius + 1 ) * 2 );

		for ( int y = 0 ; y < h ; y++ ) {
			int dy = ( int ) ( y( center ) - y );
			for ( int x = 0 ; x < w ; x++ ) {
				int dx = ( int ) ( x( center ) - x );
				double dist = Math.sqrt( dx * dx + dy * dy );
				if ( dist >= inner_radius && dist <= outer_radius ) {
					double theta = Math.atan2( dy , dx );
					// theta can go from -pi to pi
					double hue = ( theta + PI ) / ( TWO_PI );
					double sat , val;
					if ( dist < ( inner_radius + ( outer_radius - inner_radius ) / 2 ) ) {
						sat = map( dist , inner_radius , outer_radius , 0 , 2 );
						val = 1;
					} else {
						sat = 1;
						val = map( dist , inner_radius , outer_radius , 2 , 0 );
					}
					buffer.set( x , y , HSVtoRGB( hue , sat , val ) );
				}
			}
		}
		buffer.endDraw( );
	}

	public void setHue( double theH ) {

		hsl[ 0 ] = Math.max( 0 , Math.min( 1 , theH ) );
		isUpdateColor = true;
	}

	public void setSaturation( double theS ) {

		hsl[ 1 ] = Math.max( 0 , Math.min( 1 , theS ) );
		isUpdateColor = true;
	}

	public void setLightness( double theL ) {

		hsl[ 2 ] = Math.max( 0 , Math.min( 1 , theL ) );
		isUpdateColor = true;
	}

	public ColorWheel setHSL( double theH , double theS , double theL ) {
		setHue( theH );
		setSaturation( theS );
		setLightness( theL );
		isUpdateColor = true;
		return this;
	}

	public int getRGB( ) {
		return _myColorValue;
	}

	public ColorWheel setRGB( int theColor ) {
		double[] t = RGBtoHSL( theColor );
		hsl[ 0 ] = t[ 0 ];
		hsl[ 2 ] = t[ 2 ];

		float theta = ( float ) ( t[ 0 ] * TWO_PI ) - PI;
		float d0 = getWidth( ) * 0.1f;
		float d1 = ( ( getWidth( ) / 2 ) * scalar ) + 1f;
		float s = ( float ) map( t[ 2 ] , 0f , 1f , d1 , d0 );
		float x = _myColorResources.get( "default" ).width / 2 - ( float ) Math.cos( theta ) * s;
		float y = _myColorResources.get( "default" ).height / 2 - ( float ) Math.sin( theta ) * s;
		set( _myCursor , x , y );
		setSaturation( t[ 1 ] );
		// TODO resolve rounding error issue as reported here https://github.com/sojamo/controlp5/issues/21
		isUpdateColor = true;
		return this;
	}

	public ColorWheel setAlpha( int theAlpha ) {
		/* TODO */
		return this;
	}

	/**
	 * @exclude
	 */
	@Override @ControlP5.Invisible public ColorWheel updateDisplayMode( int theMode ) {
		return updateViewMode( theMode );
	}


	private void updateColor() {
		if(isUpdateColor) {
			System.out.println("colorwheel update");
			_myColorValue = HSLtoRGB(hsl);
			setValue(_myColorValue);
			isUpdateColor = false;
		}
	}

	/**
	 * @exclude
	 */
	@ControlP5.Invisible public ColorWheel updateViewMode( int theMode ) {
		_myDisplayMode = theMode;
		switch ( theMode ) {
		case ( DEFAULT ):
		case ( IMAGE ):
		case ( CUSTOM ):
			_myControllerView = new ColorWheelView( );
		default:
			break;

		}
		return this;
	}

	public int a( ) {
		int a = ( _myColorValue & 0xff000000 ) >> 24;
		return ( a < 0 ) ? 255 : a;
	}

	public int r( ) {
		return ( _myColorValue & 0x00ff0000 ) >> 16;
	}

	public int g( ) {
		return ( _myColorValue & 0x0000ff00 ) >> 8;
	}

	public int b( ) {
		return ( _myColorValue & 0x000000ff ) >> 0;
	}

	private class ColorWheelView implements ControllerView< ColorWheel > {

		public void display( PGraphics theGraphics , ColorWheel theController ) {

			updateColor();

			PGraphics buffer = _myColorResources.get( "default" );

			theGraphics.fill( getColor().getBackground());
			theGraphics.rect( 0 , 0 , getWidth( ) , getHeight( ) );
			theGraphics.ellipseMode( PApplet.CENTER );
			theGraphics.pushMatrix( );
			theGraphics.translate( 0 , -yoff );
			theGraphics.image( buffer , 0 , 0 );
			theGraphics.pushMatrix( );
			theGraphics.translate( x( _myCursor ) , y( _myCursor ) );
			theGraphics.strokeWeight( 2 );
			theGraphics.noFill( );
			theGraphics.stroke( 255 , 40 );
			theGraphics.ellipse( 1 , 1 , 10 , 10 );
			theGraphics.stroke( 250 );
			theGraphics.fill( _myColorValue );
			theGraphics.ellipse( 0 , 0 , 10 , 10 );

			theGraphics.popMatrix( );
			theGraphics.noStroke( );
			theGraphics.translate( 0 , -yoff );
			theGraphics.fill( HSLtoRGB( hsl[ 0 ] , hsl[ 1 ] , hsl[ 2 ] ) );
			theGraphics.rect( 0 , getHeight( ) , getWidth( ) , yoff * 2 );
			theGraphics.popMatrix( );
			if ( isLabelVisible ) {
				_myCaptionLabel.draw( theGraphics , 0 , 0 , theController );
			}
			if ( isInfo ) {
				_myInfoLabel.setText( String.format( "RGB %d %d %d\nALPHA %d\nHSL %d %.2f %.2f " , r( ) , g( ) , b( ) , a( ) , ( int ) ( hsl[ 0 ] * 360 ) , hsl[ 1 ] , hsl[ 2 ] ) );
				_myInfoLabel.draw( theGraphics , 0 , 0 , theController );
			}
			theGraphics.fill( 255 );
			theGraphics.pushMatrix( );
			int s = _sideHandleHeight / 2;
			float v = ( getHeight( ) - ( yoff * 2 ) - _sideHandleHeight );
			theGraphics.translate( 2 , ( int ) map( hsl[ 1 ] , 1 , 0 , 0 , v ) );
			theGraphics.triangle( 0 , s , s , 0 , s , _sideHandleHeight );
			theGraphics.popMatrix( );
			/* TODO alpha handle theGraphics.pushMatrix( );
			 * theGraphics.translate( getWidth( ) - s - 2 ,
			 * ( int ) map( a( ) , 255 , 0 , 0 , v ) );
			 * theGraphics.triangle( s , s , 0 , 0 , 0 ,
			 * _sideHandleHeight ); theGraphics.popMatrix(
			 * ); */
		}
	}

	public double[] RGBtoHSL( int theRGB ) {
		return RGBtoHSL( theRGB >> 16 & 0xff , theRGB >> 8 & 0xff , theRGB >> 0 & 0xff );
	}

	/**
	 * 
	 * @param theR value between 0 and 255
	 * @param theG value between 0 and 255
	 * @param theB value between 0 and 255
	 * @return double[] values h,s,l are between 0-1
	 */
	public double[] RGBtoHSL( int theR , int theG , int theB ) {
		double[] rgb = new double[ 3 ];
		rgb[ 0 ] = theR / 255.0;
		rgb[ 1 ] = theG / 255.0;
		rgb[ 2 ] = theB / 255.0;
		double max = Math.max( rgb[ 0 ] , Math.max( rgb[ 1 ] , rgb[ 2 ] ) );
		double min = Math.min( rgb[ 0 ] , Math.min( rgb[ 1 ] , rgb[ 2 ] ) );
		double h = ( max + min ) / 2;
		double s = ( max + min ) / 2;
		double l = ( max + min ) / 2;

		if ( max == min ) {
			h = s = 0; // achromatic
		} else {
			double d = max - min;
			s = l > 0.5 ? ( d / ( 2 - max - min ) ) : ( d / ( max + min ) );
			if ( max == rgb[ 0 ] ) {
				h = ( rgb[ 1 ] - rgb[ 2 ] ) / d + ( rgb[ 1 ] < rgb[ 2 ] ? 6 : 0 );
			} else if ( max == rgb[ 1 ] ) {
				h = ( rgb[ 2 ] - rgb[ 0 ] ) / d + 2;
			} else if ( max == rgb[ 2 ] ) {
				h = ( rgb[ 0 ] - rgb[ 1 ] ) / d + 4;
			}
			h /= 6;
		}

		return new double[] { h , s , l };
	}

	public int HSVtoRGB( double[] hsv ) {
		return HSVtoRGB( hsv[ 0 ] , hsv[ 1 ] , hsv[ 2 ] );
	}

	/**
	 * 
	 * @param H value between 0-1
	 * @param S value between 0-1
	 * @param V value between 0-1
	 * @return int
	 */
	public int HSVtoRGB( double H , double S , double V ) {

		/* http://viziblr.com/news/2011/12/1/drawing-a-color-
		 * hue-wheel-with-c.html */

		double[] rgb = new double[ 3 ];

		if ( H == 1.0 ) {
			H = 0.0;
		}

		double step = 1.0 / 6.0;
		double vh = H / step;

		int i = ( int ) Math.floor( vh );

		double f = vh - i;
		double p = V * ( 1.0 - S );
		double q = V * ( 1.0 - ( S * f ) );
		double t = V * ( 1.0 - ( S * ( 1.0 - f ) ) );

		switch ( i ) {
		case 0: {
			rgb[ 0 ] = V;
			rgb[ 1 ] = t;
			rgb[ 2 ] = p;
			break;
		}
		case 1: {
			rgb[ 0 ] = q;
			rgb[ 1 ] = V;
			rgb[ 2 ] = p;
			break;
		}
		case 2: {
			rgb[ 0 ] = p;
			rgb[ 1 ] = V;
			rgb[ 2 ] = t;
			break;
		}
		case 3: {
			rgb[ 0 ] = p;
			rgb[ 1 ] = q;
			rgb[ 2 ] = V;
			break;
		}
		case 4: {
			rgb[ 0 ] = t;
			rgb[ 1 ] = p;
			rgb[ 2 ] = V;
			break;
		}
		case 5: {
			rgb[ 0 ] = V;
			rgb[ 1 ] = p;
			rgb[ 2 ] = q;
			break;
		}
		default: {
			// not possible - if we get here it is an
			// internal error
			// throw new ArgumentException();
			System.out.println( "hsv to rgb not possible" );
		}
		}
		return ( a( ) << 24 ) | ( ( int ) ( rgb[ 0 ] * 255 ) << 16 ) | ( ( int ) ( rgb[ 1 ] * 255 ) << 8 ) | ( int ) ( rgb[ 2 ] * 255 );
	}

	public final double[] RGBtoHSV( final int c ) {
		return RGBtoHSV( ( c & 0xff0000 ) >> 16 , ( c & 0x00ff00 ) >> 8 , ( c & 0x0000ff ) >> 0 );
	}

	/**
	 * 
	 * @param theR value between 0 and 255
	 * @param theG value between 0 and 255
	 * @param theB value between 0 and 255
	 * @return hsv [ hue (0-1) sat (0-1) val (0-1) ]
	 */
	public final double[] RGBtoHSV( final int theR , final int theG , final double theB ) {

		double hue = 0;

		double sat = 0;

		double val = 0;

		final double r = theR / 255.0;
		final double g = theG / 255.0;
		final double b = theB / 255.0;

		double minRGB = Math.min( r , Math.min( g , b ) );
		double maxRGB = Math.max( r , Math.max( g , b ) );

		// Black-gray-white
		if ( minRGB == maxRGB ) {
			return new double[] { 0 , 0 , minRGB };
		}

		// Colors other than black-gray-white:
		double d = ( r == minRGB ) ? g - b : ( ( b == minRGB ) ? r - g : b - r );
		double h = ( r == minRGB ) ? 3 : ( ( b == minRGB ) ? 1 : 5 );
		hue = map( ( h - ( d / ( maxRGB - minRGB ) ) ) , 0 , 6.0 , 0 , 1.0 );
		sat = ( maxRGB - minRGB ) / maxRGB;
		val = maxRGB;
		return new double[] { hue , sat , val };
	}

	public int HSLtoRGB( final double[] theHSL ) {
		if ( theHSL.length == 3 ) {
			return HSLtoRGB( theHSL[ 0 ] , theHSL[ 1 ] , theHSL[ 2 ] );
		} else {
			String message = "HSLtoRGB(double[]) a length of 3 is expected. ";
			throw new IllegalArgumentException( message );
		}

	}

	/**
	 * 
	 * @param h value between 0 and 360
	 * @param s value between 0 and 100
	 * @param l) value between 0 and 100
	 * @param alpha value between 0 and 1
	 * @return
	 */
	public int HSLtoRGB( final double h , double s , double l ) {
		if ( h < 0.0 || h > 1.0 ) {
			String message = "Color parameter outside of expected range - Hue ( 0.0 - 1.0 )";
			throw new IllegalArgumentException( message );
		}
		if ( s < 0.0 || s > 1.0 ) {
			String message = "Color parameter outside of expected range - Saturation ( 0.0 - 1.0 )";
			throw new IllegalArgumentException( message );
		}

		if ( l < 0.0 || l > 1.0 ) {
			String message = "Color parameter outside of expected range - Luminance ( 0.0 - 1.0 )";
			throw new IllegalArgumentException( message );
		}

		double q = 0;

		if ( l < 0.5 ) {
			q = l * ( 1 + s );
		} else {
			q = ( l + s ) - ( s * l );
		}

		double p = 2 * l - q;

		double r = Math.max( 0 , HueToRGB( p , q , h + ( 1.0f / 3.0f ) ) );
		double g = Math.max( 0 , HueToRGB( p , q , h ) );
		double b = Math.max( 0 , HueToRGB( p , q , h - ( 1.0f / 3.0f ) ) );

		return ( 255 << 24 ) | ( ( int ) ( r * 255 ) << 16 ) | ( ( int ) ( g * 255 ) << 8 ) | ( int ) ( b * 255 );
	}

	private static double HueToRGB( double p , double q , double h ) {
		if ( h < 0 )
			h += 1;

		if ( h > 1 )
			h -= 1;

		if ( 6 * h < 1 ) {
			return p + ( ( q - p ) * 6 * h );
		}

		if ( 2 * h < 1 ) {
			return q;
		}

		if ( 3 * h < 2 ) {
			return p + ( ( q - p ) * 6 * ( ( 2.0f / 3.0f ) - h ) );
		}

		return p;
	}

	private final double map( double val , double a1 , double a2 , double b1 , double b2 ) {
		return b1 + ( b2 - b1 ) * ( ( val - a1 ) / ( a2 - a1 ) );
	}
}
