/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2002-2008, Open Source Geospatial Foundation (OSGeo)
 *
 *    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.shapefile;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;
import java.util.logging.Logger;

import org.geotools.data.DataSourceException;
import org.geotools.data.DataStore;
import org.geotools.data.DataUtilities;
import org.geotools.data.FileDataStore;
import org.geotools.data.FileDataStoreFactorySpi;
import org.geotools.data.directory.DirectoryDataStore;
import org.geotools.data.directory.FileStoreFactory;
import org.geotools.data.shapefile.indexed.IndexType;
import org.geotools.data.shapefile.indexed.IndexedShapefileDataStore;
import org.geotools.util.KVP;

import com.vividsolutions.jts.geom.Geometry;

/**
 * Implementation of the DataStore service provider interface for Shapefiles.
 * <p>
 * The specific implementation of ShapefileDataStore created by this class is
 * not specified. For more information on the connection parameters please
 * review the following public Param constants.
 * <ul>
 * <li>{@link URLP}
 * <li>{@link NAMESPACEP}
 * <li>{@link CREATE_SPATIAL_INDEX}
 * <li>{@link MEMORY_MAPPED}
 * <li>{@link DBFCHARSET}
 * </ul>
 * 
 * @author Chris Holmes, TOPP
 *
 * @source $URL: http://svn.osgeo.org/geotools/branches/2.7.x/modules/plugin/shapefile/src/main/java/org/geotools/data/shapefile/ShapefileDataStoreFactory.java $
 * @version $Id: ShapefileDataStoreFactory.java 27856 2007-11-12 17:23:35Z
 *          desruisseaux $
 */
public class ShapefileDataStoreFactory implements FileDataStoreFactorySpi {

    public static final Logger LOGGER = org.geotools.util.logging.Logging
            .getLogger("org.geotools.data.shapefile");
    /**
     * url to the .shp file.
     */
    public static final Param URLP = new Param("url", URL.class,
            "url to a .shp file",true, null,
            new KVP(Param.EXT,"shp"));
    

    /**
     * Optional - uri of the FeatureType's namespace
     */
    public static final Param NAMESPACEP = new Param("namespace", URI.class,
            "uri to a the namespace", false, null,  // not required
            new KVP(Param.LEVEL,"advanced") );

    /**
     * Optional - enable/disable the use of memory-mapped io
     */
    public static final Param MEMORY_MAPPED = new Param("memory mapped buffer",
            Boolean.class, "enable/disable the use of memory-mapped io", false, false,
            new KVP(Param.LEVEL,"advanced") );
    
    /**
     * Optional - enable/disable the use of memory-mapped io
     */
    public static final Param CACHE_MEMORY_MAPS = new Param("cache and reuse memory maps",
            Boolean.class, "only memory map a file one, then cache and reuse the map", false, true,
            new KVP(Param.LEVEL,"advanced") );
    
    /**
     * Optional - discriminator for directory stores
     */
    public static final Param FILE_TYPE = new Param("filetype",
            String.class, "Discriminator for directory stores", false, "shapefile",
            new KVP(Param.LEVEL,"advanced") );

    /**
     * Optional - Enable/disable the automatic creation of spatial index
     */
    public static final Param CREATE_SPATIAL_INDEX = new Param(
            "create spatial index", Boolean.class,
            "enable/disable the automatic creation of spatial index", false, true,
            new KVP(Param.LEVEL,"advanced") );

    /**
     * Optional - character used to decode strings from the DBF file
     */
    public static final Param DBFCHARSET = new Param("charset", Charset.class,
            "character used to decode strings from the DBF file", false,
            Charset.forName("ISO-8859-1"),
            new KVP(Param.LEVEL,"advanced")) {
        /*
         * This is an example of a non simple Param type where a custom parse
         * method is required.
         * 
         * @see org.geotools.data.DataStoreFactorySpi.Param#parse(java.lang.String)
         */
        public Object parse(String text) throws IOException {
            return Charset.forName(text);
        }

        public String text(Object value) {
            return ((Charset) value).name();
        }
    };
    
    /**
     * Optional - timezone to decode dates from the DBF file
     */
    public static final Param DBFTIMEZONE = new Param("timezone", TimeZone.class,
            "time zone used to read dates from the DBF file", false,
            TimeZone.getDefault(),
            new KVP(Param.LEVEL,"advanced")) {

    	public Object parse(String text) throws IOException {
            return TimeZone.getTimeZone(text);
        }

        public String text(Object value) {
            return ((TimeZone) value).getID();
        }
    };


    /**
     * Takes a map of parameters which describes how to access a DataStore and
     * determines if it can be read by the ShapefileDataStore or
     * IndexedShapefileDataStore implementations.
     * 
     * @param params
     *                A map of parameters describing the location of a
     *                datastore. Files should be pointed to by a 'url' param.
     * 
     * @return true iff params contains a url param which points to a file
     *         ending in shp
     */
    public boolean canProcess(Map params) {
        boolean accept = false;
        if (params.containsKey(URLP.key)) {
            try {
                URL url = (URL) URLP.lookUp(params);
                accept = canProcess(url);
                if(!accept) {
                    // maybe it's a directory?
                    Object fileType = FILE_TYPE.lookUp(params);
                    File dir = DataUtilities.urlToFile(url);
                    // check for null fileType for backwards compatibility
                    accept = dir.isDirectory() && (fileType == null || "shapefile".equals(fileType));  
                }
            } catch (IOException ioe) {
                // yes, I am eating this - since it is my job to return a
                // true/false
            }
        }
        return accept;
    }

    /**
     * Returns an instance of DataStore iff the resource pointed to the Map of
     * paramers can be handled as a shapefile.
     * <p>
     * The specific implementation of ShapefileDataStore returned is not
     * specified, and depends on the parameters given. For more information
     * please review the public static Param instances available for this class.
     * </p>
     * <ul>
     * <li>{@link URLP}
     * <li>{@link NAMESPACEP}
     * <li>{@link CREATE_SPATIAL_INDEX}
     * <li>{@link MEMORY_MAPPED}
     * <li>{@link DBFCHARSET}
     * </ul>
     * 
     * @param params
     *                A param list with information on the location of a
     *                restore. For shapefiles this should contain a 'url' param
     *                which points to a file which ends in shp.
     * 
     * @return DataStore A ShapefileDatastore
     * @throws IOException
     *                 If a connection error (such as the file not existing
     *                 occurs)
     * @throws DataSourceException
     *                 Thrown if the datastore which is created cannot be
     *                 attached to the restore specified in params.
     */
    public DataStore createDataStore(Map params) throws IOException {
        return createNewDataStore(params);
    }

    /**
     * Creates a new DataStore - for a file that does not exist yet.
     * <p>
     * This method has different logic than createDataStore. It is willing to be
     * memory mapped, and generate an index for a local file that does not exist
     * yet.
     * 
     */
    public DataStore createNewDataStore(Map params) throws IOException {
        // basic param lookup
        URL url = (URL) URLP.lookUp(params);
        Boolean isMemoryMapped = (Boolean) MEMORY_MAPPED.lookUp(params);
        Boolean cacheMemoryMaps = (Boolean) CACHE_MEMORY_MAPS.lookUp(params);
        URI namespace = (URI) NAMESPACEP.lookUp(params);
        Charset dbfCharset = (Charset) DBFCHARSET.lookUp(params);
        TimeZone dbfTimeZone = (TimeZone) DBFTIMEZONE.lookUp(params);
        Boolean isCreateSpatialIndex = (Boolean) CREATE_SPATIAL_INDEX
                .lookUp(params);
        if (isCreateSpatialIndex == null) {
            // should not be needed as default is TRUE
            assert (true);
            isCreateSpatialIndex = Boolean.TRUE;
        }
        if (dbfCharset == null) {
            assert (true);
            // this should not happen as Charset.forName("ISO-8859-1") was used
            // as the param default?
            dbfCharset = Charset.forName("ISO-8859-1");
        }
        if(dbfTimeZone == null) {
        	dbfTimeZone = TimeZone.getDefault();
        }
        if (isMemoryMapped == null) {
            assert (true);
            // this should not happen as false was the default
            isMemoryMapped = Boolean.FALSE;
        }
        if (cacheMemoryMaps == null) {
        	cacheMemoryMaps = Boolean.FALSE;
        }
        
        // are we creating a directory of shapefiles store, or a single one?
        File dir = DataUtilities.urlToFile(url);
        if(dir != null && dir.isDirectory()) {
            return new DirectoryDataStore(DataUtilities.urlToFile(url), new ShpFileStoreFactory(this, params));
        } else {
            ShpFiles shpFiles = new ShpFiles(url);
    
            boolean isLocal = shpFiles.isLocal();
            boolean useMemoryMappedBuffer = isLocal
                    && isMemoryMapped.booleanValue();
            boolean createIndex = isCreateSpatialIndex.booleanValue() && isLocal;
    
        	ShapefileDataStore store;
            if (createIndex) {
                store = new IndexedShapefileDataStore(url, namespace,
                        useMemoryMappedBuffer, cacheMemoryMaps, true, IndexType.QIX, dbfCharset);
            } else {
                store = new ShapefileDataStore(url, namespace,
                        useMemoryMappedBuffer, cacheMemoryMaps, dbfCharset);
            }
            store.setDbftimeZone(dbfTimeZone);
            return store;
        }
    }

    public String getDisplayName() {
        return "Shapefile";
    }

    /**
     * Describes the type of data the datastore returned by this factory works
     * with.
     * 
     * @return String a human readable description of the type of restore
     *         supported by this datastore.
     */
    public String getDescription() {
        return "ESRI(tm) Shapefiles (*.shp)";
    }

    // public DataSourceMetadataEnity createMetadata( Map params )
    // throws IOException {
    //        
    // URL url = (URL) URLP.lookUp(params);
    // Boolean mm = (Boolean) MEMORY_MAPPED.lookUp(params);
    //        
    // String server;
    // String name;
    // if( url.getProtocol().equals("file")){
    // server = "localhost";
    // name = url.getPath();
    // }
    // else {
    // server = url.getHost()+":"+url.getPort();
    // name = url.getFile();
    // }
    // return new DataSourceMetadataEnity( server, name, "Shapefile access for
    // "+url );
    // }
    /**
     * Test to see if this datastore is available, if it has all the appropriate
     * libraries to construct a datastore.
     * 
     * This datastore just checks for the ShapefileDataStore,
     * IndexedShapefileDataStore and Geometry implementations.
     * 
     * @return <tt>true</tt> if and only if this factory is available to
     *         create DataStores.
     */
    public boolean isAvailable() {
        try {
            ShapefileDataStore.class.getName();
            IndexedShapefileDataStore.class.getName();
            Geometry.class.getName();
        } catch (Exception e) {
            return false;
        }

        return true;
    }

    /**
     * @see org.geotools.data.dir.FileDataStoreFactorySpi#canProcess(java.net.URL)
     */
    public boolean canProcess(URL f) {
        return f.getFile().toUpperCase().endsWith("SHP");
    }

    /**
     * We may need to create a new datastore if the provided file does not
     * exist.
     * 
     * @see org.geotools.data.dir.FileDataStoreFactorySpi#createDataStore(java.net.URL)
     */
    public FileDataStore createDataStore(URL url) throws IOException {
        Map params = new HashMap();
        params.put(URLP.key, url);

        boolean isLocal = url.getProtocol().equalsIgnoreCase("file");
        File file = DataUtilities.urlToFile(url);
        if(file != null && file.isDirectory()) {
            return null;
        } else {
            if (isLocal && !file.exists()) {
                return (FileDataStore) createNewDataStore(params);
            } else {
                return (FileDataStore) createDataStore(params);
            }
        }
    }

    /**
     * Returns the implementation hints. The default implementation returns an
     * empty map.
     * <p>
     * When we have FeatureFactory, GeometryFactory and so on hooked up this map
     * will return Hints we paid attention too when we were constructed.
     * 
     * @return An empty map.
     */
    public Map getImplementationHints() {
        return Collections.EMPTY_MAP;
    }

    
    /**
     * A delegates that allow to build a directory of shapfiles store
     * @author Andrea Aime - OpenGeo
     */
    public static class ShpFileStoreFactory implements FileStoreFactory {
        
        ShapefileDataStoreFactory shpFactory;
        Map originalParams;
        
        public ShpFileStoreFactory(ShapefileDataStoreFactory factory, Map originalParams) {
            this.shpFactory = factory;
            this.originalParams = originalParams;
        }
        

        public DataStore getDataStore(File file) throws IOException {
            final URL url = DataUtilities.fileToURL(file);
            if(shpFactory.canProcess(url)) {
                Map params = new HashMap(originalParams);
                params.put(URLP.key, url);
                return shpFactory.createDataStore(params);
            } else {
                return null;
            }
        }
        
    }

}
