/*
 * BaseModelFunction.java
 *
 * Created on September 4, 2002, 5:26 AM
 */

package hep.aida.ref.function;
import java.util.*;
import hep.aida.*;
import hep.aida.ref.*;
import hep.aida.ext.*;
import hep.aida.ref.function.FunctionChangedEvent;
import hep.aida.ref.function.FunctionDispatcher;
import java.util.ArrayList;

/**
 *
 * @author  serbo
 */
public class BaseModelFunction extends ManagedObject implements IModelFunction, FunctionDispatcher {
    
    protected String[] varNames;
    protected IAnnotation annotation;
    protected String codeletString;
    protected String title;
    protected FunctionCore function;
    protected FunctionCore functionNotNormalized;
    protected FunctionCore functionNormalized;
    
    //private boolean providesNormalization;
    private boolean isNormalized;
    private boolean normalizationValid;
    private double normalizationAmplitude;
    private RangeSet[] rangeSet;
    
    private ArrayList listeners = new ArrayList();
    
    // One of FunctionCores can be "null"
    protected BaseModelFunction() {
        super(null);
    }
    public BaseModelFunction(String name, String tit, IFunction func) {
        super(name);
        IFunctionCoreNotNorm notNorm = new IFunctionCoreNotNorm(func);
        init(tit, notNorm, null);
        annotation = func.annotation();
        
        setCodeletString(func.codeletString());
        String[] funcVarNames = func.variableNames();
        for (int i=0; i<function.dimension(); i++) {
            varNames[i] = funcVarNames[i];
        }
    }
    public BaseModelFunction(String name, String title, FunctionCore notNorm, FunctionCore norm) {
        super(name);
        init(title, notNorm, norm);
    }
    protected void init(String tit, FunctionCore notNorm, FunctionCore norm) {
        if (notNorm == null && norm == null)
            throw new IllegalArgumentException("Normalized and NotNormalized FunctionCores can not both be null");
        
        annotation = new Annotation();
        annotation.addItem(Annotation.titleKey,"Title", true);
        if (tit != null) setTitle(tit);
        
        codeletString = title;
        
        functionNotNormalized = notNorm;
        functionNormalized = norm;
        
        if (notNorm != null) {
            isNormalized = false;
            function = functionNotNormalized;
            //providesNormalization = false;
            normalizationValid = true;
            normalizationAmplitude = 1.;
        } else {
            isNormalized = true;
            function = functionNormalized;
            //providesNormalization = true;
            normalizationValid = false;
            normalizationAmplitude = 1.;
        }
        
        rangeSet = new RangeSet[function.dimension()];
        varNames = new String[function.dimension()];
        for (int i=0; i<function.dimension(); i++) {
            varNames[i] = "x" + i;
            rangeSet[i] = new RangeSet();
        }
    }
    
    public FunctionCore core() {
        return function;
    }
    
    public int dimension() { return function.dimension(); }
    
    public int numberOfParameters() { return function.numberOfParameters(); }
    
    public double functionValue(double[] var) { return function.functionValue(var); }
    
    public final double value(double[] var) {
        if (!normalizationValid) {
            calculateNormalizationAmplitude();
            //System.out.print("   Value = " + normalizationAmplitude * function.functionValue(var)+", var = "+var[0]);
        }
        double val =  normalizationAmplitude * function.functionValue(var);
        if (isNormalized && val<0) val = 0.;
        return val;
    }
    
    public IAnnotation annotation() { return annotation; }
    
    public String variableName(int i)  { return varNames[i]; }
    
    public String[] variableNames() { return varNames; }
    
    public String[] parameterNames() { return function.parameterNames(); }
    
    public int indexOfParameter(String name) { return function.indexOfParameter(name);	}
    
    public void setParameters(double[] params) {
        //System.out.print("\nSetting parameters:  ");
        //for (int i=0; i<numberOfParameters(); i++) { System.out.print(params[i]+"   "); }
        //System.out.print("\n");
        if (isNormalized) normalizationValid = false;
        function.setParameters(params);
        notifyFunctionChanged(new FunctionChangedEvent( FunctionChangedEvent.PARAMETER_VALUE_CHANGED ) );
    }
    
    public void setParameter(String name, double x) throws IllegalArgumentException {
        if (isNormalized) normalizationValid = false;
        function.setParameter(name, x);
        notifyFunctionChanged(new FunctionChangedEvent( FunctionChangedEvent.PARAMETER_VALUE_CHANGED ) );
    }
    
    public double[] parameters() { return function.parameters(); }
    
    public double parameter(String name) {
        return function.parameter(name);
    }
    
    public boolean isEqual(IFunction f) {
        throw new UnsupportedOperationException("This method is not implemented yet");
    }
    
    public boolean providesGradient() { return function.providesGradient(); }
    
    public double[] gradient(double[] x) {
        double[] val = new double[dimension()];
        if (function.providesGradient()) {
            if (!normalizationValid) {
                calculateNormalizationAmplitude();
            }
            val = function.gradient(x);
            for (int i=0; i<val.length; i++) val[i] =  normalizationAmplitude*val[i];
            //System.out.print("gradient: Value = " + val[0] +", var = "+x[0]);
        } else throw new UnsupportedOperationException("This function does not provide gradient");
        //else return numericGradient(x);
        
        return val;
    }
    
    public String codeletString() { return codeletString; }
    
    public void setCodeletString(String codelet) { codeletString = codelet; }
    
    
    // IModelFunction methods
    public boolean providesNormalization() { return function.providesNormalization(); }
    
    public void normalize(boolean on) {
        boolean notify = on != isNormalized;
        if (on) {
            if (functionNormalized == null)
                throw new IllegalArgumentException("This function can not be converted into Normalized form!");
            function = functionNormalized;
            isNormalized = true;
            normalizationValid = false;
            normalizationAmplitude = 1.;
        }
        else {
            if (functionNotNormalized == null)
                throw new IllegalArgumentException("This function can not be converted into Not-Normalized form!");
            function = functionNotNormalized;
            isNormalized = false;
            normalizationValid = true;
            normalizationAmplitude = 1.;
        }
        if ( notify )
            notifyFunctionChanged(new FunctionChangedEvent( FunctionChangedEvent.RANGE_CHANGED ) );
    }
    
    public boolean isNormalized() { return isNormalized; }
    
    public double[] parameterGradient(double[] x) {
        double[] val = new double[numberOfParameters()];
        if (function.providesParameterGradient()) {
            if (!normalizationValid) {
                calculateNormalizationAmplitude();
            }
            val = function.parameterGradient(x);
            for (int i=0; i<val.length; i++) val[i] =  normalizationAmplitude*val[i];
            //System.out.print("parameterGradient: Value = " + val[0] +", var = "+x[0]);
        } else throw new UnsupportedOperationException("This function does not provide parameter gradient");
        //else return numericParameterGradient(x);
        return val;
    }
    
    public boolean providesParameterGradient() { return function.providesParameterGradient(); }
    
    public IRangeSet normalizationRange(int iAxis) { return rangeSet[iAxis]; }
    
    public void includeNormalizationAll() {
        if (isNormalized) normalizationValid = false;
        for (int i=0; i<dimension(); i++) rangeSet[i].includeAll();
        notifyFunctionChanged(new FunctionChangedEvent( FunctionChangedEvent.RANGE_CHANGED ) );
    }
    
    public void excludeNormalizationAll() {
        if (isNormalized) normalizationValid = false;
        for (int i=0; i<dimension(); i++) rangeSet[i].excludeAll();
        notifyFunctionChanged(new FunctionChangedEvent( FunctionChangedEvent.RANGE_CHANGED ) );
    }
    
    // Extra methods
    public void calculateNormalizationAmplitude() {
        normalizationValid = true;
        double val = 0;
        normalizationAmplitude = 1;
        if (!function.providesNormalization()) {
            if ( dimension() == 1 ) val = FunctionIntegrator.integralTrapezoid(this);
            else val = FunctionIntegrator.integralMC(this);
        }
        else {            
            if (dimension() == 1) {
                double[] xMax = rangeSet[0].upperBounds();
                double[] xMin = rangeSet[0].lowerBounds();
                for (int k=0; k<rangeSet[0].size(); k++) {
                    val += function.normalizationAmplitude(xMin, xMax);
                }
            } else if (dimension() == 2) {
                double[] xMax = new double[2];
                double[] xMin = new double[2];
                
                double[] xMax0 = rangeSet[0].upperBounds();
                double[] xMin0 = rangeSet[0].lowerBounds();
                
                double[] xMax1 = rangeSet[1].upperBounds();
                double[] xMin1 = rangeSet[1].lowerBounds();
                
                for (int k=0; k<rangeSet[0].size(); k++) {
                    xMin[0] = xMin0[k];
                    xMax[0] = xMax0[k];
                    
                    for (int j=0; j<rangeSet[1].size(); j++) {
                        xMin[1] = xMin1[j];
                        xMax[1] = xMax1[j];
                        val += function.normalizationAmplitude(xMin, xMax);
                    }
                }
            } else if (dimension() == 3) {
                double[] xMax = new double[3];
                double[] xMin = new double[3];
                
                double[] xMax0 = rangeSet[0].upperBounds();
                double[] xMin0 = rangeSet[0].lowerBounds();
                
                double[] xMax1 = rangeSet[1].upperBounds();
                double[] xMin1 = rangeSet[1].lowerBounds();
                
                double[] xMax2 = rangeSet[2].upperBounds();
                double[] xMin2 = rangeSet[2].lowerBounds();
                
                for (int k=0; k<rangeSet[0].size(); k++) {
                    xMin[0] = xMin0[k];
                    xMax[0] = xMax0[k];
                    
                    for (int j=0; j<rangeSet[1].size(); j++) {
                        xMin[1] = xMin0[j];
                        xMax[1] = xMax0[j];
                        val += function.normalizationAmplitude(xMin, xMax);
                        for (int i=0; i<rangeSet[2].size(); i++) {
                            xMin[2] = xMin0[i];
                            xMax[2] = xMax0[i];
                            val += function.normalizationAmplitude(xMin, xMax);
                        }
                    }
                }
            } else
                throw new IllegalArgumentException("Temporary support only up to 3 dimensions");
            
        }
        normalizationAmplitude = 1./val;
    }
    
    public String title() {
        String t = (annotation != null) ? annotation.value(Annotation.titleKey) : title;
        return t;
    }
    
    public void setTitle(String t) {
        title = t;
        if (annotation != null) annotation.setValue(Annotation.titleKey, title);
        notifyFunctionChanged(new FunctionChangedEvent( FunctionChangedEvent.TITLE_CHANGED ) );
    }
    
    public boolean setParameterNames(String[] params) { 
        boolean result = function.setParameterNames(params); 
        notifyFunctionChanged(new FunctionChangedEvent( FunctionChangedEvent.PARAMETER_NAME_CHANGED ) );
        return result;
    }
    
    public double[] numericGradient(double[] x) {
        throw new UnsupportedOperationException("Numeric Gradient is not implemented yet");
    }
    public double[] numericParameterGradient(double[] x) {
        throw new UnsupportedOperationException("Numeric Parameter Gradient is not implemented yet");
    }
    public RangeSet[] getRangeSet() { return rangeSet; }
    
    public String toString() {
        String str = "BaseModelFunction:  Title="+title()+", name="+name();
        
        str += "\n\tDimension: "+dimension()+", number of parameters: "+numberOfParameters()+", Codelet String: " + codeletString();
        
        str += "\n\tVariable Names: ";
        String[] varNames = variableNames();
        for (int i=0; i<dimension(); i++) str += varNames[i] + ", ";
        
        str += "\t Parameters: ";
        String[] parNames  = parameterNames();
        double[] parValues = parameters();
        for (int i=0; i<numberOfParameters(); i++) str += parNames[i] + "=" + parValues[i] + ", ";
        
        str += "\n\tProvides Gradient: " + providesGradient();
        str += ",  Provides Parameter Gradient: " + providesParameterGradient();
        str += ",  Provides Normalization: " + providesNormalization();
        
        return str;
    }
    
    public void addFunctionListener(FunctionListener listener) {
        listeners.add(listener);
    }
    
    public void removeFunctionListener(FunctionListener listener) {
        listeners.remove(listener);
    }

    void notifyFunctionChanged(FunctionChangedEvent event) {
        for ( int i = 0; i < listeners.size(); i++ )
            ( (FunctionListener) listeners.get(i) ).functionChanged(event);
    }
    
}
