package hep.aida.ref.plotter;

import hep.aida.IBaseStyle;
import hep.aida.ref.plotter.StyleListener;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;

/**
 *
 * @author The AIDA team @ SLAC.
 *
 * When plotting an AIDA object there are several styles that contribute to the
 * resulting style of the plot: the default (root) style, the style of the plotter,
 * the style of the region and the style provided by the user. These styles have
 * a parent-child relationship with the default (root) style being the parent of the plotter
 * style, the plotter style the parent of the region style etc.
 * All the styles in an inheritance chain have the same set of parameters. A prameter is
 * defined by the following attributes:
 *   - options, i.e. possible values. This is meant to be an hint of what the possible
 *                                    values are.
 *   - current value. The current value is, by default null.
 * When plotting, the top level style is considered first and all the other in 
 * sequence when a given parameter is not specified (still null) by the top level style.
 *
 * Please notice that parameters, options and values are CASE INSENSITIVE.
 *
 */
public abstract class BaseStyle implements IBaseStyle, StyleListener {
    
    private ArrayList parameters  = new ArrayList();
    private ArrayList parOptions  = new ArrayList();
    private ArrayList parValues   = new ArrayList();
    private ArrayList parDefaults = new ArrayList();
    
    private Hashtable baseStyles = new Hashtable();
    
    private BaseStyle parent = null;
    private String[] noOptions = new String[0];
    
    private String name = "plotter";
    
    private ArrayList listeners = new ArrayList();
    
    /**
     * Create a BaseStyle without a parent.
     * The parent, if ever available, can be set via the setParent method.
     *
     */
    protected BaseStyle() {
        initializeBaseStyle();
        setParent( null );
    }
    
    /**
     * Create a clone of a BaseStyle.
     * @param style The BaseStyle to be cloned.
     *
     */
    protected BaseStyle( BaseStyle style ) {
        this();
        copyStyle( this, style );
    }
    
    private void copyStyle( BaseStyle newStyle, BaseStyle oldStyle ) {
        String[] availPars = oldStyle.availableParameters();
        for ( int i = 0; i < availPars.length; i ++ ) {
            Object obj = oldStyle.parameterObject( availPars[i] );
            if ( obj != null )
                newStyle.setParameter(availPars[i], (String) obj );
        }
        String[] names = newStyle.baseStyleNames();
        for ( int i = 0; i < names.length; i++ )
            copyStyle( newStyle.baseStyle( names[i] ), oldStyle.baseStyle( names[i] ) );
    }
    
    /**
     * Initialize the BaseStyle.
     * This method has to be overwritten by all Style that has to add parameters or
     * internal BaseStyles.
     *
     */
    protected abstract void initializeBaseStyle();
    
    
    /**
     * Set the name of this BaseStyle.
     * @param name The name.
     *
     */
    protected void setName( String name )  {
        this.name = name;
    }
    
    /**
     * Get the name of this BaseStyle.
     * @return The name of the BaseStyle.
     *
     */
    protected String name() {
        return name;
    }
    
    /**
     * Set the parent for this BaseStyle.
     * The parent can be set only once; if this is done a second time a RuntimeException is
     * thrown.
     * @param p The parent for this BaseStyle.
     *
     */
    public void setParent( IBaseStyle p ) {
        BaseStyle parent = (BaseStyle)p;
        this.parent = parent;
        if ( parent != null ) {
//            if ( ! parent.hasListener( this ) )
//                parent.addStyleListener(this);
            Enumeration keys = baseStyles.keys();
            while ( keys.hasMoreElements() ) {
                String name = (String) keys.nextElement();
                baseStyle(name).setParent( parent.baseStyle(name) );
            }
        }
    }
    
    /**
     * Get the parent for this BaseStyle.
     * @return The parent.
     *
     */
    public BaseStyle parent() {
        return parent;
    }

    /**
     * Add a BaseStyle to this BaseStyle.
     * This method is to be invoked when a BaseStyle contains other BaseStyles in order
     * for the reset and the setParent methods to work properly.
     *
     */
    protected void addBaseStyle( IBaseStyle baseStyle, String name ) {
        BaseStyle bs = (BaseStyle) baseStyle;
        bs.setName(name);
        if ( baseStyles.get( bs.name() ) != null )
            ( (BaseStyle) baseStyles.get(bs.name()) ).removeStyleListener(this);
        baseStyles.put( bs.name(), baseStyle );
        bs.addStyleListener(this);
        if ( parent != null )
            bs.setParent( parent.baseStyle( bs.name() ) );
        notifyStyleChanged();
    }
    
    /**
     * Get a BaseStyle contained in this BaseStyle.
     * The indexing is the internal one.
     *
     */
    private BaseStyle baseStyle( String name ) {
        return (BaseStyle) baseStyles.get(name);
    }
    
    /**
     * Get the available BaseStyle names attached to this BaseStyle.
     * 
     */
    private String[] baseStyleNames() {
        String[] names = new String[ baseStyles.size() ];
        Enumeration e = baseStyles.keys();
        int count = 0;
        while( e.hasMoreElements() )
            names[ count++ ] = (String) e.nextElement();
        return names;
    }
    
    /**
     * Add a new parameter to this BaseStyle.
     * @param parameterName The name of the new parameter.
     *
     */
    protected void addParameter( String parameterName ) {
        addParameter( parameterName, null, null );
    }

    /**
     * Add a new parameter to this BaseStyle.
     * @param parameterName The name of the new parameter.
     * @param defaultValue  The default value of the given parameter.
     *                      The used, if present, when asking for the value of a parameter;
     *                      If the parameter is not set, first we look in the inheritance chain
     *                      and finally, return the default value.
     *
     */
    protected void addParameter( String parameterName, String defaultValue ) {
        addParameter( parameterName, null, defaultValue );
    }

    /**
     * Add a new parameter to this BaseStyle.
     * @param parameterName The name of the new parameter.
     * @param options       The available values for this parameter. 
     *
     */
    protected void addParameter( String parameterName, String[] options ) {
        addParameter( parameterName, options, null );
    }

    /**
     * Add a new parameter to this BaseStyle.
     * @param parameterName The name of the new parameter.
     * @param options       The available values for this parameter. 
     * @param defaultValue  The default value of the given parameter.
     *                      The used, if present, when asking for the value of a parameter;
     *                      If the parameter is not set, first we look in the inheritance chain
     *                      and finally, return the default value.
     *
     */
    protected void addParameter( String parameterName, String[] options, String defaultValue ) {
        try {
            parameterIndex( parameterName );
            throw new IllegalArgumentException("Parameter "+parameterName+" already belongs to this BaseStyle");
        } catch (IllegalArgumentException iae) {
            parameters.add(parameterName);
            parOptions.add(options);
            parDefaults.add(defaultValue);
            parValues.add(null);
        }
    }
    
    /**
     * Get the internal index of a give parameter.
     * @param parameterName The name of the parameter.
     * @return -1 if the parameter does not exist.
     *
     */
    private int parameterIndex( String parameterName ) {
        for ( int i = 0; i < parameters.size(); i++ )
            if ( ( (String) parameters.get(i) ).toLowerCase().equals( parameterName.toLowerCase() ) )
                return i;
        throw new IllegalArgumentException("Parameter "+parameterName+" is not part of this Style.");
    }

    /**
     * Get the value of a given parameter by the BaseStyle internal index.
     * This is where the inheritance tree is accessed to find the value of
     * the given parameter.
     * @return The first available parameter value.
     *
     */        
    private String parameterValue(int parIndex) {
        Object obj = parameterObject(parIndex);
        if ( obj != null )
            return (String) obj;
        if ( parent() == null )
            return parameterDefaultValue(parIndex);
        return parent().parameterValue(parIndex);
    }
  
    /**
     * Get the defaultValue.
     *
     */
    protected String parameterDefaultValue(String parameterName) {
        return (String) parDefaults.get( parameterIndex(parameterName) );
    }
    protected String parameterDefaultValue(int parIndex) {
        return (String) parDefaults.get( parIndex );
    }
    
    /**
     * Below are the AIDA methods.
     *
     */
    public String[] availableParameterOptions(String parameterName) {
        int parIndex = parameterIndex( parameterName );
        Object obj = parOptions.get(parIndex);
        return obj == null ? noOptions : (String[]) obj;
    }
    
    public String[] availableParameters() {
        int size = parameters.size();
        String[] pars = new String[ size ];
        for ( int i = 0; i < size; i++ )
            pars[i] = (String) parameters.get(i);
        return pars;
    }
    
    public String parameterValue(String parameterName) {
        int parIndex = parameterIndex( parameterName );
        return parameterValue(parIndex);
    }
    
    public void reset() {
        reset(true);
    }
    
    private void reset( boolean sendNotification ) {
        parValues.clear();
        Enumeration keys = baseStyles.keys();
        while( keys.hasMoreElements() )
            baseStyle( (String) keys.nextElement() ).reset(false);
        if ( sendNotification )
            notifyStyleChanged();
    }
        
        
    
    public boolean setParameter(String parameterName) {
        int parIndex = parameterIndex( parameterName );
        parValues.set(parIndex,null);
        notifyStyleChanged();
        return true;
    }
    
    public boolean setParameter(String parameterName, String parValue) {
        int parIndex = parameterIndex( parameterName );
        parValues.set( parIndex, parValue );
        notifyStyleChanged();
        return true;
    }    
    
    private Object parameterObject(int parIndex) {
        return parValues.get(parIndex);
    }
    
    private Object parameterObject(String parameterName) {
        int parIndex = parameterIndex( parameterName );
        return parameterObject(parIndex);
    }
    
    /**
     * Add a StyleListener.
     * 
     */
    void addStyleListener( StyleListener listener ) {
        listeners.add(listener);
    }
    
    /**
     * Remove a StyleListener.
     *
     */
    void removeStyleListener( StyleListener listener ) {
        if ( listeners.contains( listener ) )
            listeners.remove(listener);
    }
    
    /**
     * Notify the listeners that the style has changed.
     *
     */
    void notifyStyleChanged() {
        for ( int i = 0; i < listeners.size(); i++ ) {
            StyleListener listener = (StyleListener) listeners.get(i);
            listener.styleChanged(this);
        }
    }
    
    boolean hasListener( StyleListener listener ) {
        for ( int i = 0; i < listeners.size(); i++ )
            if ( listener == listeners.get(i) )
                return true;
        return false;
    }
            
    public void styleChanged(BaseStyle style) {
        notifyStyleChanged();
    }
    
}
