/*
 * 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.commons.vfs2.provider;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.vfs2.FileName;
import org.apache.commons.vfs2.FileSystemException;
import org.apache.commons.vfs2.FileType;
import org.apache.commons.vfs2.NameScope;
import org.apache.commons.vfs2.VFS;

/**
 * A default file name implementation.
 */
public abstract class AbstractFileName implements FileName {

    // URI Characters that are possible in local file names, but must be escaped
    // for proper URI handling.
    //
    // How reserved URI chars were selected:
    //
    // URIs can contain :, /, ?, #, @
    // See https://docs.oracle.com/javase/8/docs/api/java/net/URI.html
    // https://datatracker.ietf.org/doc/html/rfc3986#section-2.2
    //
    // Since : and / occur before the path, only chars after path are escaped (i.e., # and ?)
    // ? is a reserved filesystem character for Windows and Unix, so can't be part of a file name.
    // Therefore only # is a reserved char in a URI as part of the path that can be in the file name.
    private static final char[] RESERVED_URI_CHARS = {'#', ' '};

    private final String scheme;
    private final String absPath;
    private FileType type;

    // Cached attributes
    private String uriString;
    private String baseName;
    private String rootUri;
    private String extension;
    private String decodedAbsPath;

    private String key;

    /**
     * Constructs a new instance.
     *
     * @param scheme The scheme.
     * @param absolutePath the absolute path, maybe empty or null.
     * @param type the file type.
     */
    public AbstractFileName(final String scheme, final String absolutePath, final FileType type) {
        this.rootUri = null;
        this.scheme = scheme;
        this.type = type;
        if (StringUtils.isEmpty(absolutePath)) {
            this.absPath = ROOT_PATH;
        } else if (absolutePath.length() > 1 && absolutePath.endsWith("/")) {
            this.absPath = absolutePath.substring(0, absolutePath.length() - 1);
        } else {
            this.absPath = absolutePath;
        }
    }

    /**
     * Checks whether a path fits in a particular scope of another path.
     *
     * @param basePath An absolute, normalised path.
     * @param path An absolute, normalised path.
     * @param scope The NameScope.
     * @return true if the path fits in the scope, false otherwise.
     */
    public static boolean checkName(final String basePath, final String path, final NameScope scope) {
        if (scope == NameScope.FILE_SYSTEM) {
            // All good
            return true;
        }

        if (!path.startsWith(basePath)) {
            return false;
        }

        int baseLen = basePath.length();
        if (VFS.isUriStyle()) {
            // strip the trailing "/"
            baseLen--;
        }

        if (scope == NameScope.CHILD) {
            return path.length() != baseLen && (baseLen <= 1 || path.charAt(baseLen) == SEPARATOR_CHAR)
                    && path.indexOf(SEPARATOR_CHAR, baseLen + 1) == -1;
        }
        if (scope == NameScope.DESCENDENT) {
            return path.length() != baseLen && (baseLen <= 1 || path.charAt(baseLen) == SEPARATOR_CHAR);
        }
        if (scope == NameScope.DESCENDENT_OR_SELF) {
            return baseLen <= 1 || path.length() <= baseLen || path.charAt(baseLen) == SEPARATOR_CHAR;
        }
        throw new IllegalArgumentException();
    }

    /**
     * Builds the root URI for this file name. Note that the root URI must not end with a separator character.
     *
     * @param buffer A StringBuilder to use to construct the URI.
     * @param addPassword true if the password should be added, false otherwise.
     */
    protected abstract void appendRootUri(StringBuilder buffer, boolean addPassword);

    /**
     * Implement Comparable.
     *
     * @param obj another abstract file name
     * @return negative number if less than, 0 if equal, positive if greater than.
     */
    @Override
    public int compareTo(final FileName obj) {
        final AbstractFileName name = (AbstractFileName) obj;
        return getKey().compareTo(name.getKey());
    }

    /**
     * Factory method for creating name instances.
     *
     * @param absolutePath The absolute path.
     * @param fileType The FileType.
     * @return The FileName.
     */
    public abstract FileName createName(String absolutePath, FileType fileType);

    protected String createURI() {
        return createURI(false, true);
    }

    private String createURI(final boolean useAbsolutePath, final boolean usePassword) {
        final StringBuilder buffer = new StringBuilder();
        appendRootUri(buffer, usePassword);
        buffer.append(handleURISpecialCharacters(useAbsolutePath ? absPath : getPath()));
        return buffer.toString();
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        final AbstractFileName that = (AbstractFileName) o;

        return getKey().equals(that.getKey());
    }

    /**
     * Returns the base name of the file.
     *
     * @return The base name of the file.
     */
    @Override
    public String getBaseName() {
        if (baseName == null) {
            final int idx = getPath().lastIndexOf(SEPARATOR_CHAR);
            if (idx == -1) {
                baseName = getPath();
            } else {
                baseName = getPath().substring(idx + 1);
            }
        }

        return baseName;
    }

    /**
     * Returns the depth of this file name, within its file system.
     *
     * @return The depth of the file name.
     */
    @Override
    public int getDepth() {
        final int len = getPath().length();
        if (len == 0 || len == 1 && getPath().charAt(0) == SEPARATOR_CHAR) {
            return 0;
        }
        int depth = 1;
        for (int pos = 0; pos > -1 && pos < len; depth++) {
            pos = getPath().indexOf(SEPARATOR_CHAR, pos + 1);
        }
        return depth;
    }

    /**
     * Returns the extension of this file name.
     *
     * @return The file extension.
     */
    @Override
    public String getExtension() {
        if (extension == null) {
            getBaseName();
            final int pos = baseName.lastIndexOf('.');
            // if ((pos == -1) || (pos == baseName.length() - 1))
            // imario@ops.co.at: Review of patch from adagoubard@chello.nl
            // do not treat file names like
            // .bashrc c:\windows\.java c:\windows\.javaws c:\windows\.jedit c:\windows\.appletviewer
            // as extension
            if (pos < 1 || pos == baseName.length() - 1) {
                // No extension
                extension = "";
            } else {
                extension = baseName.substring(pos + 1).intern();
            }
        }
        return extension;
    }

    /**
     * Returns the URI without a password.
     *
     * @return Returns the URI without a password.
     */
    @Override
    public String getFriendlyURI() {
        return createURI(false, false);
    }

    /**
     * Create a path that does not use the FileType since that field is not immutable.
     *
     * @return The key.
     */
    private String getKey() {
        if (key == null) {
            key = getURI();
        }
        return key;
    }

    /**
     * Returns the name of the parent of the file.
     *
     * @return the FileName of the parent.
     */
    @Override
    public FileName getParent() {
        final String parentPath;
        final int idx = getPath().lastIndexOf(SEPARATOR_CHAR);
        if (idx == -1 || idx == getPath().length() - 1) {
            // No parent
            return null;
        }
        if (idx == 0) {
            // Root is the parent
            parentPath = SEPARATOR;
        } else {
            parentPath = getPath().substring(0, idx);
        }
        return createName(parentPath, FileType.FOLDER);
    }

    /**
     * Returns the absolute path of the file, relative to the root of the file system that the file belongs to.
     *
     * @return The path String.
     */
    @Override
    public String getPath() {
        if (VFS.isUriStyle()) {
            return absPath + getUriTrailer();
        }
        return absPath;
    }

    /**
     * Returns the decoded path.
     *
     * @return The decoded path String.
     * @throws FileSystemException If an error occurs.
     */
    @Override
    public String getPathDecoded() throws FileSystemException {
        if (decodedAbsPath == null) {
            decodedAbsPath = UriParser.decode(getPath());
        }

        return decodedAbsPath;
    }

    /**
     * Converts a file name to a relative name, relative to this file name.
     *
     * @param name The FileName.
     * @return The relative path to the file.
     * @throws FileSystemException if an error occurs.
     */
    @Override
    public String getRelativeName(final FileName name) throws FileSystemException {
        final String path = name.getPath();

        // Calculate the common prefix
        final int basePathLen = getPath().length();
        final int pathLen = path.length();

        // Deal with root
        if (basePathLen == 1 && pathLen == 1) {
            return ".";
        }
        if (basePathLen == 1) {
            return path.substring(1);
        }

        final int maxlen = Math.min(basePathLen, pathLen);
        int pos = 0;
        while (pos < maxlen && getPath().charAt(pos) == path.charAt(pos)) {
            pos++;
        }

        if (pos == basePathLen && pos == pathLen) {
            // Same names
            return ".";
        }
        if (pos == basePathLen && pos < pathLen && path.charAt(pos) == SEPARATOR_CHAR) {
            // A descendent of the base path
            return path.substring(pos + 1);
        }

        // Strip the common prefix off the path
        final StringBuilder buffer = new StringBuilder();
        if (pathLen > 1 && (pos < pathLen || getPath().charAt(pos) != SEPARATOR_CHAR)) {
            // Not a direct ancestor, need to back up
            pos = getPath().lastIndexOf(SEPARATOR_CHAR, pos);
            buffer.append(path.substring(pos));
        }

        // Prepend a '../' for each element in the base path past the common
        // prefix
        buffer.insert(0, "..");
        pos = getPath().indexOf(SEPARATOR_CHAR, pos + 1);
        while (pos != -1) {
            buffer.insert(0, "../");
            pos = getPath().indexOf(SEPARATOR_CHAR, pos + 1);
        }

        return buffer.toString();
    }

    /**
     * find the root of the file system.
     *
     * @return The root FileName.
     */
    @Override
    public FileName getRoot() {
        FileName root = this;
        while (root.getParent() != null) {
            root = root.getParent();
        }

        return root;
    }

    /**
     * Returns the root URI of the file system this file belongs to.
     *
     * @return The URI of the root.
     */
    @Override
    public String getRootURI() {
        if (rootUri == null) {
            final StringBuilder buffer = new StringBuilder();
            appendRootUri(buffer, true);
            buffer.append(SEPARATOR_CHAR);
            rootUri = buffer.toString().intern();
        }
        return rootUri;
    }

    /**
     * Returns the URI scheme of this file.
     *
     * @return The protocol used to access the file.
     */
    @Override
    public String getScheme() {
        return scheme;
    }

    /**
     * Returns the requested or current type of this name.
     * <p>
     * The "requested" type is the one determined during resolving the name. n this case the name is a
     * {@link FileType#FOLDER} if it ends with an "/" else it will be a {@link FileType#FILE}.
     * </p>
     * <p>
     * Once attached it will be changed to reflect the real type of this resource.
     * </p>
     *
     * @return {@link FileType#FOLDER} or {@link FileType#FILE}
     */
    @Override
    public FileType getType() {
        return type;
    }

    /**
     * Returns the absolute URI of the file.
     *
     * @return The absolute URI of the file.
     */
    @Override
    public String getURI() {
        if (uriString == null) {
            uriString = createURI();
        }
        return uriString;
    }

    protected String getUriTrailer() {
        return getType().hasChildren() ? "/" : "";
    }

    private String handleURISpecialCharacters(String uri) {
        if (!StringUtils.isEmpty(uri)) {
            try {
                // VFS-325: Handle URI special characters in file name
                // Decode the base URI and re-encode with URI special characters
                uri = UriParser.decode(uri);

                return UriParser.encode(uri, RESERVED_URI_CHARS);
            } catch (final FileSystemException e) {
                // Default to base URI value?
                return uri;
            }
        }

        return uri;
    }

    @Override
    public int hashCode() {
        return getKey().hashCode();
    }

    /**
     * Determines if another file name is an ancestor of this file name.
     *
     * @param ancestor The FileName to check.
     * @return true if the FileName is an ancestor, false otherwise.
     */
    @Override
    public boolean isAncestor(final FileName ancestor) {
        if (!ancestor.getRootURI().equals(getRootURI())) {
            return false;
        }
        return checkName(ancestor.getPath(), getPath(), NameScope.DESCENDENT);
    }

    /**
     * Determines if another file name is a descendent of this file name.
     *
     * @param descendent The FileName to check.
     * @return true if the FileName is a descendent, false otherwise.
     */
    @Override
    public boolean isDescendent(final FileName descendent) {
        return isDescendent(descendent, NameScope.DESCENDENT);
    }

    /**
     * Determines if another file name is a descendent of this file name.
     *
     * @param descendent The FileName to check.
     * @param scope The NameScope.
     * @return true if the FileName is a descendent, false otherwise.
     */
    @Override
    public boolean isDescendent(final FileName descendent, final NameScope scope) {
        if (!descendent.getRootURI().equals(getRootURI())) {
            return false;
        }
        return checkName(getPath(), descendent.getPath(), scope);
    }

    /**
     * Checks if this file name is a name for a regular file by using its type.
     *
     * @return true if this file is a regular file.
     * @throws FileSystemException may be thrown by subclasses.
     * @see #getType()
     * @see FileType#FILE
     */
    @Override
    public boolean isFile() throws FileSystemException {
        // Use equals instead of == to avoid any class loader worries.
        return FileType.FILE.equals(this.getType());
    }

    /**
     * Sets the type of this file e.g. when it will be attached.
     *
     * @param type {@link FileType#FOLDER} or {@link FileType#FILE}
     * @throws FileSystemException if an error occurs.
     */
    void setType(final FileType type) throws FileSystemException {
        if (type != FileType.FOLDER && type != FileType.FILE && type != FileType.FILE_OR_FOLDER) {
            throw new FileSystemException("vfs.provider/filename-type.error");
        }

        this.type = type;
    }

    /**
     * Returns the URI of the file.
     *
     * @return the FileName as a URI.
     */
    @Override
    public String toString() {
        return getURI();
    }
}
