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

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;

/**
 * @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.
 */
public final class ImageReader implements AutoCloseable {
    private final SharedImageReader reader;

    private volatile boolean closed;

    private ImageReader(SharedImageReader reader) {
        this.reader = reader;
    }

    public static ImageReader open(Path imagePath, ByteOrder byteOrder) throws IOException {
        Objects.requireNonNull(imagePath);
        Objects.requireNonNull(byteOrder);

        return SharedImageReader.open(imagePath, byteOrder);
    }

    public static ImageReader open(Path imagePath) throws IOException {
        return open(imagePath, ByteOrder.nativeOrder());
    }

    @Override
    public void close() throws IOException {
        if (closed) {
            throw new IOException("image file already closed");
        }
        reader.close(this);
        closed = true;
    }

    private void ensureOpen() throws IOException {
        if (closed) {
            throw new IOException("image file closed");
        }
    }

    private void requireOpen() {
        if (closed) {
            throw new IllegalStateException("image file closed");
        }
    }

    // directory management interface
    public Directory getRootDirectory() throws IOException {
        ensureOpen();
        return reader.getRootDirectory();
    }


    public Node findNode(String name) throws IOException {
        ensureOpen();
        return reader.findNode(name);
    }

    public byte[] getResource(Node node) throws IOException {
        ensureOpen();
        return reader.getResource(node);
    }

    public byte[] getResource(Resource rs) throws IOException {
        ensureOpen();
        return reader.getResource(rs);
    }

    public ImageHeader getHeader() {
        requireOpen();
        return reader.getHeader();
    }

    public static void releaseByteBuffer(ByteBuffer buffer) {
        BasicImageReader.releaseByteBuffer(buffer);
    }

    public String getName() {
        requireOpen();
        return reader.getName();
    }

    public ByteOrder getByteOrder() {
        requireOpen();
        return reader.getByteOrder();
    }

    public Path getImagePath() {
        requireOpen();
        return reader.getImagePath();
    }

    public ImageStringsReader getStrings() {
        requireOpen();
        return reader.getStrings();
    }

    public ImageLocation findLocation(String mn, String rn) {
        requireOpen();
        return reader.findLocation(mn, rn);
    }

    public boolean verifyLocation(String mn, String rn) {
        requireOpen();
        return reader.verifyLocation(mn, rn);
    }

    public ImageLocation findLocation(String name) {
        requireOpen();
        return reader.findLocation(name);
    }

    public String[] getEntryNames() {
        requireOpen();
        return reader.getEntryNames();
    }

    public String[] getModuleNames() {
        requireOpen();
        int off = "/modules/".length();
        return reader.findNode("/modules")
                     .getChildren()
                     .stream()
                     .map(Node::getNameString)
                     .map(s -> s.substring(off, s.length()))
                     .toArray(String[]::new);
    }

    public long[] getAttributes(int offset) {
        requireOpen();
        return reader.getAttributes(offset);
    }

    public String getString(int offset) {
        requireOpen();
        return reader.getString(offset);
    }

    public byte[] getResource(String name) {
        requireOpen();
        return reader.getResource(name);
    }

    public byte[] getResource(ImageLocation loc) {
        requireOpen();
        return reader.getResource(loc);
    }

    public ByteBuffer getResourceBuffer(ImageLocation loc) {
        requireOpen();
        return reader.getResourceBuffer(loc);
    }

    public InputStream getResourceStream(ImageLocation loc) {
        requireOpen();
        return reader.getResourceStream(loc);
    }

    private static final class SharedImageReader extends BasicImageReader {
        static final int SIZE_OF_OFFSET = Integer.BYTES;

        static final Map<Path, SharedImageReader> OPEN_FILES = new HashMap<>();

        // List of openers for this shared image.
        final Set<ImageReader> openers;

        // attributes of the .jimage file. jimage file does not contain
        // attributes for the individual resources (yet). We use attributes
        // of the jimage file itself (creation, modification, access times).
        // Iniitalized lazily, see {@link #imageFileAttributes()}.
        BasicFileAttributes imageFileAttributes;

        // directory management implementation
        final HashMap<String, Node> nodes;
        volatile Directory rootDir;

        Directory packagesDir;
        Directory modulesDir;

        private SharedImageReader(Path imagePath, ByteOrder byteOrder) throws IOException {
            super(imagePath, byteOrder);
            this.openers = new HashSet<>();
            this.nodes = new HashMap<>();
        }

        public static ImageReader open(Path imagePath, ByteOrder byteOrder) throws IOException {
            Objects.requireNonNull(imagePath);
            Objects.requireNonNull(byteOrder);

            synchronized (OPEN_FILES) {
                SharedImageReader reader = OPEN_FILES.get(imagePath);

                if (reader == null) {
                    // Will fail with an IOException if wrong byteOrder.
                    reader =  new SharedImageReader(imagePath, byteOrder);
                    OPEN_FILES.put(imagePath, reader);
                } else if (reader.getByteOrder() != byteOrder) {
                    throw new IOException("\"" + reader.getName() + "\" is not an image file");
                }

                ImageReader image = new ImageReader(reader);
                reader.openers.add(image);

                return image;
            }
        }

        public void close(ImageReader image) throws IOException {
            Objects.requireNonNull(image);

            synchronized (OPEN_FILES) {
                if (!openers.remove(image)) {
                    throw new IOException("image file already closed");
                }

                if (openers.isEmpty()) {
                    close();
                    nodes.clear();
                    rootDir = null;

                    if (!OPEN_FILES.remove(this.getImagePath(), this)) {
                        throw new IOException("image file not found in open list");
                    }
                }
            }
        }

        void addOpener(ImageReader reader) {
            synchronized (OPEN_FILES) {
                openers.add(reader);
            }
        }

        boolean removeOpener(ImageReader reader) {
            synchronized (OPEN_FILES) {
                return openers.remove(reader);
            }
        }

        // directory management interface
        Directory getRootDirectory() {
            return buildRootDirectory();
        }

        /**
         * Lazily build a node from a name.
         */
        synchronized Node buildNode(String name) {
            Node n;
            boolean isPackages = name.startsWith("/packages");
            boolean isModules = !isPackages && name.startsWith("/modules");

            if (!(isModules || isPackages)) {
                return null;
            }

            ImageLocation loc = findLocation(name);

            if (loc != null) { // A sub tree node
                if (isPackages) {
                    n = handlePackages(name, loc);
                } else { // modules sub tree
                    n = handleModulesSubTree(name, loc);
                }
            } else { // Asking for a resource? /modules/java.base/java/lang/Object.class
                if (isModules) {
                    n = handleResource(name);
                } else {
                    // Possibly ask for /packages/java.lang/java.base
                    // although /packages/java.base not created
                    n = handleModuleLink(name);
                }
            }
            return n;
        }

        synchronized Directory buildRootDirectory() {
            Directory root = rootDir; // volatile read
            if (root != null) {
                return root;
            }

            root = newDirectory(null, "/");
            root.setIsRootDir();

            // /packages dir
            packagesDir = newDirectory(root, "/packages");
            packagesDir.setIsPackagesDir();

            // /modules dir
            modulesDir = newDirectory(root, "/modules");
            modulesDir.setIsModulesDir();

            root.setCompleted(true);
            return rootDir = root;
        }

        /**
         * To visit sub tree resources.
         */
        interface LocationVisitor {
            void visit(ImageLocation loc);
        }

        void visitLocation(ImageLocation loc, LocationVisitor visitor) {
            byte[] offsets = getResource(loc);
            ByteBuffer buffer = ByteBuffer.wrap(offsets);
            buffer.order(getByteOrder());
            IntBuffer intBuffer = buffer.asIntBuffer();
            for (int i = 0; i < offsets.length / SIZE_OF_OFFSET; i++) {
                int offset = intBuffer.get(i);
                ImageLocation pkgLoc = getLocation(offset);
                visitor.visit(pkgLoc);
            }
        }

        void visitPackageLocation(ImageLocation loc) {
            // Retrieve package name
            String pkgName = getBaseExt(loc);
            // Content is array of offsets in Strings table
            byte[] stringsOffsets = getResource(loc);
            ByteBuffer buffer = ByteBuffer.wrap(stringsOffsets);
            buffer.order(getByteOrder());
            IntBuffer intBuffer = buffer.asIntBuffer();
            // For each module, create a link node.
            for (int i = 0; i < stringsOffsets.length / SIZE_OF_OFFSET; i++) {
                // skip empty state, useless.
                intBuffer.get(i);
                i++;
                int offset = intBuffer.get(i);
                String moduleName = getString(offset);
                Node targetNode = findNode("/modules/" + moduleName);
                if (targetNode != null) {
                    String pkgDirName = packagesDir.getName() + "/" + pkgName;
                    Directory pkgDir = (Directory) nodes.get(pkgDirName);
                    newLinkNode(pkgDir, pkgDir.getName() + "/" + moduleName, targetNode);
                }
            }
        }

        Node handlePackages(String name, ImageLocation loc) {
            long size = loc.getUncompressedSize();
            Node n = null;
            // Only possibilities are /packages, /packages/package/module
            if (name.equals("/packages")) {
                visitLocation(loc, (childloc) -> {
                    findNode(childloc.getFullName());
                });
                packagesDir.setCompleted(true);
                n = packagesDir;
            } else {
                if (size != 0) { // children are offsets to module in StringsTable
                    String pkgName = getBaseExt(loc);
                    Directory pkgDir = newDirectory(packagesDir, packagesDir.getName() + "/" + pkgName);
                    visitPackageLocation(loc);
                    pkgDir.setCompleted(true);
                    n = pkgDir;
                } else { // Link to module
                    String pkgName = loc.getParent();
                    String modName = getBaseExt(loc);
                    Node targetNode = findNode("/modules/" + modName);
                    if (targetNode != null) {
                        String pkgDirName = packagesDir.getName() + "/" + pkgName;
                        Directory pkgDir = (Directory) nodes.get(pkgDirName);
                        Node linkNode = newLinkNode(pkgDir, pkgDir.getName() + "/" + modName, targetNode);
                        n = linkNode;
                    }
                }
            }
            return n;
        }

        // Asking for /packages/package/module although
        // /packages/<pkg>/ not yet created, need to create it
        // prior to return the link to module node.
        Node handleModuleLink(String name) {
            // eg: unresolved /packages/package/module
            // Build /packages/package node
            Node ret = null;
            String radical = "/packages/";
            String path = name;
            if (path.startsWith(radical)) {
                int start = radical.length();
                int pkgEnd = path.indexOf('/', start);
                if (pkgEnd != -1) {
                    String pkg = path.substring(start, pkgEnd);
                    String pkgPath = radical + pkg;
                    Node n = findNode(pkgPath);
                    // If not found means that this is a symbolic link such as:
                    // /packages/java.util/java.base/java/util/Vector.class
                    // and will be done by a retry of the filesystem
                    for (Node child : n.getChildren()) {
                        if (child.name.equals(name)) {
                            ret = child;
                            break;
                        }
                    }
                }
            }
            return ret;
        }

        Node handleModulesSubTree(String name, ImageLocation loc) {
            Node n;
            assert (name.equals(loc.getFullName()));
            Directory dir = makeDirectories(name);
            visitLocation(loc, (childloc) -> {
                String path = childloc.getFullName();
                if (path.startsWith("/modules")) { // a package
                    makeDirectories(path);
                } else { // a resource
                    makeDirectories(childloc.buildName(true, true, false));
                    // if we have already created a resource for this name previously, then don't
                    // recreate it
                    if (!nodes.containsKey(childloc.getFullName(true))) {
                        newResource(dir, childloc);
                    }
                }
            });
            dir.setCompleted(true);
            n = dir;
            return n;
        }

        Node handleResource(String name) {
            Node n = null;
            if (!name.startsWith("/modules/")) {
                return null;
            }
            // Make sure that the thing that follows "/modules/" is a module name.
            int moduleEndIndex = name.indexOf('/', "/modules/".length());
            if (moduleEndIndex == -1) {
                return null;
            }
            ImageLocation moduleLoc = findLocation(name.substring(0, moduleEndIndex));
            if (moduleLoc == null || moduleLoc.getModuleOffset() == 0) {
                return null;
            }

            String locationPath = name.substring("/modules".length());
            ImageLocation resourceLoc = findLocation(locationPath);
            if (resourceLoc != null) {
                Directory dir = makeDirectories(resourceLoc.buildName(true, true, false));
                Resource res = newResource(dir, resourceLoc);
                n = res;
            }
            return n;
        }

        String getBaseExt(ImageLocation loc) {
            String base = loc.getBase();
            String ext = loc.getExtension();
            if (ext != null && !ext.isEmpty()) {
                base = base + "." + ext;
            }
            return base;
        }

        synchronized Node findNode(String name) {
            buildRootDirectory();
            Node n = nodes.get(name);
            if (n == null || !n.isCompleted()) {
                n = buildNode(name);
            }
            return n;
        }

        /**
         * Returns the file attributes of the image file.
         */
        BasicFileAttributes imageFileAttributes() {
            BasicFileAttributes attrs = imageFileAttributes;
            if (attrs == null) {
                try {
                    Path file = getImagePath();
                    attrs = Files.readAttributes(file, BasicFileAttributes.class);
                } catch (IOException ioe) {
                    throw new UncheckedIOException(ioe);
                }
                imageFileAttributes = attrs;
            }
            return attrs;
        }

        Directory newDirectory(Directory parent, String name) {
            Directory dir = Directory.create(parent, name, imageFileAttributes());
            nodes.put(dir.getName(), dir);
            return dir;
        }

        Resource newResource(Directory parent, ImageLocation loc) {
            Resource res = Resource.create(parent, loc, imageFileAttributes());
            nodes.put(res.getName(), res);
            return res;
        }

        LinkNode newLinkNode(Directory dir, String name, Node link) {
            LinkNode linkNode = LinkNode.create(dir, name, link);
            nodes.put(linkNode.getName(), linkNode);
            return linkNode;
        }

        Directory makeDirectories(String parent) {
            Directory last = rootDir;
            for (int offset = parent.indexOf('/', 1);
                    offset != -1;
                    offset = parent.indexOf('/', offset + 1)) {
                String dir = parent.substring(0, offset);
                last = makeDirectory(dir, last);
            }
            return makeDirectory(parent, last);

        }

        Directory makeDirectory(String dir, Directory last) {
            Directory nextDir = (Directory) nodes.get(dir);
            if (nextDir == null) {
                nextDir = newDirectory(last, dir);
            }
            return nextDir;
        }

        byte[] getResource(Node node) throws IOException {
            if (node.isResource()) {
                return super.getResource(node.getLocation());
            }
            throw new IOException("Not a resource: " + node);
        }

        byte[] getResource(Resource rs) throws IOException {
            return super.getResource(rs.getLocation());
        }
    }

    // jimage file does not store directory structure. We build nodes
    // using the "path" strings found in the jimage file.
    // Node can be a directory or a resource
    public abstract static class Node {
        private static final int ROOT_DIR = 0b0000_0000_0000_0001;
        private static final int PACKAGES_DIR = 0b0000_0000_0000_0010;
        private static final int MODULES_DIR = 0b0000_0000_0000_0100;

        private int flags;
        private final String name;
        private final BasicFileAttributes fileAttrs;
        private boolean completed;

        protected Node(String name, BasicFileAttributes fileAttrs) {
            this.name = Objects.requireNonNull(name);
            this.fileAttrs = Objects.requireNonNull(fileAttrs);
        }

        /**
         * A node is completed when all its direct children have been built.
         *
         * @return
         */
        public boolean isCompleted() {
            return completed;
        }

        public void setCompleted(boolean completed) {
            this.completed = completed;
        }

        public final void setIsRootDir() {
            flags |= ROOT_DIR;
        }

        public final boolean isRootDir() {
            return (flags & ROOT_DIR) != 0;
        }

        public final void setIsPackagesDir() {
            flags |= PACKAGES_DIR;
        }

        public final boolean isPackagesDir() {
            return (flags & PACKAGES_DIR) != 0;
        }

        public final void setIsModulesDir() {
            flags |= MODULES_DIR;
        }

        public final boolean isModulesDir() {
            return (flags & MODULES_DIR) != 0;
        }

        public final String getName() {
            return name;
        }

        public final BasicFileAttributes getFileAttributes() {
            return fileAttrs;
        }

        // resolve this Node (if this is a soft link, get underlying Node)
        public final Node resolveLink() {
            return resolveLink(false);
        }

        public Node resolveLink(boolean recursive) {
            return this;
        }

        // is this a soft link Node?
        public boolean isLink() {
            return false;
        }

        public boolean isDirectory() {
            return false;
        }

        public List<Node> getChildren() {
            throw new IllegalArgumentException("not a directory: " + getNameString());
        }

        public boolean isResource() {
            return false;
        }

        public ImageLocation getLocation() {
            throw new IllegalArgumentException("not a resource: " + getNameString());
        }

        public long size() {
            return 0L;
        }

        public long compressedSize() {
            return 0L;
        }

        public String extension() {
            return null;
        }

        public long contentOffset() {
            return 0L;
        }

        public final FileTime creationTime() {
            return fileAttrs.creationTime();
        }

        public final FileTime lastAccessTime() {
            return fileAttrs.lastAccessTime();
        }

        public final FileTime lastModifiedTime() {
            return fileAttrs.lastModifiedTime();
        }

        public final String getNameString() {
            return name;
        }

        @Override
        public final String toString() {
            return getNameString();
        }

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

        @Override
        public final boolean equals(Object other) {
            if (this == other) {
                return true;
            }

            if (other instanceof Node) {
                return name.equals(((Node) other).name);
            }

            return false;
        }
    }

    // directory node - directory has full path name without '/' at end.
    static final class Directory extends Node {
        private final List<Node> children;

        private Directory(String name, BasicFileAttributes fileAttrs) {
            super(name, fileAttrs);
            children = new ArrayList<>();
        }

        static Directory create(Directory parent, String name, BasicFileAttributes fileAttrs) {
            Directory d = new Directory(name, fileAttrs);
            if (parent != null) {
                parent.addChild(d);
            }
            return d;
        }

        @Override
        public boolean isDirectory() {
            return true;
        }

        @Override
        public List<Node> getChildren() {
            return Collections.unmodifiableList(children);
        }

        void addChild(Node node) {
            assert !children.contains(node) : "Child " + node + " already added";
            children.add(node);
        }

        public void walk(Consumer<? super Node> consumer) {
            consumer.accept(this);
            for (Node child : children) {
                if (child.isDirectory()) {
                    ((Directory)child).walk(consumer);
                } else {
                    consumer.accept(child);
                }
            }
        }
    }

    // "resource" is .class or any other resource (compressed/uncompressed) in a jimage.
    // full path of the resource is the "name" of the resource.
    static class Resource extends Node {
        private final ImageLocation loc;

        private Resource(ImageLocation loc, BasicFileAttributes fileAttrs) {
            super(loc.getFullName(true), fileAttrs);
            this.loc = loc;
        }

        static Resource create(Directory parent, ImageLocation loc, BasicFileAttributes fileAttrs) {
            Resource rs = new Resource(loc, fileAttrs);
            parent.addChild(rs);
            return rs;
        }

        @Override
        public boolean isCompleted() {
            return true;
        }

        @Override
        public boolean isResource() {
            return true;
        }

        @Override
        public ImageLocation getLocation() {
            return loc;
        }

        @Override
        public long size() {
            return loc.getUncompressedSize();
        }

        @Override
        public long compressedSize() {
            return loc.getCompressedSize();
        }

        @Override
        public String extension() {
            return loc.getExtension();
        }

        @Override
        public long contentOffset() {
            return loc.getContentOffset();
        }
    }

    // represents a soft link to another Node
    static class LinkNode extends Node {
        private final Node link;

        private LinkNode(String name, Node link) {
            super(name, link.getFileAttributes());
            this.link = link;
        }

        static LinkNode create(Directory parent, String name, Node link) {
            LinkNode ln = new LinkNode(name, link);
            parent.addChild(ln);
            return ln;
        }

        @Override
        public boolean isCompleted() {
            return true;
        }

        @Override
        public Node resolveLink(boolean recursive) {
            return (recursive && link instanceof LinkNode) ? ((LinkNode)link).resolveLink(true) : link;
        }

        @Override
        public boolean isLink() {
            return true;
        }
    }
}
