/*
 *    Geotools2 - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2005-2006, GeoTools Project Managment Committee (PMC)
 *
 *    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;
 *    version 2.1 of the License.
 *
 *    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.
 *
 */
package org.geotools.data.complex;

import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import junit.framework.TestCase;

import org.geotools.data.DataAccessFinder;
import org.geotools.data.DataStore;
import org.geotools.data.DataStoreFinder;
import org.geotools.data.DataUtilities;
import org.geotools.data.DefaultTransaction;
import org.geotools.data.FeatureWriter;
import org.geotools.data.Transaction;
import org.geotools.data.complex.config.EmfAppSchemaReader;
import org.geotools.data.feature.FeatureAccess;
import org.geotools.data.feature.FeatureSource2;
import org.geotools.data.postgis.PostgisDataStoreFactory;
import org.geotools.feature.iso.FeatureCollections;
import org.geotools.feature.iso.Types;
import org.geotools.filter.FilterFactoryImplNamespaceAware;
import org.geotools.filter.text.cql2.CQL;
import org.geotools.xlink.bindings.XLINK;
import org.opengis.feature.Attribute;
import org.opengis.feature.ComplexAttribute;
import org.opengis.feature.Feature;
import org.opengis.feature.FeatureCollection;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.FeatureType;
import org.opengis.feature.type.Name;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.expression.PropertyName;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.NamespaceSupport;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.GeometryFactory;

/**
 * DOCUMENT ME!
 * 
 * @author Rob Atkinson
 * @version $Id: TimeSeriesStressTest.java 29135 2008-02-07 19:49:09Z desruisseaux $
 * @source $URL:
 *         http://svn.geotools.org/geotools/branches/2.4.x/modules/unsupported/community-schemas/community-schema-ds/src/test/java/org/geotools/data/complex/TimeSeriesTest.java $
 * @since 2.4
 */
public class TimeSeriesStressTest extends TestCase {
    private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger(TimeSeriesStressTest.class.getPackage()
            .getName());

    private static final String AWNS = "http://www.water.gov.au/awdip";

    private static final String CVNS = "http://www.opengis.net/cv/0.2.1";

    private static final String SANS = "http://www.opengis.net/sampling/1.0";

    private static final String OMNS = "http://www.opengis.net/om/1.0";

    private static final String SWENS = "http://www.opengis.net/swe/1.0.1";

    private static final String GMLNS = "http://www.opengis.net/gml";

 //   private static final String GEONS = "http://www.seegrid.csiro.au/xml/geometry";

    final String schemaBase = "/test-data/";

    EmfAppSchemaReader reader;

    private FeatureSource2 source;

    /**
     * DOCUMENT ME!
     * 
     * @throws Exception
     *             DOCUMENT ME!
     */
    protected void setUp() throws Exception {
        super.setUp();
        reader = EmfAppSchemaReader.newInstance();

        // Logging.GEOTOOLS.forceMonolineConsoleOutput(Level.FINEST);
    }

    /**
     * DOCUMENT ME!
     * 
     * @throws Exception
     *             DOCUMENT ME!
     */
    protected void tearDown() throws Exception {
        super.tearDown();
    }

    /**
     * DOCUMENT ME!
     * 
     * @param location
     *            schema location path discoverable through
     *            getClass().getResource()
     * 
     * @throws IOException
     *             DOCUMENT ME!
     */
    private void loadSchema(String location) throws IOException {
        // load needed GML types directly from the gml schemas
        URL schemaLocation = getClass().getResource(location);
        if (schemaLocation == null) {
            schemaLocation = new URL(location);
        }
        assertNotNull(location, schemaLocation);
        reader.parse(schemaLocation);
    }

    private Name name(String ns, String localName) {
        return Types.typeName(ns, localName);
    }

    /**
     * A rough stress test for timeseries mapping
     * 
     * @throws Exception
     */
    public void testStressDataStore() throws Exception {
        final Map dsParams = new HashMap();
        String configLocation = schemaBase + "TimeSeriesTest_properties2.xml";
        final URL url = getClass().getResource(configLocation);

        dsParams.put("dbtype", "complex");
        dsParams.put("url", url.toExternalForm());

        final Name typeName = new org.geotools.feature.Name(AWNS, "SiteSinglePhenomTimeSeries");

        FeatureAccess mappingDataStore = (FeatureAccess) DataAccessFinder.createAccess(dsParams);
        assertNotNull(mappingDataStore);

        AttributeDescriptor attDesc = (AttributeDescriptor) mappingDataStore.describe(typeName);
        assertNotNull(attDesc);
        assertTrue(attDesc.type() instanceof FeatureType);

        FeatureType fType = (FeatureType) attDesc.type();

        FeatureSource2 fSource = (FeatureSource2) mappingDataStore.access(typeName);

        Filter filter = CQL.toFilter("gml:name = 'stat_id_3000'");
        FeatureCollection features = (FeatureCollection) fSource.content(filter);
        final int expectedResults = 3000;
        final int EXPECTED_RESULT_COUNT = 1;
        final int numberOfRuns = 5;

        // LOGGER.info("stressing getCount()...");
        final double countTime = stressCount(features, EXPECTED_RESULT_COUNT, numberOfRuns);
        LOGGER.info("getCount() agv time: " + countTime + "ms");

        LOGGER.info("Stressing getFeatures()...");
        final double fetchTime = stressGetFeatures(features, EXPECTED_RESULT_COUNT, numberOfRuns,
                expectedResults);
        LOGGER.info("getFeatures() agv time: " + fetchTime + "ms");
    }

    private double stressGetFeatures(final FeatureCollection features,
            final int EXPECTED_RESULT_COUNT, final int numberOfRuns, final int expectedResults) {
        double cumulativeTime = 0;
        StopWatch timer = new StopWatch();

        NamespaceSupport namespaces = new NamespaceSupport();
        namespaces.declarePrefix("aw", AWNS);
        namespaces.declarePrefix("om", OMNS);
        namespaces.declarePrefix("swe", SWENS);
        namespaces.declarePrefix("gml", GMLNS);
        namespaces.declarePrefix("sa", SANS);
        // TODO: use commonfactoryfinder or the mechanism choosed
        // to pass namespace context to filter factory
        FilterFactory ffac = new FilterFactoryImplNamespaceAware(namespaces);

        Feature feature = null;
        int count = 0;
        final String phenomNamePath = "aw:relatedObservation/aw:PhenomenonTimeSeries/om:observedProperty/swe:Phenomenon/gml:name";

        for (int run = 0; run < numberOfRuns; run++) {
            Iterator it = features.iterator();
            count = 0;
            timer.start();
            for (; it.hasNext();) {
                feature = (Feature) it.next();
                count++;
            }
            timer.stop();
            cumulativeTime += timer.time();
            features.close(it);
        }

        PropertyName gmlName = ffac.property("gml:name");
        PropertyName phenomName = ffac.property(phenomNamePath);

        Object nameVal = gmlName.evaluate(feature, String.class);
        assertNotNull("gml:name evaluated to null", nameVal);

        Object phenomNameVal = phenomName.evaluate(feature, String.class);
        assertNotNull(phenomNamePath + " evaluated to null", phenomNameVal);

        PropertyName sampledFeatureName = ffac.property("sa:sampledFeature");
        Attribute sampledFeatureVal = (Attribute) sampledFeatureName.evaluate(feature);
        assertNotNull("sa:sampledFeature evaluated to null", sampledFeatureVal);
        assertNull(sampledFeatureVal.getValue());
        Map attributes = (Map) sampledFeatureVal.getDescriptor().getUserData(Attributes.class);
        assertNotNull(attributes);
        Name xlinkTitle = name(XLINK.NAMESPACE, "title");
        assertTrue(attributes.containsKey(xlinkTitle));
        assertNotNull(attributes.get(xlinkTitle));

        Name xlinkHref = name(XLINK.NAMESPACE, "href");
        assertTrue(attributes.containsKey(xlinkHref));
        assertNotNull(attributes.get(xlinkHref));

        assertEquals(EXPECTED_RESULT_COUNT, count);

        // /
        PropertyName elementName = ffac
                .property("aw:relatedObservation/aw:PhenomenonTimeSeries/om:result/cv:CompactDiscreteTimeCoverage");
        Object timeCovVal = elementName.evaluate(feature);
        assertNotNull(
                "aw:relatedObservation/aw:PhenomenonTimeSeries/om:result/cv:CompactDiscreteTimeCoverage",
                timeCovVal);
        assertTrue(timeCovVal instanceof Feature);
        final List elements = (List) ((Feature) timeCovVal).getValue();
        assertEquals(expectedResults, elements.size());

        ComplexAttribute element = (ComplexAttribute) elements.get(10);
        assertNotNull(element);
        Name compactTimeValuePairName = Types.typeName(CVNS, "CompactTimeValuePair");
        Name geometryName = Types.typeName(CVNS, "geometry");
        Name valueName = Types.typeName(CVNS, "value");

        List compactTimes = element.get(compactTimeValuePairName);
        assertNotNull(compactTimes);
        assertEquals(1, compactTimes.size());

        ComplexAttribute compatTimeValuePair = (ComplexAttribute) compactTimes.get(0);
        List geometries = compatTimeValuePair.get(geometryName);
        List values = compatTimeValuePair.get(valueName);

        assertNotNull(geometries);
        assertNotNull(values);
        assertEquals(1, geometries.size());
        assertEquals(1, values.size());

        Attribute geom = (Attribute) geometries.get(0);
        Attribute value = (Attribute) values.get(0);

        assertNotNull(geom.getValue());
        assertNotNull(value.getValue());
        assertNotNull(value.getDescriptor().getUserData(Attributes.class));

        return cumulativeTime / numberOfRuns;
    }

    /**
     * Runs numberOfCycles + 1, the first run does not count, returns the avg
     * time it took to count the features.
     * 
     * @param features
     * @param expectedFeatureCount
     * @param numberOfCycles
     * @return
     */
    private double stressCount(final FeatureCollection features, final int expectedFeatureCount,
            final int numberOfCycles) {
        double cumulativeTime = 0;

        StopWatch timer = new StopWatch();

        for (int i = 0; i < numberOfCycles; i++) {
            timer.start();
            // int size = ((Collection)features).size();
            int size = FeatureCollections.getSize(features);
            timer.stop();
            assertEquals(expectedFeatureCount, size);

            // int resultCount = getCount(features);
            // cumulativeTime += timer.time();
            //
            String msg = "be sure difference in result count is not due to different dataset.";
            assertEquals(msg, expectedFeatureCount, size);
        }

        return cumulativeTime / numberOfCycles;
    }

    private int getCount(FeatureCollection features) {
        Iterator iterator = features.iterator();
        int count = 0;
        try {
            while (iterator.hasNext()) {
                iterator.next();
                count++;
            }
        } finally {
            features.close(iterator);
        }
        return count;
    }

    private static class StopWatch {
        private long start;

        private long end = Long.MIN_VALUE;

        public void start() {
            start = System.currentTimeMillis();
            end = Long.MIN_VALUE;
        }

        public void stop() {
            end = System.currentTimeMillis();
        }

        public long time() {
            if (Long.MIN_VALUE == end) {
                throw new IllegalStateException("call stop() before time()");
            }
            return end - start;
        }
    }

    private static void populateTable() throws Exception {
        Map params = new HashMap();
        params.put(PostgisDataStoreFactory.DBTYPE.key, "postgis");
        params.put(PostgisDataStoreFactory.DATABASE.key, "postgis");
        params.put(PostgisDataStoreFactory.HOST.key, "localhost");
        params.put(PostgisDataStoreFactory.PORT.key, "5432");
        params.put(PostgisDataStoreFactory.USER.key, "postgres");
        params.put(PostgisDataStoreFactory.PASSWD.key, "postgres");
        
        final DataStore ds = DataStoreFinder.getDataStore(params);
        final String typeSpec = "station_id:String,POSITION:Point,station_name:String,"
                + "determinand_code:String,determinand_description:String,"
                + "sample_time_position:java.util.Date,result:Double,units:String";
        final org.geotools.feature.FeatureType schema = DataUtilities.createType("TimeSeriesTest",
                typeSpec);
        LOGGER.info("Creating schema " + schema);
        ds.createSchema(schema);

        // the two fields grouped by
        String station_id;
        String determinand_code;
        // put 1000, 2000, 3000, and 4000 groups of records
        station_id = "stat_id_1000";
        determinand_code = "det_code_1000";
        populate(ds, schema, station_id, determinand_code, 1000);

        station_id = "stat_id_2000";
        determinand_code = "det_code_2000";
        populate(ds, schema, station_id, determinand_code, 2000);

        station_id = "stat_id_3000";
        determinand_code = "det_code_3000";
        populate(ds, schema, station_id, determinand_code, 3000);

        station_id = "stat_id_4000";
        determinand_code = "det_code_4000";
        populate(ds, schema, station_id, determinand_code, 4000);

        station_id = "stat_id_12000";
        determinand_code = "det_code_12000";
        populate(ds, schema, station_id, determinand_code, 12000);
    }

    private static void populate(final DataStore ds, final org.geotools.feature.FeatureType schema,
            final String station_id, final String determinand_code, final int featureCount)
            throws Exception {
        org.geotools.feature.Feature feature;
        GeometryFactory gf = new GeometryFactory();
        LOGGER.info("Creating " + featureCount + " features for station_id " + station_id);

        Transaction transaction = new DefaultTransaction();
        FeatureWriter fw = ds.getFeatureWriterAppend(schema.getTypeName(), transaction);
        for (double i = 0; i < featureCount; i++) {
            fw.hasNext();
            feature = fw.next();
            feature.setAttribute("station_id", station_id);
            feature.setAttribute("determinand_code", determinand_code);
            feature.setAttribute("POSITION", gf.createPoint(new Coordinate(i / 1000D, i / 1000D)));
            feature.setAttribute("station_name", "stat_name_" + i);
            feature.setAttribute("determinand_description", "determinand description " + i
                    + " for " + station_id);
            feature.setAttribute("sample_time_position", new java.util.Date());
            feature.setAttribute("result", new Double(i / 1000));
            fw.write();
            if (i % 100 == 0) {
                transaction.commit();
            }
        }
        transaction.commit();
        fw.close();
        LOGGER.info(featureCount + " features added for " + station_id);
    }

    public static void main(String[] argv) {
        try {
            populateTable();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
