/*
 * 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.ignite.igfs;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.apache.ignite.binary.BinaryObjectException;
import org.apache.ignite.binary.BinaryRawReader;
import org.apache.ignite.binary.BinaryRawWriter;
import org.apache.ignite.binary.BinaryReader;
import org.apache.ignite.binary.BinaryWriter;
import org.apache.ignite.binary.Binarylizable;
import org.apache.ignite.internal.util.io.GridFilenameUtils;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.internal.A;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.jetbrains.annotations.Nullable;

/**
 * {@code IGFS} path to file in the file system. For example, to get information about
 * a file you would use the following code:
 * <pre name="code" class="java">
 *     IgfsPath dirPath = new IgfsPath("/my/working/dir");
 *     IgfsPath filePath = new IgfsPath(dirPath, "file.txt");
 *
 *     // Get metadata about file.
 *     IgfsFile file = igfs.info(filePath);
 * </pre>
 */
public final class IgfsPath implements Comparable<IgfsPath>, Externalizable, Binarylizable {
    /** */
    private static final long serialVersionUID = 0L;

    /** The directory separator character. */
    private static final char SLASH_CHAR = '/';

    /** The directory separator. */
    public static final String SLASH = "/";

    /** URI representing this path. Should never change after object creation or de-serialization. */
    private String path;

    /** Root path. */
    public static final IgfsPath ROOT = new IgfsPath(SLASH);

    /**
     * Default constructor.
     */
    public IgfsPath() {
        path = SLASH;
    }

    /**
     * Constructs a path from an URI
     *
     * @param uri URI to create path from.
     */
    public IgfsPath(URI uri) {
        A.notNull(uri, "uri");

        path = normalizePath(uri.getPath());
    }

    /**
     * Constructs a path from the URI string.
     *
     * @param path URI string.
     */
    public IgfsPath(String path) {
        A.ensure(!F.isEmpty(path), "'path' is null or empty");

        this.path = normalizePath(path);
    }

    /**
     * Resolve a child path against a parent path.
     *
     * @param parentPath Parent path.
     * @param childPath Child path.
     */
    public IgfsPath(IgfsPath parentPath, String childPath) {
        A.notNull(parentPath, "parentPath");

        String path = GridFilenameUtils.concat(parentPath.path, childPath);

        if (F.isEmpty(path))
            throw new IllegalArgumentException("Failed to parse path" +
                " [parent=" + parentPath + ", childPath=" + childPath + ']');

        this.path = normalizePath(path);
    }

    /**
     * Initialize path with (1) not-null, (2) normalized, (3) absolute and (4) unix-format path component.
     *
     * @param path Path.
     * @return Normalized path.
     */
    private static String normalizePath(String path) {
        assert path != null;

        String normalizedPath = GridFilenameUtils.normalizeNoEndSeparator(path, true);

        if (F.isEmpty(normalizedPath))
            throw new IllegalArgumentException("Failed to normalize path: " + path);

        if (!SLASH.equals(GridFilenameUtils.getPrefix(normalizedPath)))
            throw new IllegalArgumentException("Path should be absolute: " + path);

        assert !normalizedPath.isEmpty() : "Expects normalized path is not empty.";
        assert normalizedPath.length() == 1 || !normalizedPath.endsWith(SLASH) :
            "Expects normalized path is root or don't ends with '/' symbol.";

        return normalizedPath;
    }

    /**
     * Returns the final component of this path.
     *
     * @return The final component of this path.
     */
    public String name() {
        return GridFilenameUtils.getName(path);
    }

    /**
     * Split full path on components.
     *
     * @return Path components.
     */
    public List<String> components() {
        String path = this.path;

        assert path.length() >= 1 : "Path expected to be absolute: " + path;

        // Path is short-living object, so we don't need to cache component's resolution result.
        return path.length() == 1 ? Collections.<String>emptyList() : Arrays.asList(path.substring(1).split(SLASH));
    }

    /**
     * Get components in array form.
     *
     * @return Components array.
     */
    public String[] componentsArray() {
        return path.length() == 1 ? new String[0] : path.substring(1).split(SLASH);
    }

    /**
     * Returns the parent of a path or {@code null} if at root.
     *
     * @return The parent of a path or {@code null} if at root.
     */
    @Nullable public IgfsPath parent() {
        String path = this.path;

        if (path.length() == 1)
            return null; // Current path is root.

        path = GridFilenameUtils.getFullPathNoEndSeparator(path);

        return new IgfsPath(path);
    }

    /**
     * Adds a suffix to the final name in the path.
     *
     * @param suffix Suffix.
     * @return Path with suffix.
     */
    public IgfsPath suffix(String suffix) {
        A.ensure(!F.isEmpty(suffix), "'suffix' is null or empty.");
        A.ensure(!suffix.contains(SLASH), "'suffix' contains file's separator '" + SLASH + "'");

        return new IgfsPath(path + suffix);
    }

    /**
     * Return the number of elements in this path.
     *
     * @return The number of elements in this path, zero depth means root directory.
     */
    public int depth() {
        final String path = this.path;
        final int size = path.length();

        assert size >= 1 && path.charAt(0) == SLASH_CHAR : "Expects absolute path: " + path;

        if (size == 1)
            return 0;

        int depth = 1;

        // Ignore the first character.
        for (int i = 1; i < size; i++)
            if (path.charAt(i) == SLASH_CHAR)
                depth++;

        return depth;
    }

    /**
     * Checks whether this path is a sub-directory of argument.
     *
     * @param path Path to check.
     * @return {@code True} if argument is same or a sub-directory of this object.
     */
    public boolean isSubDirectoryOf(IgfsPath path) {
        A.notNull(path, "path");

        return this.path.startsWith(path.path.endsWith(SLASH) ? path.path : path.path + SLASH);
    }

    /** {@inheritDoc} */
    @SuppressWarnings("NullableProblems")
    @Override public int compareTo(IgfsPath o) {
        return path.compareTo(o.path);
    }

    /** {@inheritDoc} */
    @Override public void writeExternal(ObjectOutput out) throws IOException {
        U.writeString(out, path);
    }

    /** {@inheritDoc} */
    @Override public void readExternal(ObjectInput in) throws IOException {
        path = U.readString(in);
    }

    /** {@inheritDoc} */
    @Override public void writeBinary(BinaryWriter writer) throws BinaryObjectException {
        writeRawBinary(writer.rawWriter());
    }

    /** {@inheritDoc} */
    @Override public void readBinary(BinaryReader reader) throws BinaryObjectException {
        readRawBinary(reader.rawReader());
    }

    /**
     * Write raw binary.
     *
     * @param writer Raw writer.
     * @throws BinaryObjectException If failed.
     */
    public void writeRawBinary(BinaryRawWriter writer) throws BinaryObjectException {
        writer.writeString(path);
    }

    /**
     * Read raw binary.
     *
     * @param reader Raw reader.
     * @throws BinaryObjectException If failed.
     */
    public void readRawBinary(BinaryRawReader reader) throws BinaryObjectException {
        path = reader.readString();
    }

    /** {@inheritDoc} */
    @Override public int hashCode() {
        return path.hashCode();
    }

    /** {@inheritDoc} */
    @Override public boolean equals(Object o) {
        return o == this || o != null && getClass() == o.getClass() && path.equals(((IgfsPath)o).path);
    }

    /** {@inheritDoc} */
    @Override public String toString() {
        return path;
    }
}