/*******************************************************************************
 *  Copyright (c) 2007, 2015 IBM Corporation and others.
 *
 *  This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License 2.0
 *  which accompanies this distribution, and is available at
 *  https://www.eclipse.org/legal/epl-2.0/
 *
 *  SPDX-License-Identifier: EPL-2.0
 *
 *  Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.pde.internal.ui.util;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.pde.core.plugin.IPlugin;
import org.eclipse.pde.core.plugin.IPluginBase;
import org.eclipse.pde.core.plugin.IPluginModel;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.eclipse.pde.internal.core.TargetPlatformHelper;
import org.eclipse.pde.internal.core.ibundle.IBundlePluginBase;
import org.eclipse.pde.internal.ui.PDEUIMessages;
import org.eclipse.pde.internal.ui.wizards.product.ISplashHandlerConstants;
import org.eclipse.pde.internal.ui.wizards.product.UpdateSplashHandlerAction;
import org.eclipse.pde.internal.ui.wizards.templates.ControlStack;
import org.eclipse.pde.ui.templates.ITemplateSection;
import org.eclipse.pde.ui.templates.IVariableProvider;
import org.osgi.framework.Bundle;

/**
 * TemplateFileGenerator
 */
public class TemplateFileGenerator implements IVariableProvider {

	// TODO: MP: SPLASH: Merge this utility back with template (maybe as an abstract base class?) - Extracted from org.eclipse.pde.ui.templates.AbstractTemplateSection
	// TODO: MP: SPLASH: Major code-clean-up required

	/**
	 * The key for the main plug-in class of the plug-in that the template is
	 * used for (value="pluginClass").  The return value is a fully-qualified class name.
	 */
	public static final String KEY_PLUGIN_CLASS = "pluginClass"; //$NON-NLS-1$

	/**
	 * The key for the simple class name of a bundle activator (value="activator")
	 *
	 * @since 3.3
	 */
	public static final String KEY_ACTIVATOR_SIMPLE = "activator"; //$NON-NLS-1$
	/**
	 * The key for the plug-in id of the plug-in that the template is used for
	 * (value="pluginId").
	 */
	public static final String KEY_PLUGIN_ID = "pluginId"; //$NON-NLS-1$
	/**
	 * The key for the plug-in name of the plug-in that the template is used for
	 * (value="pluginName").
	 */
	public static final String KEY_PLUGIN_NAME = "pluginName"; //$NON-NLS-1$
	/**
	 * The key for the package name that will be created by this teamplate
	 * (value="packageName").
	 */
	public static final String KEY_PACKAGE_NAME = "packageName"; //$NON-NLS-1$

	private final IProject fProject;

	private final IPluginModelBase fModel;

	private final String fPluginID;

	private final String fPackage;

	private final String fClass;

	private final String fTemplate;

	public TemplateFileGenerator(IProject project, IPluginModelBase model, String pluginID, String targetPackage, String targetClass, String template) {
		fProject = project;
		fModel = model;
		fPluginID = pluginID;
		fPackage = targetPackage;
		fClass = targetClass;
		fTemplate = template;
	}

	/**
	 * Generates files as part of the template execution. The default
	 * implementation uses template location as a root of the file templates.
	 * {@link #generateFiles(IProgressMonitor monitor, URL locationUrl)} on how
	 * the location gets processed.
	 *
	 * @param monitor
	 *            progress monitor to use to indicate generation progress
	 */
	public void generateFiles(IProgressMonitor monitor) throws CoreException {
		// Generate files using the default template location
		generateFiles(monitor, getTemplateLocation());
		// Generate files using the shared branding location (for splash screen)
		Bundle templateBundle = Platform.getBundle("org.eclipse.pde.ui.templates"); //$NON-NLS-1$
		if (templateBundle == null) {
			return;
		}
		generateFiles(monitor, templateBundle.getEntry("branding/")); //$NON-NLS-1$
	}

	public URL getTemplateLocation() {
		Bundle bundle = Platform.getBundle("org.eclipse.pde.ui.templates"); //$NON-NLS-1$
		if (bundle == null) {
			return null;
		}

		try {
			String[] candidates = getDirectoryCandidates();
			for (String directoryCandidate : candidates) {
				if (bundle.getEntry(directoryCandidate) != null) {
					URL candidate = new URL(bundle.getEntry("/"), directoryCandidate); //$NON-NLS-1$
					return candidate;
				}
			}
		} catch (MalformedURLException e) {
		}
		return null;
	}

	/**
	 * Generates files as part of the template execution.
	 * The files found in the location are processed in the following way:
	 * <ul>
	 * <li>Files and folders found in the directory <samp>bin </samp> are
	 * copied into the target project without modification.</li>
	 * <li>Files found in the directory <samp>java </samp> are copied into the
	 * Java source folder by creating the folder structure that corresponds to
	 * the package name (variable <samp>packageName </samp>). Java files are
	 * subject to conditional generation and variable replacement.</li>
	 * <li>All other files and folders are copied directly into the target
	 * folder with the conditional generation and variable replacement for
	 * files. Variable replacement also includes file names.</li>
	 * </ul>
	 *
	 * @since 3.3
	 * @param monitor
	 *            progress monitor to use to indicate generation progress
	 * @param locationUrl a url pointing to a file/directory that will be copied into the template
	 */
	protected void generateFiles(IProgressMonitor monitor, URL locationUrl) throws CoreException {
		monitor.setTaskName(PDEUIMessages.AbstractTemplateSection_generating);

		if (locationUrl == null) {
			return;
		}
		try {
			locationUrl = FileLocator.resolve(locationUrl);
			locationUrl = FileLocator.toFileURL(locationUrl);
		} catch (IOException e) {
			return;
		}
		if ("file".equals(locationUrl.getProtocol())) { //$NON-NLS-1$
			File templateDirectory = new File(locationUrl.getFile());
			if (!templateDirectory.exists())
				return;
			generateFiles(templateDirectory, fProject, true, false, monitor);
		} else if ("jar".equals(locationUrl.getProtocol())) { //$NON-NLS-1$
			String file = locationUrl.getFile();
			int exclamation = file.indexOf('!');
			if (exclamation < 0)
				return;
			URL fileUrl = null;
			try {
				fileUrl = new URL(file.substring(0, exclamation));
			} catch (MalformedURLException mue) {
				return;
			}
			File pluginJar = new File(fileUrl.getFile());
			if (!pluginJar.exists())
				return;
			String templateDirectory = file.substring(exclamation + 1); // "/some/path/"
			IPath path = IPath.fromOSString(templateDirectory);
			try (ZipFile zipFile = new ZipFile(pluginJar)) {
				generateFiles(zipFile, path, fProject, true, false, monitor);
			} catch (IOException ioe) {
			}

		}
		monitor.subTask(""); //$NON-NLS-1$
		monitor.worked(1);
	}

	private void generateFiles(File src, IContainer dst, boolean firstLevel, boolean binary, IProgressMonitor monitor) throws CoreException {
		File[] members = src.listFiles();

		for (File member : members) {
			if (member.isDirectory()) {
				IContainer dstContainer = null;

				if (firstLevel) {
					binary = false;
					if (!isOkToCreateFolder(member))
						continue;

					if (member.getName().equals("java")) { //$NON-NLS-1$
						IFolder sourceFolder = getSourceFolder(monitor);
						dstContainer = generateJavaSourceFolder(sourceFolder, monitor);
					} else if (member.getName().equals("bin")) { //$NON-NLS-1$
						binary = true;
						dstContainer = dst;
					}
				}
				if (dstContainer == null) {
					if (isOkToCreateFolder(member) == false)
						continue;
					String folderName = getProcessedString(member.getName(), member.getName());
					dstContainer = dst.getFolder(IPath.fromOSString(folderName));
				}
				if (dstContainer instanceof IFolder && !dstContainer.exists())
					((IFolder) dstContainer).create(true, true, monitor);
				generateFiles(member, dstContainer, false, binary, monitor);
			} else {
				if (isOkToCreateFile(member)) {
					if (firstLevel)
						binary = false;
					try (InputStream in = new FileInputStream(member);) {
						copyFile(member.getName(), in, dst, binary, monitor);
					} catch (IOException ioe) {
					}
				}
			}
		}
	}

	private void copyFile(String fileName, InputStream input, IContainer dst, boolean binary, IProgressMonitor monitor) throws CoreException {
		String targetFileName = getProcessedString(fileName, fileName);

		monitor.subTask(targetFileName);
		IFile dstFile = dst.getFile(IPath.fromOSString(targetFileName));

		try (InputStream stream = getProcessedStream(fileName, input, binary)) {
			if (dstFile.exists()) {
				dstFile.setContents(stream, true, true, monitor);
			} else {
				dstFile.create(stream, true, monitor);
			}
		} catch (IOException e) {
		}
	}

	private void generateFiles(ZipFile zipFile, IPath path, IContainer dst, boolean firstLevel, boolean binary, IProgressMonitor monitor) throws CoreException {
		int pathLength = path.segmentCount();
		// Immidiate children
		Map<String, ZipEntry> childZipEntries = new HashMap<>(); // "dir/" or "dir/file.java"

		for (Enumeration<? extends ZipEntry> zipEntries = zipFile.entries(); zipEntries.hasMoreElements();) {
			ZipEntry zipEntry = zipEntries.nextElement();
			IPath entryPath = IPath.fromOSString(zipEntry.getName());
			if (entryPath.segmentCount() <= pathLength) {
				// ancestor or current directory
				continue;
			}
			if (!path.isPrefixOf(entryPath)) {
				// not a descendant
				continue;
			}
			if (entryPath.segmentCount() == pathLength + 1) {
				childZipEntries.put(zipEntry.getName(), zipEntry);
			} else {
				String name = entryPath.uptoSegment(pathLength + 1).addTrailingSeparator().toString();
				if (!childZipEntries.containsKey(name)) {
					ZipEntry dirEntry = new ZipEntry(name);
					childZipEntries.put(name, dirEntry);
				}
			}
		}

		for (ZipEntry zipEnry : childZipEntries.values()) {
			String name = IPath.fromOSString(zipEnry.getName()).lastSegment().toString();
			if (zipEnry.isDirectory()) {
				IContainer dstContainer = null;

				if (firstLevel) {
					binary = false;
					if (name.equals("java")) { //$NON-NLS-1$
						IFolder sourceFolder = getSourceFolder(monitor);
						dstContainer = generateJavaSourceFolder(sourceFolder, monitor);
					} else if (name.equals("bin")) { //$NON-NLS-1$
						binary = true;
						dstContainer = dst;
					}
				}
				if (dstContainer == null) {
					if (isOkToCreateFolder(new File(path.toFile(), name)) == false)
						continue;
					String folderName = getProcessedString(name, name);
					dstContainer = dst.getFolder(IPath.fromOSString(folderName));
				}
				if (dstContainer instanceof IFolder && !dstContainer.exists())
					((IFolder) dstContainer).create(true, true, monitor);
				generateFiles(zipFile, path.append(name), dstContainer, false, binary, monitor);
			} else {
				if (isOkToCreateFile(new File(path.toFile(), name))) {
					if (firstLevel)
						binary = false;
					try (InputStream in = zipFile.getInputStream(zipEnry)) {
						copyFile(name, in, dst, binary, monitor);
					} catch (IOException ioe) {
					}
				}
			}
		}
	}

	/**
	 * Tests if the folder found in the template location should be created in
	 * the target project. Subclasses may use this method to conditionally block
	 * the creation of entire directories (subject to user choices).
	 *
	 * @param sourceFolder
	 *            the folder that is tested
	 * @return <code>true</code> if the provided folder should be created in
	 *         the workspace, <code>false</code> if the values of the
	 *         substitution variables indicate otherwise.
	 */
	protected boolean isOkToCreateFolder(File sourceFolder) {
		boolean extensibleTemplateSelected = UpdateSplashHandlerAction.isExtensibleTemplateSelected(fTemplate);
		String sourceFolderString = sourceFolder.toString();

		if ((extensibleTemplateSelected == false) && sourceFolderString.endsWith("icons")) { //$NON-NLS-1$
			return false;
		} else if ((extensibleTemplateSelected == false) && sourceFolderString.endsWith("schema")) { //$NON-NLS-1$
			return false;
		}

		return true;
	}

	/**
	 * Tests if the file found in the template location should be created in the
	 * target project. Subclasses may use this method to conditionally block
	 * creation of the file (subject to user choices).
	 *
	 * @param sourceFile
	 *            the file found in the template location that needs to be
	 *            created.
	 * @return <samp>true </samp> if the specified file should be created in the
	 *         project or <samp>false </samp> to skip it. The default
	 *         implementation is <samp>true </samp>.
	 */
	protected boolean isOkToCreateFile(File sourceFile) {
		String javaSuffix = ".java"; //$NON-NLS-1$
		String targetFile = fClass + javaSuffix;
		String copyFile = sourceFile.toString();
		// Prevent needless copying
		// TODO: MP: SPLASH: Propagate / share copy prevention code with org.eclipse.pde.internal.ui.templates.ide.SplashHandlersTemplate
		if (copyFile.endsWith(javaSuffix)) {
			if ((copyFile.endsWith(targetFile) == false) || fProject.exists(IPath.fromOSString("src" + '/' + fPackage.replace('.', '/') + '/' + targetFile))) { //$NON-NLS-1$
				return false;
			}
		} else if (copyFile.endsWith("splash.bmp") && //$NON-NLS-1$
				(fProject.exists(IPath.fromOSString("splash.bmp")))) { //$NON-NLS-1$
			return false;
		} else if (copyFile.endsWith(".png")) { //$NON-NLS-1$
			if (copyFile.endsWith("af.png") && //$NON-NLS-1$
					fProject.exists(IPath.fromOSString("icons/af.png"))) { //$NON-NLS-1$
				return false;
			} else if (copyFile.endsWith("embedded.png") && //$NON-NLS-1$
					fProject.exists(IPath.fromOSString("icons/embedded.png"))) { //$NON-NLS-1$
				return false;
			} else if (copyFile.endsWith("enterprise.png") && //$NON-NLS-1$
					fProject.exists(IPath.fromOSString("icons/enterprise.png"))) { //$NON-NLS-1$
				return false;
			} else if (copyFile.endsWith("rcp.png") && //$NON-NLS-1$
					fProject.exists(IPath.fromOSString("icons/rcp.png"))) { //$NON-NLS-1$
				return false;
			} else if (copyFile.endsWith("languages.png") && //$NON-NLS-1$
					fProject.exists(IPath.fromOSString("icons/languages.png"))) { //$NON-NLS-1$
				return false;
			}
		} else if (copyFile.endsWith("splashExtension.exsd") && //$NON-NLS-1$
				(fProject.exists(IPath.fromOSString("schema/splashExtension.exsd")))) { //$NON-NLS-1$
			return false;
		}

		return true;
	}

	/**
	 * Returns the folder with Java files in the target project. The default
	 * implementation looks for source folders in the classpath of the target
	 * folders and picks the first one encountered. Subclasses may override this
	 * behavior.
	 *
	 * @param monitor
	 *            progress monitor to use
	 * @return source folder that will be used to generate Java files or
	 *         <samp>null </samp> if none found.
	 */

	protected IFolder getSourceFolder(IProgressMonitor monitor) {
		IFolder sourceFolder = null;

		try {
			IJavaProject javaProject = JavaCore.create(fProject);
			IClasspathEntry[] classpath = javaProject.getRawClasspath();
			for (IClasspathEntry entry : classpath) {
				if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
					IPath path = entry.getPath().removeFirstSegments(1);
					if (path.segmentCount() > 0)
						sourceFolder = fProject.getFolder(path);
					break;
				}
			}
		} catch (JavaModelException e) {
		}
		return sourceFolder;
	}

	/**
	 * @see IVariableProvider#getValue(String)
	 */

	@Override
	public Object getValue(String key) {
		return getKeyValue(key);
	}

	private IFolder generateJavaSourceFolder(IFolder sourceFolder, IProgressMonitor monitor) throws CoreException {
		Object packageValue = getValue(KEY_PACKAGE_NAME);
		//
		String packageName = packageValue != null ? packageValue.toString() : null;
		if (packageName == null)
			packageName = fModel.getPluginBase().getId();
		IPath path = IPath.fromOSString(packageName.replace('.', File.separatorChar));
		if (sourceFolder != null)
			path = sourceFolder.getProjectRelativePath().append(path);

		for (int i = 1; i <= path.segmentCount(); i++) {
			IPath subpath = path.uptoSegment(i);
			IFolder subfolder = fProject.getFolder(subpath);
			if (subfolder.exists() == false)
				subfolder.create(true, true, monitor);
		}
		return fProject.getFolder(path);
	}

	private String getProcessedString(String fileName, String source) {
		if (source.indexOf('$') == -1)
			return source;
		int loc = -1;
		StringBuilder buffer = new StringBuilder();
		boolean replacementMode = false;
		for (int i = 0; i < source.length(); i++) {
			char c = source.charAt(i);
			if (c == '$') {
				if (replacementMode) {
					String key = source.substring(loc, i);
					String value = key.length() == 0 ? "$" //$NON-NLS-1$
							: getReplacementString(fileName, key);
					buffer.append(value);
					replacementMode = false;
				} else {
					replacementMode = true;
					loc = i + 1;
					continue;
				}
			} else if (!replacementMode)
				buffer.append(c);
		}
		return buffer.toString();
	}

	/**
	 * The default implementation of this method provides values of the
	 * following keys: <samp>pluginClass </samp>, <samp>pluginId </samp> and
	 * <samp>pluginName </samp>.
	 *
	 * @see ITemplateSection#getReplacementString(String,String)
	 */
	public String getReplacementString(String fileName, String key) {
		String result = getKeyValue(key);
		return result != null ? result : key;
	}

	private String getKeyValue(String key) {
		if (fModel == null)
			return null;

		// TODO: MP: SPLASH: Broken and not needed, underlying model does not have class, id, translated name parameters defined
		if (key.equals(KEY_PLUGIN_CLASS) && fModel instanceof IPluginModel) {
			IPlugin plugin = (IPlugin) fModel.getPluginBase();
			return plugin.getClassName();
		}

		// TODO: MP: SPLASH: Broken and not needed, underlying model does not have class, id, translated name parameters defined
		if (key.equals(KEY_ACTIVATOR_SIMPLE) && fModel instanceof IPluginModel) {
			IPlugin plugin = (IPlugin) fModel.getPluginBase();
			String qualified = plugin.getClassName();
			if (qualified != null) {
				int lastDot = qualified.lastIndexOf('.');
				return (lastDot != -1 && lastDot < qualified.length() - 1) ? qualified.substring(lastDot + 1) : qualified;
			}
		}

		if (key.equals(KEY_PLUGIN_ID)) {
			return fPluginID;
			// Old implementation, does not work
			//IPluginBase plugin = fModel.getPluginBase();
			//return plugin.getId();
		}
		// TODO: MP: SPLASH: Broken and not needed, underlying model does not have class, id, translated name parameters defined
		if (key.equals(KEY_PLUGIN_NAME)) {
			IPluginBase plugin = fModel.getPluginBase();
			return plugin.getTranslatedName();
		}

		if (key.equals(KEY_PACKAGE_NAME) && fModel instanceof IPluginModel) {
			return fPackage;
			// Old implementation, does not work
//			IPlugin plugin = (IPlugin) fModel.getPluginBase();
//			String qualified = plugin.getClassName();
//			if (qualified != null) {
//				int lastDot = qualified.lastIndexOf('.');
//				return (lastDot != -1) ? qualified.substring(0, lastDot) : qualified;
//			}
		}
		return null;
	}

	private InputStream getProcessedStream(String fileName, InputStream stream, boolean binary) throws IOException, CoreException {
		if (binary)
			return stream;

		InputStreamReader reader = new InputStreamReader(stream);
		int bufsize = 1024;
		char[] cbuffer = new char[bufsize];
		int read = 0;
		StringBuilder keyBuffer = new StringBuilder();
		StringBuilder outBuffer = new StringBuilder();
		StringBuilder preBuffer = new StringBuilder();
		boolean newLine = true;
		ControlStack preStack = new ControlStack();
		preStack.setValueProvider(this);

		boolean replacementMode = false;
		boolean preprocessorMode = false;
		boolean escape = false;
		while (read != -1) {
			read = reader.read(cbuffer);
			for (int i = 0; i < read; i++) {
				char c = cbuffer[i];

				if (escape) {
					StringBuilder buf = preprocessorMode ? preBuffer : outBuffer;
					buf.append(c);
					escape = false;
					continue;
				}

				if (newLine && c == '%') {
					// preprocessor line
					preprocessorMode = true;
					preBuffer.delete(0, preBuffer.length());
					continue;
				}
				if (preprocessorMode) {
					if (c == '\\') {
						escape = true;
						continue;
					}
					if (c == '\n') {
						// handle line
						preprocessorMode = false;
						newLine = true;
						String line = preBuffer.toString().trim();
						preStack.processLine(line);
						continue;
					}
					preBuffer.append(c);

					continue;
				}

				if (preStack.getCurrentState() == false) {
					continue;
				}

				if (c == '$') {
					if (replacementMode) {
						replacementMode = false;
						String key = keyBuffer.toString();
						String value = key.length() == 0 ? "$" //$NON-NLS-1$
								: getReplacementString(fileName, key);
						outBuffer.append(value);
						keyBuffer.delete(0, keyBuffer.length());
					} else {
						replacementMode = true;
					}
				} else {
					if (replacementMode)
						keyBuffer.append(c);
					else {
						outBuffer.append(c);
						if (c == '\n') {
							newLine = true;
						} else
							newLine = false;
					}
				}
			}
		}
		return new ByteArrayInputStream(outBuffer.toString().getBytes(fProject.getDefaultCharset()));
	}

	private String[] getDirectoryCandidates() {
		double version = getTargetVersion();
		ArrayList<String> result = new ArrayList<>();
		if (version >= 3.3)
			result.add("templates_3.3" + "/" + getSectionId() + "/"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		if (version >= 3.2)
			result.add("templates_3.2" + "/" + getSectionId() + "/"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		if (version >= 3.1)
			result.add("templates_3.1" + "/" + getSectionId() + "/"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		if (version >= 3.0)
			result.add("templates_3.0" + "/" + getSectionId() + "/"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		return result.toArray(new String[result.size()]);
	}

	public String getSectionId() {
		return ISplashHandlerConstants.F_UNQUALIFIED_EXTENSION_ID;
	}

	protected double getTargetVersion() {
		try {
			IPluginBase plugin = fModel.getPluginBase();
			if (plugin instanceof IBundlePluginBase)
				return Double.parseDouble(((IBundlePluginBase) plugin).getTargetVersion());
		} catch (NumberFormatException e) {
		}
		return TargetPlatformHelper.getTargetVersion();
	}

}
