/*
 * Copyright (c) 2015, 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.tools.jlink.internal;

import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.module.ModuleDescriptor;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import jdk.internal.jimage.decompressor.Decompressor;
import jdk.internal.module.ModuleInfo.Attributes;
import jdk.internal.module.ModuleTarget;
import jdk.tools.jlink.plugin.Plugin;
import jdk.tools.jlink.builder.ImageBuilder;
import jdk.tools.jlink.plugin.PluginException;
import jdk.tools.jlink.plugin.ResourcePool;
import jdk.tools.jlink.plugin.ResourcePoolModule;
import jdk.tools.jlink.plugin.ResourcePoolEntry;
import jdk.tools.jlink.internal.ResourcePoolManager.ResourcePoolImpl;

/**
 * Plugins Stack. Plugins entry point to apply transformations onto resources
 * and files.
 */
public final class ImagePluginStack {

    public interface ImageProvider {

        ExecutableImage retrieve(ImagePluginStack stack) throws IOException;
    }

    public static final class OrderedResourcePoolManager extends ResourcePoolManager {
        class OrderedResourcePool extends ResourcePoolImpl {
            List<ResourcePoolEntry> getOrderedList() {
                return OrderedResourcePoolManager.this.getOrderedList();
            }
        }

        private final List<ResourcePoolEntry> orderedList = new ArrayList<>();
        private final ResourcePoolImpl poolImpl = new OrderedResourcePool();

        public OrderedResourcePoolManager(ByteOrder order, StringTable table) {
            super(order, table);
        }

        @Override
        public ResourcePool resourcePool() {
            return poolImpl;
        }

        /**
         * Add a resource.
         *
         * @param resource The Resource to add.
         */
        @Override
        public void add(ResourcePoolEntry resource) {
            super.add(resource);
            orderedList.add(resource);
        }

        List<ResourcePoolEntry> getOrderedList() {
            return Collections.unmodifiableList(orderedList);
        }
    }

    private final static class CheckOrderResourcePoolManager extends ResourcePoolManager {

        private final List<ResourcePoolEntry> orderedList;
        private int currentIndex;

        public CheckOrderResourcePoolManager(ByteOrder order, List<ResourcePoolEntry> orderedList, StringTable table) {
            super(order, table);
            this.orderedList = Objects.requireNonNull(orderedList);
        }

        /**
         * Add a resource.
         *
         * @param resource The Resource to add.
         */
        @Override
        public void add(ResourcePoolEntry resource) {
            ResourcePoolEntry ordered = orderedList.get(currentIndex);
            if (!resource.equals(ordered)) {
                throw new PluginException("Resource " + resource.path() + " not in the right order");
            }
            super.add(resource);
            currentIndex += 1;
        }
    }

    private static final class PreVisitStrings implements StringTable {

        private int currentid = 0;
        private final Map<String, Integer> stringsUsage = new HashMap<>();
        private final Map<String, Integer> stringsMap = new HashMap<>();
        private final Map<Integer, String> reverseMap = new HashMap<>();

        @Override
        public int addString(String str) {
            Objects.requireNonNull(str);
            Integer count = stringsUsage.get(str);
            if (count == null) {
                count = 0;
            }
            count += 1;
            stringsUsage.put(str, count);
            Integer id = stringsMap.get(str);
            if (id == null) {
                id = currentid;
                stringsMap.put(str, id);
                currentid += 1;
                reverseMap.put(id, str);
            }

            return id;
        }

        private List<String> getSortedStrings() {
            Stream<java.util.Map.Entry<String, Integer>> stream
                    = stringsUsage.entrySet().stream();
            // Remove strings that have a single occurence
            List<String> result = stream.sorted(Comparator.comparing(e -> e.getValue(),
                    Comparator.reverseOrder())).filter((e) -> {
                        return e.getValue() > 1;
                    }).map(java.util.Map.Entry::getKey).
                    collect(Collectors.toList());
            return result;
        }

        @Override
        public String getString(int id) {
            return reverseMap.get(id);
        }
    }

    private final ImageBuilder imageBuilder;
    private final Plugin lastSorter;
    private final List<Plugin> plugins = new ArrayList<>();
    private final List<ResourcePrevisitor> resourcePrevisitors = new ArrayList<>();
    private final boolean validate;

    public ImagePluginStack() {
        this(null, Collections.emptyList(), null);
    }

    public ImagePluginStack(ImageBuilder imageBuilder,
            List<Plugin> plugins,
            Plugin lastSorter) {
        this(imageBuilder, plugins, lastSorter, true);
    }

    public ImagePluginStack(ImageBuilder imageBuilder,
            List<Plugin> plugins,
            Plugin lastSorter,
            boolean validate) {
        this.imageBuilder = Objects.requireNonNull(imageBuilder);
        this.lastSorter = lastSorter;
        this.plugins.addAll(Objects.requireNonNull(plugins));
        plugins.stream().forEach((p) -> {
            Objects.requireNonNull(p);
            if (p instanceof ResourcePrevisitor) {
                resourcePrevisitors.add((ResourcePrevisitor) p);
            }
        });
        this.validate = validate;
    }

    public void operate(ImageProvider provider) throws Exception {
        ExecutableImage img = provider.retrieve(this);
        List<String> arguments = new ArrayList<>();
        plugins.stream()
                .filter(PostProcessor.class::isInstance)
                .map((plugin) -> ((PostProcessor)plugin).process(img))
                .filter((lst) -> (lst != null))
                .forEach((lst) -> {
                     arguments.addAll(lst);
                });
        img.storeLaunchArgs(arguments);
    }

    public DataOutputStream getJImageFileOutputStream() throws IOException {
        return imageBuilder.getJImageOutputStream();
    }

    public ImageBuilder getImageBuilder() {
        return imageBuilder;
    }

    /**
     * Resource Plugins stack entry point. All resources are going through all
     * the plugins.
     *
     * @param resources The set of resources to visit
     * @return The result of the visit.
     * @throws IOException
     */
    public ResourcePool visitResources(ResourcePoolManager resources)
            throws Exception {
        Objects.requireNonNull(resources);
        if (resources.isEmpty()) {
            return new ResourcePoolManager(resources.byteOrder(),
                    resources.getStringTable()).resourcePool();
        }
        PreVisitStrings previsit = new PreVisitStrings();
        resourcePrevisitors.stream().forEach((p) -> {
            p.previsit(resources.resourcePool(), previsit);
        });

        // Store the strings resulting from the previsit.
        List<String> sorted = previsit.getSortedStrings();
        sorted.stream().forEach((s) -> {
            resources.getStringTable().addString(s);
        });

        ResourcePool resPool = resources.resourcePool();
        List<ResourcePoolEntry> frozenOrder = null;
        for (Plugin p : plugins) {
            ResourcePoolManager resMgr = null;
            if (p == lastSorter) {
                if (frozenOrder != null) {
                    throw new Exception("Order of resources is already frozen. Plugin "
                            + p.getName() + " is badly located");
                }
                // Create a special Resource pool to compute the indexes.
                resMgr = new OrderedResourcePoolManager(resPool.byteOrder(),
                        resources.getStringTable());
            } else {// If we have an order, inject it
                if (frozenOrder != null) {
                    resMgr = new CheckOrderResourcePoolManager(resPool.byteOrder(),
                            frozenOrder, resources.getStringTable());
                } else {
                    resMgr = new ResourcePoolManager(resPool.byteOrder(),
                            resources.getStringTable());
                }
            }
            try {
                resPool = p.transform(resPool, resMgr.resourcePoolBuilder());
            } catch (PluginException pe) {
                if (JlinkTask.DEBUG) {
                    System.err.println("Plugin " + p.getName() + " threw exception during transform");
                    pe.printStackTrace();
                }
                throw pe;
            }
            if (resPool.isEmpty()) {
                throw new Exception("Invalid resource pool for plugin " + p);
            }
            if (resPool instanceof OrderedResourcePoolManager.OrderedResourcePool) {
                frozenOrder = ((OrderedResourcePoolManager.OrderedResourcePool)resPool).getOrderedList();
            }
        }

        return resPool;
    }

    /**
     * This pool wrap the original pool and automatically uncompress ResourcePoolEntry
     * if needed.
     */
    private class LastPoolManager extends ResourcePoolManager {
        private class LastModule implements ResourcePoolModule {

            final ResourcePoolModule module;
            // lazily initialized
            ModuleDescriptor descriptor;
            ModuleTarget target;

            LastModule(ResourcePoolModule module) {
                this.module = module;
            }

            @Override
            public String name() {
                return module.name();
            }

            @Override
            public Optional<ResourcePoolEntry> findEntry(String path) {
                Optional<ResourcePoolEntry> d = module.findEntry(path);
                return d.isPresent()? Optional.of(getUncompressed(d.get())) : Optional.empty();
            }

            @Override
            public ModuleDescriptor descriptor() {
                initModuleAttributes();
                return descriptor;
            }

            @Override
            public String targetPlatform() {
                initModuleAttributes();
                return target != null? target.targetPlatform() : null;
            }

            private void initModuleAttributes() {
                if (this.descriptor == null) {
                    Attributes attr = ResourcePoolManager.readModuleAttributes(this);
                    this.descriptor = attr.descriptor();
                    this.target = attr.target();
                }
            }

            @Override
            public Set<String> packages() {
                return module.packages();
            }

            @Override
            public String toString() {
                return name();
            }

            @Override
            public Stream<ResourcePoolEntry> entries() {
                List<ResourcePoolEntry> lst = new ArrayList<>();
                module.entries().forEach(md -> {
                    lst.add(getUncompressed(md));
                });
                return lst.stream();
            }

            @Override
            public int entryCount() {
                return module.entryCount();
            }
        }

        private final ResourcePool pool;
        Decompressor decompressor = new Decompressor();
        Collection<ResourcePoolEntry> content;

        LastPoolManager(ResourcePool pool) {
            this.pool = pool;
        }

        @Override
        public void add(ResourcePoolEntry resource) {
            throw new PluginException("pool is readonly");
        }

        @Override
        public Optional<ResourcePoolModule> findModule(String name) {
            Optional<ResourcePoolModule> module = pool.moduleView().findModule(name);
            return module.isPresent()? Optional.of(new LastModule(module.get())) : Optional.empty();
        }

        /**
         * The collection of modules contained in this pool.
         *
         * @return The collection of modules.
         */
        @Override
        public Stream<ResourcePoolModule> modules() {
            List<ResourcePoolModule> modules = new ArrayList<>();
            pool.moduleView().modules().forEach(m -> {
                modules.add(new LastModule(m));
            });
            return modules.stream();
        }

        @Override
        public int moduleCount() {
            return pool.moduleView().moduleCount();
        }

        /**
         * Get all resources contained in this pool instance.
         *
         * @return The stream of resources;
         */
        @Override
        public Stream<ResourcePoolEntry> entries() {
            if (content == null) {
                content = new ArrayList<>();
                pool.entries().forEach(md -> {
                    content.add(getUncompressed(md));
                });
            }
            return content.stream();
        }

        @Override
        public int entryCount() {
            return pool.entryCount();
        }

        /**
         * Get the resource for the passed path.
         *
         * @param path A resource path
         * @return A Resource instance if the resource is found
         */
        @Override
        public Optional<ResourcePoolEntry> findEntry(String path) {
            Objects.requireNonNull(path);
            Optional<ResourcePoolEntry> res = pool.findEntry(path);
            return res.isPresent()? Optional.of(getUncompressed(res.get())) : Optional.empty();
        }

        @Override
        public Optional<ResourcePoolEntry> findEntryInContext(String path, ResourcePoolEntry context) {
            Objects.requireNonNull(path);
            Objects.requireNonNull(context);
            Optional<ResourcePoolEntry> res = pool.findEntryInContext(path, context);
            return res.map(this::getUncompressed);
        }

        @Override
        public boolean contains(ResourcePoolEntry res) {
            return pool.contains(res);
        }

        @Override
        public boolean isEmpty() {
            return pool.isEmpty();
        }

        @Override
        public ByteOrder byteOrder() {
            return pool.byteOrder();
        }

        private ResourcePoolEntry getUncompressed(ResourcePoolEntry res) {
            if (res != null) {
                if (res instanceof ResourcePoolManager.CompressedModuleData) {
                    try {
                        byte[] bytes = decompressor.decompressResource(byteOrder(),
                                (int offset) -> ((ResourcePoolImpl)pool).getStringTable().getString(offset),
                                res.contentBytes());
                        res = res.copyWithContent(bytes);
                    } catch (IOException ex) {
                        if (JlinkTask.DEBUG) {
                            System.err.println("IOException while reading resource: " + res.path());
                            ex.printStackTrace();
                        }
                        throw new PluginException(ex);
                    }
                }
            }
            return res;
        }
    }

    /**
     * Make the imageBuilder to store files.
     *
     * @param original
     * @param transformed
     * @param writer
     * @throws java.lang.Exception
     */
    public void storeFiles(ResourcePool original, ResourcePool transformed,
            BasicImageWriter writer)
            throws Exception {
        Objects.requireNonNull(original);
        Objects.requireNonNull(transformed);
        ResourcePool lastPool = new LastPoolManager(transformed).resourcePool();
        if (validate) {
            ResourcePoolConfiguration.validate(lastPool);
        }
        imageBuilder.storeFiles(lastPool);
    }

    public ExecutableImage getExecutableImage() throws IOException {
        return imageBuilder.getExecutableImage();
    }
}
