package controlP5;

import java.util.Iterator;
import java.util.LinkedHashMap;

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

/**
 * Use charts to display float array data as line chart, yet experimental, but see the
 * ControlP5chart example for more details.
 * 
 * @example controllers/ControlP5chart
 */
public class Chart extends Controller< Chart > {

	public final static int LINE = 0;
	public final static int BAR = 1;
	public final static int BAR_CENTERED = 2;
	public final static int HISTOGRAM = 3;
	public final static int PIE = 4;
	public final static int AREA = 5;

	protected final LinkedHashMap< String , ChartDataSet > _myDataSet;
	protected float resolution = 1;
	protected float strokeWeight = 1;
	protected float _myMin = 0;
	protected float _myMax = 1;

	/**
	 * Convenience constructor to extend Chart.
	 * 
	 * @example use/ControlP5extendController
	 * @param theControlP5
	 * @param theName
	 */
	public Chart( ControlP5 theControlP5 , String theName ) {
		this( theControlP5 , theControlP5.getDefaultTab( ) , theName , 0 , 0 , 200 , 100 );
		theControlP5.register( theControlP5.papplet , theName , this );
	}

	protected Chart( ControlP5 theControlP5 , ControllerGroup< ? > theParent , String theName , float theX , float theY , int theWidth , int theHeight ) {
		super( theControlP5 , theParent , theName , theX , theY , theWidth , theHeight );
		setRange( 0 , theHeight );
		_myDataSet = new LinkedHashMap< String , ChartDataSet >( );
		getCaptionLabel( ).align( LEFT, BOTTOM_OUTSIDE ).paddingX = 0;
		
	}

	public Chart setRange( float theMin , float theMax ) {
		_myMin = theMin;
		_myMax = theMax;
		return this;
	}

	public Chart setColors( String theSetIndex , int ... theColors ) {
		getDataSet( ).get( theSetIndex ).setColors( theColors );
		return this;
	}

	public Chart addData( ChartData theItem ) {
		return addData( getFirstDataSetIndex( ) , theItem );
	}

	private String getFirstDataSetIndex( ) {
		return getDataSet( ).keySet( ).iterator( ).next( );
	}

	private String getLastDataSetIndex( ) {
		Iterator< String > it = getDataSet( ).keySet( ).iterator( );
		String last = null;
		while ( it.hasNext( ) ) {
			last = it.next( );
		}
		return last;
	}

	public Chart addData( String theSetIndex , ChartData theItem ) {
		getDataSet( theSetIndex ).add( theItem );
		return this;
	}

	public Chart addData( float theValue ) {
		ChartData cdi = new ChartData( theValue );
		getDataSet( getFirstDataSetIndex( ) ).add( cdi );
		return this;
	}

	public Chart addData( String theSetIndex , float theValue ) {
		ChartData cdi = new ChartData( theValue );
		getDataSet( theSetIndex ).add( cdi );
		return this;
	}

	public Chart addData( ChartDataSet theChartData , float theValue ) {
		ChartData cdi = new ChartData( theValue );
		theChartData.add( cdi );
		return this;
	}

	// array operations see syntax
	// http://www.w3schools.com/jsref/jsref_obj_array.asp

	/**
	 * adds a new float at the beginning of the data set.
	 */
	public Chart unshift( float theValue ) {
		return unshift( getFirstDataSetIndex( ) , theValue );
	}

	public Chart unshift( String theSetIndex , float theValue ) {
		if ( getDataSet( theSetIndex ).size( ) > ( getWidth() / resolution ) ) {
			removeLast( theSetIndex );
		}
		return addFirst( theSetIndex , theValue );
	}

	public Chart push( float theValue ) {
		return push( getFirstDataSetIndex( ) , theValue );
	}

	public Chart push( String theSetIndex , float theValue ) {
		if ( getDataSet( theSetIndex ).size( ) > ( getWidth() / resolution ) ) {
			removeFirst( theSetIndex );
		}
		return addLast( theSetIndex , theValue );
	}

	public Chart addFirst( float theValue ) {
		return addFirst( getFirstDataSetIndex( ) , theValue );
	}

	public Chart addFirst( String theSetIndex , float theValue ) {
		ChartData cdi = new ChartData( theValue );
		getDataSet( theSetIndex ).add( 0 , cdi );
		return this;
	}

	public Chart addLast( float theValue ) {
		return addLast( getFirstDataSetIndex( ) , theValue );
	}

	public Chart addLast( String theSetIndex , float theValue ) {
		ChartData cdi = new ChartData( theValue );
		getDataSet( theSetIndex ).add( cdi );
		return this;
	}

	public Chart removeLast( ) {
		return removeLast( getFirstDataSetIndex( ) );
	}

	public Chart removeLast( String theSetIndex ) {
		return removeData( theSetIndex , getDataSet( theSetIndex ).size( ) - 1 );
	}

	public Chart removeFirst( ) {
		return removeFirst( getFirstDataSetIndex( ) );
	}

	public Chart removeFirst( String theSetIndex ) {
		return removeData( theSetIndex , 0 );
	}

	public Chart removeData( ChartData theItem ) {
		removeData( getFirstDataSetIndex( ) , theItem );
		return this;
	}

	public Chart removeData( String theSetIndex , ChartData theItem ) {
		getDataSet( theSetIndex ).remove( theItem );
		return this;
	}

	public Chart removeData( int theItemIndex ) {
		removeData( getFirstDataSetIndex( ) , theItemIndex );
		return this;
	}

	public Chart removeData( String theSetIndex , int theItemIndex ) {
		if ( getDataSet( theSetIndex ).size( ) < 1 ) {
			return this;
		}
		getDataSet( theSetIndex ).remove( theItemIndex );
		return this;
	}

	public Chart setData( int theItemIndex , ChartData theItem ) {
		getDataSet( getFirstDataSetIndex( ) ).set( theItemIndex , theItem );
		return this;
	}

	public Chart setData( String theSetItem , int theItemIndex , ChartData theItem ) {
		getDataSet( theSetItem ).set( theItemIndex , theItem );
		return this;
	}

	public Chart addDataSet( String theName ) {
		getDataSet( ).put( theName , new ChartDataSet( theName ) );
		return this;
	}

	public Chart setDataSet( ChartDataSet theItems ) {
		setDataSet( getFirstDataSetIndex( ) , theItems );
		return this;
	}

	public Chart setDataSet( String theSetIndex , ChartDataSet theChartData ) {
		getDataSet( ).put( theSetIndex , theChartData );
		return this;
	}

	public Chart removeDataSet( String theIndex ) {
		getDataSet( ).remove( theIndex );
		return this;
	}

	public Chart setData( float ... theValues ) {
		setData( getFirstDataSetIndex( ) , theValues );
		return this;
	}

	public Chart setData( String theSetIndex , float ... theValues ) {
		if ( getDataSet( ).get( theSetIndex ).size( ) != theValues.length ) {
			getDataSet( ).get( theSetIndex ).clear( );
			for ( int i = 0 ; i < theValues.length ; i++ ) {
				getDataSet( ).get( theSetIndex ).add( new ChartData( 0 ) );
			}
		}
		int n = 0;
		resolution = ( float ) getWidth() / ( getDataSet( ).get( theSetIndex ).size( ) - 1 );
		for ( float f : theValues ) {
			getDataSet( ).get( theSetIndex ).get( n++ ).setValue( f );
		}
		return this;
	}

	public Chart updateData( float ... theValues ) {
		return setData( theValues );
	}

	public Chart updateData( String theSetIndex , float ... theValues ) {
		return setData( theSetIndex , theValues );
	}

	public LinkedHashMap< String , ChartDataSet > getDataSet( ) {
		return _myDataSet;
	}

	public ChartDataSet getDataSet( String theIndex ) {
		return getDataSet( ).get( theIndex );
	}

	public float[] getValuesFrom( String theIndex ) {
		return getDataSet( theIndex ).getValues( );
	}

	public ChartData getData( String theIndex , int theItemIndex ) {
		return getDataSet( theIndex ).get( theItemIndex );
	}

	public int size( ) {
		return getDataSet( ).size( );
	}

	@Override
	public void onEnter( ) {
	}

	@Override
	public void onLeave( ) {
	}

	@Override
	public Chart setValue( float theValue ) {
		// TODO Auto-generated method stub
		return this;
	}

	public Chart setStrokeWeight( float theWeight ) {
		strokeWeight = theWeight;
		for ( ChartDataSet c : getDataSet( ).values( ) ) {
			c.setStrokeWeight( theWeight );
		}
		return this;
	}

	public float getStrokeWeight( ) {
		return strokeWeight;
	}

	/**
	 * ?
	 * 
	 * @param theValue
	 * @return
	 */
	public Chart setResolution( int theValue ) {
		resolution = theValue;
		return this;
	}

	public int getResolution( ) {
		return ( int ) resolution;
	}

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

	/**
	 * @exclude
	 */
	@ControlP5.Invisible
	public Chart updateViewMode( int theMode ) {
		_myDisplayMode = theMode;
		switch ( theMode ) {
		case ( DEFAULT ):
			_myControllerView = new ChartViewPie( );
			break;
		case ( IMAGE ):
			// _myDisplay = new ChartImageDisplay();
			break;
		case ( SPRITE ):
			// _myDisplay = new ChartSpriteDisplay();
			break;
		case ( CUSTOM ):
		default:
			break;
		}
		return this;
	}

	public class ChartViewBar implements ControllerView< Chart > {

		public void display( PGraphics theGraphics , Chart theController ) {
			theGraphics.pushStyle( );
			theGraphics.fill( getColor( ).getBackground( ) );
			theGraphics.rect( 0 , 0 , getWidth( ) , getHeight( ) );
			theGraphics.noStroke( );

			Iterator< String > it = getDataSet( ).keySet( ).iterator( );
			String index = null;
			float o = 0;
			while ( it.hasNext( ) ) {
				index = it.next( );
				float s = getDataSet( index ).size( );
				for ( int i = 0 ; i < s ; i++ ) {
					theGraphics.fill( getDataSet( index ).getColor( i ) );
					float ww = ( ( getWidth() / s ) );
					float hh = PApplet.map( getDataSet( index ).get( i ).getValue( ) , _myMin , _myMax , 0 , getHeight( ) );
					theGraphics.rect( o + i * ww , getHeight( ) , ( ww / getDataSet( ).size( ) ) , -PApplet.min( getHeight( ) , PApplet.max( 0 , hh ) ) );
				}
				o += ( ( getWidth() / s ) ) / getDataSet( ).size( );
			}
			theGraphics.popStyle( );
		}
	}

	public class ChartViewBarCentered implements ControllerView< Chart > {

		public void display( PGraphics theGraphics , Chart theController ) {
			theGraphics.pushStyle( );
			theGraphics.fill( getColor( ).getBackground( ) );
			theGraphics.rect( 0 , 0 , getWidth( ) , getHeight( ) );
			theGraphics.noStroke( );

			Iterator< String > it = getDataSet( ).keySet( ).iterator( );
			String index = null;
			float o = 0;
			int n = 4;
			int off = ( getDataSet( ).size( ) - 1 ) * n;
			while ( it.hasNext( ) ) {
				index = it.next( );
				int s = getDataSet( index ).size( );
				float step = ( float ) getWidth() / ( float ) ( s );
				float ww = step - ( getWidth() % step );
				ww -= 1;
				ww = PApplet.max( 1 , ww );

				for ( int i = 0 ; i < s ; i++ ) {
					theGraphics.fill( getDataSet( index ).getColor( i ) );
					ww = ( ( getWidth() / s ) * 0.5f );
					float hh = PApplet.map( getDataSet( index ).get( i ).getValue( ) , _myMin , _myMax , 0 , getHeight( ) );
					theGraphics.rect( -off / 2 + o + i * ( ( getWidth() / s ) ) + ww / 2 , getHeight( ) , ww , -PApplet.min( getHeight( ) , PApplet.max( 0 , hh ) ) );
				}
				o += n;
			}
			theGraphics.popStyle( );
		}
	}

	public class ChartViewLine implements ControllerView< Chart > {

		public void display( PGraphics theGraphics , Chart theController ) {

			theGraphics.pushStyle( );
			theGraphics.fill( getColor( ).getBackground( ) );
			theGraphics.rect( 0 , 0 , getWidth( ) , getHeight( ) );
			theGraphics.noFill( );
			Iterator< String > it = getDataSet( ).keySet( ).iterator( );
			String index = null;
			while ( it.hasNext( ) ) {
				index = it.next( );
				theGraphics.stroke( getDataSet( index ).getColor( 0 ) );
				theGraphics.strokeWeight( getDataSet( index ).getStrokeWeight( ) );

				theGraphics.beginShape( );
				float res = ( ( float ) getWidth( ) ) / ( getDataSet( index ).size( ) - 1 );
				for ( int i = 0 ; i < getDataSet( index ).size( ) ; i++ ) {
					float hh = PApplet.map( getDataSet( index ).get( i ).getValue( ) , _myMin , _myMax , getHeight( ) , 0 );
					theGraphics.vertex( i * res , PApplet.min( getHeight( ) , PApplet.max( 0 , hh ) ) );
				}
				theGraphics.endShape( );
			}
			theGraphics.noStroke( );
			theGraphics.popStyle( );
			getCaptionLabel( ).draw( theGraphics , 0 , 0 , theController );
		}
	}

	public class ChartViewArea implements ControllerView< Chart > {

		public void display( PGraphics theGraphics , Chart theController ) {

			theGraphics.pushStyle( );
			theGraphics.fill( getColor( ).getBackground( ) );
			theGraphics.rect( 0 , 0 , getWidth( ) , getHeight( ) );
			theGraphics.noStroke( );

			Iterator< String > it = getDataSet( ).keySet( ).iterator( );
			String index = null;
			while ( it.hasNext( ) ) {
				index = it.next( );
				float res = ( ( float ) getWidth( ) ) / ( getDataSet( index ).size( ) - 1 );

				theGraphics.fill( getDataSet( index ).getColor( 0 ) );
				theGraphics.beginShape( );
				theGraphics.vertex( 0 , getHeight( ) );

				for ( int i = 0 ; i < getDataSet( index ).size( ) ; i++ ) {
					float hh = PApplet.map( getDataSet( index ).get( i ).getValue( ) , _myMin , _myMax , getHeight( ) , 0 );
					theGraphics.vertex( i * res , PApplet.min( getHeight( ) , PApplet.max( 0 , hh ) ) );
				}
				theGraphics.vertex( getWidth( ) , getHeight( ) );
				theGraphics.endShape( PApplet.CLOSE );
			}
			theGraphics.noStroke( );
			theGraphics.popStyle( );
		}
	}

	public class ChartViewPie implements ControllerView< Chart > {

		public void display( PGraphics theGraphics , Chart theController ) {
			theGraphics.pushStyle( );
			theGraphics.pushMatrix( );

			Iterator< String > it = getDataSet( ).keySet( ).iterator( );
			String index = null;
			while ( it.hasNext( ) ) {
				index = it.next( );
				float total = 0;
				for ( int i = 0 ; i < getDataSet( index ).size( ) ; i++ ) {
					total += getDataSet( index ).get( i ).getValue( );
				}

				float segment = TWO_PI / total;
				float angle = -HALF_PI;

				theGraphics.noStroke( );
				for ( int i = 0 ; i < getDataSet( index ).size( ) ; i++ ) {
					theGraphics.fill( getDataSet( index ).getColor( i ) );
					float nextAngle = angle + getDataSet( index ).get( i ).getValue( ) * segment;

					// a tiny offset to even out render artifacts when in smooth() mode.
					float a = PApplet.max( 0 , PApplet.map( getWidth( ) , 0 , 200 , 0.05f , 0.01f ) );

					theGraphics.arc( 0 , 0 , getWidth( ) , getHeight( ) , angle - a , nextAngle );
					angle = nextAngle;
				}
				theGraphics.translate( 0 , ( getHeight( ) + 10 ) );
			}
			theGraphics.popMatrix( );
			theGraphics.popStyle( );
		}
	}

	public Chart setView( int theType ) {
		switch ( theType ) {
		case ( PIE ):
			setView( new ChartViewPie( ) );
			break;
		case ( LINE ):
			setView( new ChartViewLine( ) );
			break;
		case ( BAR ):
			setView( new ChartViewBar( ) );
			break;
		case ( BAR_CENTERED ):
			setView( new ChartViewBarCentered( ) );
			break;
		case ( AREA ):
			setView( new ChartViewArea( ) );
			break;
		default:
			System.out.println( "Sorry, this ChartView does not exist" );
			break;
		}
		return this;
	}

	@Override
	public String getInfo( ) {
		return "type:\tChart\n" + super.toString( );
	}

	@Override
	public String toString( ) {
		return super.toString( ) + " [ " + getValue( ) + " ]" + " Chart " + "(" + this.getClass( ).getSuperclass( ) + ")";
	}

}

/* NOTES what is the difference in meaning between chart and graph
 * http://answers.yahoo.com/question/index?qid=20090101193325AA3mgMl
 * 
 * more charts to implement: from https://vimeo.com/groups/oaod/videos/60013194 (44:40) scatter
 * plot, star plot, histogram, dendrogram, box plot, physical map, tree, 2d 3d isosurfaces table,
 * half matrix, graph, hierarchical pie, line graph, numeric matrix, heat map, permutation matrix
 * bar graph, radial graph, */
