/*
 * $Id$
 *
 * SARL is an general-purpose agent programming language.
 * More details on http://www.sarl.io
 *
 * Copyright (C) 2014-2026 SARL.io, the original authors and main authors.
 *
 * Licensed 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.
 *
 *------- FORKED SOURCE CODE:
 *
 * THIS CODE IS FORKED FROM JDK.JAVADOC INTERNAL PACKAGE AND ADAPTED TO THE SARL PURPOSE.
 * THE FORK WAS NECESSARY BECAUSE IT IS IMPOSSIBLE TO SUBCLASS THE TYPES FOR THE.
 * STANDARD HTML DOCLET THAT IS PROVIDED BY JDK.JAVADOC MODULE.
 *
 * Copyright (c) 2003, 2021, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package io.sarl.docs.doclet2.framework;

import static javax.lang.model.element.Modifier.ABSTRACT;
import static javax.lang.model.element.Modifier.FINAL;
import static javax.lang.model.element.Modifier.PRIVATE;
import static javax.lang.model.element.Modifier.PROTECTED;
import static javax.lang.model.element.Modifier.PUBLIC;
import static javax.lang.model.element.Modifier.STATIC;
import static javax.lang.model.type.TypeKind.NONE;

import java.lang.annotation.Documented;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.ModuleElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.RecordComponentElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ErrorType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.IntersectionType;
import javax.lang.model.type.NoType;
import javax.lang.model.type.NullType;
import javax.lang.model.type.PrimitiveType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.type.UnionType;
import javax.lang.model.type.WildcardType;
import javax.lang.model.util.AbstractTypeVisitor9;
import javax.lang.model.util.ElementKindVisitor9;
import javax.lang.model.util.Elements;
import javax.lang.model.util.SimpleAnnotationValueVisitor9;
import javax.lang.model.util.SimpleElementVisitor9;
import javax.lang.model.util.SimpleTypeVisitor9;
import javax.lang.model.util.Types;

import org.eclipse.xtext.common.types.JvmVisibility;
import org.eclipse.xtext.util.Strings;

import com.google.inject.Inject;
import com.sun.source.doctree.DocTree;
import com.sun.source.doctree.LinkTree;
import com.sun.source.doctree.ProvidesTree;
import com.sun.source.doctree.ReferenceTree;
import com.sun.source.doctree.SeeTree;
import com.sun.source.doctree.SerialFieldTree;
import com.sun.source.doctree.UsesTree;
import com.sun.source.doctree.ValueTree;
import com.sun.source.util.SimpleDocTreeVisitor;

import io.sarl.lang.core.Agent;
import io.sarl.lang.core.Behavior;
import io.sarl.lang.core.Capacity;
import io.sarl.lang.core.Event;
import io.sarl.lang.core.Skill;
import io.sarl.lang.core.annotation.PrivateAPI;
import io.sarl.lang.jvmmodel.IDefaultVisibilityProvider;
import io.sarl.lang.services.SARLGrammarKeywordAccess;

/** Utilities for object elements.
 *
 * @author $Author: sgalland$
 * @version $FullVersion$
 * @mavengroupid $GroupId$
 * @mavenartifactid $ArtifactId$
 * @since 0.13
 */
public class ElementUtilsImpl implements ElementUtils {

	/** Default name for an element.
	 */
	public static final String DEFAULT_ELEMENT_NAME = "<Unnamed>"; //$NON-NLS-1$

	private static final String SPACE = " "; //$NON-NLS-1$

	private static final Pattern MEMBER_NAME_PATTERN = Pattern.compile("(.+?)(?:\\((.*)\\))?"); //$NON-NLS-1$

	private Map<String, TypeMirror> typeCache = new HashMap<>();

	private Elements elementUtils;

	private Types typeUtils;

	private DocUtils docUtils;

	private SARLGrammarKeywordAccess keywords;

	private TypeMirror agentTypeCache;

	private TypeMirror behaviorTypeCache;

	private TypeMirror skillTypeCache;

	private TypeMirror eventTypeCache;

	private TypeMirror capacityTypeCache;

	private final Comparator<ModuleElement> moduleElementComparator = (a, b) -> {
		if (a == b) {
			return 0;
		}
		if (a == null) {
			return Integer.MIN_VALUE;
		}
		if (b == null) {
			return Integer.MAX_VALUE;
		}
		final var na = a.getQualifiedName().toString();
		final var nb = b.getQualifiedName().toString();
		final var cmp = na.compareToIgnoreCase(nb);
		if (cmp != 0) {
			return cmp;
		}
		return na.compareTo(nb);
	};

	private final Comparator<PackageElement> packageElementComparator = (a, b) -> {
		if (a == b) {
			return 0;
		}
		if (a == null) {
			return Integer.MIN_VALUE;
		}
		if (b == null) {
			return Integer.MAX_VALUE;
		}
		final var na = a.getQualifiedName().toString();
		final var nb = b.getQualifiedName().toString();
		final var cmp = na.compareToIgnoreCase(nb);
		if (cmp != 0) {
			return cmp;
		}
		return na.compareTo(nb);
	};

	private final Comparator<TypeElement> typeElementComparator = (a, b) -> {
		if (a == b) {
			return 0;
		}
		if (a == null) {
			return Integer.MIN_VALUE;
		}
		if (b == null) {
			return Integer.MAX_VALUE;
		}
		final var na = a.getQualifiedName().toString();
		final var nb = b.getQualifiedName().toString();
		final var cmp = na.compareToIgnoreCase(nb);
		if (cmp != 0) {
			return cmp;
		}
		return na.compareTo(nb);
	};

	private final Comparator<TypeElement> typeElementBasenameComparator = (a, b) -> {
		if (a == b) {
			return 0;
		}
		if (a == null) {
			return Integer.MIN_VALUE;
		}
		if (b == null) {
			return Integer.MAX_VALUE;
		}
		final var na = a.getSimpleName().toString();
		final var nb = b.getSimpleName().toString();
		final var cmp = na.compareToIgnoreCase(nb);
		if (cmp != 0) {
			return cmp;
		}
		final var na0 = a.getQualifiedName().toString();
		final var nb0 = b.getQualifiedName().toString();
		final var cmp0 = na0.compareToIgnoreCase(nb0);
		if (cmp0 != 0) {
			return cmp0;
		}
		return na0.compareTo(nb0);
	};

	private final Comparator<TypeMirror> typeMirrorComparator = (a, b) -> {
		if (a == b) {
			return 0;
		}
		if (a == null) {
			return Integer.MIN_VALUE;
		}
		if (b == null) {
			return Integer.MAX_VALUE;
		}
		return this.typeElementComparator.compare(asTypeElement(a, this.typeUtils), asTypeElement(b, this.typeUtils));
	};

	private final Comparator<ExecutableElement> executableComparator = (a, b) -> {
		if (a == b) {
			return 0;
		}
		if (a == null) {
			return Integer.MIN_VALUE;
		}
		if (b == null) {
			return Integer.MAX_VALUE;
		}
		final var na0 = a.getSimpleName().toString();
		final var nb0 = b.getSimpleName().toString();
		var nameCmp = na0.compareToIgnoreCase(nb0);
		if (nameCmp != 0) {
			return nameCmp;
		}
		nameCmp = na0.compareTo(nb0);
		if (nameCmp != 0) {
			return nameCmp;
		}
		final var nparamCmp = a.getParameters().size() - b.getParameters().size();
		if (nparamCmp != 0) {
			return nparamCmp;
		}
		assert a.getParameters().size() == b.getParameters().size();
		final var sz = a.getParameters().size();
		for (var i = 0; i < sz; ++i) {
			final var pa = a.getParameters().get(i);
			final var pb = b.getParameters().get(i);
			final var ta = pa.asType();
			final var tb = pb.asType();
			final var cmp = this.typeMirrorComparator.compare(ta, tb);
			if (cmp != 0) {
				return cmp;
			}
		}
		return 0;
	};

	private final Comparator<VariableElement> variableComparator = (a, b) -> {
		if (a == b) {
			return 0;
		}
		if (a == null) {
			return Integer.MIN_VALUE;
		}
		if (b == null) {
			return Integer.MAX_VALUE;
		}
		final var na0 = a.getSimpleName().toString();
		final var nb0 = b.getSimpleName().toString();
		final var cmp = na0.compareToIgnoreCase(nb0);
		if (cmp != 0) {
			return cmp;
		}
		return na0.compareTo(nb0);
	};

	/** Replies the root type for the SARL agents.
	 *
	 * @return the root agent type.
	 */
	protected TypeMirror getAgentType() {
		if (this.agentTypeCache == null) {
			this.agentTypeCache = this.elementUtils.getTypeElement(Agent.class.getName()).asType();
		}
		return this.agentTypeCache;
	}

	/** Replies the root type for the SARL behaviors.
	 *
	 * @return the root behavior type.
	 */
	protected TypeMirror getBehaviorType() {
		if (this.behaviorTypeCache == null) {
			this.behaviorTypeCache = this.elementUtils.getTypeElement(Behavior.class.getName()).asType();
		}
		return this.behaviorTypeCache;
	}

	/** Replies the root type for the SARL skills.
	 *
	 * @return the root skill type.
	 */
	protected TypeMirror getSkillType() {
		if (this.skillTypeCache == null) {
			this.skillTypeCache = this.elementUtils.getTypeElement(Skill.class.getName()).asType();
		}
		return this.skillTypeCache;
	}

	/** Replies the root type for the SARL events.
	 *
	 * @return the root event type.
	 */
	protected TypeMirror getEventType() {
		if (this.eventTypeCache == null) {
			this.eventTypeCache = this.elementUtils.getTypeElement(Event.class.getName()).asType();
		}
		return this.eventTypeCache;
	}

	/** Replies the root type for the SARL capacities.
	 *
	 * @return the root capacity type.
	 */
	protected TypeMirror getCapacityType() {
		if (this.capacityTypeCache == null) {
			this.capacityTypeCache = this.elementUtils.getTypeElement(Capacity.class.getName()).asType();
		}
		return this.capacityTypeCache;
	}

	/** Change the documentation utilities.
	 *
	 * @param utils the utilities.
	 */
	@Inject
	public void setDocUtils(DocUtils utils) {
		this.docUtils = utils;
	}

	/** Replies the documentation utilities.
	 *
	 * @return the utilities.
	 */
	public DocUtils getDocUtils() {
		return this.docUtils;
	}

	/** Change the accessor to the SARL keywords.
	 *
	 * @param accessor is the accessor.
	 */
	@Inject
	public void setSARLGrammarKeywordAccess(SARLGrammarKeywordAccess accessor) {
		this.keywords = accessor;
	}

	/** Replies the accessor to the SARL keywords.
	 *
	 * @return the accessor.
	 */
	public SARLGrammarKeywordAccess getSARLGrammarKeywordAccess() {
		return this.keywords;
	}

	@Override
	public void setElements(Elements elementUtils) {
		this.elementUtils = elementUtils;
	}

	@Override
	public void setTypes(Types typeUtils) {
		this.typeUtils = typeUtils;
	}

	private static boolean isNoType(TypeMirror t) {
		return t.getKind() == NONE;
	}

	@Override
	public TypeMirror getSymbol(String signature, QualifiedNameSetBuilder findQualifiedNames) {
		final var signatures = new LinkedList<String>();
		signatures.addLast(signature);
		var useQualifiedNames = findQualifiedNames != null;
		while (!signatures.isEmpty()) {
			final var sig = signatures.removeFirst();
			var type = this.typeCache.get(sig);
			if (type != null) {
				return type;
			}
			var typeElement = this.elementUtils.getTypeElement(sig);
			if (typeElement != null) {
				type = typeElement.asType();
				if (type != null) {
					this.typeCache.put(sig, type);
					return type;
				}
			}
			if (useQualifiedNames) {
				assert findQualifiedNames != null;
				useQualifiedNames = false;
				final var qualifiedNames = findQualifiedNames.buildCandidateList(sig);
				if (qualifiedNames != null) {
					for (final var qn : qualifiedNames) {
						signatures.addLast(qn);
					}
				}
			}
		}
		return null;
	}

	@Override
	public TypeMirror getObjectType() {
		return getSymbol(Object.class.getName());
	}

	/** Replies if the given type is annotated.
	 *
	 * @param e the mirror.
	 * @return {@code true} if the mirror is annotated.
	 */
	public static boolean isAnnotated(TypeMirror e) {
		return !e.getAnnotationMirrors().isEmpty();
	}

	@Override
	public TypeElement asTypeElement(TypeMirror type, Types typeUtils) {
		return new SimpleTypeVisitor9<TypeElement, Void>() {

			@Override
			public TypeElement visitDeclared(DeclaredType t, Void p) {
				return (TypeElement) t.asElement();
			}

			@Override
			public TypeElement visitArray(ArrayType t, Void p) {
				return visit(t.getComponentType());
			}

			@Override
			public TypeElement visitTypeVariable(TypeVariable t, Void p) {
				if (isAnnotated(t)) {
					return visit(t.asElement().asType());
				}
				return visit(typeUtils.erasure(t));
			}

			@Override
			public TypeElement visitWildcard(WildcardType t, Void p) {
				return visit(typeUtils.erasure(t));
			}

			@Override
			public TypeElement visitError(ErrorType t, Void p) {
				return (TypeElement) t.asElement();
			}

			@Override
			protected TypeElement defaultAction(TypeMirror e, Void p) {
				return super.defaultAction(e, p);
			}
		}.visit(type);
	}

	@Override
	public boolean isPublic(Element element) {
		return element.getModifiers().contains(Modifier.PUBLIC);
	}

	@Override
	public boolean isProtected(Element element) {
		return element.getModifiers().contains(Modifier.PROTECTED);
	}

	/** Replies the element that may be considered as includable from the given element.
	 *
	 * @param element the source element.
	 * @return the includable element from the inut.
	 */
	protected Element toIncludableElement(Element element) {
		switch (element.getKind()) {
		case ANNOTATION_TYPE:
		case CLASS:
		case ENUM:
		case INTERFACE:
		case MODULE:
		case PACKAGE:
			return element;
		case CONSTRUCTOR:
		case ENUM_CONSTANT:
		case EXCEPTION_PARAMETER:
		case FIELD:
		case INSTANCE_INIT:
		case LOCAL_VARIABLE:
		case METHOD:
		case PARAMETER:
		case RESOURCE_VARIABLE:
		case STATIC_INIT:
		case TYPE_PARAMETER:
			return getEnclosingTypeElement(element);
		case OTHER:
		default:
			break;
		}
		return null;
	}

	@Override
	public boolean isExternal(Element element, SarlDocletEnvironment environment) {
		final var packageElement = environment.getElementUtils().getPackageOf(element);
		if (packageElement == null || packageElement.isUnnamed()) {
			return false;
		}
		final var elt = toIncludableElement(element);
		return !environment.getIncludedElements().contains(elt);
	}

	@Override
	public boolean isLinkable(TypeElement typeElem, SarlDocletEnvironment environment) {
		return
				(typeElem != null && environment.isIncluded(typeElem)) ||
				(isExternal(typeElem, environment) && (isPublic(typeElem) || isProtected(typeElem)));
	}

	@Override
	public TypeMirror getFirstVisibleSuperType(TypeElement typeElement, boolean assumeObject, SarlDocletEnvironment environment) {
		var superType = typeElement.getSuperclass();
		if (isNoType(superType)) {
			if (assumeObject) {
				superType = getObjectType();
			} else {
				return null;
			}
		}
		var superClass = asTypeElement(superType, environment.getTypeUtils());
		// skip "hidden" classes
		while (superClass != null
				&& ((!environment.isIncluded(superClass))
				|| (!isPublic(superClass) && !isLinkable(superClass, environment)))) {
			var supersuperType = superClass.getSuperclass();
			var supersuperClass = asTypeElement(supersuperType, environment.getTypeUtils());
			if (supersuperClass == null
					|| supersuperClass.getQualifiedName().equals(superClass.getQualifiedName())) {
				break;
			}
			superType = supersuperType;
			superClass = supersuperClass;
		}
		if (typeElement.asType().equals(superType)) {
			return null;
		}
		return superType;
	}

	@Override
	public String getElementName(ModuleElement element) {
		if (element == null || element.isUnnamed()) {
			return DEFAULT_ELEMENT_NAME;
		}
		return element.getQualifiedName().toString();
	}

	@Override
	public String getElementName(PackageElement element) {
		if (element == null || element.isUnnamed()) {
			return DEFAULT_ELEMENT_NAME;
		}
		return element.getQualifiedName().toString();
	}

	@Override
	public SortedSet<? extends TypeMirror> getAllInterfaces(TypeElement typeElement, SarlDocletEnvironment environment) {
		final var candidates = new LinkedList<TypeMirror>();
		final var done = new TreeSet<TypeMirror>((a, b) -> a.toString().compareTo(b.toString()));
		final var interfaces = new TreeSet<TypeMirror>((a, b) -> a.toString().compareTo(b.toString()));
		final var types = environment.getTypeUtils();
		//
		var superType = typeElement.getSuperclass();
		if (superType.getKind() != TypeKind.NONE) {
			candidates.add(superType);
		}
		candidates.addAll(typeElement.getInterfaces());
		//
		while (!candidates.isEmpty()) {
			final var candidate = candidates.removeFirst();
			if (done.add(candidate)) {
				final var superElement = asTypeElement(candidate, types);
				if (superElement.getKind() == ElementKind.INTERFACE) {
					interfaces.add(candidate);
				} else {
					superType = superElement.getSuperclass();
					if (superType.getKind() != TypeKind.NONE) {
						candidates.add(superType);
					}
				}
				candidates.addAll(superElement.getInterfaces());
			}
		}
		return interfaces;
	}

	/** Replies the default visibility for the given element. This function uses the default visibility flags from
	 * the SARL compiler.
	 * 
	 * @param element the element to test.
	 * @return the visibility modifier.
	 */
	protected Modifier getDefaultVisibilityFor(Element element) {
		var jvmVisibility = JvmVisibility.PUBLIC;
		final var enclosing = element.getEnclosingElement();
		if (enclosing != null) {
			final var enclosingType = enclosing.asType();
			final var tester = new IDefaultVisibilityProvider.Tester() {
				@Override
				public boolean isInterface() {
					return enclosing.getKind() == ElementKind.INTERFACE;
				}

				@Override
				public boolean isEvent() {
					return enclosing.getKind() == ElementKind.CLASS
							&& ElementUtilsImpl.this.typeUtils.isAssignable(enclosingType, getEventType());
				}

				@Override
				public boolean isAnnotationType() {
					return enclosing.getKind() == ElementKind.ANNOTATION_TYPE;
				}

				@Override
				public boolean isAgent() {
					return enclosing.getKind() == ElementKind.CLASS
							&& ElementUtilsImpl.this.typeUtils.isAssignable(enclosingType, getAgentType());
				}
			};
			switch (element.getKind()) {
			case ANNOTATION_TYPE:
				jvmVisibility = IDefaultVisibilityProvider.getAnnotationTypeDefaultVisibilityIn(tester);
				break;
			case CLASS:
				jvmVisibility = IDefaultVisibilityProvider.getClassDefaultVisibilityIn(tester);
				break;
			case ENUM:
				jvmVisibility = IDefaultVisibilityProvider.getEnumerationDefaultVisibilityIn(tester);
				break;
			case FIELD:
				jvmVisibility = IDefaultVisibilityProvider.getFieldDefaultVisibilityIn(tester);
				break;
			case INTERFACE:
				jvmVisibility = IDefaultVisibilityProvider.getInterfaceDefaultVisibilityIn(tester);
				break;
			case METHOD:
				jvmVisibility = IDefaultVisibilityProvider.getActionDefaultVisibilityIn(tester);
				break;
			default:
			}
		}
		switch (jvmVisibility) {
		case PRIVATE:
			return Modifier.PRIVATE;
		case PROTECTED:
			return Modifier.PROTECTED;
		case PUBLIC:
			return Modifier.PUBLIC;
		case DEFAULT:
		default:
			return Modifier.DEFAULT;
		}
	}

	@Override
	public String getVisibilityModifiersString(Element element, boolean trailingSpace) {
		final var modifierList = new TreeSet<>(element.getModifiers());
		removeModifiersToIgnore(modifierList);
		final var defaultVisibility = getDefaultVisibilityFor(element);
		return new ElementKindVisitor9<String, SortedSet<Modifier>>() {
			final StringBuilder stringRepresentation = new StringBuilder();

			private void addVisibilityModifier(Set<Modifier> modifiers) {
				if (modifiers.contains(PUBLIC)) {
					if (defaultVisibility == null || defaultVisibility != PUBLIC) {
						this.stringRepresentation.append(getSARLGrammarKeywordAccess().getPublicKeyword()).append(SPACE);
					}
				} else if (modifiers.contains(PROTECTED)) {
					if (defaultVisibility == null || defaultVisibility != PROTECTED) {
						this.stringRepresentation.append(getSARLGrammarKeywordAccess().getProtectedKeyword()).append(SPACE);
					}
				} else if (modifiers.contains(PRIVATE)) {
					if (defaultVisibility == null || defaultVisibility != PRIVATE) {
						this.stringRepresentation.append(getSARLGrammarKeywordAccess().getPrivateKeyword()).append(SPACE);
					}
				} else if (defaultVisibility == null) {
					this.stringRepresentation.append(getSARLGrammarKeywordAccess().getPackageKeyword()).append(SPACE);
				}
			}

			private String finalString(String s) {
				this.stringRepresentation.append(s);
				if (trailingSpace) {
					if (this.stringRepresentation.lastIndexOf(SPACE) == this.stringRepresentation.length() - 1) {
						return this.stringRepresentation.toString();
					}
					return this.stringRepresentation.append(SPACE).toString();
				}
				return this.stringRepresentation.toString().trim();
			}

			@Override
			public String visitTypeAsInterface(TypeElement elt, SortedSet<Modifier> param) {
				addVisibilityModifier(param);
				return finalString(getSARLGrammarKeywordAccess().getInterfaceKeyword());
			}

			@Override
			public String visitTypeAsEnum(TypeElement elt, SortedSet<Modifier> param) {
				addVisibilityModifier(param);
				return finalString(getSARLGrammarKeywordAccess().getEnumKeyword());
			}

			@Override
			public String visitTypeAsAnnotationType(TypeElement elt, SortedSet<Modifier> param) {
				addVisibilityModifier(param);
				return finalString(getSARLGrammarKeywordAccess().getAnnotationKeyword());
			}

			@Override
			public String visitTypeAsClass(TypeElement elt, SortedSet<Modifier> param) {
				addVisibilityModifier(param);
				return finalString(getSARLGrammarKeywordAccess().getClassKeyword());
			}

			@Override
			public String visitTypeAsRecord(TypeElement elt, SortedSet<Modifier> param) {
				addVisibilityModifier(param);
				//TODO: Record is not part of the SARL syntax yet
				return finalString("record"); //$NON-NLS-1$
			}

			@Override
			protected String defaultAction(Element elt, SortedSet<Modifier> param) {
				addVisibilityModifier(param);
				return this.stringRepresentation.toString().trim();
			}

		}.visit(element, modifierList);
	}

	private static void removeModifiersToIgnore(SortedSet<Modifier> modifierList) {
		modifierList.remove(Modifier.NATIVE);
		modifierList.remove(Modifier.STRICTFP);
		modifierList.remove(Modifier.SYNCHRONIZED);
	}

	@Override
	public String getModifiersString(Element element, boolean trailingSpace, boolean showFinalModifier, boolean showVarValModifier) {
		final var modifierList = new TreeSet<>(element.getModifiers());
		removeModifiersToIgnore(modifierList);
		final var defaultVisibility = getDefaultVisibilityFor(element);
		return new ElementKindVisitor9<String, SortedSet<Modifier>>() {
			final StringBuilder stringRepresentation = new StringBuilder();

			private void addVisibilityModifier(Set<Modifier> modifiers) {
				if (modifiers.contains(PUBLIC)) {
					if (defaultVisibility == null || defaultVisibility != PUBLIC) {
						this.stringRepresentation.append(getSARLGrammarKeywordAccess().getPublicKeyword()).append(SPACE);
					}
				} else if (modifiers.contains(PROTECTED)) {
					if (defaultVisibility == null || defaultVisibility != PROTECTED) {
						this.stringRepresentation.append(getSARLGrammarKeywordAccess().getProtectedKeyword()).append(SPACE);
					}
				} else if (modifiers.contains(PRIVATE)) {
					if (defaultVisibility == null || defaultVisibility != PRIVATE) {
						this.stringRepresentation.append(getSARLGrammarKeywordAccess().getPrivateKeyword()).append(SPACE);
					}
				} else if (defaultVisibility == null) {
					this.stringRepresentation.append(getSARLGrammarKeywordAccess().getPackageKeyword()).append(SPACE);
				}
			}

			private void addStaticModifier(Set<Modifier> modifiers) {
				if (modifiers.contains(STATIC)) {
					this.stringRepresentation.append(getSARLGrammarKeywordAccess().getStaticStaticKeyword()).append(SPACE);
				}
			}

			private void addAbstractModifier(Set<Modifier> modifiers, ElementKind elementType) {
				if (modifiers.contains(ABSTRACT) && elementType != ElementKind.INTERFACE) {
					this.stringRepresentation.append(getSARLGrammarKeywordAccess().getAbstractKeyword()).append(SPACE);
				}
			}

			private void addFinalModifier(Set<Modifier> modifiers) {
				if (modifiers.contains(FINAL) && showFinalModifier) {
					this.stringRepresentation.append(getSARLGrammarKeywordAccess().getFinalKeyword()).append(SPACE);
				}
			}

			private void addVarValModifier(Set<Modifier> modifiers) {
				if (showVarValModifier) {
					final String kw;
					if (modifiers.contains(FINAL)) {
						kw = getSARLGrammarKeywordAccess().getValKeyword();
					} else {
						kw = getSARLGrammarKeywordAccess().getWriteableVarKeyword();
					}
					this.stringRepresentation.append(kw).append(SPACE);
				}
			}

			private void addOtherModifiers(Set<Modifier> modifiers) {
				final var str = modifierList.stream().filter(
						it -> it != null && it != PUBLIC && it != PROTECTED && it != PRIVATE && it != STATIC && it != FINAL && it != ABSTRACT)
						.map(Modifier::toString).collect(Collectors.joining(SPACE));
				this.stringRepresentation.append(str);
				if (!str.isEmpty()) {
					this.stringRepresentation.append(SPACE);
				}
			}

			private String addTypeModifier(String s) {
				this.stringRepresentation.append(s);
				if (trailingSpace) {
					if (this.stringRepresentation.lastIndexOf(SPACE) == this.stringRepresentation.length() - 1) {
						return this.stringRepresentation.toString();
					}
					return this.stringRepresentation.append(SPACE).toString();
				}
				return this.stringRepresentation.toString().trim();
			}

			private String getSarlSpecificInterfaceKeyword(TypeElement type) {
				final var tm = type.asType();
				if (ElementUtilsImpl.this.typeUtils.isAssignable(tm, getCapacityType())) {
					return getSARLGrammarKeywordAccess().getCapacityKeyword();
				}
				return null;
			}

			private String getSarlSpecificClassKeyword(TypeElement type) {
				final var tm = type.asType();
				if (ElementUtilsImpl.this.typeUtils.isAssignable(tm, getAgentType())) {
					return getSARLGrammarKeywordAccess().getAgentKeyword();
				}
				if (ElementUtilsImpl.this.typeUtils.isAssignable(tm, getEventType())) {
					return getSARLGrammarKeywordAccess().getEventKeyword();
				}
				if (ElementUtilsImpl.this.typeUtils.isAssignable(tm, getBehaviorType())) {
					return getSARLGrammarKeywordAccess().getBehaviorKeyword();
				}
				if (ElementUtilsImpl.this.typeUtils.isAssignable(tm, getSkillType())) {
					return getSARLGrammarKeywordAccess().getSkillKeyword();
				}
				return null;
			}

			private void addModifiersInStandardOrder(SortedSet<Modifier> param, ElementKind elementType) {
				addVisibilityModifier(param);
				addStaticModifier(param);
				addAbstractModifier(param, elementType);
				addFinalModifier(param);
				addVarValModifier(param);
				addOtherModifiers(param);
			}

			@Override
			public String visitTypeAsInterface(TypeElement elt, SortedSet<Modifier> param) {
				addModifiersInStandardOrder(param, elt.getKind());
				final var sarlKw = getSarlSpecificInterfaceKeyword(elt);
				if (!Strings.isEmpty(sarlKw)) {
					return addTypeModifier(sarlKw);
				}
				return addTypeModifier(getSARLGrammarKeywordAccess().getInterfaceKeyword());
			}

			@Override
			public String visitTypeAsEnum(TypeElement elt, SortedSet<Modifier> param) {
				addModifiersInStandardOrder(param, elt.getKind());
				return addTypeModifier(getSARLGrammarKeywordAccess().getEnumKeyword());
			}

			@Override
			public String visitTypeAsAnnotationType(TypeElement elt, SortedSet<Modifier> param) {
				addModifiersInStandardOrder(param, elt.getKind());
				return addTypeModifier(getSARLGrammarKeywordAccess().getAnnotationKeyword());
			}

			@Override
			public String visitTypeAsClass(TypeElement elt, SortedSet<Modifier> param) {
				addModifiersInStandardOrder(param, elt.getKind());
				final var sarlKw = getSarlSpecificClassKeyword(elt);
				if (!Strings.isEmpty(sarlKw)) {
					return addTypeModifier(sarlKw);
				}
				return addTypeModifier(getSARLGrammarKeywordAccess().getClassKeyword());
			}

			@Override
			public String visitTypeAsRecord(TypeElement elt, SortedSet<Modifier> param) {
				addModifiersInStandardOrder(param, elt.getKind());
				final var sarlKw = getSarlSpecificClassKeyword(elt);
				if (!Strings.isEmpty(sarlKw)) {
					return addTypeModifier(sarlKw);
				}
				//TODO: Record is not part of the SARL syntax yet
				return addTypeModifier("record"); //$NON-NLS-1$
			}

			@Override
			protected String defaultAction(Element elt, SortedSet<Modifier> param) {
				addModifiersInStandardOrder(param, elt.getKind());
				return this.stringRepresentation.toString().trim();
			}

		}.visit(element, modifierList);
	}

	@Override
	public String getFullyQualifiedName(Element e, final boolean outer) {
		return new SimpleElementVisitor9<String, Void>() {
			@Override
			public String visitModule(ModuleElement e, Void p) {
				return e.getQualifiedName().toString();
			}

			@Override
			public String visitPackage(PackageElement e, Void p) {
				return e.getQualifiedName().toString();
			}

			@Override
			public String visitType(TypeElement e, Void p) {
				return e.getQualifiedName().toString();
			}

			@Override
			protected String defaultAction(Element e, Void p) {
				if (outer) {
					final var outerName = visit(e.getEnclosingElement());
					if (!Strings.isEmpty(outerName)) {
						return outerName + "." + e.getSimpleName().toString(); //$NON-NLS-1$
					}
				}
				return e.getSimpleName().toString();
			}
		}.visit(e);
	}

	@Override
	public String getLocalIdentifier(Element e) {
		return new SimpleElementVisitor9<String, Void>() {
			@Override
			public String visitExecutable(ExecutableElement e, Void p) {
				final var basename = new StringBuilder();
				basename.append(e.getSimpleName().toString());
				basename.append("("); //$NON-NLS-1$
				var first = true;
				for (final var parameter : e.getParameters()) {
					if (first) {
						first = false;
					} else {
						basename.append(","); //$NON-NLS-1$
					}
					final var typeMirror = parameter.asType();
					final var type = asTypeElement(typeMirror, ElementUtilsImpl.this.typeUtils);
					if (type != null) {
						basename.append(type.getQualifiedName().toString());
					} else {
						basename.append(typeMirror.toString());
					}
				}
				basename.append(")"); //$NON-NLS-1$
				return basename.toString();
			}

			@Override
			public String visitRecordComponent(RecordComponentElement elt, Void param) {
				return e.getSimpleName().toString();
			}
			
			@Override
			protected String defaultAction(Element e, Void p) {
				return e.getSimpleName().toString();
			}
		}.visit(e);
	}

	@Override
	public String getFullIdentifier(TypeMirror type) {
		return new AbstractTypeVisitor9<String, Void>() {
			@Override
			public String visitPrimitive(PrimitiveType t, Void p) {
				return t.toString();
			}

			@Override
			public String visitNull(NullType t, Void p) {
				return ""; //$NON-NLS-1$
			}

			@Override
			public String visitArray(ArrayType t, Void p) {
