/*
 * 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.jackrabbit.vault.packaging.impl;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URISyntaxException;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import javax.jcr.RepositoryException;
import javax.jcr.Session;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.jackrabbit.vault.fs.Mounter;
import org.apache.jackrabbit.vault.fs.api.RepositoryAddress;
import org.apache.jackrabbit.vault.fs.api.VaultFileSystem;
import org.apache.jackrabbit.vault.fs.api.VaultFsConfig;
import org.apache.jackrabbit.vault.fs.config.DefaultMetaInf;
import org.apache.jackrabbit.vault.fs.config.MetaInf;
import org.apache.jackrabbit.vault.fs.impl.AggregateManagerImpl;
import org.apache.jackrabbit.vault.fs.io.Archive;
import org.apache.jackrabbit.vault.fs.io.JarExporter;
import org.apache.jackrabbit.vault.fs.spi.ProgressTracker;
import org.apache.jackrabbit.vault.packaging.ExportOptions;
import org.apache.jackrabbit.vault.packaging.PackageId;
import org.apache.jackrabbit.vault.packaging.PackageManager;
import org.apache.jackrabbit.vault.packaging.PackageProperties;
import org.apache.jackrabbit.vault.packaging.VaultPackage;
import org.apache.jackrabbit.vault.packaging.events.PackageEvent;
import org.apache.jackrabbit.vault.packaging.events.impl.PackageEventDispatcher;
import org.apache.jackrabbit.vault.util.Constants;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * Implements the package manager
 */
public class PackageManagerImpl implements PackageManager {

    /**
     * event dispatcher
     */
    @Nullable
    private PackageEventDispatcher dispatcher;

    /**
     * {@inheritDoc}
     */
    @Override
    public @NotNull VaultPackage open(@NotNull Archive archive) throws IOException {
        return new ZipVaultPackage(archive, false);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public @NotNull VaultPackage open(@NotNull Archive archive, boolean strict) throws IOException {
        return new ZipVaultPackage(archive, strict);
    }
    
    /**
     * {@inheritDoc}
     */
    @Override
    public VaultPackage open(File file) throws IOException {
        return open(file, false);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public VaultPackage open(File file, boolean strict) throws IOException {
        return new ZipVaultPackage(file, false, strict);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public VaultPackage assemble(Session s, ExportOptions opts, File file)
            throws IOException, RepositoryException {
        OutputStream out = null;
        boolean isTmp = false;
        boolean success = false;
        try {
            if (file == null) {
                file = File.createTempFile("filevault", ".zip");
                isTmp = true;
            }
            out = FileUtils.openOutputStream(file);
            assemble(s, opts, out);
            IOUtils.closeQuietly(out);
            success = true;
            return new ZipVaultPackage(file, isTmp);
        } finally {
            IOUtils.closeQuietly(out);
            if (isTmp && !success) {
                FileUtils.deleteQuietly(file);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void assemble(Session s, ExportOptions opts, OutputStream out)
            throws IOException, RepositoryException {
        RepositoryAddress addr;
        try {
            String mountPath = opts.getMountPath();
            if (mountPath == null || mountPath.length() == 0) {
                mountPath = "/";
            }
            addr = new RepositoryAddress("/" + s.getWorkspace().getName() + mountPath);
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException(e);
        }
        MetaInf metaInf = opts.getMetaInf();
        if (metaInf == null) {
            metaInf = new DefaultMetaInf();
        }

        VaultFsConfig config = metaInf.getConfig();
        if (metaInf.getProperties() != null) {
            if ("true".equals(metaInf.getProperties().getProperty(PackageProperties.NAME_USE_BINARY_REFERENCES))) {
                config = AggregateManagerImpl.getDefaultBinaryReferencesConfig();
            }
        }

        VaultFileSystem jcrfs = Mounter.mount(config, metaInf.getFilter(), addr, opts.getRootPath(), s);
        JarExporter exporter = new JarExporter(out, opts.getCompressionLevel());
        exporter.setProperties(metaInf.getProperties());
        if (opts.getListener() != null) {
            exporter.setVerbose(opts.getListener());
        }
        if (opts.getPostProcessor() != null) {
            exporter.export(jcrfs.getRoot(), true);
            opts.getPostProcessor().process(exporter);
            exporter.close();
        } else {
            exporter.export(jcrfs.getRoot());
        }
        jcrfs.unmount();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public VaultPackage rewrap(ExportOptions opts, VaultPackage src, File file)
            throws IOException, RepositoryException {
        OutputStream out = null;
        boolean isTmp = false;
        boolean success = false;
        try {
            if (file == null) {
                file = File.createTempFile("filevault", ".zip");
                isTmp = true;
            }
            out = FileUtils.openOutputStream(file);
            rewrap(opts, src, out);
            IOUtils.closeQuietly(out);
            success = true;
            VaultPackage pack =  new ZipVaultPackage(file, isTmp);
            dispatch(PackageEvent.Type.REWRAPP, pack.getId(), null);
            return pack;
        } finally {
            IOUtils.closeQuietly(out);
            if (isTmp && !success) {
                FileUtils.deleteQuietly(file);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void rewrap(ExportOptions opts, VaultPackage src, OutputStream out)
            throws IOException {
        MetaInf metaInf = opts.getMetaInf();
        if (metaInf == null) {
            metaInf = new DefaultMetaInf();
        }
        try (JarExporter exporter = new JarExporter(out, opts.getCompressionLevel())) {
            exporter.open();
            exporter.setProperties(metaInf.getProperties());
            ProgressTracker tracker = null;
            if (opts.getListener() != null) {
                tracker = new ProgressTracker();
                exporter.setVerbose(opts.getListener());
            }
    
            // merge
            MetaInf inf = opts.getMetaInf();
            try (ZipFile zip = new ZipFile(src.getFile(), ZipFile.OPEN_READ)) {
                if (opts.getPostProcessor() == null) {
                    // no post processor, we keep all files except the properties
                    Enumeration<? extends ZipEntry> e = zip.entries();
                    while (e.hasMoreElements()) {
                        ZipEntry entry = (ZipEntry) e.nextElement();
                        String path = entry.getName();
                        if (!path.equals(Constants.META_DIR + "/" + Constants.PROPERTIES_XML)) {
                            exporter.write(zip, entry);
                        }
                    }
                } else {
                    Set<String> keep = new HashSet<String>();
                    keep.add(Constants.META_DIR + "/");
                    keep.add(Constants.META_DIR + "/" + Constants.NODETYPES_CND);
                    keep.add(Constants.META_DIR + "/" + Constants.CONFIG_XML);
                    keep.add(Constants.META_DIR + "/" + Constants.FILTER_XML);
                    Enumeration<? extends ZipEntry> e = zip.entries();
                    while (e.hasMoreElements()) {
                        ZipEntry entry = (ZipEntry) e.nextElement();
                        String path = entry.getName();
                        if (!path.startsWith(Constants.META_DIR + "/") || keep.contains(path)) {
                            exporter.write(zip, entry);
                        }
                    }
                }
            }
    
            // write updated properties
            ByteArrayOutputStream tmpOut = new ByteArrayOutputStream();
            inf.getProperties().storeToXML(tmpOut, "FileVault Package Properties", "utf-8");
            exporter.writeFile(new ByteArrayInputStream(tmpOut.toByteArray()), Constants.META_DIR + "/" + Constants.PROPERTIES_XML);
            if (tracker != null) {
                tracker.track("A", Constants.META_DIR + "/" + Constants.PROPERTIES_XML);
            }
    
            if (opts.getPostProcessor() != null) {
                opts.getPostProcessor().process(exporter);
            }
        }
    }

    @Nullable
    PackageEventDispatcher getDispatcher() {
        return dispatcher;
    }

    public void setDispatcher(@Nullable PackageEventDispatcher dispatcher) {
        this.dispatcher = dispatcher;
    }

    void dispatch(@NotNull PackageEvent.Type type, @NotNull PackageId id, @Nullable PackageId[] related) {
        if (dispatcher == null) {
            return;
        }
        dispatcher.dispatch(type, id, related);
    }

}
