package controlP5;

/**
 * controlP5 is a processing gui library.
 *
 *  2006-2012 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	07/30/2015
 * @version		2.2.5
 *
 */

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 * Used to convert Annotations into individual controllers this method of creating controllers is
 * derived from cp5magic by Karsten Schmidt http://hg.postspectacular.com/cp5magic/wiki/Home
 */

class ControllerAutomator {

	static Map< Set< Class< ? >> , Class< ? extends Controller< ? >>> mapping = new HashMap< Set< Class< ? >> , Class< ? extends Controller< ? >>>( );

	static {
		mapping.put( makeKey( boolean.class ) , Toggle.class );
		mapping.put( makeKey( int.class ) , Slider.class );
		mapping.put( makeKey( float.class ) , Slider.class );
		mapping.put( makeKey( String.class ) , Textfield.class );
	}

	static Map< String , Class< ? extends ControllerInterface< ? >>> types = new HashMap< String , Class< ? extends ControllerInterface< ? >>>( );

	static {
		types.put( "slider" , Slider.class );
		types.put( "knob" , Knob.class );
		types.put( "numberbox" , Numberbox.class );
		types.put( "toggle" , Toggle.class );
		types.put( "bang" , Bang.class );
		types.put( "toggle" , Toggle.class );
		types.put( "textfield" , Textfield.class );
		types.put( "label" , Textlabel.class );
		types.put( "textlabel" , Textlabel.class );
		types.put( "list" , ListBox.class );
		types.put( "dropdown" , DropdownList.class );
		types.put( "scrollable" , ScrollableList.class );
	}

	static Set< Class< ? >> makeKey( Class< ? > ... cs ) {
		Set< Class< ? >> set = new HashSet< Class< ? >>( );
		for ( Class< ? > c : cs ) {
			set.add( c );
		}
		return set;
	}

	private ControlP5 cp5;

	ControllerAutomator( ControlP5 c ) {
		cp5 = c;
	}

	private Object[] getParameters( Class< ? >[] cs , String v ) {

		if ( cs[ 0 ] == int.class ) {
			return new Object[] { i( v , 0 ) };
		} else if ( cs[ 0 ] == float.class ) {
			return new Object[] { Float.parseFloat( v ) };
		} else if ( cs[ 0 ] == String.class ) {
			return new Object[] { v };
		} else if ( cs[ 0 ] == boolean.class ) {
			return new Object[] { Boolean.parseBoolean( v ) };
		}
		return new Object[ 0 ];
	}

	/**
	 * analyzes an object and adds fields with ControlElement annotations to controlP5.
	 * 
	 */
	void addControllersFor( final String theAddressSpace , final Object t ) {

		if ( t instanceof List< ? > ) {
			return;
		}

		Class< ? > c = t.getClass( );

		Field[] fs = c.getDeclaredFields( );

		Method[] ms = c.getDeclaredMethods( );

		Map< ControllerInterface , Integer > controllersIndexed = new HashMap< ControllerInterface , Integer >( );

		for ( Method m : ms ) {

			int zindex = 0;

			if ( m.isAnnotationPresent( ControlElement.class ) ) {

				ControlElement ce = m.getAnnotation( ControlElement.class );

				Map< String , String > params = new HashMap< String , String >( );

				Class< ? extends ControllerInterface< ? >> type = null;

				for ( String s : ce.properties( ) ) {
					String[] a = s.split( "=" );
					if ( a[ 0 ].startsWith( "type" ) ) {
						type = types.get( a[ 1 ].toLowerCase( ) );
					} else if ( a[ 0 ].equals( "z-index" ) ) {
						zindex = i( a[ 1 ] , 0 );
					} else {
						params.put( "set" + capitalize( a[ 0 ] ) , a[ 1 ] );
					}
				}

				if ( type == null ) {
					type = mapping.get( makeKey( m.getParameterTypes( ) ) );
				}
				if ( type != null ) {

					ControllerInterface< ? > cntr = null;

					if ( params.containsKey( "setItems" ) ) {

						if ( type.equals( ListBox.class ) ) {

							cntr = cp5.addScrollableList( t , theAddressSpace , m.getName( ) , ce.x( ) , ce.y( ) , 100 , 100 );
							( ( ScrollableList ) cntr ).addItems( params.get( "setItems" ).split( "," ) );
							( ( ScrollableList ) cntr ).setOpen( true );
							( ( ScrollableList ) cntr ).setType(ScrollableList.LIST);

						} else if ( type.equals( DropdownList.class ) ) {

							cntr = cp5.addScrollableList( t , theAddressSpace , m.getName( ) , ce.x( ) , ce.y( ) , 100 , 100 );
							( ( ScrollableList ) cntr ).addItems( params.get( "setItems" ).split( "," ) );
							( ( ScrollableList ) cntr ).setOpen( false );
							( ( ScrollableList ) cntr ).setType(ScrollableList.DROPDOWN);
							
						} else if ( type.equals( ScrollableList.class ) ) {

							cntr = cp5.addScrollableList( t , theAddressSpace , m.getName( ) , ce.x( ) , ce.y( ) , 100 , 100 );
							( ( ScrollableList ) cntr ).addItems( params.get( "setItems" ).split( "," ) );

						}

					} else {
						cntr = cp5.addController( t , theAddressSpace , m.getName( ) , type , ce.x( ) , ce.y( ) );
					}

					controllersIndexed.put( cntr , zindex );

					if ( ce.label( ).length( ) > 0 ) {
						cntr.setCaptionLabel( ce.label( ) );
					}

					for ( Iterator< String > i = params.keySet( ).iterator( ) ; i.hasNext( ) ; ) {
						String k = ( String ) i.next( );
						String v = ( String ) params.get( k );
						for ( Method method : cntr.getClass( ).getMethods( ) ) {
							if ( method.getName( ).equals( k ) ) {
								try {
									Object[] os = getParameters( method.getParameterTypes( ) , v );
									method.setAccessible( true );
									method.invoke( cntr , os );
								} catch ( Exception e ) {
									/* TODO is thrown when running ControlP5annotation example */
									// ControlP5.logger.severe( e.toString( ) );
								}
							}
						}
					}
				}
			}
		}

		for ( Field f : fs ) {

			int zindex = 0;

			if ( f.isAnnotationPresent( ControlElement.class ) ) {

				ControlElement ce = f.getAnnotation( ControlElement.class );

				Map< String , String > params = new HashMap< String , String >( );

				Class< ? extends ControllerInterface< ? >> type = null;

				for ( String s : ce.properties( ) ) {
					String[] a = s.split( "=" );
					if ( a[ 0 ].startsWith( "type" ) ) {
						type = types.get( a[ 1 ].toLowerCase( ) );
					} else if ( a[ 0 ].equals( "z-index" ) ) {
						zindex = i( a[ 1 ] , 0 );
					} else {
						params.put( "set" + capitalize( a[ 0 ] ) , a[ 1 ] );
					}
				}

				ControllerInterface< ? > cntr = null;

				f.setAccessible( true );

				if ( f.getType( ) == float.class || f.getType( ) == int.class ) {

					if ( type == Knob.class ) {

						cntr = cp5.addKnob( t , theAddressSpace , f.getName( ) );

					} else if ( type == Numberbox.class ) {

						cntr = cp5.addNumberbox( t , theAddressSpace , f.getName( ) );

					} else {
						cntr = cp5.addSlider( t , theAddressSpace , f.getName( ) );

					}
					try {
						if ( f.getType( ) == float.class ) {
							cntr.setValue( f.getFloat( t ) );
						} else {
							cntr.setValue( f.getInt( t ) );
						}
					} catch ( Exception e ) {
						ControlP5.logger.severe( e.toString( ) );
					}
				} else if ( f.getType( ) == String.class ) {
					if ( type == Textlabel.class ) {
						String s = "";
						try {
							s = "" + f.get( t );
							if ( f.get( t ) == null ) {
								s = ce.label( );
							}
						} catch ( Exception e ) {
						}
						cntr = cp5.addTextlabel( t , theAddressSpace , f.getName( ) , s );
					} else {
						cntr = cp5.addTextfield( t , theAddressSpace , f.getName( ) );
					}
				} else if ( f.getType( ) == boolean.class ) {
					cntr = cp5.addToggle( t , theAddressSpace , f.getName( ) );
					try {
						cntr.setValue( f.getBoolean( t ) ? 1 : 0 );
					} catch ( Exception e ) {
						ControlP5.logger.severe( e.toString( ) );
					}
				}

				if ( cntr != null ) {

					controllersIndexed.put( cntr , zindex );

					if ( ce.label( ).length( ) > 0 ) {
						cntr.setCaptionLabel( ce.label( ) );
					}
					cntr.setPosition( ce.x( ) , ce.y( ) );

					for ( Iterator< String > i = params.keySet( ).iterator( ) ; i.hasNext( ) ; ) {
						String k = ( String ) i.next( );
						String v = ( String ) params.get( k );
						for ( Method method : cntr.getClass( ).getMethods( ) ) {
							if ( method.getName( ).equals( k ) ) {
								try {
									Object[] os = getParameters( method.getParameterTypes( ) , v );
									method.setAccessible( true );
									method.invoke( cntr , os );
								} catch ( Exception e ) {
									ControlP5.logger.severe( e.toString( ) );
								}
							}
						}
					}
				}
			}
		}

		/* */
		for ( Entry< ControllerInterface , Integer > entry : entriesSortedByValues( controllersIndexed ) ) {
			entry.getKey( ).bringToFront( );
		}
	}

	private static < K , V extends Comparable< ? super V >> List< Entry< K , V >> entriesSortedByValues( Map< K , V > map ) {

		List< Entry< K , V >> sortedEntries = new ArrayList< Entry< K , V >>( map.entrySet( ) );

		Collections.sort( sortedEntries , new Comparator< Entry< K , V >>( ) {
			@Override
			public int compare( Entry< K , V > e1 , Entry< K , V > e2 ) {
				return e1.getValue( ).compareTo( e2.getValue( ) );
			}
		} );

		return sortedEntries;
	}

	/**
	 * capitalizes a string.
	 * 
	 * @param theString
	 * @return String
	 */
	static String capitalize( String theString ) {
		final StringBuilder result = new StringBuilder( theString.length( ) );
		String[] words = theString.split( "\\s" );
		for ( int i = 0 , l = words.length ; i < l ; ++i ) {
			if ( i > 0 )
				result.append( " " );
			result.append( Character.toUpperCase( words[ i ].charAt( 0 ) ) ).append( words[ i ].substring( 1 ) );
		}
		return result.toString( );
	}

	private int i( String o , int theDefault ) {
		return isNumeric( o ) ? Integer.parseInt( o ) : isHex( o ) ? o.length( ) == 6 ? ( int ) Long.parseLong( "FF" + o , 16 ) : ( int ) Long.parseLong( o , 16 ) : theDefault;
	}

	private boolean isNumeric( String str ) {
		return str.matches( "(-|\\+)?\\d+(\\.\\d+)?" );
	}

	private boolean isHex( String str ) {
		// (?:0[xX])?[0-9a-fA-F]+ (This will match with or without 0x prefix)
		// System.out.println( "isHex? " + str + " " + str.matches( "[\\dA-Fa-f]+" ) );
		return str.matches( "[\\dA-Fa-f]+" );
	}
}
