/**
 * Copyright © CENRA (2016)
 *
 * remi.clement@espaces-naturels.fr
 * 
 * This software is a multi-platform and portable application based on 
 * the scientifical toolbox to monitor wetlands. With this toolbox it’s 
 * possible to calculate indicators «RhoMéO» to monitor quality, 
 * fonctionalities and urban and agricultural pressures of wetlands. 
 * Indicators are calculated using naturalist data entered and/or imported,
 * and geographic data downloaded from a geographic server.
 * 
 * This software is governed by the CeCILL license under French law and
 * abiding by the rules of distribution of free software.  You can  use, 
 * modify and/ or redistribute the software under the terms of the CeCILL
 * license as circulated by CEA, CNRS and INRIA at the following URL
 * "http://www.cecill.info". 
 * 
 * As a counterpart to the access to the source code and  rights to copy,
 * modify and redistribute granted by the license, users are provided only
 * with a limited warranty  and the software's author,  the holder of the
 * economic rights,  and the successive licensors  have only  limited
 * liability. 
 * 
 * In this respect, the user's attention is drawn to the risks associated
 * with loading,  using,  modifying and/or developing or reproducing the
 * software by the user in light of its specific status of free software,
 * that may mean  that it is complicated to manipulate,  and  that  also
 * therefore means  that it is reserved for developers  and  experienced
 * professionals having in-depth computer knowledge. Users are therefore
 * encouraged to load and test the software's suitability as regards their
 * requirements in conditions enabling the security of their systems and/or 
 * data to be ensured and,  more generally, to use and operate it in the 
 * same conditions as regards security. 
 * 
 * The fact that you are presently reading this means that you have had
 * knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.core.data.county;

import com.vividsolutions.jts.geom.Geometry;
import fr.cenra.rhomeo.core.RhomeoCore;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.regex.Matcher;
import javafx.util.StringConverter;
import javax.annotation.PreDestroy;
import org.apache.sis.storage.DataStoreException;
import org.geotoolkit.data.FeatureCollection;
import org.geotoolkit.data.FeatureReader;
import org.geotoolkit.data.query.QueryBuilder;
import org.geotoolkit.data.shapefile.ShapefileFeatureStore;
import org.geotoolkit.factory.FactoryFinder;
import org.geotoolkit.factory.Hints;
import org.geotoolkit.feature.Feature;
import org.geotoolkit.nio.IOUtilities;
import org.geotoolkit.referencing.CRS;
import org.opengis.filter.FilterFactory2;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.util.FactoryException;
import org.opengis.util.GenericName;
import org.springframework.stereotype.Component;

/**
 * A repository to load / search for counties.
 *
 * @author Alexis Manin (Geomatys)
 */
@Component
public final class CountyRepository implements AutoCloseable {

    /**
     * List of resources to load to read County data.
     */
    private static final String[] RESOURCES = new String[]{
        "DEPARTEMENT.shp",
        "DEPARTEMENT.shx",
        "DEPARTEMENT.dbf",
        "DEPARTEMENT.prj"
    };

    /**
     * Attributes to read into the shapefile embedding county data.
     */
    private static final String COUNTY_CODE = "CODE_DEPT";
    private static final String COUNTY_NAME = "NOM_DEPT";

    private final ShapefileFeatureStore store;
    private final GenericName name;
    private final StringConverter converter;
    private final CoordinateReferenceSystem siteCRS;
    private final FilterFactory2 ff;

    private final Map<String, County> counties;

    private final Path dataDir;

    CountyRepository() throws IOException, URISyntaxException, DataStoreException, FactoryException {
        // Copy resources on local filesystem
        dataDir = Files.createTempDirectory("countyShape");
        for (final String str : RESOURCES) {
            try (final InputStream tmpStream = County.class.getResourceAsStream(str)) {
                Files.copy(tmpStream, dataDir.resolve(str));
            }
        }

        siteCRS = RhomeoCore.getSiteCRS();
        store = new ShapefileFeatureStore(dataDir.resolve(RESOURCES[0]).toUri());
        name = store.getName();
        converter = new Converter();

        final Hints hints = new Hints();
        hints.put(Hints.FILTER_FACTORY, FilterFactory2.class);
        ff = (FilterFactory2) FactoryFinder.getFilterFactory(hints);

        counties = readCounties();
    }

    /**
     * Find a county for the given code.
     * @param code The code of the county. Ex : 34 for Hérault.
     * @return The matching county, or null if we cannot find any.
     */
    public County get(final String code) {
        return code == null? null : counties.get(code);
    }

    /**
     *
     * @param geom The geometry to find counties for.
     * @return List of counties intersecting the input geometry. Never null, but
     * can be empty.
     */
    public List<County> get(final Geometry geom) {
        final ArrayList<County> result = new ArrayList<>();
        if (geom == null || geom.isEmpty())
            return result;
        try {
            final QueryBuilder builder = new QueryBuilder(name);
            final CoordinateReferenceSystem fCRS = store.getFeatureType(name).getCoordinateReferenceSystem();
            if (fCRS != null && !CRS.equalsApproximatively(fCRS, siteCRS)) {
                builder.setCRS(siteCRS);
            }

            final GenericName geomName = store.getFeatureType().getGeometryDescriptor().getName();
            builder.setProperties(new String[]{COUNTY_CODE, geomName.tip().toString()});

            builder.setFilter(ff.intersects(ff.property(geomName), ff.literal(geom)));
            try (FeatureReader reader = store.getFeatureReader(builder.buildQuery())) {
                while (reader.hasNext()) {
                    result.add(counties.get(reader.next().getPropertyValue(COUNTY_CODE)));
                }
            }

        } catch (Exception e) {
            RhomeoCore.LOGGER.log(Level.FINE, "Cannot perform intersection on counties !", e);
        }

        return result;
    }

    /**
     *
     * @return Complete list of metropolitan France counties, indexed by code.
     */
    public Map<String, County> getAll() {
        return counties;
    }

    public FeatureCollection getFeatures() {
        return store.createSession(false).getFeatureCollection(QueryBuilder.all(name));
    }

    /**
     * Load all counties in memory.
     * @return An unmodifiable map of available counties. Key is code, value is
     * county.
     * @throws DataStoreException
     * @throws FactoryException
     */
    private Map<String, County> readCounties() throws DataStoreException, FactoryException {
        final QueryBuilder builder = new QueryBuilder(name);
        final CoordinateReferenceSystem fCRS = store.getFeatureType(name).getCoordinateReferenceSystem();
        if (fCRS != null && !CRS.equalsApproximatively(fCRS, siteCRS)) {
            builder.setCRS(siteCRS);
        }

        builder.setProperties(new String[]{COUNTY_CODE, COUNTY_NAME});

        // read and convert data.
        Map<String, County> tmpCounties = new HashMap();
        try (FeatureReader reader = store.getFeatureReader(builder.buildQuery())) {
            Feature dep;
            County county;
            while (reader.hasNext()) {
                dep = reader.next();
                county = new County(
                        dep.getPropertyValue(COUNTY_CODE).toString(),
                        dep.getPropertyValue(COUNTY_NAME).toString()
                );
                tmpCounties.put(county.code, county);
            }
        }

        return Collections.unmodifiableMap(tmpCounties);
    }

    @PreDestroy
    @Override
    public void close() throws Exception {
        try {
            store.close();
        } finally {
            IOUtilities.deleteRecursively(dataDir);
        }
    }

    public StringConverter<County> getStringConverter() {
        return converter;
    }

    private class Converter extends StringConverter<County> {

        @Override
        public String toString(County object) {
            if (object != null)
                return object.toString();
            return null;

        }

        @Override
        public County fromString(String string) {
            if (string == null || (string = string.trim()).isEmpty()) {
                return null;
            }

            final Matcher matcher = RhomeoCore.CODE_PATTERN.matcher(string);
            if (matcher.find()) {
                return get(matcher.group());
            }

            return null;
        }
    }
}
