/*
 * $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.
 */

package io.sarl.docs.generator.parser;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.lang.ref.WeakReference;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.arakhne.afc.vmutil.FileSystem;
import org.arakhne.afc.vmutil.ReflectionUtil;
import org.eclipse.xtext.Constants;
import org.eclipse.xtext.xbase.lib.Functions.Function2;
import org.eclipse.xtext.xbase.lib.Procedures.Procedure1;

import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.name.Named;

import io.sarl.docs.validator.NoXtextResourceException;
import io.sarl.docs.validator.ReflectExtensions;
import io.sarl.docs.validator.ScriptExecutor;
import io.sarl.lang.core.Capacity;
import io.sarl.lang.core.Event;
import io.sarl.lang.core.util.OutParameter;
import io.sarl.lang.sarl.actionprototype.IActionPrototypeProvider;

/** Generator of the marker language files for the modified marker language for SARL.
 *
 * @author $Author: sgalland$
 * @author $Author: alombard$
 * @version $FullVersion$
 * @mavengroupid $GroupId$
 * @mavenartifactid $ArtifactId$
 * @since 0.6
 */
public class SarlDocumentationParser {

	/** Default pattern for formatting inline code.
	 */
	public static final String DEFAULT_INLINE_FORMAT = "`{0}`"; //$NON-NLS-1$

	/** Default string to put for the outline location.
	 */
	public static final String DEFAULT_OUTLINE_OUTPUT_TAG = "[::Outline::]"; //$NON-NLS-1$

	/** Default text for line continuation.
	 */
	public static final String DEFAULT_LINE_CONTINUATION = " "; //$NON-NLS-1$

	private static final String DEFAULT_TAG_NAME_PATTERN = "\\[:(.*?)[:!]?\\]"; //$NON-NLS-1$

	private static final int PATTERN_COMPILE_OPTIONS = Pattern.DOTALL | Pattern.MULTILINE;

	private Injector injector;

	private IActionPrototypeProvider actionPrototypeProvider;

	private Map<Tag, String> rawPatterns = new HashMap<>();

	private Map<Tag, Pattern> compiledPatterns = new HashMap<>();

	private String inlineFormat;

	private Function2<String, String, String> blockFormat;

	private String outlineOutputTag;

	private String dynamicNameExtractionPattern;

	private Deque<Properties> additionalPropertyProvidersByPriority = new LinkedList<>();

	private String lineSeparator;

	private String languageName;

	private ScriptExecutor scriptExecutor;

	private String lineContinuation;

	/** Constructor.
	 */
	public SarlDocumentationParser() {
		reset();
	}

	/** Change the injector.
	 *
	 * @param injector the injector.
	 */
	@Inject
	public void setInjector(Injector injector) {
		assert injector != null;
		this.injector =  injector;
	}

	/** Change the name of the language.
	 *
	 * @param outputLanguage the language name, or {@code null} for ignoring the language name.
	 */
	@Inject
	public void setOutputLanguage(@Named(Constants.LANGUAGE_NAME) String outputLanguage) {
		if (!Strings.isNullOrEmpty(outputLanguage)) {
			final var parts = outputLanguage.split("\\.+"); //$NON-NLS-1$
			if (parts.length > 0) {
				final var simpleName = parts[parts.length - 1];
				if (!Strings.isNullOrEmpty(simpleName)) {
					this.languageName = simpleName;
					return;
				}
			}
		}
		this.languageName = null;
	}

	/** Change the script executor.
	 *
	 * @param executor the script executor.
	 */
	@Inject
	public void setScriptExecutor(ScriptExecutor executor) {
		this.scriptExecutor = executor;
	}

	/** Replies the script executor.
	 *
	 * @return the script executor.
	 */
	public ScriptExecutor getScriptExecutor() {
		return this.scriptExecutor;
	}

	/** Replies the string of character to put in the text when line continuation  is detected.
	 *
	 * @return the line continuation string of characters, or {@code null} to ignore line continuations.
	 */
	public String getLineContinuation() {
		return this.lineContinuation;
	}

	/** Change the string of character to put in the text when line continuation  is detected.
	 *
	 * @param lineContinuationText the line continuation string of characters, or {@code null} to ignore line continuations.
	 */
	public void setLineContinuation(String lineContinuationText) {
		this.lineContinuation = lineContinuationText;
	}

	/** Replies the fenced code block formatter.
	 *
	 * <p>This code block formatter is usually used by Github.
	 *
	 * @return the formatter.
	 */
	public static Function2<String, String, String> getFencedCodeBlockFormatter() {
		return (languageName, content) -> {
			/*final StringBuilder result = new StringBuilder();
			result.append("<div class=\\\"highlight"); //$NON-NLS-1$
			if (!Strings.isNullOrEmpty(languageName)) {
				result.append(" highlight-").append(languageName); //$NON-NLS-1$
			}
			result.append("\"><pre>\n").append(content).append("</pre></div>\n"); //$NON-NLS-1$ //$NON-NLS-2$
			return result.toString();*/
			return "```" + Strings.nullToEmpty(languageName).toLowerCase() + "\n" //$NON-NLS-1$ //$NON-NLS-2$
					+ content
					+ "```\n"; //$NON-NLS-1$
		};
	}

	/** Replies the basic code block formatter.
	 *
	 * @return the formatter.
	 */
	public static Function2<String, String, String> getBasicCodeBlockFormatter() {
		return (languageName, content) -> {
			return Pattern.compile("^", Pattern.MULTILINE).matcher(content).replaceAll("\t"); //$NON-NLS-1$ //$NON-NLS-2$
		};
	}

	/** Replies the name of the language.
	 *
	 * @return the language name, or {@code null} for ignoring the language name.
	 */
	public String getOutputLanguage() {
		return this.languageName;
	}

	/** Change the provider of action prototypes.
	 *
	 * @param provider the provider.
	 */
	@Inject
	public void setActionPrototypeProvider(IActionPrototypeProvider provider) {
		assert provider != null;
		this.actionPrototypeProvider =  provider;
	}

	/** Replies the provider of action prototypes.
	 *
	 * @return the provider.
	 */
	@Inject
	public IActionPrototypeProvider getActionPrototypeProvider() {
		return this.actionPrototypeProvider;
	}

	/** Reset to the default settings.
	 */
	public void reset() {
		this.rawPatterns.clear();
		this.compiledPatterns.clear();
		this.inlineFormat = DEFAULT_INLINE_FORMAT;
		this.blockFormat = null;
		this.outlineOutputTag = DEFAULT_OUTLINE_OUTPUT_TAG;
		this.dynamicNameExtractionPattern = DEFAULT_TAG_NAME_PATTERN;
		this.lineContinuation = DEFAULT_LINE_CONTINUATION;
	}

	/** Add a provider of properties that could be used for finding replacement values.
	 *
	 * <p>The given provider will be consider prior to the already declared providers.
	 *
	 * @param properties the property provider.
	 */
	public void addHighPropertyProvider(Properties properties) {
		this.additionalPropertyProvidersByPriority.addFirst(properties);
	}

	/** Add a provider of properties that could be used for finding replacement values.
	 *
	 * <p>The given provider will be consider after the already declared providers.
	 *
	 * @param properties the property provider.
	 */
	public void addLowPropertyProvider(Properties properties) {
		this.additionalPropertyProvidersByPriority.addLast(properties);
	}

	/** Replies additional providers of properties that could be used for finding replacement values.
	 *
	 * <p>The providers are provided from the higher priority to the lower priority.
	 *
	 * @return the property providers.
	 */
	public Iterable<Properties> getPropertyProvidersByPriority() {
		return Collections.unmodifiableCollection(this.additionalPropertyProvidersByPriority);
	}

	/** Change the pattern of the tag.
	 *
	 * @param tag the tag.
	 * @param regex the regular expression.
	 */
	public void setPattern(Tag tag, String regex) {
		if (Strings.isNullOrEmpty(regex)) {
			this.rawPatterns.remove(tag);
			this.compiledPatterns.remove(tag);
		} else {
			this.rawPatterns.put(tag, regex);
			this.compiledPatterns.put(tag, Pattern.compile("^\\s*" + regex, PATTERN_COMPILE_OPTIONS)); //$NON-NLS-1$
		}
	}

	/** Replies the pattern of the tag.
	 *
	 * @param tag the tag.
	 * @return the regular expression pattern.
	 */
	public String getPattern(Tag tag) {
		final var pattern = this.rawPatterns.get(tag);
		if (pattern == null) {
			return tag.getDefaultPattern();
		}
		return pattern;
	}

	/** Replies the tag that is matching the given text.
	 *
	 * @param text the text to match.
	 * @return the tag or {@code null}.
	 */
	public Tag getTagForPattern(CharSequence text) {
		for (final var tag : Tag.values()) {
			var pattern = this.compiledPatterns.get(tag);
			if (pattern == null) {
				pattern = Pattern.compile("^\\s*" + getPattern(tag), Pattern.DOTALL); //$NON-NLS-1$
				this.compiledPatterns.put(tag, pattern);
			}
			final var matcher = pattern.matcher(text);
			if (matcher.find()) {
				return tag;
			}
		}
		return null;
	}

	/** Change the pattern for the failure tag.
	 *
	 * @param regex the regular expression.
	 */
	public final void setFailurePattern(String regex) {
		setPattern(Tag.FAILURE, regex);
	}

	/** Replies the pattern for the failure tag.
	 *
	 * @return the regular expression.
	 */
	public final String getFailurePattern() {
		return getPattern(Tag.FAILURE);
	}

	/** Change the pattern for the success tag.
	 *
	 * @param regex the regular expression.
	 */
	public final void setSuccessPattern(String regex) {
		setPattern(Tag.SUCCESS, regex);
	}

	/** Replies the pattern for the success tag.
	 *
	 * @return the regular expression.
	 */
	public final String getSuccessPattern() {
		return getPattern(Tag.SUCCESS);
	}

	/** Change the pattern for the definition tag.
	 *
	 * @param regex the regular expression.
	 */
	public final void setDefinitionPattern(String regex) {
		setPattern(Tag.DEFINITION, regex);
	}

	/** Replies the pattern for the definition tag.
	 *
	 * @return the regular expression.
	 */
	public final String getDefinitionPattern() {
		return getPattern(Tag.DEFINITION);
	}

	/** Change the pattern for the reference tag.
	 *
	 * @param regex the regular expression.
	 */
	public final void setReferencePattern(String regex) {
		setPattern(Tag.REFERENCE, regex);
	}

	/** Replies the pattern for the reference tag.
	 *
	 * @return the regular expression.
	 */
	public final String getReferencePattern() {
		return getPattern(Tag.REFERENCE);
	}

	/** Change the pattern for the "off" tag.
	 *
	 * @param regex the regular expression.
	 */
	public final void setOffPattern(String regex) {
		setPattern(Tag.OFF, regex);
	}

	/** Replies the pattern for the "off" tag.
	 *
	 * @return the regular expression.
	 */
	public final String getOffPattern() {
		return getPattern(Tag.OFF);
	}

	/** Change the pattern for the "on" tag.
	 *
	 * @param regex the regular expression.
	 */
	public final void setOnPattern(String regex) {
		setPattern(Tag.ON, regex);
	}

	/** Replies the pattern for the "on" tag.
	 *
	 * @return the regular expression.
	 */
	public final String getOnPattern() {
		return getPattern(Tag.ON);
	}

	/** Change the pattern for the fact tag.
	 *
	 * @param regex the regular expression.
	 */
	public final void setFactPattern(String regex) {
		setPattern(Tag.FACT, regex);
	}

	/** Replies the pattern for the fact tag.
	 *
	 * @return the regular expression.
	 */
	public final String getFactPattern() {
		return getPattern(Tag.FACT);
	}

	/** Change the pattern for the include tag.
	 *
	 * @param regex the regular expression.
	 */
	public final void setIncludePattern(String regex) {
		setPattern(Tag.INCLUDE, regex);
	}

	/** Replies the pattern for the include tag.
	 *
	 * @return the regular expression.
	 */
	public final String getIncludePattern() {
		return getPattern(Tag.INCLUDE);
	}

	/** Change the pattern for the outline tag.
	 *
	 * @param regex the regular expression.
	 */
	public final void setOutlinePattern(String regex) {
		setPattern(Tag.OUTLINE, regex);
	}

	/** Replies the pattern for the outline tag.
	 *
	 * @return the regular expression.
	 */
	public final String getOutlinePattern() {
		return getPattern(Tag.OUTLINE);
	}

	/** Set the template for inline codes.
	 *
	 * <p>The template should follow the {@link MessageFormat} specifications.
	 *
	 * @param template the template.
	 */
	public void setInlineCodeTemplate(String template) {
		if (!Strings.isNullOrEmpty(template)) {
			this.inlineFormat = template;
		}
	}

	/** Replies the template for inline codes.
	 *
	 * <p>The template should follow the {@link MessageFormat} specifications.
	 *
	 * @return the template.
	 */
	public String getInlineCodeTemplate() {
		return this.inlineFormat;
	}

	/** Set the template for block codes.
	 *
	 * <p>The first parameter of the function is the language name. The second parameter is
	 * the code to format.
	 *
	 * @param template the template.
	 */
	public void setBlockCodeTemplate(Function2<String, String, String> template) {
		this.blockFormat = template;
	}

	/** Replies the template for block codes.
	 *
	 * <p>The first parameter of the function is the language name. The second parameter is
	 * the code to format.
	 *
	 * @return the template.
	 */
	public Function2<String, String, String> getBlockCodeTemplate() {
		return this.blockFormat;
	}

	/** Change the outline output tag that will be output when the outline
	 * tag is found in the input content.
	 *
	 * @param tag the tag for outline.
	 */
	protected void setOutlineOutputTag(String tag) {
		this.outlineOutputTag = tag;
	}

	/** Replies the outline output tag that will be output when the outline
	 * tag is found in the input content.
	 *
	 * @return the tag.
	 */
	public String getOutlineOutputTag() {
		return this.outlineOutputTag;
	}

	/** Set the regular expression for extracting the dynamic name of a tag string.
	 *
	 * @param pattern the regex.
	 */
	public void setDynamicNameExtractionPattern(String pattern) {
		if (!Strings.isNullOrEmpty(pattern)) {
			this.dynamicNameExtractionPattern = pattern;
		}
	}

	/** Replies the regular expression for extracting the dynamic name of a tag string.
	 *
	 * @return the regex.
	 */
	public String getDynamicNameExtractionPattern() {
		return this.dynamicNameExtractionPattern;
	}

	/** Replies the OS-dependent line separator.
	 *
	 * @return the line separator from the {@code "line.separator"} property, or {@code "\n"}.
	 */
	public String getLineSeparator() {
		if (Strings.isNullOrEmpty(this.lineSeparator)) {
			final var nl = System.getProperty("line.separator"); //$NON-NLS-1$
			if (Strings.isNullOrEmpty(nl)) {
				return "\n"; //$NON-NLS-1$
			}
			return nl;
		}
		return this.lineSeparator;
	}

	/** Change the OS-dependent line separator.
	 *
	 * @param lineSeparator the line separator or {@code null} to use the system configuration.
	 */
	public void setLineSeparator(String lineSeparator) {
		this.lineSeparator = lineSeparator;
	}

	private String buildGeneralTagPattern() {
		final var pattern = new StringBuilder();
		var nbTags = 0;
		for (final var tag : Tag.values()) {
			if (nbTags >= 1) {
				pattern.append("|"); //$NON-NLS-1$
			}
			pattern.append("(?:"); //$NON-NLS-1$
			if (tag.isEnclosingSpaceCouldRemovable()) {
				pattern.append("[ \\t]*"); //$NON-NLS-1$
			}
			pattern.append("("); //$NON-NLS-1$
			pattern.append(getPattern(tag));
			pattern.append(")"); //$NON-NLS-1$
			if (tag.hasParameter()) {
				pattern.append("(?:"); //$NON-NLS-1$
				pattern.append("(?:\\(\\s*([^\\)]*?)\\s*\\))|"); //$NON-NLS-1$
				pattern.append("(?:\\{\\s*([^\\}]*?)\\s*\\})|"); //$NON-NLS-1$
				pattern.append("(?:\\|\\s*([^\\|]*?)\\s*\\|)|"); //$NON-NLS-1$
				pattern.append("(?:\\$\\s*([^\\$]*?)\\s*\\$)"); //$NON-NLS-1$
				pattern.append(")"); //$NON-NLS-1$
			}
			if (tag.isEnclosingSpaceCouldRemovable()) {
				pattern.append("[ \\t]*"); //$NON-NLS-1$
			}
			pattern.append(")"); //$NON-NLS-1$
			++nbTags;
		}
		if (nbTags > 0) {
			pattern.insert(0, "(" + org.eclipse.xtext.util.Strings.convertToJavaString(getLineSeparator()) //$NON-NLS-1$
			+ ")|"); //$NON-NLS-1$
			return pattern.toString();
		}
		return null;
	}

	/** Extract the dynamic name of that from the raw text.
	 *
	 * @param tag the tag to extract for.
	 * @param name the raw text.
	 * @param dynamicName the dynamic name.
	 */
	protected void extractDynamicName(Tag tag, CharSequence name, OutParameter<String> dynamicName) {
		if (tag.hasDynamicName()) {
			final var pattern = Pattern.compile(getDynamicNameExtractionPattern());
			final var matcher = pattern.matcher(name);
			if (matcher.matches()) {
				dynamicName.set(Strings.nullToEmpty(matcher.group(1)));
				return;
			}
		}
		dynamicName.set(name.toString());
	}

	private static int findFirstGroup(Matcher matcher, int startIdx) {
		final var len = matcher.groupCount();
		for (var i = startIdx + 1; i <= len; ++i) {
			final var value = matcher.group(i);
			if (value != null) {
				return i;
			}
		}
		return 1;
	}

	/** Read the given file and transform its content in order to have a raw text.
	 *
	 * @param inputFile the input file.
	 * @return the raw file context.
	 */
	public String transform(File inputFile) {
		final String content;
		try (FileReader reader = new FileReader(inputFile)) {
			content = read(reader, null);
		} catch (IOException exception) {
			reportError(Messages.SarlDocumentationParser_0, exception);
			return null;
		}
		return transform(content, inputFile);
	}

	/** Read the given input stream and transform its content in order to have a raw text.
	 *
	 * @param reader the input stream.
	 * @param inputFile the name of the input file for locating included features and formatting error messages.
	 * @return the raw file context.
	 */
	public String transform(Reader reader, File inputFile) {
		final String content;
		try {
			content = read(reader, null);
		} catch (IOException exception) {
			reportError(Messages.SarlDocumentationParser_0, exception);
			return null;
		}
		return transform(content, inputFile);
	}

	/** Read the given input content and transform it in order to have a raw text.
	 *
	 * @param content the content to parse.
	 * @param inputFile the name of the input file for locating included features and formatting error messages.
	 * @return the raw file context.
	 */
	public String transform(CharSequence content, File inputFile) {
		final var rootContextForReplacements = new ParsingContext();
		initializeContext(rootContextForReplacements);
		var rawContent = preProcessing(content);
		Stage stage = Stage.first();
		do {
			final var interceptor = new ContentParserInterceptor();
			// Reset the lineno and offset because they are not reset between the different stages.
			rootContextForReplacements.setLineNo(1);
			rootContextForReplacements.setOffset(0);
			final var hasChanged = parse(rawContent, inputFile, 0, stage, rootContextForReplacements, interceptor);
			if (hasChanged) {
				rawContent = interceptor.getResult();
			}
			stage = stage.next();
		} while (stage != null);
		return postProcessing(rawContent);
	}

	/** Do a pre processing of the text.
	 *
	 * @param text the text to pre process.
	 * @return the pre-processed text.
	 */
	@SuppressWarnings("static-method")
	protected CharSequence preProcessing(CharSequence text) {
		return text;
	}

	/** Do a post processing of the text.
	 *
	 * @param text the text to post process.
	 * @return the post-processed text.
	 */
	protected String postProcessing(CharSequence text) {
		final var lineContinuation = getLineContinuation();
		if (lineContinuation != null) {
			final var pattern = Pattern.compile(
					"\\s*\\\\[\\n\\r]+\\s*", //$NON-NLS-1$
					Pattern.DOTALL);
			final var matcher = pattern.matcher(text.toString().trim());
			return matcher.replaceAll(lineContinuation);
		}
		return text.toString().trim();
	}

	/** Read the given input content and extract validation components.
	 *
	 * @param inputFile the input file.
	 * @param observer the oberserver to be called with extracted information. The parameter of the lambda maps
	 *     the tags to the associated list of the extraction information.
	 */
	public void extractValidationComponents(File inputFile,
			Procedure1<Map<Tag, List<ValidationComponentData>>> observer) {
		final String content;
		final var nblines = new AtomicInteger();
		try (var reader = new FileReader(inputFile)) {
			content = read(reader, nblines);
		} catch (IOException exception) {
			reportError(Messages.SarlDocumentationParser_0, exception);
			return;
		}
		extractValidationComponents(content, inputFile, observer);
	}

	/** Read the given input content and extract validation components.
	 *
	 * @param reader the input stream.
	 * @param inputFile the name of the input file for locating included features and formatting error messages.
	 * @param observer the oberserver to be called with extracted information. The parameter of the lambda maps
	 *     the tags to the associated list of the extraction information.
	 */
	public void extractValidationComponents(Reader reader, File inputFile,
			Procedure1<Map<Tag, List<ValidationComponentData>>> observer) {
		final String content;
		final var nblines = new AtomicInteger();
		try {
			content = read(reader, nblines);
		} catch (IOException exception) {
			reportError(Messages.SarlDocumentationParser_0, exception);
			return;
		}
		extractValidationComponents(content, inputFile, observer);
	}

	/** Read the given input content and extract validation components.
	 *
	 * @param content the content to parse.
	 * @param inputFile the name of the input file for locating included features and formatting error messages.
	 * @param observer the oberserver to be called with extracted information. The parameter of the lambda maps
	 *     the tags to the associated list of the extraction information.
	 */
	public void extractValidationComponents(CharSequence content, File inputFile,
			Procedure1<Map<Tag, List<ValidationComponentData>>> observer) {
		//
		// STEP 1: Extract the raw text
		//
		final var components = new TreeMap<Tag, List<ValidationComponentData>>();
		final var interceptor = new ContentParserInterceptor(new ParserInterceptor() {
			@Override
			public void tag(ParsingContext context, Tag tag, String dynamicName, String parameter,
					String blockValue) {
				if (tag.isOpeningTag() || tag.hasParameter()) {
					var values = components.get(tag);
					if (values == null) {
						values = new ArrayList<>();
						components.put(tag, values);
					}
					final var data = new ValidationComponentData();
					data.file = context.getCurrentFile();
					data.lineno = context.getLineNo();
					data.endLineno = context.getEndLineNo();
					data.offset = context.getOffset();
					data.length = context.getLength();
					data.deprecationAsError = context.isDeprecationIssuesAsErrors();
					if (tag.isOpeningTag()) {
						data.code = Strings.nullToEmpty(blockValue).trim();
					} else {
						data.code = Strings.nullToEmpty(parameter).trim();
					}
					values.add(data);
				}
			}
		});
		final var rootContextForReplacements = new ParsingContext(true, true);
		initializeContext(rootContextForReplacements);
		parse(content, inputFile, 0, Stage.FIRST, rootContextForReplacements, interceptor);
		//
		// STEP 2: Do macro replacement in the captured elements.
		//
		final var allTexts = new ArrayList<>(components.values());
		for (final var values : allTexts) {
			for (final var data : values) {
				final var localInterceptor = new ContentParserInterceptor(interceptor);
				parse(data.code, inputFile, 0, Stage.SECOND, rootContextForReplacements, localInterceptor);
				final var newCapturedText = localInterceptor.getResult();
				data.code = newCapturedText;
			}
		}

		observer.apply(components);
	}

	/** Initialize the given context.
	 *
	 * @param context the context.
	 */
	protected void initializeContext(ParsingContext context) {
		context.setLineNo(1);
		context.setOffset(0);
		context.setScriptExecutor(getScriptExecutor());
	}

	/** Parse the given source text.
	 *
	 * @param source the source text.
	 * @param file the file to be read.
	 * @param startIndex the start index in the file.
	 * @param stage the number of the stage to run.
	 * @param parentContext the parent context, or {@code null} if none.
	 * @param interceptor interceptor of the parsed elements.
	 * @return {@code true} if a special tag was found.
	 */
	protected boolean parse(CharSequence source, File file, int startIndex, Stage stage,
			ParsingContext parentContext, ParserInterceptor interceptor) {
		final var context = this.injector.getInstance(ParsingContext.class);
		context.setParser(this);
		context.setText(source);
		context.setCurrentFile(file);
		context.setStartIndex(startIndex);
		context.setParserInterceptor(interceptor);
		context.setInlineCodeFormat(getInlineCodeTemplate());
		context.setBlockCodeFormat(getBlockCodeTemplate());
		context.setOutlineOutputTag(getOutlineOutputTag());
		context.setLineSeparator(getLineSeparator());
		context.setOutputLanguage(getOutputLanguage());
		context.setStage(stage);
		final var regex = buildGeneralTagPattern();
		if (!Strings.isNullOrEmpty(regex)) {
			final var patterns = Pattern.compile(regex, PATTERN_COMPILE_OPTIONS);
			final var matcher = patterns.matcher(source);
			context.setMatcher(matcher);
			if (parentContext != null) {
				context.linkTo(parentContext);
			}
			final var foundSpecialTag = parse(context);
			if (parentContext != null) {
				context.propagateTo(parentContext);
			}
			return foundSpecialTag;
		}
		return false;
	}

	/** Parse the given source text.
	 *
	 * @param context the parsing context.
	 * @return {@code true} if a special tag was found.
	 * @throws ParsingException a runtime exception
	 */
	protected boolean parse(ParsingContext context) {
		try {
			context.getParserInterceptor().openContext(context);
			var specialTagFound = false;
			final var lineSeparator = getLineSeparator();
			while (context.getMatcher().find()) {
				var groupIndex = findFirstGroup(context.getMatcher(), 0);
				final var tagName = context.getMatcher().group(groupIndex);
				if (lineSeparator.equals(tagName)) {
					context.incrementLineNo();
					context.incrementOffset(lineSeparator.length());
					continue;
				}

				final var regionOffset = context.getMatcher().start();
				final var regionLength = context.getMatcher().end() - regionOffset;
				context.setOffset(regionOffset);
				context.setLength(regionLength);
				final var startLine = context.getLineNo();
				final var nbLines = countLines(context.getMatcher().group());
				final var endLine = startLine + nbLines - 1;
				context.setEndLineNo(endLine);

				final var tag = getTagForPattern(tagName);
				if (tag != null) {
					if (tag.isActive(context)) {
						final String tagDynamicName;
						if (tag.hasDynamicName()) {
							final var dname = new OutParameter<String>();
							extractDynamicName(tag, tagName, dname);
							tagDynamicName = dname.get();
						} else {
							tagDynamicName = null;
						}
						final String parameterValue;
						if (tag.hasParameter()) {
							groupIndex = findFirstGroup(context.getMatcher(), groupIndex);
							final var parameter = Strings.nullToEmpty(context.getMatcher().group(groupIndex));
							final var subInterceptor = new ContentParserInterceptor(context.getParserInterceptor());
							final var inBlock = context.setInBlock(false);
							final var inParam = context.setInParameter(true);
							parse(parameter, context.getCurrentFile(),
									context.getMatcher().start(groupIndex) + context.getStartIndex(),
									context.getStage(),
									context,
									subInterceptor);
							context.setInParameter(inParam);
							context.setInBlock(inBlock);
							parameterValue = Strings.emptyToNull(subInterceptor.getResult());
						} else {
							parameterValue = null;
						}
						final String blockContent;
						if (tag.isOpeningTag()) {
							groupIndex = findFirstGroup(context.getMatcher(), groupIndex);
							final var tagContent = context.getMatcher().group(groupIndex);
							final var subInterceptor = new ContentParserInterceptor(context.getParserInterceptor());
							final var inBlock = context.setInBlock(true);
							final var inParam = context.setInParameter(false);
							context.setVisibleInBlock(false);
							parse(Strings.nullToEmpty(tagContent), context.getCurrentFile(),
									context.getMatcher().start(groupIndex) + context.getStartIndex(),
									context.getStage(),
									context,
									subInterceptor);
							context.setInParameter(inParam);
							context.setInBlock(inBlock);
							blockContent = Strings.nullToEmpty(subInterceptor.getResult());
						} else {
							blockContent = null;
						}
						specialTagFound = true;
						try {
							context.getParserInterceptor().tag(context, tag, tagDynamicName, parameterValue, blockContent);
						} catch (Throwable exception) {
							reportError(context, Messages.SarlDocumentationParser_6, tagName, exception);
							return false;
						}
					} else {
						// Ignore the tag for this stage.
					}
				} else {
					reportError(context, Messages.SarlDocumentationParser_1, tagName);
					return false;
				}

				context.setLineNo(endLine);
				context.setEndLineNo(endLine);
			}
			context.getParserInterceptor().closeContext(context);
			return specialTagFound;
		} catch (ParsingException exception) {
			throw exception;
		} catch (Throwable exception) {
			final var rootException = Throwables.getRootCause(exception);
			throw new ParsingException(
					rootException.getClass().getName() + " - " + rootException.getLocalizedMessage(), //$NON-NLS-1$
					context.getCurrentFile(),
					context.getLineNo(),
					rootException);
		}
	}

	private int countLines(String text) {
		final var lines = text.split(Pattern.quote(getLineSeparator()));
		return lines.length;
	}

	/** Report an error.
	 *
	 * @param context the context.
	 * @param message the message in a format compatible with {@link MessageFormat}. The first argument is
	 *     the filename. The second argument is the line number. The third argument is the position in the file.
	 * @param parameter additional parameter, starting at {4}.
	 */
	protected static void reportError(ParsingContext context, String message, Object... parameter) {
		final var file = context.getCurrentFile();
		final var offset = context.getMatcher().start() + context.getStartIndex();
		final var lineno = context.getLineNo();
		Throwable cause = null;
		for (final var param : parameter) {
			if (param instanceof Throwable cvalue) {
				cause = cvalue;
				break;
			}
		}
		final var args = new Object[parameter.length + 3];
		args[0] = file;
		args[1] = Integer.valueOf(lineno);
		args[2] = Integer.valueOf(offset);
		System.arraycopy(parameter, 0, args, 3, parameter.length);
		final var msg = MessageFormat.format(message, args);
		if (cause != null) {
			throw new ParsingException(msg, file, lineno, Throwables.getRootCause(cause));
		}
		throw new ParsingException(msg, file, lineno);
	}

	/** Report an error.
	 *
	 * @param message the message in a format compatible with {@link MessageFormat}.
	 * @param parameters the parameters, starting at {1}.
	 */
	protected static void reportError(String message, Object... parameters) {
		Throwable cause = null;
		for (var i = 0; cause == null && i < parameters.length; ++i) {
			if (parameters[i] instanceof Throwable cvalue) {
				cause = cvalue;
			}
		}
		final var msg = MessageFormat.format(message, parameters);
		if (cause != null) {
			throw new ParsingException(msg, null, 1, Throwables.getRootCause(cause));
		}
		throw new ParsingException(msg, null, 1);
	}

	/** Read the content of a file.
	 *
	 * @param context the current parsing context.
	 * @param file the file to read.
	 * @return the content.
	 * @throws IOException if the content cannot be read.
	 */
