/*
 * Copyright (c) 2014, 2022, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package jdk.internal.jrtfs;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.NonWritableChannelException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.ClosedFileSystemException;
import java.nio.file.CopyOption;
import java.nio.file.DirectoryStream;
import java.nio.file.FileStore;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemException;
import java.nio.file.InvalidPathException;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
import java.nio.file.NotDirectoryException;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.ReadOnlyFileSystemException;
import java.nio.file.StandardOpenOption;
import java.nio.file.WatchService;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.FileTime;
import java.nio.file.attribute.UserPrincipalLookupService;
import java.nio.file.spi.FileSystemProvider;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
import jdk.internal.jimage.ImageReader.Node;
import static java.util.stream.Collectors.toList;

/**
 * jrt file system implementation built on System jimage files.
 *
 * @implNote This class needs to maintain JDK 8 source compatibility.
 *
 * It is used internally in the JDK to implement jimage/jrtfs access,
 * but also compiled and delivered as part of the jrtfs.jar to support access
 * to the jimage file provided by the shipped JDK by tools running on JDK 8.
 */
class JrtFileSystem extends FileSystem {

    private final JrtFileSystemProvider provider;
    private final JrtPath rootPath = new JrtPath(this, "/");
    private volatile boolean isOpen;
    private volatile boolean isClosable;
    private SystemImage image;

    JrtFileSystem(JrtFileSystemProvider provider, Map<String, ?> env)
            throws IOException
    {
        this.provider = provider;
        this.image = SystemImage.open();  // open image file
        this.isOpen = true;
        this.isClosable = env != null;
    }

    // FileSystem method implementations
    @Override
    public boolean isOpen() {
        return isOpen;
    }

    @Override
    public void close() throws IOException {
        if (!isClosable)
            throw new UnsupportedOperationException();
        cleanup();
    }

    @Override
    public FileSystemProvider provider() {
        return provider;
    }

    @Override
    public Iterable<Path> getRootDirectories() {
        return Collections.singleton(getRootPath());
    }

    @Override
    public JrtPath getPath(String first, String... more) {
        if (more.length == 0) {
            return new JrtPath(this, first);
        }
        StringBuilder sb = new StringBuilder();
        sb.append(first);
        for (String path : more) {
            if (!path.isEmpty()) {
                if (sb.length() > 0) {
                    sb.append('/');
                }
                sb.append(path);
            }
        }
        return new JrtPath(this, sb.toString());
    }

    @Override
    public final boolean isReadOnly() {
        return true;
    }

    @Override
    public final UserPrincipalLookupService getUserPrincipalLookupService() {
        throw new UnsupportedOperationException();
    }

    @Override
    public final WatchService newWatchService() {
        throw new UnsupportedOperationException();
    }

    @Override
    public final Iterable<FileStore> getFileStores() {
        return Collections.singleton(getFileStore(getRootPath()));
    }

    private static final Set<String> supportedFileAttributeViews
            = Collections.unmodifiableSet(
                    new HashSet<String>(Arrays.asList("basic", "jrt")));

    @Override
    public final Set<String> supportedFileAttributeViews() {
        return supportedFileAttributeViews;
    }

    @Override
    public final String toString() {
        return "jrt:/";
    }

    @Override
    public final String getSeparator() {
        return "/";
    }

    @Override
    public PathMatcher getPathMatcher(String syntaxAndInput) {
        int pos = syntaxAndInput.indexOf(':');
        if (pos <= 0) {
            throw new IllegalArgumentException();
        }
        String syntax = syntaxAndInput.substring(0, pos);
        String input = syntaxAndInput.substring(pos + 1);
        String expr;
        if (syntax.equalsIgnoreCase("glob")) {
            expr = JrtUtils.toRegexPattern(input);
        } else if (syntax.equalsIgnoreCase("regex")) {
            expr = input;
        } else {
                throw new UnsupportedOperationException("Syntax '" + syntax
                        + "' not recognized");
        }
        // return matcher
        final Pattern pattern = Pattern.compile(expr);
        return (Path path) -> pattern.matcher(path.toString()).matches();
    }

    JrtPath resolveLink(JrtPath path) throws IOException {
        Node node = checkNode(path);
        if (node.isLink()) {
            node = node.resolveLink();
            return new JrtPath(this, node.getName());  // TBD, normalized?
        }
        return path;
    }

    JrtFileAttributes getFileAttributes(JrtPath path, LinkOption... options)
            throws IOException {
        Node node = checkNode(path);
        if (node.isLink() && followLinks(options)) {
            return new JrtFileAttributes(node.resolveLink(true));
        }
        return new JrtFileAttributes(node);
    }

    /**
     * returns the list of child paths of the given directory "path"
     *
     * @param path name of the directory whose content is listed
     * @return iterator for child paths of the given directory path
     */
    Iterator<Path> iteratorOf(JrtPath path, DirectoryStream.Filter<? super Path> filter)
            throws IOException {
        Node node = checkNode(path).resolveLink(true);
        if (!node.isDirectory()) {
            throw new NotDirectoryException(path.getName());
        }
        if (filter == null) {
            return node.getChildren()
                       .stream()
                       .map(child -> (Path)(path.resolve(new JrtPath(this, child.getNameString()).getFileName())))
                       .iterator();
        }
        return node.getChildren()
                   .stream()
                   .map(child -> (Path)(path.resolve(new JrtPath(this, child.getNameString()).getFileName())))
                   .filter(p ->  { try { return filter.accept(p);
                                   } catch (IOException x) {}
                                   return false;
                                  })
                   .iterator();
    }

    // returns the content of the file resource specified by the path
    byte[] getFileContent(JrtPath path) throws IOException {
        Node node = checkNode(path);
        if (node.isDirectory()) {
            throw new FileSystemException(path + " is a directory");
        }
        //assert node.isResource() : "resource node expected here";
        return image.getResource(node);
    }

    /////////////// Implementation details below this point //////////

    // static utility methods
    static ReadOnlyFileSystemException readOnly() {
        return new ReadOnlyFileSystemException();
    }

    // do the supplied options imply that we have to chase symlinks?
    static boolean followLinks(LinkOption... options) {
        if (options != null) {
            for (LinkOption lo : options) {
                Objects.requireNonNull(lo);
                if (lo == LinkOption.NOFOLLOW_LINKS) {
                    return false;
                } else {
                    throw new AssertionError("should not reach here");
                }
            }
        }
        return true;
    }

    // check that the options passed are supported by (read-only) jrt file system
    static void checkOptions(Set<? extends OpenOption> options) {
        // check for options of null type and option is an instance of StandardOpenOption
        for (OpenOption option : options) {
            Objects.requireNonNull(option);
            if (!(option instanceof StandardOpenOption)) {
                throw new IllegalArgumentException(
                    "option class: " + option.getClass());
            }
        }
        if (options.contains(StandardOpenOption.WRITE) ||
            options.contains(StandardOpenOption.APPEND)) {
            throw readOnly();
        }
    }

    // clean up this file system - called from finalize and close
    synchronized void cleanup() throws IOException {
        if (isOpen) {
            isOpen = false;
            image.close();
            image = null;
        }
    }

    // These methods throw read only file system exception
    final void setTimes(JrtPath jrtPath, FileTime mtime, FileTime atime, FileTime ctime)
            throws IOException {
        throw readOnly();
    }

    // These methods throw read only file system exception
    final void createDirectory(JrtPath jrtPath, FileAttribute<?>... attrs) throws IOException {
        throw readOnly();
    }

    final void deleteFile(JrtPath jrtPath, boolean failIfNotExists)
            throws IOException {
        throw readOnly();
    }

    final OutputStream newOutputStream(JrtPath jrtPath, OpenOption... options)
            throws IOException {
        throw readOnly();
    }

    final void copyFile(boolean deletesrc, JrtPath srcPath, JrtPath dstPath, CopyOption... options)
            throws IOException {
        throw readOnly();
    }

    final FileChannel newFileChannel(JrtPath path,
            Set<? extends OpenOption> options,
            FileAttribute<?>... attrs)
            throws IOException {
        throw new UnsupportedOperationException("newFileChannel");
    }

    final InputStream newInputStream(JrtPath path) throws IOException {
        return new ByteArrayInputStream(getFileContent(path));
    }

    final SeekableByteChannel newByteChannel(JrtPath path,
            Set<? extends OpenOption> options,
            FileAttribute<?>... attrs)
            throws IOException {
        checkOptions(options);

        byte[] buf = getFileContent(path);
        final ReadableByteChannel rbc
                = Channels.newChannel(new ByteArrayInputStream(buf));
        final long size = buf.length;
        return new SeekableByteChannel() {
            long read = 0;

            @Override
            public boolean isOpen() {
                return rbc.isOpen();
            }

            @Override
            public long position() throws IOException {
                return read;
            }

            @Override
            public SeekableByteChannel position(long pos)
                    throws IOException {
                throw new UnsupportedOperationException();
            }

            @Override
            public int read(ByteBuffer dst) throws IOException {
                int n = rbc.read(dst);
                if (n > 0) {
                    read += n;
                }
                return n;
            }

            @Override
            public SeekableByteChannel truncate(long size)
                    throws IOException {
                throw new NonWritableChannelException();
            }

            @Override
            public int write(ByteBuffer src) throws IOException {
                throw new NonWritableChannelException();
            }

            @Override
            public long size() throws IOException {
                return size;
            }

            @Override
            public void close() throws IOException {
                rbc.close();
            }
        };
    }

    final JrtFileStore getFileStore(JrtPath path) {
        return new JrtFileStore(path);
    }

    final void ensureOpen() throws IOException {
        if (!isOpen()) {
            throw new ClosedFileSystemException();
        }
    }

    final JrtPath getRootPath() {
        return rootPath;
    }

    boolean isSameFile(JrtPath path1, JrtPath path2) throws IOException {
        return checkNode(path1) == checkNode(path2);
    }

    boolean isLink(JrtPath path) throws IOException {
        return checkNode(path).isLink();
    }

    boolean exists(JrtPath path) throws IOException {
        try {
            checkNode(path);
        } catch (NoSuchFileException exp) {
            return false;
        }
        return true;
    }

    boolean isDirectory(JrtPath path, boolean resolveLinks)
            throws IOException {
        Node node = checkNode(path);
        return resolveLinks && node.isLink()
                ? node.resolveLink(true).isDirectory()
                : node.isDirectory();
    }

    JrtPath toRealPath(JrtPath path, LinkOption... options)
            throws IOException {
        Node node = checkNode(path);
        if (followLinks(options) && node.isLink()) {
            node = node.resolveLink();
        }
        // image node holds the real/absolute path name
        return new JrtPath(this, node.getName(), true);
    }

    private Node lookup(String path) {
        try {
            return image.findNode(path);
        } catch (RuntimeException | IOException ex) {
            throw new InvalidPathException(path, ex.toString());
        }
    }

    private Node lookupSymbolic(String path) {
        int i = 1;
        while (i < path.length()) {
            i = path.indexOf('/', i);
            if (i == -1) {
                break;
            }
            String prefix = path.substring(0, i);
            Node node = lookup(prefix);
            if (node == null) {
                break;
            }
            if (node.isLink()) {
                Node link = node.resolveLink(true);
                // resolved symbolic path concatenated to the rest of the path
                String resPath = link.getName() + path.substring(i);
                node = lookup(resPath);
                return node != null ? node : lookupSymbolic(resPath);
            }
            i++;
        }
        return null;
    }

    Node checkNode(JrtPath path) throws IOException {
        ensureOpen();
        String p = path.getResolvedPath();
        Node node = lookup(p);
        if (node == null) {
            node = lookupSymbolic(p);
            if (node == null) {
                throw new NoSuchFileException(p);
            }
        }
        return node;
    }
}
