/*
 * The MIT License
 *
 * Copyright 2013 Jesse Glick.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package jenkins.util;

import hudson.FilePath;
import hudson.model.DirectoryBrowserSupport;
import hudson.remoting.Callable;
import hudson.remoting.Channel;
import hudson.remoting.VirtualChannel;
import hudson.util.DirScanner;
import hudson.util.FileVisitor;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.LinkOption;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nonnull;

import jenkins.MasterToSlaveFileCallable;

/**
 * Abstraction over {@link File}, {@link FilePath}, or other items such as network resources or ZIP entries.
 * Assumed to be read-only and makes very limited assumptions, just enough to display content and traverse directories.
 *
 * <p>
 * To obtain a {@link VirtualFile} representation for an existing file, use {@link #forFile(File)} or {@link FilePath#toVirtualFile()}
 *
 * <h2>How are VirtualFile and FilePath different?</h2>
 * <p>
 * FilePath abstracts away {@link File}s on machines that are connected over {@link Channel}, whereas
 * {@link VirtualFile} makes no assumption about where the actual files are, or whether there really exists
 * {@link File}s somewhere. This makes VirtualFile more abstract.
 *
 * @see DirectoryBrowserSupport
 * @see FilePath
 * @since 1.532
 */
public abstract class VirtualFile implements Comparable<VirtualFile>, Serializable {
    
    /**
     * Gets the base name, meaning just the last portion of the path name without any
     * directories.
     *
     * For a “root directory” this may be the empty string.
     * @return a simple name (no slashes)
     */
    public abstract @Nonnull String getName();

    /**
     * Gets a URI.
     * Should at least uniquely identify this virtual file within its root, but not necessarily globally.
     * @return a URI (need not be absolute)
     */
    public abstract URI toURI();

    /**
     * Gets the parent file.
     * Need only operate within the originally given root.
     * @return the parent
     */
    public abstract VirtualFile getParent();

    /**
     * Checks whether this file exists and is a directory.
     * @return true if it is a directory, false if a file or nonexistent
     * @throws IOException in case checking status failed
     */
    public abstract boolean isDirectory() throws IOException;

    /**
     * Checks whether this file exists and is a plain file.
     * @return true if it is a file, false if a directory or nonexistent
     * @throws IOException in case checking status failed
     */
    public abstract boolean isFile() throws IOException;

    /**
     * Checks whether this file exists.
     * @return true if it is a plain file or directory, false if nonexistent
     * @throws IOException in case checking status failed
     */
    public abstract boolean exists() throws IOException;

    /**
     * Lists children of this directory.
     * @return a list of children (files and subdirectories); empty for a file or nonexistent directory
     * @throws IOException if this directory exists but listing was not possible for some other reason
     */
    public abstract @Nonnull VirtualFile[] list() throws IOException;

    /**
     * Lists recursive files of this directory with pattern matching.
     * @param glob an Ant-style glob
     * @return a list of relative names of children (files directly inside or in subdirectories)
     * @throws IOException if this is not a directory, or listing was not possible for some other reason
     */
    public abstract @Nonnull String[] list(String glob) throws IOException;

    /**
     * Obtains a child file.
     * @param name a relative path, possibly including {@code /} (but not {@code ..})
     * @return a representation of that child, whether it actually exists or not
     */
    public abstract @Nonnull VirtualFile child(@Nonnull String name);

    /**
     * Gets the file length.
     * @return a length, or 0 if inapplicable (e.g. a directory)
     * @throws IOException if checking the length failed
     */
    public abstract long length() throws IOException;

    /**
     * Gets the file timestamp.
     * @return a length, or 0 if inapplicable
     * @throws IOException if checking the timestamp failed
     */
    public abstract long lastModified() throws IOException;

    /**
     * Checks whether this file can be read.
     * @return true normally
     * @throws IOException if checking status failed
     */
    public abstract boolean canRead() throws IOException;

    /**
     * Opens an input stream on the file so its contents can be read.
     * @return an open stream
     * @throws IOException if it could not be opened
     */
    public abstract InputStream open() throws IOException;

    /**
     * Does case-insensitive comparison.
     * {@inheritDoc}
     */
    @Override public final int compareTo(VirtualFile o) {
        return getName().compareToIgnoreCase(o.getName());
    }

    /**
     * Compares according to {@link #toURI}.
     * {@inheritDoc}
     */
    @Override public final boolean equals(Object obj) {
        return obj instanceof VirtualFile && toURI().equals(((VirtualFile) obj).toURI());
    }

    /**
     * Hashes according to {@link #toURI}.
     * {@inheritDoc}
     */
    @Override public final int hashCode() {
        return toURI().hashCode();
    }

    /**
     * Displays {@link #toURI}.
     * {@inheritDoc}
     */
    @Override public final String toString() {
        return toURI().toString();
    }

    /**
     * Does some calculations in batch.
     * For a remote file, this can be much faster than doing the corresponding operations one by one as separate requests.
     * The default implementation just calls the block directly.
     * @param <V> a value type
     * @param callable something to run all at once (only helpful if any mentioned files are on the same system)
     * @return the callable result
     * @throws IOException if remote communication failed
     * @since 1.554
     */
    public <V> V run(Callable<V,IOException> callable) throws IOException {
        return callable.call();
    }

    /**
     * Creates a virtual file wrapper for a local file.
     * @param f a disk file (need not exist)
     * @return a wrapper
     */
    public static VirtualFile forFile(final File f) {
        return new FileVF(f, f);
    }
    private static final class FileVF extends VirtualFile {
        private final File f;
        private final File root;
        FileVF(File f, File root) {
            this.f = f;
            this.root = root;
        }
            @Override public String getName() {
                return f.getName();
            }
            @Override public URI toURI() {
                return f.toURI();
            }
            @Override public VirtualFile getParent() {
                return new FileVF(f.getParentFile(), root);
            }
            @Override public boolean isDirectory() throws IOException {
                if (isIllegalSymlink()) {
                    return false;
                }
                return f.isDirectory();
            }
            @Override public boolean isFile() throws IOException {
                if (isIllegalSymlink()) {
                    return false;
                }
                return f.isFile();
            }
            @Override public boolean exists() throws IOException {
                if (isIllegalSymlink()) {
                    return false;
                }
                return f.exists();
            }
            @Override public VirtualFile[] list() throws IOException {
                if (isIllegalSymlink()) {
                    return new VirtualFile[0];
                }
                File[] kids = f.listFiles();
                if (kids == null) {
                    return new VirtualFile[0];
                }
                VirtualFile[] vfs = new VirtualFile[kids.length];
                for (int i = 0; i < kids.length; i++) {
                    vfs[i] = new FileVF(kids[i], root);
                }
                return vfs;
            }
            @Override public String[] list(String glob) throws IOException {
                if (isIllegalSymlink()) {
                    return new String[0];
                }
                return new Scanner(glob).invoke(f, null);
            }
            @Override public VirtualFile child(String name) {
                return new FileVF(new File(f, name), root);
            }
            @Override public long length() throws IOException {
                if (isIllegalSymlink()) {
                    return 0;
                }
                return f.length();
            }
            @Override public long lastModified() throws IOException {
                if (isIllegalSymlink()) {
                    return 0;
                }
                return f.lastModified();
            }
            @Override public boolean canRead() throws IOException {
                if (isIllegalSymlink()) {
                    return false;
                }
                return f.canRead();
            }
            @Override public InputStream open() throws IOException {
                if (isIllegalSymlink()) {
                    throw new FileNotFoundException(f.getPath());
                }
                try {
                    return Files.newInputStream(f.toPath());
                } catch (InvalidPathException e) {
                    throw new IOException(e);
                }
            }
        private boolean isIllegalSymlink() { // TODO JENKINS-26838
            try {
                String myPath = f.toPath().toRealPath(new LinkOption[0]).toString();
                String rootPath = root.toPath().toRealPath(new LinkOption[0]).toString();
                if (!myPath.equals(rootPath) && !myPath.startsWith(rootPath + File.separatorChar)) {
                    return true;
                }
            } catch (IOException x) {
                Logger.getLogger(VirtualFile.class.getName()).log(Level.FINE, "could not determine symlink status of " + f, x);
            } catch (InvalidPathException x2) {
                // if this cannot be converted to a path, it cannot be an illegal symlink, as it cannot exist
                Logger.getLogger(VirtualFile.class.getName()).log(Level.FINE, "Could not convert " + f + " to path", x2);
            }
            return false;
        }
    }

    /**
     * Creates a virtual file wrapper for a remotable file.
     * @param f a local or remote file (need not exist)
     * @return a wrapper
     */
    public static VirtualFile forFilePath(final FilePath f) {
        return new FilePathVF(f);
    }
    private static final class FilePathVF extends VirtualFile {
        private final FilePath f;
        FilePathVF(FilePath f) {
            this.f = f;
        }
            @Override public String getName() {
                return f.getName();
            }
            @Override public URI toURI() {
                try {
                    return f.toURI();
                } catch (Exception x) {
                    return URI.create(f.getRemote());
                }
            }
            @Override public VirtualFile getParent() {
                return f.getParent().toVirtualFile();
            }
            @Override public boolean isDirectory() throws IOException {
                try {
                    return f.isDirectory();
                } catch (InterruptedException x) {
                    throw (IOException) new IOException(x.toString()).initCause(x);
                }
            }
            @Override public boolean isFile() throws IOException {
                // TODO should probably introduce a method for this purpose
                return exists() && !isDirectory();
            }
            @Override public boolean exists() throws IOException {
                try {
                    return f.exists();
                } catch (InterruptedException x) {
                    throw (IOException) new IOException(x.toString()).initCause(x);
                }
            }
            @Override public VirtualFile[] list() throws IOException {
                try {
                    List<FilePath> kids = f.list();
                    if (kids == null) {
                        return new VirtualFile[0];
                    }
                    VirtualFile[] vfs = new VirtualFile[kids.size()];
                    for (int i = 0; i < vfs.length; i++) {
                        vfs[i] = forFilePath(kids.get(i));
                    }
                    return vfs;
                } catch (InterruptedException x) {
                    throw (IOException) new IOException(x.toString()).initCause(x);
                }
            }
            @Override public String[] list(String glob) throws IOException {
                try {
                    return f.act(new Scanner(glob));
                } catch (InterruptedException x) {
                    throw (IOException) new IOException(x.toString()).initCause(x);
                }
            }
            @Override public VirtualFile child(String name) {
                return forFilePath(f.child(name));
            }
            @Override public long length() throws IOException {
                try {
                    return f.length();
                } catch (InterruptedException x) {
                    throw (IOException) new IOException(x.toString()).initCause(x);
                }
            }
            @Override public long lastModified() throws IOException {
                try {
                    return f.lastModified();
                } catch (InterruptedException x) {
                    throw (IOException) new IOException(x.toString()).initCause(x);
                }
            }
            @Override public boolean canRead() throws IOException {
                try {
                    return f.act(new Readable());
                } catch (InterruptedException x) {
                    throw (IOException) new IOException(x.toString()).initCause(x);
                }
            }
            @Override public InputStream open() throws IOException {
                try {
                    return f.read();
                } catch (InterruptedException x) {
                    throw (IOException) new IOException(x.toString()).initCause(x);
                }
            }
            @Override public <V> V run(Callable<V,IOException> callable) throws IOException {
                try {
                    return f.act(callable);
                } catch (InterruptedException x) {
                    throw (IOException) new IOException(x.toString()).initCause(x);
                }
            }
    }
    private static final class Scanner extends MasterToSlaveFileCallable<String[]> {
        private final String glob;
        Scanner(String glob) {
            this.glob = glob;
        }
        @Override public String[] invoke(File f, VirtualChannel channel) throws IOException {
            final List<String> paths = new ArrayList<String>();
            new DirScanner.Glob(glob, null).scan(f, new FileVisitor() {
                @Override
                public void visit(File f, String relativePath) throws IOException {
                    paths.add(relativePath);
                }
            });
            return paths.toArray(new String[paths.size()]);
        }

    }
    private static final class Readable extends MasterToSlaveFileCallable<Boolean> {
        @Override public Boolean invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
            return f.canRead();
        }
    }

}
