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('\\', '/');
	}
}
