package org.bndtools.utils.copy.bundleresource;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.util.Collection;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;

import org.osgi.framework.Bundle;

import aQute.lib.io.IO;

public class BundleResourceCopier {
    /** the bundle holding the resources */
    private Bundle bundle = null;

    /**
     * Constructor
     *
     * @param bundle the bundle holding the resources
     */
    public BundleResourceCopier(Bundle bundle) {
        super();
        this.bundle = bundle;
    }

    private void addOrRemoveDirectoryRecursive(File dstDir, String bundleDir, String relativePath, CopyMode mode, List<File> affected) throws IOException {
        String resourcePath = formatBundleEntryPath(new File(bundleDir, relativePath).getPath());
        Enumeration<String> resourcePathEntries = bundle.getEntryPaths(resourcePath);
        if (resourcePathEntries != null) {
            while (resourcePathEntries.hasMoreElements()) {
                String resourcePathEntry = resourcePathEntries.nextElement();
                if (resourcePathEntry.startsWith(bundleDir)) {
                    resourcePathEntry = resourcePathEntry.substring(bundleDir.length());
                }

                if (resourcePathEntry.endsWith("/")) {
                    addOrRemoveDirectoryRecursive(dstDir, bundleDir, resourcePathEntry, mode, affected);
                } else {
                    affected.addAll(addOrRemoveFile(dstDir, bundleDir, resourcePathEntry, mode));
                }
            }
        }
    }

    /**
     * Add/remove a file (backed by a bundle resource) to/from a directory.
     *
     * @param dstDir the destination directory under which to add/remove a file
     * @param bundleDir the bundle directory under which the resource is located
     * @param relativePath the path of the resource (relative to bundleDir) in the bundle. The resource will be
     *            added/removed to/from the same path relative to dstDir. This parameter must only hold the path of a
     *            file.
     * @param mode
     * @throws IOException when relativePath is null or empty, when the resource could not be found in the bundle, when
     *             the directory holding the file could not be created (when add is true), or when the file could not be
     *             removed (when add is false)
     */
    public Collection<File> addOrRemoveFile(File dstDir, String bundleDir, String relativePath, CopyMode mode) throws IOException {
        List<File> affected = new LinkedList<>();

        if (relativePath == null || relativePath.length() == 0) {
            throw new IOException("Resource relative path can't be empty");
        }

        File dstFile = new File(dstDir, relativePath);
        File relativeDstFile = new File(relativePath);

        if (mode == null) {
            throw new IllegalArgumentException("Copy mode may not be null");
        } else if (mode == CopyMode.REMOVE) {
            Files.deleteIfExists(dstFile.toPath());
        } else if (mode == CopyMode.CHECK) {
            if (dstFile.exists())
                affected.add(relativeDstFile);
        } else {
            if (dstFile.exists() && !dstFile.isFile())
                throw new IOException("Target path exists and is not a plain file: " + dstFile);

            if (dstFile.exists() && mode == CopyMode.ADD) {
                affected.add(relativeDstFile);
            } else {
                String resourcePath = formatBundleEntryPath(new File(bundleDir, relativePath).getPath());
                URL resourceUrl = bundle.getEntry(resourcePath);
                if (resourceUrl == null)
                    throw new IOException("Resource " + resourcePath + " not found in bundle " + bundle.getSymbolicName());

                if (mode == CopyMode.REPLACE || !dstFile.exists()) {
                    File dstFileDir = dstFile.getParentFile();
                    if (dstFileDir != null)
                        Files.createDirectories(dstFileDir.toPath());
                    IO.copy(resourceUrl, dstFile);
                }
            }
        }

        return affected;
    }

    /**
     * Add/remove files (backed by bundle resources) to/from a directory.
     *
     * @param dstDir the destination directory under which to add/remove files
     * @param bundleDir the bundle directory under which the resources are located
     * @param relativePaths the paths of the resources (relative to bundleDir) in the bundle. The resources will be
     *            added/removed to/from the same paths relative to dstDir. This parameter must only hold paths of files.
     * @param mode
     * @throws IOException when a relative path is null or empty, when a resource could not be found in the bundle, when
     *             a directory holding a file could not be created (when add is true), or when a file could not be
     *             removed (when add is false)
     */
    public Collection<File> addOrRemoveFiles(File dstDir, String bundleDir, String[] relativePaths, CopyMode mode) throws IOException {
        List<File> affected = new LinkedList<>();
        for (String templatePath : relativePaths) {
            affected.addAll(addOrRemoveFile(dstDir, bundleDir, templatePath, mode));
        }
        return affected;
    }

    /**
     * Recursively add/remove a directory and its files (backed by bundle resources) to/from a directory.
     *
     * @param dstDir the destination directory under which to add/remove the directory and its files
     * @param bundleDir the bundle directory under which the resources are located
     * @param relativePath the path of the resources (relative to bundleDir) in the bundle. The resources will be
     *            recursively added/removed to/from the same paths relative to dstDir. This parameter must only hold a
     *            paths of a directory. When null then "/" will be used.
     * @param mode
     * @return A list of existing files that were/would have been affected.
     * @throws IOException when a relative path is null or empty, when a resource could not be found in the bundle, when
     *             a directory holding a file could not be created (if add is true), or when a file could not be removed
     *             (when add is false)
     */
    public Collection<File> addOrRemoveDirectory(File dstDir, String bundleDir, String relativePath, CopyMode mode) throws IOException {
        List<File> affected = new LinkedList<>();

        String bundleDirFixed = bundleDir.replaceAll("^/+", "");
        if (!bundleDirFixed.endsWith("/")) {
            bundleDirFixed = bundleDirFixed + "/";
        }

        String relativePathFixed = relativePath;
        if (relativePathFixed == null) {
            relativePathFixed = "/";
        }
        if (!relativePathFixed.endsWith("/")) {
            relativePathFixed = relativePathFixed + "/";
        }

        addOrRemoveDirectoryRecursive(dstDir, bundleDirFixed, relativePathFixed, mode, affected);
        return affected;
    }

    /**
     * Recursively add/remove directories and their files (backed by bundle resources) to/from a directory.
     *
     * @param dstDir the destination directory under which to add/remove directories and files
     * @param bundleDir the bundle directory under which the resources are located
     * @param relativePaths the paths of the resources (relative to bundleDir) in the bundle. The resources will be
     *            recursively added/removed to/from the same paths relative to dstDir. This parameter must only hold
     *            paths of directories.
     * @param mode
     * @throws IOException when a relative path is null or empty, when a resource could not be found in the bundle, when
     *             a directory holding a file could not be created (when add is true), or when a file could not be
     *             removed (when add is false)
     */
    public void addOrRemoveDirectories(File dstDir, String bundleDir, String[] relativePaths, CopyMode mode) throws IOException {
        for (String templatePath : relativePaths) {
            addOrRemoveDirectory(dstDir, bundleDir, templatePath, mode);
        }
    }

    private String formatBundleEntryPath(String path) {
        // Bundle.getEntry* doesn't grok backslashes
        if (File.separatorChar != '\\') {
            return path;
        }

        return path.replace('\\', '/');
    }
}