/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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.apache.jackrabbit.core.fs;

import java.io.ByteArrayOutputStream;
import java.util.BitSet;

/**
 * Utility class for handling paths in a file system.
 */
public final class FileSystemPathUtil {

    /**
     * Array of lowercase hexadecimal characters used in creating hex escapes.
     */
    private static final char[] HEX_TABLE = "0123456789abcdef".toCharArray();

    /**
     * The escape character used to mark hex escape sequences.
     */
    private static final char ESCAPE_CHAR = '%';

    /**
     * The list of characters that are not encoded by the <code>escapeName(String)</code>
     * and <code>unescape(String)</code> methods. They contains the characters
     * which can safely be used in file names:
     */
    public static final BitSet SAFE_NAMECHARS;

    /**
     * The list of characters that are not encoded by the <code>escapePath(String)</code>
     * and <code>unescape(String)</code> methods. They contains the characters
     * which can safely be used in file paths:
     */
    public static final BitSet SAFE_PATHCHARS;

    static {
        // build list of valid name characters
        SAFE_NAMECHARS = new BitSet(256);
        int i;
        for (i = 'a'; i <= 'z'; i++) {
            SAFE_NAMECHARS.set(i);
        }
        for (i = 'A'; i <= 'Z'; i++) {
            SAFE_NAMECHARS.set(i);
        }
        for (i = '0'; i <= '9'; i++) {
            SAFE_NAMECHARS.set(i);
        }
        SAFE_NAMECHARS.set('-');
        SAFE_NAMECHARS.set('_');
        SAFE_NAMECHARS.set('.');

        // build list of valid path characters (includes name characters)
        SAFE_PATHCHARS = (BitSet) SAFE_NAMECHARS.clone();
        SAFE_PATHCHARS.set(FileSystem.SEPARATOR_CHAR);
    }

    /**
     * private constructor
     */
    private FileSystemPathUtil() {
    }

    /**
     * Escapes the given string using URL encoding for all bytes not included
     * in the given set of safe characters.
     *
     * @param s the string to escape
     * @param safeChars set of safe characters (bytes)
     * @return escaped string
     */
    private static String escape(String s, BitSet safeChars) {
        byte[] bytes = s.getBytes();
        StringBuilder out = new StringBuilder(bytes.length);
        for (int i = 0; i < bytes.length; i++) {
            int c = bytes[i] & 0xff;
            if (safeChars.get(c) && c != ESCAPE_CHAR) {
                out.append((char) c);
            } else {
                out.append(ESCAPE_CHAR);
                out.append(HEX_TABLE[(c >> 4) & 0x0f]);
                out.append(HEX_TABLE[(c) & 0x0f]);
            }
        }
        return out.toString();
    }

    /**
     * Encodes the specified <code>path</code>. Same as
     * <code>{@link #escapeName(String)}</code> except that the separator
     * character <b><code>/</code></b> is regarded as a legal path character
     * that needs no escaping.
     *
     * @param path the path to encode.
     * @return the escaped path
     */
    public static String escapePath(String path) {
        return escape(path, SAFE_PATHCHARS);
    }

    /**
     * Encodes the specified <code>name</code>. Same as
     * <code>{@link #escapePath(String)}</code> except that the separator character
     * <b><code>/</code></b> is regarded as an illegal character that needs
     * escaping.
     *
     * @param name the name to encode.
     * @return the escaped name
     */
    public static String escapeName(String name) {
        return escape(name, SAFE_NAMECHARS);
    }

    /**
     * Decodes the specified path/name.
     *
     * @param pathOrName the escaped path/name
     * @return the unescaped path/name
     */
    public static String unescape(String pathOrName) {
        ByteArrayOutputStream out = new ByteArrayOutputStream(pathOrName.length());
        for (int i = 0; i < pathOrName.length(); i++) {
            char c = pathOrName.charAt(i);
            if (c == ESCAPE_CHAR) {
                try {
                    out.write(Integer.parseInt(pathOrName.substring(i + 1, i + 3), 16));
                } catch (NumberFormatException e) {
                    IllegalArgumentException iae = new IllegalArgumentException("Failed to unescape escape sequence");
                    iae.initCause(e);
                    throw iae;
                }
                i += 2;
            } else {
                out.write(c);
            }
        }
        return new String(out.toByteArray());
    }

    /**
     * Tests whether the specified path represents the root path, i.e. "/".
     *
     * @param path path to test
     * @return true if the specified path represents the root path; false otherwise.
     */
    public static boolean denotesRoot(String path) {
        return path.equals(FileSystem.SEPARATOR);
    }

    /**
     * Checks if <code>path</code> is a valid path.
     *
     * @param path the path to be checked
     * @throws FileSystemException If <code>path</code> is not a valid path
     */
    public static void checkFormat(String path) throws FileSystemException {
        if (path == null) {
            throw new FileSystemException("null path");
        }

        // path must be absolute, i.e. starting with '/'
        if (!path.startsWith(FileSystem.SEPARATOR)) {
            throw new FileSystemException("not an absolute path: " + path);
        }

        // trailing '/' is not allowed (except for root path)
        if (path.endsWith(FileSystem.SEPARATOR) && path.length() > 1) {
            throw new FileSystemException("malformed path: " + path);
        }

        String[] names = path.split(FileSystem.SEPARATOR);
        for (int i = 1; i < names.length; i++) {
            // name must not be empty
            if (names[i].length() == 0) {
                throw new FileSystemException("empty name: " + path);
            }
            // leading/trailing whitespace is not allowed
            String trimmed = names[i].trim();
            if (!trimmed.equals(names[i])) {
                throw new FileSystemException("illegal leading or trailing whitespace in name: " + path);
            }
        }
    }

    /**
     * Returns the parent directory of the specified <code>path</code>.
     *
     * @param path a file system path denoting a directory or a file.
     * @return the parent directory.
     */
    public static String getParentDir(String path) {
        int pos = path.lastIndexOf(FileSystem.SEPARATOR_CHAR);
        if (pos > 0) {
            return path.substring(0, pos);
        }
        return FileSystem.SEPARATOR;
    }

    /**
     * Returns the name of the specified <code>path</code>.
     *
     * @param path a file system path denoting a directory or a file.
     * @return the name.
     */
    public static String getName(String path) {
        int pos = path.lastIndexOf(FileSystem.SEPARATOR_CHAR);
        if (pos != -1) {
            return path.substring(pos + 1);
        }
        return path;
    }

}
