/*
 * Copyright (C) 2005 Alfresco, Inc.
 *
 * Licensed under the Mozilla Public License version 1.1 
 * with a permitted attribution clause. You may obtain a
 * copy of the License at
 *
 *   http://www.alfresco.org/legal/license.txt
 *
 * 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.
 */
package org.alfresco.filesys.server.filesys;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.StringTokenizer;

/**
 * <p>
 * Provides utility methods for manipulating file names.
 */
public final class FileName
{

    // DOS file name seperator

    public static final char DOS_SEPERATOR = '\\';
    public static final String DOS_SEPERATOR_STR = "\\";

    // NTFS Stream seperator

    public static final String NTFSStreamSeperator = ":";

    /**
     * Build a path using the specified components.
     * 
     * @param dev java.lang.String
     * @param path java.lang.String
     * @param filename java.lang.String
     * @param sep char
     * @return java.lang.String
     */
    public static String buildPath(String dev, String path, String filename, char sep)
    {

        // Build the path string

        StringBuffer fullPath = new StringBuffer();

        // Check for a device name

        if (dev != null)
        {

            // Add the device name

            fullPath.append(dev);

            // Check if the device name has a file seperator

            if (dev.length() > 0 && dev.charAt(dev.length() - 1) != sep)
                fullPath.append(sep);
        }

        // Check for a path

        if (path != null)
        {

            // Add the path

            if (fullPath.length() > 0 && path.length() > 0
                    && (path.charAt(0) == sep || path.charAt(0) == DOS_SEPERATOR))
                fullPath.append(path.substring(1));
            else
                fullPath.append(path);

            // Add a trailing seperator, if required

            if (path.length() > 0 && path.charAt(path.length() - 1) != sep && filename != null)
                fullPath.append(sep);
        }

        // Check for a file name

        if (filename != null)
        {

            // Add the file name

            if (fullPath.length() > 0 && filename.length() > 0
                    && (filename.charAt(0) == sep || filename.charAt(0) == DOS_SEPERATOR))
                fullPath.append(filename.substring(1));
            else
                fullPath.append(filename);
        }

        // Convert the file seperator characters in the path if we are not using the normal
        // DOS file seperator character.

        if (sep != DOS_SEPERATOR)
            return convertSeperators(fullPath.toString(), sep);
        return fullPath.toString();
    }

    /**
     * Convert the file seperators in a path to the specified path seperator character.
     * 
     * @param path java.lang.String
     * @param sep char
     * @return java.lang.String
     */
    public static String convertSeperators(String path, char sep)
    {

        // Check if the path contains any DOS seperators

        if (path.indexOf(DOS_SEPERATOR) == -1)
            return path;

        // Convert DOS path seperators to the specified seperator

        StringBuffer newPath = new StringBuffer();
        int idx = 0;

        while (idx < path.length())
        {

            // Get the current character from the path and check if it is a DOS path
            // seperator character.

            char ch = path.charAt(idx++);
            if (ch == DOS_SEPERATOR)
                newPath.append(sep);
            else
                newPath.append(ch);
        }

        // Return the new path string

        return newPath.toString();
    }

    /**
     * Map the input path to a real path, this may require changing the case of various parts of the
     * path. The base path is not checked, it is assumed to exist.
     * 
     * @param base java.lang.String
     * @param path java.lang.String
     * @return java.lang.String
     * @exception java.io.FileNotFoundException The path could not be mapped to a real path.
     */
    public static final String mapPath(String base, String path) throws java.io.FileNotFoundException
    {

        // Split the path string into seperate directory components

        String pathCopy = path;
        if (pathCopy.length() > 0 && pathCopy.startsWith(DOS_SEPERATOR_STR))
            pathCopy = pathCopy.substring(1);

        StringTokenizer token = new StringTokenizer(pathCopy, "\\/");
        int tokCnt = token.countTokens();

        // The mapped path string, if it can be mapped

        String mappedPath = null;

        if (tokCnt > 0)
        {

            // Allocate an array to hold the directory names

            String[] dirs = new String[token.countTokens()];

            // Get the directory names

            int idx = 0;
            while (token.hasMoreTokens())
                dirs[idx++] = token.nextToken();

            // Check if the path ends with a directory or file name, ie. has a trailing '\' or not

            int maxDir = dirs.length;

            if (path.endsWith(DOS_SEPERATOR_STR) == false)
            {

                // Ignore the last token as it is a file name

                maxDir--;
            }

            // Build up the path string and validate that the path exists at each stage.

            StringBuffer pathStr = new StringBuffer(base);
            if (base.endsWith(java.io.File.separator) == false)
                pathStr.append(java.io.File.separator);

            int lastPos = pathStr.length();
            idx = 0;
            File lastDir = null;
            if (base != null && base.length() > 0)
                lastDir = new File(base);
            File curDir = null;

            while (idx < maxDir)
            {

                // Append the current directory to the path

                pathStr.append(dirs[idx]);
                pathStr.append(java.io.File.separator);

                // Check if the current path exists

                curDir = new File(pathStr.toString());

                if (curDir.exists() == false)
                {

                    // Check if there is a previous directory to search

                    if (lastDir == null)
                        throw new FileNotFoundException();

                    // Search the current path for a matching directory, the case may be different

                    String[] fileList = lastDir.list();
                    if (fileList == null || fileList.length == 0)
                        throw new FileNotFoundException();

                    int fidx = 0;
                    boolean foundPath = false;

                    while (fidx < fileList.length && foundPath == false)
                    {

                        // Check if the current file name matches the required directory name

                        if (fileList[fidx].equalsIgnoreCase(dirs[idx]))
                        {

                            // Use the current directory name

                            pathStr.setLength(lastPos);
                            pathStr.append(fileList[fidx]);
                            pathStr.append(java.io.File.separator);

                            // Check if the path is valid

                            curDir = new File(pathStr.toString());
                            if (curDir.exists())
                            {
                                foundPath = true;
                                break;
                            }
                        }

                        // Update the file name index

                        fidx++;
                    }

                    // Check if we found the required directory

                    if (foundPath == false)
                        throw new FileNotFoundException();
                }

                // Set the last valid directory file

                lastDir = curDir;

                // Update the end of valid path location

                lastPos = pathStr.length();

                // Update the current directory index

                idx++;
            }

            // Check if there is a file name to be added to the mapped path

            if (path.endsWith(DOS_SEPERATOR_STR) == false)
            {

                // Map the file name

                String[] fileList = lastDir.list();
                String fileName = dirs[dirs.length - 1];

                // Check if the file list is valid, if not then the path is not valid

                if (fileList == null)
                    throw new FileNotFoundException(path);

                // Search for the required file

                idx = 0;
                boolean foundFile = false;

                while (idx < fileList.length && foundFile == false)
                {
                    if (fileList[idx].compareTo(fileName) == 0)
                        foundFile = true;
                    else
                        idx++;
                }

                // Check if we found the file name, if not then do a case insensitive search

                if (foundFile == false)
                {

                    // Search again using a case insensitive search

                    idx = 0;

                    while (idx < fileList.length && foundFile == false)
                    {
                        if (fileList[idx].equalsIgnoreCase(fileName))
                        {
                            foundFile = true;
                            fileName = fileList[idx];
                        }
                        else
                            idx++;
                    }
                }

                // Append the file name

                pathStr.append(fileName);
            }

            // Set the new path string

            mappedPath = pathStr.toString();
        }

        // Return the mapped path string, if successful.

        return mappedPath;
    }

    /**
     * Remove the file name from the specified path string.
     * 
     * @param path java.lang.String
     * @return java.lang.String
     */
    public final static String removeFileName(String path)
    {

        // Find the last path seperator

        int pos = path.lastIndexOf(DOS_SEPERATOR);
        if (pos != -1)
            return path.substring(0, pos);

        // Return an empty string, no path seperators

        return "";
    }

    /**
     * Split the path into seperate directory path and file name strings.
     * 
     * @param path Full path string.
     * @param sep Path seperator character.
     * @return java.lang.String[]
     */
    public static String[] splitPath(String path)
    {
        return splitPath(path, DOS_SEPERATOR, null);
    }

    /**
     * Split the path into seperate directory path and file name strings.
     * 
     * @param path Full path string.
     * @param sep Path seperator character.
     * @return java.lang.String[]
     */
    public static String[] splitPath(String path, char sep)
    {
        return splitPath(path, sep, null);
    }

    /**
     * Split the path into seperate directory path and file name strings.
     * 
     * @param path Full path string.
     * @param sep Path seperator character.
     * @param list String list to return values in, or null to allocate
     * @return java.lang.String[]
     */
    public static String[] splitPath(String path, char sep, String[] list)
    {
        if (path == null)
            throw new IllegalArgumentException("Path may not be null");

        // Create an array of strings to hold the path and file name strings
        String[] pathStr = list;
        if (pathStr == null)
            pathStr = new String[] {"", ""};

        // Check if the path is valid
        if (path.length() > 0)
        {
            // Check if the path has a trailing seperator, if so then there is no file name.
            int pos = path.lastIndexOf(sep);
            if (pos == -1 || pos == (path.length() - 1))
            {
                // Set the path string in the returned string array
                pathStr[0] = path;
            }
            else
            {
                // Split the path into directory list and file name strings
                pathStr[1] = path.substring(pos + 1);

                if (pos == 0)
                    pathStr[0] = path.substring(0, pos + 1);
                else
                    pathStr[0] = path.substring(0, pos);
            }
        }

        // Return the path strings
        return pathStr;
    }

    /**
     * Split the path into all the component directories and filename
     * 
     * @param path String
     * @return String[]
     */
    public static String[] splitAllPaths(String path)
    {

        // Check if the path is valid

        if (path == null || path.length() == 0)
            return null;

        // Determine the number of components in the path

        StringTokenizer token = new StringTokenizer(path, DOS_SEPERATOR_STR);
        String[] names = new String[token.countTokens()];

        // Split the path

        int i = 0;

        while (i < names.length && token.hasMoreTokens())
            names[i++] = token.nextToken();

        // Return the path components

        return names;
    }

    /**
     * Split a path string into directory path, file name and stream name components
     * 
     * @param path Full path string.
     * @return java.lang.String[]
     */
    public static String[] splitPathStream(String path)
    {

        // Allocate the return list

        String[] pathStr = new String[3];

        // Split the path into directory path and file/stream name

        FileName.splitPath(path, DOS_SEPERATOR, pathStr);
        if (pathStr[1] == null)
            return pathStr;

        // Split the file name into file and stream names

        int pos = pathStr[1].indexOf(NTFSStreamSeperator);

        if (pos != -1)
        {

            // Split the file/stream name

            pathStr[2] = pathStr[1].substring(pos);
            pathStr[1] = pathStr[1].substring(0, pos);
        }

        // Return the path components list

        return pathStr;
    }

    /**
     * Test if a file name contains an NTFS stream name
     * 
     * @param path String
     * @return boolean
     */
    public static boolean containsStreamName(String path)
    {

        // Check if the path contains the stream name seperator character

        if (path.indexOf(NTFSStreamSeperator) != -1)
            return true;
        return false;
    }

    /**
     * Normalize the path to uppercase the directory names and keep the case of the file name.
     * 
     * @param path String
     * @return String
     */
    public final static String normalizePath(String path)
    {

        // Split the path into directories and file name, only uppercase the directories to
        // normalize
        // the path.

        String normPath = path;

        if (path.length() > 3)
        {

            // Split the path to seperate the folders/file name

            int pos = path.lastIndexOf(DOS_SEPERATOR);
            if (pos != -1)
            {

                // Get the path and file name parts, normalize the path

                String pathPart = path.substring(0, pos).toUpperCase();
                String namePart = path.substring(pos);

                // Rebuild the path string

                normPath = pathPart + namePart;
            }
        }

        // Return the normalized path

        return normPath;
    }

    /**
     * Make a path relative to the base path for the specified path.
     * 
     * @param basePath String
     * @param fullPath String
     * @return String
     */
    public final static String makeRelativePath(String basePath, String fullPath)
    {

        // Check if the base path is the root path

        if (basePath.length() == 0 || basePath.equals(DOS_SEPERATOR_STR))
        {

            // Return the full path, strip any leading seperator

            if (fullPath.length() > 0 && fullPath.charAt(0) == DOS_SEPERATOR)
                return fullPath.substring(1);
            return fullPath;
        }

        // Split the base and full paths into seperate components

        String[] baseNames = splitAllPaths(basePath);
        String[] fullNames = splitAllPaths(fullPath);

        // Check that the full path is actually within the base path tree

        if (baseNames != null && baseNames.length > 0 && fullNames != null && fullNames.length > 0
                && baseNames[0].equalsIgnoreCase(fullNames[0]) == false)
            return null;

        // Match the path names

        int idx = 0;

        while (idx < baseNames.length && idx < fullNames.length && baseNames[idx].equalsIgnoreCase(fullNames[idx]))
            idx++;

        // Build the relative path

        StringBuffer relPath = new StringBuffer(128);

        while (idx < fullNames.length)
        {
            relPath.append(fullNames[idx++]);
            if (idx < fullNames.length)
                relPath.append(DOS_SEPERATOR);
        }

        // Return the relative path

        return relPath.toString();
    }
}