/*
Copyright (C) 2001, 2006 United States Government
as represented by the Administrator of the
National Aeronautics and Space Administration.
All Rights Reserved.
*/
package gov.nasa.worldwind;

/**
 * @author tag
 * @version $Id: AbstractFileCache.java 748 2007-02-03 19:22:54Z tgaskins $
 */
public class AbstractFileCache implements FileCache
{
    private final java.util.LinkedList<java.io.File> cacheDirs = new java.util.LinkedList<java.io.File>();
    private java.io.File cacheWriteDir = null;

    public AbstractFileCache()
    {
    }

    protected void initialize(java.io.InputStream xmlConfigStream)
    {
        javax.xml.parsers.DocumentBuilderFactory docBuilderFactory =
            javax.xml.parsers.DocumentBuilderFactory.newInstance();

        try
        {
            javax.xml.parsers.DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
            org.w3c.dom.Document doc = docBuilder.parse(xmlConfigStream);

            // The order of the following two calls is important, because building the writable location may entail
            // creating a location that's included in the specified read locations.
            this.buildWritePaths(doc);
            this.buildReadPaths(doc);

            if (this.cacheWriteDir == null)
            {
                String message = WorldWind.retrieveErrMsg("FileCache.NoFileCacheWriteLocation");
                WorldWind.logger().log(java.util.logging.Level.FINE, message);
            }

            if (this.cacheDirs.size() == 0)
            {
                // This should not happen because the writable cache is added to the read list, but check nonetheless
                String message = WorldWind.retrieveErrMsg("FileCache.NoFileCacheReadLocations");
                WorldWind.logger().log(java.util.logging.Level.FINE, message);
                throw new IllegalStateException(message);
            }
        }
        catch (javax.xml.parsers.ParserConfigurationException e)
        {
            e.printStackTrace();
        }
        catch (org.xml.sax.SAXException e)
        {
            e.printStackTrace();
        }
        catch (java.io.IOException e)
        {
            e.printStackTrace();
        }
    }

    public void addCacheLocation(String newPath)
    {
        this.addCacheLocation(this.cacheDirs.size(), newPath);
    }

    public void addCacheLocation(int index, String newPath)
    {
        if (newPath == null || newPath.length() == 0)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.FileCachePathIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        if (index < 0)
        {
            String message = WorldWind.retrieveErrMsg("generic.invalidIndex");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        if (index > 0 && index > this.cacheDirs.size())
            index = this.cacheDirs.size();

        java.io.File newFile = new java.io.File(newPath);

        if (this.cacheDirs.contains(newFile))
            this.cacheDirs.remove(newFile);

        this.cacheDirs.add(index, newFile);
    }

    public void removeCacheLocation(String newPath)
    {
        if (newPath == null || newPath.length() == 0)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.FileCachePathIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            // Just warn and return.
            return;
        }

        java.io.File newFile = new java.io.File(newPath);

        if (newFile.equals(this.cacheWriteDir))
        {
            String message = WorldWind.retrieveErrMsg("FileCache.CannotRemoveWriteLocationFromSearchList");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        this.cacheDirs.remove(new java.io.File(newPath));
    }

    public java.util.List<java.io.File> getCacheLocations()
    {
        // Return a copy.
        return new java.util.LinkedList<java.io.File>(this.cacheDirs);
    }

    public java.io.File getWriteLocation()
    {
        return this.cacheWriteDir;
    }

    private void buildReadPaths(org.w3c.dom.Node dataFileCacheNode)
    {
        javax.xml.xpath.XPathFactory pathFactory = javax.xml.xpath.XPathFactory.newInstance();
        javax.xml.xpath.XPath pathFinder = pathFactory.newXPath();

        try
        {
            org.w3c.dom.NodeList locationNodes = (org.w3c.dom.NodeList) pathFinder.evaluate(
                "/dataFileCache/readLocations/location",
                dataFileCacheNode.getFirstChild(),
                javax.xml.xpath.XPathConstants.NODESET);
            for (int i = 0; i < locationNodes.getLength(); i++)
            {
                org.w3c.dom.Node location = locationNodes.item(i);
                String prop = pathFinder.evaluate("@property", location);
                String wwDir = pathFinder.evaluate("@wwDir", location);
                String append = pathFinder.evaluate("@append", location);

                String path = buildLocationPath(prop, append, wwDir);
                if (path == null)
                {
                    String message = WorldWind.retrieveErrMsg("FileCache.CacheLocationInvalid");
                    message += prop != null ? prop : WorldWind.retrieveErrMsg("generic.unknown");
                    WorldWind.logger().log(java.util.logging.Level.FINE, message);
                    continue;
                }

                // Even paths that don't exist or are otherwise problematic are added to the list because they may
                // become readable during the session. E.g., removable media. So add them to the search list.

                java.io.File pathFile = new java.io.File(path);
                if (pathFile.exists() && !pathFile.isDirectory())
                {
                    String message = WorldWind.retrieveErrMsg("FileCache.CacheLocationIsFile");
                    message += pathFile.getPath();
                    WorldWind.logger().log(java.util.logging.Level.FINER, message);
                }

                if (!this.cacheDirs.contains(pathFile)) // filter out duplicates
                {
                    this.cacheDirs.add(pathFile);
                }
            }
        }
        catch (javax.xml.xpath.XPathExpressionException e)
        {
            e.printStackTrace();
        }
    }

    private void buildWritePaths(org.w3c.dom.Node dataFileCacheNode)
    {
        javax.xml.xpath.XPathFactory pathFactory = javax.xml.xpath.XPathFactory.newInstance();
        javax.xml.xpath.XPath pathFinder = pathFactory.newXPath();

        try
        {
            org.w3c.dom.NodeList locationNodes = (org.w3c.dom.NodeList) pathFinder.evaluate(
                "/dataFileCache/writeLocations/location",
                dataFileCacheNode.getFirstChild(),
                javax.xml.xpath.XPathConstants.NODESET);
            for (int i = 0; i < locationNodes.getLength(); i++)
            {
                org.w3c.dom.Node location = locationNodes.item(i);
                String prop = pathFinder.evaluate("@property", location);
                String wwDir = pathFinder.evaluate("@wwDir", location);
                String append = pathFinder.evaluate("@append", location);
                String create = pathFinder.evaluate("@create", location);

                String path = buildLocationPath(prop, append, wwDir);
                if (path == null)
                {
                    String message = WorldWind.retrieveErrMsg("FileCache.CacheLocationInvalid");
                    message += prop != null ? prop : WorldWind.retrieveErrMsg("generic.unknown");
                    WorldWind.logger().log(java.util.logging.Level.FINE, message);
                    continue;
                }

                java.io.File pathFile = new java.io.File(path);
                if (!pathFile.exists() && create != null && (create.contains("t") || create.contains("T")))
                {
                    pathFile.mkdirs();
                }

                if (pathFile.isDirectory() && pathFile.canWrite() && pathFile.canRead())
                {
                    this.cacheWriteDir = pathFile;
                    this.cacheDirs.addFirst(pathFile); // writable location is always first in search path
                    break; // only need one
                }
            }
        }
        catch (javax.xml.xpath.XPathExpressionException e)
        {
            e.printStackTrace();
        }
    }

    private static String buildLocationPath(String property, String append, String wwDir)
    {
        String path = propertyToPath(property);

        if (append != null && append.length() != 0)
            path = appendPathPart(path, append.trim());

        if (wwDir != null && wwDir.length() != 0)
            path = appendPathPart(path, wwDir.trim());

        return path;
    }

    private static String appendPathPart(String firstPart, String secondPart)
    {
        if (secondPart == null || secondPart.length() == 0)
            return firstPart;
        if (firstPart == null || secondPart.length() == 0)
            return secondPart;

        firstPart = stripTrailingSeparator(firstPart);
        secondPart = stripLeadingSeparator(secondPart);

        return firstPart + System.getProperty("file.separator") + secondPart;
    }

    private static String stripTrailingSeparator(String s)
    {
        if (s.endsWith("/") || s.endsWith("\\"))
            return s.substring(0, s.length() - 1);
        else
            return s;
    }

    private static String stripLeadingSeparator(String s)
    {
        if (s.startsWith("/") || s.startsWith("\\"))
            return s.substring(1, s.length());
        else
            return s;
    }

    private static String propertyToPath(String propName)
    {
        if (propName == null || propName.length() == 0)
            return null;

        String prop = System.getProperty(propName);
        if (prop != null)
            return prop;

        if (propName.equalsIgnoreCase("gov.nasa.worldwind.platform.alluser.cache"))
            return determineAllUserCacheDir();

        if (propName.equalsIgnoreCase("gov.nasa.worldwind.platform.user.cache"))
            return determineSingleUserCacheDir();

        return null;
    }

    private static String determineAllUserCacheDir()
    {
        if (gov.nasa.worldwind.Configuration.isMacOS())
        {
            return "/Library/Caches";
        }
        else if (gov.nasa.worldwind.Configuration.isWindowsOS())
        {
            String path = System.getenv("ALLUSERSPROFILE");
            if (path == null)
            {
                String message = WorldWind.retrieveErrMsg("generic.AllUsersWindowsProfileNotKnown");
                WorldWind.logger().log(java.util.logging.Level.FINE, message);
                return null;
            }
            return path + "\\Application Data";
        }
        else if (gov.nasa.worldwind.Configuration.isLinuxOS() || gov.nasa.worldwind.Configuration.isUnixOS() || gov.nasa
            .worldwind.Configuration
            .isSolarisOS())
        {
            return "/var/cache/";
        }
        else
        {
            String message = WorldWind.retrieveErrMsg("generic.UnknownOperatingSystem");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            return null;
        }
    }

    private static String determineSingleUserCacheDir()
    {
        String home = getUserHomeDir();
        if (home == null)
        {
            String message = WorldWind.retrieveErrMsg("generic.UsersHomeDirectoryNotKnown");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            return null;
        }

        String path = null;

        if (gov.nasa.worldwind.Configuration.isMacOS())
        {
            path = "/Library/Caches";
        }
        else if (gov.nasa.worldwind.Configuration.isWindowsOS())
        {
            path = System.getenv("USERPROFILE");
            if (path == null)
            {
                String message = WorldWind.retrieveErrMsg("generic.UsersWindowsProfileNotKnown");
                WorldWind.logger().log(java.util.logging.Level.FINE, message);
                return null;
            }
            path += "\\Application Data";
        }
        else if (gov.nasa.worldwind.Configuration.isLinuxOS() || gov.nasa.worldwind.Configuration.isUnixOS() || gov.nasa
            .worldwind.Configuration
            .isSolarisOS())
        {
            path = "/var/cache/";
        }
        else
        {
            String message = WorldWind.retrieveErrMsg("generic.UnknownOperatingSystem");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
        }

        if (path == null)
            return null;

        return home + path;
    }

    private static String getUserHomeDir()
    {
        return System.getProperty("user.home");
    }

    public boolean contains(String fileName)
    {
        if (fileName == null)
            return false;

        for (java.io.File cacheDir : this.cacheDirs)
        {
            java.io.File file;
            if (fileName.startsWith(cacheDir.getAbsolutePath()))
                file = new java.io.File(fileName);
            else
                file = this.cachePathForFile(cacheDir, fileName);

            if (file.exists())
                return true;
        }

        return false;
    }

    private java.io.File cachePathForFile(java.io.File file, String fileName)
    {
        return new java.io.File(file.getAbsolutePath() + "/" + fileName);
    }

    private String makeFullPath(java.io.File dir, String fileName)
    {
        return dir.getAbsolutePath() + "/" + fileName;
    }

    /**
     * @param fileName the name to give the newly created file
     * @return a handle to the newly created file if it could be created and added to the cache, otherwise null
     * @throws IllegalArgumentException if <code>fileName</code> is null
     */
    public java.io.File newFile(String fileName)
    {
        if (fileName == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.FilePathIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        if (this.cacheWriteDir != null)
        {
            String fullPath = this.makeFullPath(this.cacheWriteDir, fileName);
            java.io.File file = new java.io.File(fullPath);
            if (file.getParentFile().exists())
                return file;
            else if (file.getParentFile().mkdirs())
                return file;
            else
                return null;
        }

        return null;
    }

    /**
     * @param fileName       the name of the file to find
     * @param checkClassPath if <code>true</code>, the class path is first searched for the file, otherwise the class
     *                       path is not searched unless it's one of the explicit paths in the cache search directories
     * @return a handle to the requested file if it exists in the cache, otherwise null
     * @throws IllegalArgumentException if <code>fileName</code> is null
     */
    public java.net.URL findFile(String fileName, boolean checkClassPath)
    {
        if (fileName == null)
        {
            String message = WorldWind.retrieveErrMsg("nullValue.FilePathIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        if (checkClassPath)
        {
            java.net.URL url = this.getClass().getClassLoader().getResource(fileName);
            if (url != null)
                return url;
        }

        for (java.io.File dir : this.cacheDirs)
        {
            if (!dir.exists())
                continue;

            java.io.File file = new java.io.File(this.makeFullPath(dir, fileName));
            if (file.exists())
            {
                try
                {
                    return file.toURI().toURL();
                }
                catch (java.net.MalformedURLException e)
                {
                    String message = WorldWind.retrieveErrMsg("FileCache.ExceptionCreatingURLForFile");
                    WorldWind.logger().log(java.util.logging.Level.FINE, message + file.getPath());
                }
            }
        }

        return null;
    }

    /**
     * @param url the "file:" URL of the file to remove from the cache
     * @throws IllegalArgumentException if <code>url</code> is null
     */
    public void removeFile(java.net.URL url)
    {
        if (url == null)
        {
            String msg = WorldWind.retrieveErrMsg("nullValue.URLIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, msg);
            throw new IllegalArgumentException(msg);
        }

        try
        {
            java.io.File file = new java.io.File(url.toURI());

            if (file.exists())
                file.delete();
        }
        catch (java.net.URISyntaxException e)
        {
            String message = WorldWind.retrieveErrMsg("FileCache.ExceptionRemovingFile");
            WorldWind.logger().log(java.util.logging.Level.FINE, message + url);
        }
    }
}