/*******************************************************************************
 * Copyright (c) 2017, 2018 GK Software AG, 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:
 *     Stephan Herrmann - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.core;

import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jdt.core.IClasspathAttribute;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaModelStatusConstants;
import org.eclipse.jdt.core.IModuleDescription;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.env.IModuleAwareNameEnvironment;
import org.eclipse.jdt.internal.compiler.env.IUpdatableModule;
import org.eclipse.jdt.internal.compiler.env.IUpdatableModule.UpdateKind;
import org.eclipse.jdt.internal.compiler.env.IUpdatableModule.UpdatesByKind;
import org.eclipse.jdt.internal.compiler.lookup.ModuleBinding;
import org.eclipse.jdt.internal.core.util.Util;

/**
 * An instance of this class collects <code>add-exports</code> and <code>add-reads</code> options from
 * a project's class path entries, and performs the corresponding updates when requested by the compiler.
 * <p>For <code>patch-module</code> and <code>limit-modules</code> see
 * org.eclipse.jdt.internal.core.builder.ModuleEntryProcessor.</p>
 */
public class ModuleUpdater {

	private final JavaProject javaProoject;

	private final Map<String,UpdatesByKind> moduleUpdates = new HashMap<>();

	public ModuleUpdater(JavaProject javaProject) {
		this.javaProoject = javaProject;
	}

	/**
	 * Detects any ADD_EXPORTS or ADD_READS classpath attributes, parses the value,
	 * and collects the resulting module updates.
	 * @param entry a classpath entry of the current project.
	 */
	public void computeModuleUpdates(IClasspathEntry entry) throws JavaModelException {
		for (IClasspathAttribute attribute : entry.getExtraAttributes()) {
			String attributeName = attribute.getName();
			String values = attribute.getValue(); // the attributes considered here may have multiple values separated by ':'
			if (attributeName.equals(IClasspathAttribute.ADD_EXPORTS)) {
				for (String value : values.split(":")) { // format: <source-module>/<package>=<target-module>(,<target-module>)* //$NON-NLS-1$
					int slash = value.indexOf('/');
					int equals = value.indexOf('=');
					if (slash != -1 && equals != -1) {
						String modName = value.substring(0, slash);
						char[] packName = value.substring(slash+1, equals).toCharArray();
						char[][] targets = CharOperation.splitOn(',', value.substring(equals+1).toCharArray());
						addModuleUpdate(modName, new IUpdatableModule.AddExports(packName, targets), UpdateKind.PACKAGE);
					} else {
						Util.log(IStatus.WARNING, "Invalid argument to add-exports: "+value); //$NON-NLS-1$
					}
				}
			} else if (attributeName.equals(IClasspathAttribute.ADD_READS)) {
				for (String value : values.split(":")) { // format: <source-module>=<target-module> //$NON-NLS-1$
					int equals = value.indexOf('=');
					if (equals != -1) {
						String srcMod = value.substring(0, equals);
						char[] targetMod = value.substring(equals+1).toCharArray();
						addModuleUpdate(srcMod, new IUpdatableModule.AddReads(targetMod), UpdateKind.MODULE);
					} else {
						Util.log(IStatus.WARNING, "Invalid argument to add-reads: "+value); //$NON-NLS-1$
					}
				}
			} else if (attributeName.equals(IClasspathAttribute.MODULE_MAIN_CLASS)) {
				IModuleDescription module = this.javaProoject.getModuleDescription();
				if (module == null)
					throw new JavaModelException(new JavaModelStatus(IJavaModelStatusConstants.ELEMENT_DOES_NOT_EXIST));
				addModuleUpdate(module.getElementName(), m -> m.setMainClassName(values.toCharArray()), UpdateKind.MODULE);
			}
		}
	}

	public void addModuleUpdate(String moduleName, Consumer<IUpdatableModule> update, UpdateKind kind) {
		UpdatesByKind updates = this.moduleUpdates.get(moduleName);
		if (updates == null) {
			this.moduleUpdates.put(moduleName, updates = new UpdatesByKind());
		}
		updates.getList(kind, true).add(update);
	}

	/**
	 * @see IModuleAwareNameEnvironment#applyModuleUpdates(IUpdatableModule, UpdateKind)
	 */
	public void applyModuleUpdates(IUpdatableModule compilerModule, UpdateKind kind) {
		char[] name = compilerModule.name();
		if (name != ModuleBinding.UNNAMED) { // can't update the unnamed module
			UpdatesByKind updates = this.moduleUpdates.get(String.valueOf(name));
			if (updates != null) {
				for (Consumer<IUpdatableModule> update : updates.getList(kind, false))
					update.accept(compilerModule);
			}
		}
	}

	private static boolean containsNonModularDependency(IClasspathEntry[] entries) {
		for (IClasspathEntry e : entries) {
			if (e.getEntryKind() != IClasspathEntry.CPE_SOURCE && !((ClasspathEntry) e).isModular())
				return true;
		}
		return false;
	}

	// Bug 520713: allow test code to access code on the classpath
	public void addReadUnnamedForNonEmptyClasspath(JavaProject project, IClasspathEntry[] expandedClasspath)
			throws JavaModelException {
		for (String moduleName : determineModulesOfProjectsWithNonEmptyClasspath(project, expandedClasspath)) {
			addModuleUpdate(moduleName, new IUpdatableModule.AddReads(ModuleBinding.ALL_UNNAMED), UpdateKind.MODULE);
		}
	}

	public static Set<String> determineModulesOfProjectsWithNonEmptyClasspath(JavaProject project,
			IClasspathEntry[] expandedClasspath) throws JavaModelException {
		LinkedHashSet<String> list = new LinkedHashSet<>();
		if (containsNonModularDependency(expandedClasspath)) {
			IModuleDescription moduleDescription = project.getModuleDescription();
			if (moduleDescription != null) {
				list.add(moduleDescription.getElementName());
			}
		}
		for (IClasspathEntry e1 : expandedClasspath) {
			if (e1.getEntryKind() == IClasspathEntry.CPE_PROJECT) {
				Object target = JavaModel.getTarget(e1.getPath(), true);
				if (target instanceof IProject) {
					IProject prereqProject = (IProject) target;
					if (JavaProject.hasJavaNature(prereqProject)) {
						JavaProject prereqJavaProject = (JavaProject) JavaCore.create(prereqProject);
						if (containsNonModularDependency(prereqJavaProject.getResolvedClasspath())) {
							IModuleDescription prereqModuleDescription = prereqJavaProject.getModuleDescription();
							if (prereqModuleDescription != null) {
								list.add(prereqModuleDescription.getElementName());
							}
						}
					}
				}
			}
		}
		return list;
	}
	public UpdatesByKind getUpdates(String moduleName) {
		return this.moduleUpdates.get(moduleName);
	}
}
