/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
/*
 * NbStatisticalDataset.java
 *
 * Created on October 16, 2002, 8:39 PM
 */

package org.netbeans.performance.impl.chart;
import com.jrefinery.chart.*;
import com.jrefinery.data.*;
import org.netbeans.performance.spi.*;
import java.util.*;
/**An implementation of a JFreeChart statistical data set that understands
 * DataAggregations and queries on them to find its data.  Contains null
 * implementations of the required listener interfaces;  it is presumed that
 * the data has already been put in the DataAggregation, and cannot change.
 * <P>Note that this class assumes any objects to be graphed will contain
 * values that are subclasses of Number, which implement the Named interface
 * (it is difficult to graph something if it doesn't have a numeric value
 * and you don't know what to call it!).  ClassCastExceptions will occur if
 * the query + keys return a LogElement that does not implement both of
 * these interfaces.
 *
 * @author  Tim Boudreau
 */
public class NbStatisticalDataset  implements
            Dataset, CategoryDataset, StatisticalCategoryDataset, 
            SeriesDataset, SeriesChangeListener {
    String query;
    String[] fields;
    Comparator comparator = null;
    ElementFilter filter = null;
    DataAggregation source = null;
    /** Creates a new instance of NbStatisticalDataset.  The query arguments defines
     *  a query (see DataAggregation.query()) such as <code>/mytest*javaconfig</code> which
     *  will match all of the elements of a DataAggregation whose paths match this
     *  query.  The fields argument determines what child elements of this aggregation
     *  (for example, the <code>Average time in GC<code>Seconds spent in GC</code>
     *  child of all elements matching the query).  The DataAggregation is the aggregation
     *  that contains data parsed from logs of a series of runs.
     * @param query A query string, as defined in DataAggregation, which will
     * match some of the elements in a DataAggregation containing data to be graphed.
     * For example, if you have a set of log wrappers that store the value of something
     * like the frequency of garbage collection, you will want a query string that will
     * match all of the wrappers in question.
     * @param fields The particular children of the wrapper (or other DataAggregation) which
     * should be graphed.  For each field there will be generated one bar on the
     * graph per query result.
     * @param data The DataAggregation containing the data to be queried.
     * Generally this data is generated by running NetBeans inside
     * the testing infrastructure, with some logging options turned
     * on, and then parsing the resulting log files from several
     * runs into a DataAggregation.
     * @see org.netbeans.performance.spi.DataAggregation
     * @see org.netbeans.performance.spi.AbstractLogFile
     */
    public NbStatisticalDataset(String query, String fields[], DataAggregation data) {
        if ((query == null) || (fields.length==0) || (data == null)) 
            throw new IllegalArgumentException ("Cannot construct a dataset from null");
        this.query = "/run/GC Tuning*javaconfig";
        this.fields = fields;
        this.source = data;
    }

    /** Creates a new instance of NbStatisticalDataset.  The query arguments defines
     *  a query (see DataAggregation.query()) such as <code>/mytest*javaconfig</code> which
     *  will match all of the elements of a DataAggregation whose paths match this
     *  query.  The fields argument determines what child elements of this aggregation
     *  (for example, the <code>Average time in GC<code>Seconds spent in GC</code>
     *  child of all elements matching the query).  The DataAggregation is the aggregation
     *  that contains data parsed from logs of a series of runs.
     * @param fields The particular children of the wrapper (or other DataAggregation) which
     * should be graphed.  For each field there will be generated one bar on the
     * graph per query result.
     * @param data The DataAggregation containing the data to be queried.
     * Generally this data is generated by running NetBeans inside
     * the testing infrastructure, with some logging options turned
     * on, and then parsing the resulting log files from several
     * runs into a DataAggregation.
     * @param filter An object implementing the ElementFilter interface, which can be used
     * to weed out uninteresting results, so they will not be graphed.
     * @param comparator A comparator used to sort the data.  Data will appear in the
     * resulting chart(s) using the sort order determined by this
     * comparator.
     * @see org.netbeans.performance.spi.ElementFilter
     * @see org.netbeans.performance.spi.DataAggregation
     * @see org.netbeans.performance.spi.AbstractLogFile
     */    
    public NbStatisticalDataset (String query, String fields[], DataAggregation data, ElementFilter filter, Comparator comparator) {
        this (query, fields, data);
        this.filter = filter;
        this.comparator = comparator;
    }
    
    private LogElement[] queryElements=null;
    /**Gets the LogElement objects that were part of the query.
     */
    private synchronized LogElement[] getQueryElements() {
        if (queryElements == null) {
            if (filter == null) {
                queryElements = source.query (query);
            } else {
                queryElements = source.query (query, filter);
            }
        if (comparator != null) Arrays.sort (queryElements, comparator);
        }
        return queryElements;
    }
    
    private List categories = null;
    public synchronized List getCategories() {
        if (categories == null) {
            categories = new LinkedList();
            LogElement[] els = getQueryElements();
            for (int i=0; i < els.length; i++) {
                if (filter == null) {
                    categories.add (els[i].getParent().getName());
                } else {
                    LogElement el = els[i];
                    if (filter.accept (els[i])) {
                        categories.add (els[i].getParent().getName());
                    } else {
                        discarded.add (els[i].getParent());
                    }
                }
            }
        }
        return categories;
    }
    
    //XXX may want to list the elements that were not included in the 
    //dataset somewhere, so store this.
    private List discarded = new LinkedList();
    
    public List getDiscarded() {
        //make sure it's initialized
        getCategories();
        return Collections.unmodifiableList(discarded);
    }
    
    /** Returns the number of categories in the dataset.
     *
     * @return The category count.
     *
     */
    public int getCategoryCount() {
        return getQueryElements().length;
    }
    
    /** Returns the value for a series and category.
     *
     * @param series    The series (zero-based index).
     * @param category  The category.
     *
     * @return the value for a series and category.
     *
     */
    public Number getValue(int series, Object category) {
        return fetchNumberValue (getAggregationForCategory(category), fields[series]);
    }
    
    /** Returns the mean value for the specified series
     * (zero-based index) and category.
     *
     * @param series The series index (zero-based).
     * @param category The category.
     * @return the mean value
     */
    public Number getMeanValue(int series, Object category) {
        return (Number) getElementForSeries(series, category).getValue();
    }
    
    private ValueLogElement getElementForSeries (int series, Object category) {
        DataAggregation ag = getAggregationForCategory (category);
        ValueLogElement el = (ValueLogElement) ag.findChild (fields[series]);
        return el;
    }
    
    private DataAggregation getAggregationForCategory(Object category) {
        LogElement[] els = getQueryElements();
        int idx=0;
        DataAggregation result = null;
        while ((idx < els.length) && (result == null)) {
            try {
                if (category.toString().equals (((Named) els[idx]).getName())) {
                    try {
                       result = (DataAggregation) els[idx];
                    } catch (ClassCastException cce0) {
                        System.out.println("Tried to cast " + els[idx].getPath() + " as an instance of DataAggregation.  It is " + els[idx].getClass());
                        throw cce0;
                    }
                }
            } catch (ClassCastException cce) {
                System.out.println("Tried to cast " + els[idx].getPath() + " as an instance of Named.  It is " + els[idx].getClass());
                throw cce;
            }
        }
        return result;
    }
    
    /** Returns the standard deviation value for the specified series
     * (zero-based index) and category. Returns 0 if the object requested
     * does not implement the Average interface.
     *
     * @param series The series index (zero-based).
     * @param category The category.
     * @return the standard deviation
     */
    public Number getStdDevValue(int series, Object category) {
        return fetchStdDev (getAggregationForCategory(category), fields[series]);
    }

    /**
     * Returns the number of series in the dataset.
     *
     * @return The number of series in the dataset.
     */
    public int getSeriesCount() {
        return fields.length;
    }

    /**Makes a reasonable guess at how big a png of the
     * chart should be.
     */
    public int getOptimalWidth () {
        return getSeriesCount() * getCategoryCount() * 40;
    }
    
    /**
     * Returns the name of a series.
     *
     * @param series The series (zero-based index).
     *
     * @return The series name.
     */
    public String getSeriesName(int series) {
        return fields[series];
    }

    /**Convenience method to retrieve an element from an aggregation.
     */
    private static LogElement fetchElement (DataAggregation da, String key) {
        return da.findChild (key);
    }
    
    /**Convenience method to retrieve the standard deviation of a value
     * from an aggregation.  If the LogElement requested does not implement
     * the Average interface, this method will fail silently returning 0.
     */
    private static Number fetchStdDev (DataAggregation da, String key) {
        LogElement el = fetchElement (da, key);
            if (!(el instanceof ValueLogElement)) 
                throw new IllegalArgumentException (el.getPath() + 
                " is not an instance of ValueLogElement.  Cannot determine its value in order to chart it.");
            
        Object val = ((ValueLogElement) el).getValue();
        if (val instanceof Average) {
            return ((Average) el).getStandardDeviation();
        }
        return new Double (0);
    }
    
    /**Convenience method to retrieve a the value of a ValueLogElement
     * from an aggregation.  If the ValueLogElement's value class is
     * not a subclass of Number, an exception will be thrown.
     */
    private static Number fetchNumberValue (DataAggregation da, String key) {
        LogElement el = fetchElement (da, key);
            if (!(el instanceof ValueLogElement)) 
                throw new IllegalArgumentException (el.getPath() + " is not an instance of ValueLogElement.  Cannot determine its value in order to chart it.");
        Object result = ((ValueLogElement) el).getValue();
        if (!(result instanceof Number)) 
           throw new IllegalArgumentException (el.getPath() + " has a value property that is not an instance of Number - it is an instance of "
             + result.getClass() + " I cannot determine how to evalue it it in order to graph it.");
        return (Number) result;
    }

    /**
     */
    private static String findStringValue (DataAggregation da, String key) {
        return ((ValueLogElement) fetchElement (da, key)).getValue().toString();
    }

    public void seriesChanged(SeriesChangeEvent event) {
        //null implementation - data will not change
    }    
    
    public void addChangeListener(DatasetChangeListener listener) {
        //empty impl - this data will not change dynamically
    }    
    
    public void removeChangeListener(DatasetChangeListener listener) {
        //empty impl - this data will not change dynamically
    }    
    
}
