/*
 * $Id$
 *
 * File is automatically generated by the Xtext language generator.
 * Do not change it.
 *
 * 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.lang.documentation;

import io.sarl.lang.services.SARLGrammarAccess;
import jakarta.inject.Inject;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.TreeSet;
import org.eclipse.xtext.AbstractElement;
import org.eclipse.xtext.AbstractRule;
import org.eclipse.xtext.Group;
import org.eclipse.xtext.Keyword;
import org.eclipse.xtext.UntilToken;
import org.eclipse.xtext.formatting2.ITextReplacerContext;
import org.eclipse.xtext.formatting2.regionaccess.IComment;
import org.eclipse.xtext.formatting2.regionaccess.ILineRegion;
import org.eclipse.xtext.formatting2.regionaccess.ITextRegionAccess;
import org.eclipse.xtext.formatting2.regionaccess.ITextSegment;
import org.eclipse.xtext.util.Strings;
import org.eclipse.xtext.xbase.compiler.IAppendable;
import org.eclipse.xtext.xbase.compiler.StringBuilderBasedAppendable;
import org.eclipse.xtext.xbase.compiler.output.FakeTreeAppendable;
import org.eclipse.xtext.xbase.lib.Pure;

/** Formatter a documentation string.
	 * @see "DocumentationBuilderFragment.java : appendTo : 654"
 */
public class DocumentationFormatter implements IDocumentationFormatter {

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 662"
	 */
	private static final String SPACE_CHAR = " ";
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 665"
	 */
	private static final String NL_CHAR = "\n";
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 668"
	 */
	private static final String EMPTY_STR = "";
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 671"
	 */
	private String mlLinePrefix;
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 674"
	 */
	private String mlStart;
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 677"
	 */
	private String mlEnd;
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 680"
	 */
	private String slPrefix;

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 684"
	 */
	protected static boolean isNewLine(char character) {
		if (character == '\n' || character == '\r' || character == '\f') {
			return true;
		}
	return ((((1 << Character.LINE_SEPARATOR)
			| (1 << Character.PARAGRAPH_SEPARATOR)) >> Character.getType((int) character)) & 1) != 0;
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 700"
	 */
	@Pure
	public String getMultilineCommentStartSymbols() {
		return this.mlStart;
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 711"
	 */
	public void setMultilineCommentStartSymbols(String symbols) {
		this.mlStart = symbols;
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 719"
	 */
	@Pure
	public String getMultilineCommentEndSymbols() {
		return this.mlEnd;
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 730"
	 */
	public void setMultilineCommentEndSymbols(String symbols) {
		this.mlEnd = symbols;
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 738"
	 */
	@Pure
	public String getMultilineCommentLinePrefix() {
		return this.mlLinePrefix;
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 749"
	 */
	public void setMultilineCommentLinePrefix(String prefix) {
		this.mlLinePrefix = prefix;
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 757"
	 */
	@Pure
	public String getSinglelineCommentPrefix() {
		return this.slPrefix;
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 768"
	 */
	@Pure
	protected Set<Character> getSinglelineCommentSpecialChars() {
		final Set<Character> set = new TreeSet<>();
		set.add('*');
		set.add('+');
		set.add('-');
		set.add('=');
		return set;
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 795"
	 */
	public void setSinglelineCommentPrefix(String prefix) {
		this.slPrefix = prefix;
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 803"
	 */
	@Inject
	public void setGrammarAccess(SARLGrammarAccess access) {
		if (this.mlStart == null || this.mlEnd == null) {
			AbstractRule mlRule = access.getML_COMMENTRule();
			for (AbstractElement element : ((Group) mlRule.getAlternatives()).getElements()) {
				if (element instanceof Keyword $c$value && this.mlStart == null) {
					this.mlStart = $c$value.getValue();
				} else if (element instanceof UntilToken $c$value && this.mlEnd == null) {
					this.mlEnd = ((Keyword) $c$value.getTerminal()).getValue();
				}
			}
		}
		if (this.mlLinePrefix == null) {
			this.mlLinePrefix = this.mlStart.substring(this.mlStart.length() - 1);
		}
		if (this.slPrefix == null) {
			AbstractRule slRule = access.getSL_COMMENTRule();
			for (AbstractElement element : ((Group) slRule.getAlternatives()).getElements()) {
				if (element instanceof Keyword $c$value) {
					this.slPrefix = $c$value.getValue().trim();
					break;
				}
			}
		}
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 878"
	 */
	@Pure
	public String formatMultilineComment(String doc) {
		return formatMultilineComment(doc, (String) null);
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 889"
	 */
	@Pure
	public String formatMultilineComment(String doc, String indentation) {
		IAppendable appendable = new StringBuilderBasedAppendable();
		formatMultilineComment(doc, indentation, appendable);
		return appendable.getContent();
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 908"
	 */
	@Pure
	public void formatMultilineComment(String doc, IAppendable appendable) {
		formatMultilineComment(doc, null, appendable);
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 921"
	 */
	@Pure
	public void formatMultilineComment(String doc, String indentation, IAppendable appendable) {
		if (!Strings.isEmpty(doc)) {
			final SortedMap<Integer, Replacement> replacements = new TreeMap();
			formatMultlineComment(indentation, Strings.newLine(), new AppendableAccessor(appendable, doc, replacements, 0, doc.length()));
		}
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 946"
	 */
	@Pure
	public void formatMultilineComment(ITextReplacerContext context, IComment comment) {
		formatMultlineComment(context.getIndentationString(), context.getNewLinesString(1), new RegionAccessor(context, comment));
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 961"
	 */
	@Pure
	public String formatSinglelineComment(String doc) {
		return formatSinglelineComment(doc, (String) null);
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 972"
	 */
	@Pure
		public String formatSinglelineComment(String doc, String indentation) {
		StringBuilderBasedAppendable appendable = new StringBuilderBasedAppendable();
		formatSinglelineComment(doc, indentation, appendable);
		return appendable.getContent();
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 991"
	 */
	@Pure
	public void formatSinglelineComment(String doc, IAppendable appendable) {
		formatSinglelineComment(doc, null, appendable);
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1004"
	 */
	@Pure
	public void formatSinglelineComment(String doc, String indentation, IAppendable appendable) {
		if (!Strings.isEmpty(doc)) {
			final SortedMap<Integer, Replacement> replacements = new TreeMap<>();
			int offset = doc.indexOf(getSinglelineCommentPrefix());
			if (offset < 0) {
				offset = 0;
			}
			int endOffset = doc.indexOf(NL_CHAR, offset);
			if (endOffset < 0) {
				endOffset = doc.length();
			}
			formatSinglelineComment(
				indentation,
				new AppendableAccessor(appendable, doc, replacements, offset, endOffset));
		}
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1049"
	 */
	public void formatSinglelineComment(ITextReplacerContext context, IComment comment) {
		formatSinglelineComment(context.getIndentationString(), new RegionAccessor(context, comment));
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1061"
	 */
	private <T> void formatSinglelineComment(String indentationString, FormattedTextAccessor<T> backend) {
		final String indent = Strings.emptyIfNull(indentationString);
		final String comment = backend.getCommentText();
		// Compute the starting offset of the text inside the comment
		int offset = comment.indexOf(getSinglelineCommentPrefix());
		if (offset < 0) {
			backend.replace(0, 0, getSinglelineCommentPrefix());
			offset = 0;
		} else {
			offset += getSinglelineCommentPrefix().length();
		}
		final int endOffset = comment.length();
		T currentLine = backend.getFirstLine(backend.getCommentOffset());
		boolean firstLine = true;
		while (currentLine != null) {
			String lineText = backend.getLineText(currentLine);
			int lineOffset = backend.getLineOffset(currentLine);
			int lineLength = backend.getLineLength(currentLine);
			// Clamp the line text to the comment area.
			if (firstLine) {
				if (lineOffset < offset) {
					final int len = offset - lineOffset;
					lineText = lineText.substring(len);
					lineOffset += len;
					lineLength -= len;
				}
			} else if (lineOffset >= endOffset) {
				// After the end of comment
				backend.applyReplacements();
				return;
			} else {
				final String prefix;
				if (!startsWith(lineText, 0, getSinglelineCommentPrefix())) {
					prefix = indent + getSinglelineCommentPrefix();
				} else {
					prefix = indent;
				}
				backend.replace(lineOffset, 0, prefix);
			}
			// Skip the comment characters that corresponds to the Javadoc format: //[*-+=].
			int realCommentStart = 0;
			final Set<Character> specialChars = getSinglelineCommentSpecialChars();
			while (realCommentStart < lineLength && specialChars.contains(lineText.charAt(realCommentStart))) {
				++realCommentStart;
			}
			// Search for the first non whitespace
			int firstNonWhiteSpacePos = realCommentStart;
			while (firstNonWhiteSpacePos < lineLength && Character.isWhitespace(lineText.charAt(firstNonWhiteSpacePos))) {
				++firstNonWhiteSpacePos;
			}
			// Add whitespace at the beginning.
			if (firstNonWhiteSpacePos == lineLength) {
				// Empty comment
				if (realCommentStart < firstNonWhiteSpacePos) {
					backend.replace(realCommentStart + lineOffset, lineLength - realCommentStart, EMPTY_STR);
				}
			} else {
				final int expectedNbWhiteSpaces = getWhiteSpacesOnFirstLine();
				final int nbWhiteSpaces = firstNonWhiteSpacePos - realCommentStart;
				if (nbWhiteSpaces != expectedNbWhiteSpaces) {
					backend.replace(realCommentStart + lineOffset, nbWhiteSpaces, makeWhiteSpaces(expectedNbWhiteSpaces));
				}
				// Format the comment text
				formatLineText(
					lineText.substring(firstNonWhiteSpacePos, lineLength), true,
					new SubAccessor<>(backend, lineOffset + firstNonWhiteSpacePos));
				// Remove trailing whitespaces
				int endOfText = lineLength;
				while ((endOfText - 1) > firstNonWhiteSpacePos && Character.isWhitespace(lineText.charAt(endOfText - 1))) {
					--endOfText;
				}
				if (endOfText < lineLength) {
					backend.replace(endOfText + lineOffset, lineLength - endOfText, EMPTY_STR);
				}
			}
			firstLine = false;
			currentLine = backend.getNextLine(currentLine);
		}
		backend.applyReplacements();
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1227"
	 */
	private static String safeSubstring(String text, int start, int length) {
		if (text == null) {
			return EMPTY_STR;
		}
		final int index = Math.max(0, start);
		final int len = Math.max(0, Math.min(length, text.length()));
		return text.substring(index, index + len);
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1245"
	 */
	private static boolean startsWith(String text, int start, String pattern) {
		return safeSubstring(text, start, pattern.length()).equals(pattern);
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1253"
	 */
	private static String makeWhiteSpaces(int nb) {
		final StringBuilder b = new StringBuilder();
		for (int i = 0; i < nb; ++i) {
			b.append(SPACE_CHAR);
		}
		return b.toString();
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1269"
	 */
	protected int getWhiteSpacesOnFirstLine() {
		return 1;
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1277"
	 */
	protected int getWhiteSpacesOnOtherLines() {
		return 1;
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1285"
	 */
	protected <T> void formatLineText(String lineText, boolean isMultlineComment, FormattedTextAccessor<T> backend) {
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1291"
	 */
	private <T> boolean formatMultlineCommentFirstLine(String lineText, String indentationString, String newLineString, int endCommentOffset, FormattedTextAccessor<T> backend) {
		// Skip the comment characters that corresponds to the Javadoc format: /**.
		int realCommentStart = 0;
		while (realCommentStart < lineText.length() && startsWith(lineText, realCommentStart, getMultilineCommentLinePrefix())) {
			realCommentStart += getMultilineCommentLinePrefix().length();
		}
		// Search for the first non whitespace
		int firstNonWhiteSpacePos = realCommentStart;
		boolean hasNonSpaceChar = false;
		while (firstNonWhiteSpacePos < lineText.length() && Character.isWhitespace(lineText.charAt(firstNonWhiteSpacePos))) {
			if (!Character.isSpaceChar(lineText.charAt(firstNonWhiteSpacePos))) {
				hasNonSpaceChar = true;
			}
			++firstNonWhiteSpacePos;
		}
		// Add whitespace at the beginning.
		final int expectedNbWhiteSpaces = getWhiteSpacesOnFirstLine();
		final int nbWhiteSpaces = firstNonWhiteSpacePos - realCommentStart;
		if (hasNonSpaceChar || nbWhiteSpaces != expectedNbWhiteSpaces) {
			backend.replace(realCommentStart, nbWhiteSpaces, makeWhiteSpaces(expectedNbWhiteSpaces));
		}
		// Treat the end of comment
		if (endCommentOffset <= lineText.length()) {
			// Comment end at the first line. Insert a newline character
			// Search for the end of comment text.
			int endPos = endCommentOffset;
			final int end = endPos;
			while ((endPos - 1) > firstNonWhiteSpacePos && Character.isWhitespace(lineText.charAt(endPos - 1))) {
				--endPos;
			}
			// Format the comment text
			formatLineText(lineText.substring(firstNonWhiteSpacePos, endPos), true, new SubAccessor<>(backend, firstNonWhiteSpacePos));
			// Do the replacement
			backend.replace(endPos, end - endPos, newLineString + indentationString + SPACE_CHAR);
			// We don't need to treat more line
			return true;
		}
		// Format the comment text
		formatLineText(lineText.substring(firstNonWhiteSpacePos, lineText.length()), true, new SubAccessor<>(backend, firstNonWhiteSpacePos));
		return false;
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1375"
	 */
	private <T> boolean formatMultlineCommentOtherLines(String lineText, String indentationString, String newLineString, int endCommentOffset, FormattedTextAccessor<T> backend) {
		// Search for the comment prefix (usually " * "
		int realCommentStart = 0;
		while (realCommentStart < lineText.length() && Character.isWhitespace(lineText.charAt(realCommentStart))) {
			++realCommentStart;
		}
		boolean foundStar = false;
		if (realCommentStart < lineText.length() && startsWith(lineText, realCommentStart, getMultilineCommentLinePrefix())) {
			realCommentStart += getMultilineCommentLinePrefix().length();
			foundStar = true;
			while (realCommentStart < lineText.length() && Character.isWhitespace(lineText.charAt(realCommentStart))) {
				++realCommentStart;
			}
		}
		// Compute the standard prefix.
		StringBuilder prefix = new StringBuilder(indentationString);
		prefix.append(SPACE_CHAR);
		prefix.append(getMultilineCommentLinePrefix());
		prefix.append(makeWhiteSpaces(getWhiteSpacesOnOtherLines()));
		// Force replacement by the line's prefix
		int minBoundForEnd = 0;
		if (endCommentOffset > lineText.length() || foundStar || realCommentStart < endCommentOffset) {
			backend.replace(0, realCommentStart, prefix.toString());
			if (foundStar) {
				minBoundForEnd = prefix.length();
			}
		}
		// Format the comment text
		if (endCommentOffset <= lineText.length()) {
			// End of comment on the current line.
			int endPosition = endCommentOffset;
			final int end = endPosition;
			while ((endPosition - 1) >= minBoundForEnd && Character.isWhitespace(lineText.charAt(endPosition - 1))) {
				--endPosition;
			}
			if (endPosition > 0) {
				// Comment end with a text before. Insert a newline character
				backend.replace(endPosition, end - endPosition, newLineString + indentationString + SPACE_CHAR);
			} else {
				// Replace spaces before end of comment if they exist
				backend.replace(endPosition, end - endPosition, indentationString + SPACE_CHAR);
			}
			// We don't need to treat more line
			return true;
		}
		return false;
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1471"
	 */
	private <T> void formatMultlineComment(String indentationString, String newLineString, FormattedTextAccessor<T> backend) {
		final String indent = Strings.emptyIfNull(indentationString);
		final String comment = backend.getCommentText();
		// Compute the starting offset of the text inside the comment
		int offset = comment.indexOf(getMultilineCommentStartSymbols());
		if (offset < 0) {
			backend.replace(0, 0, getMultilineCommentStartSymbols());
			offset = 0;
			} else {
				offset += getMultilineCommentStartSymbols().length();
			}
			// Compute the ending offset of the text inside the comment
			int endOffset = comment.indexOf(getMultilineCommentEndSymbols(), offset);
			if (endOffset < 0) {
				endOffset = comment.length();
				backend.replace(endOffset, 0, getMultilineCommentEndSymbols());
			}
			// Go through the lines
			T currentLine = backend.getFirstLine(backend.getCommentOffset());
			boolean firstLine = true;
			while (currentLine != null) {
				String lineText = backend.getLineText(currentLine);
				int lineOffset = backend.getLineOffset(currentLine);
				int lineLength = backend.getLineLength(currentLine);
				// Clamp the line text to the comment area.
				if (lineOffset < offset) {
					final int len = offset - lineOffset;
					lineText = lineText.substring(len);
					lineOffset += len;
					lineLength -= len;
				}
				if ((lineOffset + lineLength) > endOffset) {
					final int len = lineOffset + lineLength - endOffset;
					lineText = lineText.substring(0, lineText.length() - len);
					lineLength -= len;
				}
				if (firstLine) {
					if (formatMultlineCommentFirstLine(lineText, indent, newLineString, endOffset - lineOffset, new SubAccessor(backend, lineOffset))) {
						backend.applyReplacements();
						return;
					}
				} else {
				if (formatMultlineCommentOtherLines(lineText, indent, newLineString, endOffset - lineOffset, new SubAccessor(backend, lineOffset))) {
					backend.applyReplacements();
					return;
				}
			}
			firstLine = false;
			currentLine = backend.getNextLine(currentLine);
		}
		backend.applyReplacements();
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1578"
	 */
	public interface FormattedTextAccessor<T> {
		T getFirstLine(int offset);
		T getNextLine(T currentLine);
		int getLineOffset(T currentLine);
		int getLineLength(T currentLine);
		String getLineText(T line);
		String getCommentText();
		int getCommentOffset();
		int getCommentEndOffset();
		Replacement replace(int offset, int length, String newText);
		void applyReplacements();
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1604"
	 */
	public class SubAccessor<T> implements FormattedTextAccessor<T> {
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1607"
	 */
		private final FormattedTextAccessor<T> parent;
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1610"
	 */
		private final int offsetInParent;
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1613"
	 */
		public SubAccessor(FormattedTextAccessor<T> parent, int offsetInParent) {
			assert parent != null;
			this.parent = parent;
			this.offsetInParent = offsetInParent;
		}
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1624"
	 */
		public T getFirstLine(int offset) {
			return this.parent.getFirstLine(offset);
		}
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1631"
	 */
		public T getNextLine(T currentLine) {
			return this.parent.getNextLine(currentLine);
		}
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1638"
	 */
		public int getLineOffset(T currentLine) {
			return this.parent.getLineOffset(currentLine);
		}
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1645"
	 */
		public int getLineLength(T currentLine) {
			return this.parent.getLineLength(currentLine);
		}
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1652"
	 */
		public String getLineText(T line) {
			return this.parent.getLineText(line);
		}
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1659"
	 */
		public String getCommentText() {
			return this.parent.getCommentText();
		}
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1666"
	 */
		public int getCommentOffset() {
			return this.parent.getCommentOffset();
		}
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1673"
	 */
		public int getCommentEndOffset() {
			return this.parent.getCommentEndOffset();
		}
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1680"
	 */
		public Replacement replace(int offset, int length, String newText) {
			return this.parent.replace(this.offsetInParent + offset, length, newText);
		}
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1687"
	 */
		public final void applyReplacements() {
			throw new UnsupportedOperationException();
		}
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1697"
	 */
	public static class Line {
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1700"
	 */
		private final int offset;
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1703"
	 */
		private final int length;
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1706"
	 */
		public static Line newInstance(String text, int offset) {
			if (offset < 0 || offset >= text.length()) {
				return null;
			}
			int soffset = offset;
			while (soffset >= 0 && !isNewLine(text.charAt(soffset))) {
				--soffset;
			}
			++soffset;
			int eoffset = soffset;
			while (eoffset < text.length() && !isNewLine(text.charAt(eoffset))) {
				++eoffset;
			}
			final int length = Math.max(0, eoffset - soffset);
			return new Line(soffset, length);
		}
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1741"
	 */
		private Line(int offset, int length) {
			this.offset = offset;
			this.length = length;
		}
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1750"
	 */
		public int getOffset() {
			return this.offset;
		}
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1757"
	 */
		public int getLength() {
			return this.length;
		}
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1764"
	 */
		public String toString() {
			return "offset: " + getOffset() + "; length: " + getLength();
		}
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1774"
	 */
	public static abstract class AbstractReplacementAccessor<T> implements FormattedTextAccessor<T> {
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1777"
	 */
		private final String documentation;
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1780"
	 */
		private SortedMap<Integer, Replacement> replacements;
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1785"
	 */
		private boolean applied;
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1788"
	 */
		public AbstractReplacementAccessor(String documentation, SortedMap<Integer, Replacement> replacements) {
			this.documentation = documentation;
			this.replacements = replacements;
		}
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1799"
	 */
		protected final void checkNotApplied() {
			if (this.applied) {
				throw new IllegalStateException("Changes are already applied");
			}
			this.applied = true;
		}
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1812"
	 */
		public String getCommentText() {
			return this.documentation;
		}
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1819"
	 */
		protected SortedMap<Integer, Replacement> getReplacements() {
			if (this.replacements == null) {
				this.replacements = new TreeMap<>();
			}
			return this.replacements;
		}
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1836"
	 */
		public Replacement replace(int offset, int length, String newText) {
			Replacement rep = getReplacements().remove(offset);
			if (rep == null) {
				rep = new Replacement(offset, length, newText);
			} else {
				rep = new Replacement(offset, rep.getLength() + length, rep.getText() + newText);
			}
			getReplacements().put(offset, rep);
			return rep;
		}
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1857"
	 */
		protected static void applyReplacements(IAppendable appendable, String text, Map<Integer, Replacement> replacements) {
			int offset = 0;
			for (final Replacement replacement : replacements.values()) {
				if (replacement.getOffset() < offset) {
					appendable.append("<<<Conflicting replacements>>>");
				} else {
					assert offset >= 0;
					assert replacement.getOffset() <= text.length();
					String notReplacedString = text.substring(offset, replacement.getOffset());
					appendable.append(notReplacedString);
					offset += notReplacedString.length();
					appendable.append(replacement.getText());
					offset += replacement.getLength();
				}
			}
			if (offset < text.length()) {
				String notReplacedString = text.substring(offset);
				appendable.append(notReplacedString);
			}
		}
	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1905"
	 */
	public static abstract class AbstractDebuggingAccessor<T> extends AbstractReplacementAccessor<T> {
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1908"
	 */
		private String buffer;
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1911"
	 */
		public AbstractDebuggingAccessor(String text, SortedMap<Integer, Replacement> replacements) {
			super(text, replacements);
		}
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1920"
	 */
		private String computeBuffer() {
			IAppendable appendable = new FakeTreeAppendable();
			applyReplacements(appendable, getCommentText(), getReplacements());
			return appendable.getContent();
		}
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1935"
	 */
		public String toString() {
			if (this.buffer == null) {
				this.buffer = computeBuffer();
			}
			return this.buffer;
		}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1949"
	 */
		public Replacement replace(int offset, int length, String newText) {
			final Replacement rep = super.replace(offset, length, newText);
			this.buffer = computeBuffer();
			return rep;
		}

	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1964"
	 */
	public static class RegionAccessor extends AbstractReplacementAccessor<ILineRegion> {
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1969"
	 */
		private final ITextReplacerContext context;
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1974"
	 */
		private final ITextRegionAccess access;
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1979"
	 */
		private final IComment comment;
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 1984"
	 */
		public RegionAccessor(ITextReplacerContext context, IComment comment) {
			super(comment.getText(), null);
			this.context = context;
			this.comment = comment;
			this.access = comment.getTextRegionAccess();
		}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 2002"
	 */
		public String getCommentText() {
			return this.comment.getText();
		}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 2010"
	 */
		public String getLineText(ILineRegion line) {
			ITextSegment segment = this.access.regionForOffset(line.getOffset(), line.getLength());
			return segment.getText();
		}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 2024"
	 */
		public int getCommentOffset() {
			return this.comment.getOffset();
		}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 2032"
	 */
		public int getCommentEndOffset() {
			return this.comment.getEndOffset();
		}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 2040"
	 */
		public ILineRegion getFirstLine(int offset) {
			return this.access.regionForLineAtOffset(offset);
		}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 2050"
	 */
		public ILineRegion getNextLine(ILineRegion currentLine) {
			return currentLine.getNextLine();
		}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 2062"
	 */
		public int getLineOffset(ILineRegion currentLine) {
			return currentLine.getOffset() - getCommentOffset();
		}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 2072"
	 */
		public int getLineLength(ILineRegion currentLine) {
			return currentLine.getLength();
		}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 2082"
	 */
		public void applyReplacements() {
			checkNotApplied();
			for (Replacement replacement : getReplacements().values()) {
				ITextSegment target = this.access.regionForOffset(replacement.getOffset() + getCommentOffset(), replacement.getLength());
				this.context.addReplacement(target.replaceWith(replacement.getText()));
			}
		}

	}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 2103"
	 */
	private static class AppendableAccessor extends AbstractReplacementAccessor<Line> {
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 2106"
	 */
		private final IAppendable target;
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 2111"
	 */
		private final int commentOffset;
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 2114"
	 */
		private final int commentEndOffset;
	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 2117"
	 */
		public AppendableAccessor(IAppendable target, String documentation, SortedMap<Integer, Replacement> replacements, int commentOffset, int commentEndOffset) {
			super(documentation, replacements);
			this.target = target;
			this.commentOffset = commentOffset;
			this.commentEndOffset = commentEndOffset;
		}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 2135"
	 */
		public Line getFirstLine(int offset) {
			return Line.newInstance(getCommentText(), offset);
		}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 2143"
	 */
		public Line getNextLine(Line currentLine) {
			int index = getCommentText().indexOf(NL_CHAR, currentLine.getOffset());
			if (index < 0) {
				return null;
			}
			return Line.newInstance(getCommentText(), index + 1);
		}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 2159"
	 */
		public int getLineOffset(Line currentLine) {
			return currentLine.getOffset();
		}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 2167"
	 */
		public int getLineLength(Line currentLine) {
			return currentLine.getLength();
		}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 2175"
	 */
		public String getLineText(Line line) {
			final int offset = line.getOffset();
			return getCommentText().substring(offset, offset + line.getLength());
		}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 2185"
	 */
		public int getCommentOffset() {
			return this.commentOffset;
		}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 2193"
	 */
		public int getCommentEndOffset() {
			return this.commentEndOffset;
		}

	/**
	 * @see "DocumentationBuilderFragment.java : appendTo : 2201"
	 */
		public void applyReplacements() {
			checkNotApplied();
