/*******************************************************************************
 * Copyright (c) 2014, 2019 Mateusz Matela and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Mateusz Matela <mateusz.matela@gmail.com> - [formatter] Formatter does not format Java code correctly, especially when max line width is set - https://bugs.eclipse.org/303519
 *     Mateusz Matela <mateusz.matela@gmail.com> - [formatter] follow up bug for comments - https://bugs.eclipse.org/458208
 *     Mateusz Matela <mateusz.matela@gmail.com> - NPE in WrapExecutor during Java text formatting  - https://bugs.eclipse.org/465669
 *******************************************************************************/
package org.aspectj.org.eclipse.jdt.internal.formatter.linewrap;

import static org.aspectj.org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameCOMMENT_BLOCK;
import static org.aspectj.org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameCOMMENT_JAVADOC;
import static org.aspectj.org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameCOMMENT_LINE;
import static org.aspectj.org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameTextBlock;
import static org.aspectj.org.eclipse.jdt.internal.compiler.parser.TerminalTokens.TokenNameWHITESPACE;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;

import org.aspectj.org.eclipse.jdt.internal.compiler.parser.ScannerHelper;
import org.aspectj.org.eclipse.jdt.internal.formatter.DefaultCodeFormatterOptions;
import org.aspectj.org.eclipse.jdt.internal.formatter.DefaultCodeFormatterOptions.Alignment;
import org.aspectj.org.eclipse.jdt.internal.formatter.Token;
import org.aspectj.org.eclipse.jdt.internal.formatter.TokenManager;
import org.aspectj.org.eclipse.jdt.internal.formatter.TokenTraverser;
import org.aspectj.org.eclipse.jdt.internal.formatter.Token.WrapMode;
import org.aspectj.org.eclipse.jdt.internal.formatter.Token.WrapPolicy;

public class WrapExecutor {

	private static class WrapInfo {
		public int wrapTokenIndex;
		public int indent;

		public WrapInfo(int wrapIndex, int indent) {
			this.wrapTokenIndex = wrapIndex;
			this.indent = indent;
		}

		public WrapInfo() {
			// empty constructor
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + this.indent;
			result = prime * result + this.wrapTokenIndex;
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			WrapInfo other = (WrapInfo) obj;
			if (this.indent != other.indent)
				return false;
			if (this.wrapTokenIndex != other.wrapTokenIndex)
				return false;
			return true;
		}

	}

	private static class WrapResult {

		public static final WrapResult NO_WRAP_NEEDED = new WrapResult(0, 0, null);

		/** Penalty for used wraps */
		public final double penalty;
		/** Penalty for exceeding line limit and extra lines in comments */
		public final int extraPenalty;
		/**
		 * Contains information about the next wrap in the result or <code>null</code> if this is the last wrap.
		 * Can be used as a key in {@link WrapExecutor#wrapSearchResults} to retrieve the next wraps.
		 */
		public final WrapInfo nextWrap;

		WrapResult(double penalty, int extraPenalty, WrapInfo nextWrap) {
			this.penalty = penalty;
			this.extraPenalty = extraPenalty;
			this.nextWrap = nextWrap;
		}
	}

	private class LineAnalyzer extends TokenTraverser {

		private final TokenManager tm2 = WrapExecutor.this.tm;
		private final CommentWrapExecutor commentWrapper;
		private int lineIndent;
		int firstPotentialWrap;
		int activeTopPriorityWrap;
		int minStructureDepth;
		int extraLines;
		int lineWidthExtent;
		boolean isNextLineWrapped;
		final List<Integer> extraLinesPerComment = new ArrayList<Integer>();
		final List<Integer> topPriorityGroupStarts = new ArrayList<Integer>();
		private int currentTopPriorityGroupEnd;
		private boolean isNLSTagInLine;

		public LineAnalyzer(TokenManager tokenManager, DefaultCodeFormatterOptions options) {
			this.commentWrapper = new CommentWrapExecutor(tokenManager, options);
		}

		/**
		 * @return index of the last token in line
		 */
		public int analyzeLine(int startIndex, int indent) {
			Token startToken = this.tm2.get(startIndex);
			assert startToken.getLineBreaksBefore() > 0;
			this.counter = this.tm2.toIndent(indent, startToken.isWrappable());
			this.lineIndent = indent;
			this.firstPotentialWrap = -1;
			this.activeTopPriorityWrap = -1;
			this.minStructureDepth = Integer.MAX_VALUE;
			this.extraLines = 0;
			this.lineWidthExtent = 0;
			this.isNextLineWrapped = false;
			this.extraLinesPerComment.clear();
			this.topPriorityGroupStarts.clear();
			this.currentTopPriorityGroupEnd = -1;
			this.isNLSTagInLine = false;
			int lastIndex = this.tm2.traverse(startIndex, this);
			return lastIndex + (this.isNextLineWrapped ? 1 : 0);
		}

		@Override
		protected boolean token(Token token, int index) {
			setIndent(token, this.lineIndent);

			if (token.hasNLSTag())
				this.isNLSTagInLine = true;

			if (token.isWrappable()) {
				WrapPolicy wrapPolicy = token.getWrapPolicy();
				if (wrapPolicy.wrapMode == WrapMode.TOP_PRIORITY && getLineBreaksBefore() == 0
						&& index > this.currentTopPriorityGroupEnd) {
					if (isActiveTopPriorityWrap(index, wrapPolicy)) {
						this.activeTopPriorityWrap = index;
					} else {
						this.topPriorityGroupStarts.add(index);
						this.currentTopPriorityGroupEnd = wrapPolicy.groupEndIndex;
					}
					if (this.firstPotentialWrap < 0)
						this.firstPotentialWrap = index;
				} else if (this.firstPotentialWrap < 0 && getWrapIndent(token) < this.counter) {
					this.firstPotentialWrap = index;
				}
				this.minStructureDepth = Math.min(this.minStructureDepth, wrapPolicy.structureDepth);
			}

			if (token.getAlign() > 0) {
				this.counter = token.getAlign();
			} else if (isSpaceBefore() && getLineBreaksBefore() == 0 && index > 0
					&& token.tokenType != TokenNameCOMMENT_LINE) {
				this.counter++;
			}

			if (token.tokenType == TokenNameTextBlock) {
				List<Token> lines = token.getInternalStructure();
				if (lines == null) {
					this.counter = this.tm2.getLength(token, 0);
				} else {
					this.lineWidthExtent = Math.max(this.lineWidthExtent,
							this.counter + this.tm2.getLength(lines.get(0), this.counter));
					this.counter = this.lineIndent + lines.get(1).getIndent();
					lines.stream().skip(1).forEach(e -> this.lineWidthExtent = Math.max(this.lineWidthExtent,
							this.counter + this.tm2.getLength(e, this.counter)));
					this.counter += this.tm2.getLength(lines.get(lines.size() - 1), this.counter);
				}
			} else if (!token.isComment()) {
				this.counter += this.tm2.getLength(token, this.counter);
			} else if (token.tokenType != TokenNameCOMMENT_LINE) {
				this.counter = this.commentWrapper.wrapMultiLineComment(token, this.counter, true, this.isNLSTagInLine);
				this.extraLines += this.commentWrapper.getLinesCount() - 1;
				this.extraLinesPerComment.add(this.commentWrapper.getLinesCount() - 1);
			}

			this.lineWidthExtent = Math.max(this.lineWidthExtent, this.counter);
			if (this.lineWidthExtent > WrapExecutor.this.options.page_width && this.firstPotentialWrap >= 0) {
				return false;
			}

			if (getNext() != null && getNext().isWrappable() && getLineBreaksAfter() > 0) {
				this.isNextLineWrapped = true;
				if (this.firstPotentialWrap < 0)
					this.firstPotentialWrap = index + 1;
				return false; 
			}

			boolean isLineEnd = getLineBreaksAfter() > 0 || getNext() == null || (getNext().isNextLineOnWrap()
					&& this.tm2.get(this.tm2.findFirstTokenInLine(index)).isWrappable());
			return !isLineEnd;
		}

		private boolean isActiveTopPriorityWrap(int index, WrapPolicy wrapPolicy) {
			if (this.activeTopPriorityWrap >= 0)
				return false;

			for (int i = index - 1; i > wrapPolicy.wrapParentIndex; i--) {
				Token token = this.tm2.get(i);
				if (token.isWrappable() && token.getWrapPolicy().wrapParentIndex == wrapPolicy.wrapParentIndex
					&& (token.getLineBreaksBefore() > 0 || this.tm2.get(i - 1).getLineBreaksAfter() > 0)) {
						return true;
				}
			}
			return false;
		}
	}

	private class WrapsApplier extends TokenTraverser {

		private ArrayDeque<Token> stack = new ArrayDeque<>();
		private int initialIndent;
		private int currentIndent;
		private WrapInfo nextWrap;

		public WrapsApplier() {
			// nothing to do
		}

		@Override
		protected boolean token(Token token, int index) {
			if (index == 0 || getLineBreaksBefore() > 0) {
				newLine(token, index);
			} else if ((this.nextWrap != null && index == this.nextWrap.wrapTokenIndex)
					|| checkForceWrap(token, index, this.currentIndent)
					|| (token.isNextLineOnWrap() && WrapExecutor.this.tm
							.get(WrapExecutor.this.tm.findFirstTokenInLine(index)).isWrappable())) {
				token.breakBefore();
				newLine(token, index);
			} else {
				setIndent(token, this.currentIndent);
			}
			return true;
		}

		private void newLine(Token token, int index) {
			while (!this.stack.isEmpty() && index > this.stack.peek().getWrapPolicy().groupEndIndex)
				this.stack.pop();
			if (token.getWrapPolicy() != null) {
				setIndent(token, getWrapIndent(token));
				handleOnColumnIndent(index, token.getWrapPolicy());
				this.stack.push(token);
			} else if (this.stack.isEmpty()) {
				this.initialIndent = token.getIndent();
				WrapExecutor.this.wrapSearchResults.clear();
			}

			this.currentIndent = this.stack.isEmpty() ? this.initialIndent : this.stack.peek().getIndent();
			setIndent(token, this.currentIndent);
			this.nextWrap = findWrapsCached(index, this.currentIndent).nextWrap;
		}
	}

	private class NLSTagHandler extends TokenTraverser {
		private final ArrayList<Token> nlsTags = new ArrayList<Token>();

		public NLSTagHandler() {
			// nothing to do
		}

		@Override
		protected boolean token(final Token token, final int index) {
			if (token.hasNLSTag())
				this.nlsTags.add(token.getNLSTag());

			if (getLineBreaksAfter() > 0 || getNext() == null) {
				// make sure there's a line comment with all necessary NLS tags
				Token lineComment = token;
				if (token.tokenType != TokenNameCOMMENT_LINE) {
					if (this.nlsTags.isEmpty())
						return true;
					lineComment = new Token(token.originalEnd + 1, token.originalEnd + 1, TokenNameCOMMENT_LINE);
					lineComment.breakAfter();
					lineComment.spaceBefore();
					lineComment.setAlign(WrapExecutor.this.tm.getNLSAlign(index));
					lineComment.setInternalStructure(new ArrayList<Token>());
					WrapExecutor.this.tm.insert(index + 1, lineComment);
					structureChanged();
					return true; // will fill the line comment structure in next step
				}

				List<Token> structure = lineComment.getInternalStructure();
				if (structure == null) {
					if (this.nlsTags.isEmpty())
						return true;
					structure = new ArrayList<Token>();
					structure.add(lineComment);
					lineComment.setInternalStructure(structure);
				}

				boolean isPrefixMissing = false;
				for (int i = 0; i < structure.size(); i++) {
					Token fragment = structure.get(i);
					// remove NLS tags that are not associated with this line
					// (these have been added on wrapped lines earlier)
					if (fragment.hasNLSTag()) {
						if (!this.nlsTags.remove(fragment)) {
							if (i == 0)
								isPrefixMissing = true;
							structure.remove(i--);
						} else {
							isPrefixMissing = false;
						}
					} else if (isPrefixMissing) {
						// remove trailing whitespace
						int pos = fragment.originalStart;
						while (pos <= fragment.originalEnd
								&& ScannerHelper.isWhitespace(WrapExecutor.this.tm.charAt(pos)))
							pos++;
						if (pos > fragment.originalEnd) {
							structure.remove(i--);
							continue;
						}
						if (pos > fragment.originalStart) {
							fragment = new Token(pos, fragment.originalEnd, TokenNameCOMMENT_LINE);
							structure.set(i, fragment);
						}

						String fragmentString = WrapExecutor.this.tm.toString(fragment);
						if (!fragmentString.startsWith("//")) { //$NON-NLS-1$
							// forge a prefix
							Token prefix = new Token(lineComment.originalStart, lineComment.originalStart + 1,
									TokenNameCOMMENT_LINE);
							prefix.spaceBefore();
							structure.add(i, prefix);
						}
						isPrefixMissing = false;
					}
				}
				// add all remaining tags in this line
				// (these are currently in a future line comment but will be removed)
				structure.addAll(this.nlsTags);

				if (structure.isEmpty()
						|| (structure.size() == 1 && structure.get(0).tokenType == TokenNameWHITESPACE)) {
					// all the tags have been moved to other lines
					WrapExecutor.this.tm.remove(index);
					structureChanged();
				}

				this.nlsTags.clear();
			}
			return true;
		}
	}

	private final static int[] EMPTY_ARRAY = {};

	final HashMap<WrapInfo, WrapResult> wrapSearchResults = new HashMap<WrapInfo, WrapResult>();
	private final ArrayDeque<WrapInfo> wrapSearchStack = new ArrayDeque<>();

	private final LineAnalyzer lineAnalyzer;

	final TokenManager tm;
	final DefaultCodeFormatterOptions options;

	private final WrapInfo wrapInfoTemp = new WrapInfo();

	public WrapExecutor(TokenManager tokenManager, DefaultCodeFormatterOptions options) {
		this.tm = tokenManager;
		this.options = options;
		this.lineAnalyzer = new LineAnalyzer(tokenManager, options);
	}

	public void executeWraps() {
		this.tm.traverse(0, new WrapsApplier());
		this.tm.traverse(0, new NLSTagHandler());
	}

	WrapResult findWrapsCached(final int startTokenIndex, final int indent) {
		this.wrapInfoTemp.wrapTokenIndex = startTokenIndex;
		this.wrapInfoTemp.indent = indent;
		WrapResult wrapResult = this.wrapSearchResults.get(this.wrapInfoTemp);

		// pre-existing result may be based on different wrapping of earlier tokens and therefore be wrong
		WrapResult wr = wrapResult;
		boolean cacheMissAllowed = true;
		int lookupLimit = 50;
		while (wr != null && wr.nextWrap != null && lookupLimit --> 0) {
			WrapInfo wi = wr.nextWrap;
			Token token = this.tm.get(wi.wrapTokenIndex);
			if (token.getWrapPolicy().wrapParentIndex < startTokenIndex && getWrapIndent(token) != wi.indent) {
				wrapResult = null;
				cacheMissAllowed = false;
				break;
			}
			wr = this.wrapSearchResults.get(wi);
		}

		if (wrapResult != null)
			return wrapResult;

		this.wrapSearchStack.push(new WrapInfo(startTokenIndex, indent));
		if (this.wrapSearchStack.size() > 1 && cacheMissAllowed)
			return null; // cache miss, need to find wraps later in main stack processing

		ArrayList<WrapInfo> reverseStackTemp = new ArrayList<>();
		// run main stack processing
		while (true) {
			final WrapInfo item = this.wrapSearchStack.peek();
			Token token = this.tm.get(item.wrapTokenIndex);
			token.setWrapped(true);
			wrapResult = findWraps(item.wrapTokenIndex, item.indent);

			assert (wrapResult == null) == (this.wrapSearchStack.peek() != item);
			if (wrapResult != null) {
				token.setWrapped(false);
				this.wrapSearchStack.pop();
				this.wrapSearchResults.put(item, wrapResult);
				assert wrapResult.nextWrap == null || this.wrapSearchResults.get(wrapResult.nextWrap) != null; 
				if (item.wrapTokenIndex == startTokenIndex && item.indent == indent)
					break;
			} else {
				// reverse order of new items
				while (this.wrapSearchStack.peek() != item)
					reverseStackTemp.add(this.wrapSearchStack.pop());
				for (WrapInfo item2 : reverseStackTemp)
					this.wrapSearchStack.push(item2);
				reverseStackTemp.clear();
			}
		}
		assert wrapResult != null;
		return wrapResult;
	}

	/**
	 * The main algorithm that looks for optimal places to wrap.
	 * Calls itself recursively to get results for wrapped sub-lines.  
	 */
	private WrapResult findWraps(int wrapTokenIndex, int indent) {
		final int lastIndex = this.lineAnalyzer.analyzeLine(wrapTokenIndex, indent);
		final boolean nextLineWrapped = this.lineAnalyzer.isNextLineWrapped;
		int lineOverflow = Math.max(0, this.lineAnalyzer.lineWidthExtent - this.options.page_width);
		final boolean wrapRequired = lineOverflow > 0 || nextLineWrapped;
		int extraLines = this.lineAnalyzer.extraLines;
		final int firstPotentialWrap = this.lineAnalyzer.firstPotentialWrap;
		final int activeTopPriorityWrap = this.lineAnalyzer.activeTopPriorityWrap;

		final int[] extraLinesPerComment = toArray(this.lineAnalyzer.extraLinesPerComment);
		int commentIndex = extraLinesPerComment.length;

		final int[] topPriorityGroupStarts = toArray(this.lineAnalyzer.topPriorityGroupStarts);
		int topPriorityIndex = topPriorityGroupStarts.length - 1;
		int nearestGroupEnd = topPriorityIndex == -1 ? 0
				: this.tm.get(topPriorityGroupStarts[topPriorityIndex]).getWrapPolicy().groupEndIndex;

		double bestTotalPenalty = getWrapPenalty(wrapTokenIndex, indent, lastIndex + 1, -1, WrapResult.NO_WRAP_NEEDED);
		int bestExtraPenalty = lineOverflow + extraLines;
		int bestNextWrap = -1;
		int bestIndent = 0;
		boolean cacheMiss = false;

		if (!wrapRequired && activeTopPriorityWrap < 0
				&& (!this.options.join_wrapped_lines || !this.options.wrap_outer_expressions_when_nested)) {
			return new WrapResult(bestTotalPenalty, bestExtraPenalty, null);
		}

		// optimization: if there's a possible wrap at depth lower than line start, ignore the rest
		int depthLimit = Integer.MAX_VALUE;
		Token token = this.tm.get(wrapTokenIndex);
		if (token.isWrappable() && this.options.wrap_outer_expressions_when_nested && activeTopPriorityWrap < 0) {
			int currentDepth = token.getWrapPolicy().structureDepth;
			if (this.lineAnalyzer.minStructureDepth < currentDepth)
				depthLimit = currentDepth;
		}
		// optimization: turns out there's no point checking multiple wraps with the same policy 
		LinkedHashSet<WrapPolicy> policiesTried = new LinkedHashSet<>();

		for (int i = lastIndex; firstPotentialWrap >= 0 && i >= firstPotentialWrap; i--) {
			token = this.tm.get(i);
			if (commentIndex > 0
					&& (token.tokenType == TokenNameCOMMENT_BLOCK || token.tokenType == TokenNameCOMMENT_JAVADOC)) {
				extraLines -= extraLinesPerComment[--commentIndex];
				if (extraLinesPerComment[commentIndex] > 0)
					policiesTried.clear();
			}
			if (topPriorityIndex >= 0 && i <= nearestGroupEnd) {
				if (i > topPriorityGroupStarts[topPriorityIndex])
					continue;
				assert i == topPriorityGroupStarts[topPriorityIndex];
				topPriorityIndex--;
				nearestGroupEnd = topPriorityIndex == -1 ? 0
						: this.tm.get(topPriorityGroupStarts[topPriorityIndex]).getWrapPolicy().groupEndIndex;
			}

			WrapPolicy wrapPolicy = token.getWrapPolicy();
			if (!token.isWrappable()
					|| (activeTopPriorityWrap >= 0 && i != activeTopPriorityWrap)
					|| policiesTried.contains(wrapPolicy)
					|| wrapPolicy.structureDepth >= depthLimit)
				continue;
			policiesTried.add(wrapPolicy);

			int nextWrapIndent = getWrapIndent(token);
			WrapResult nextWrapResult = findWrapsCached(i, nextWrapIndent);
			cacheMiss |= nextWrapResult == null;
			if (cacheMiss)
				continue;

			double totalPenalty = getWrapPenalty(wrapTokenIndex, indent, i, nextWrapIndent, nextWrapResult);
			int totalExtraPenalty = nextWrapResult.extraPenalty + extraLines;
			if (lineOverflow > 0) {
				int position = this.tm.getPositionInLine(i - 1);
				position += this.tm.getLength(this.tm.get(i - 1), position);
				lineOverflow = position - this.options.page_width;
				totalExtraPenalty += Math.max(0, lineOverflow);
			}
			boolean isBetter = totalExtraPenalty < bestExtraPenalty
					|| i == activeTopPriorityWrap
					|| (bestNextWrap < 0 && wrapRequired);
			if (!isBetter && totalExtraPenalty == bestExtraPenalty)
				isBetter = totalPenalty < bestTotalPenalty || bestTotalPenalty == Double.MAX_VALUE;
			if (isBetter) {
				bestTotalPenalty = totalPenalty;
				bestExtraPenalty = totalExtraPenalty;
				bestNextWrap = i;
				bestIndent = nextWrapIndent;

				if (!this.options.wrap_outer_expressions_when_nested || i == activeTopPriorityWrap || nextLineWrapped)
					break;
			}
		}
		if (cacheMiss)
			return null;

		return new WrapResult(bestTotalPenalty, bestExtraPenalty,
				bestNextWrap == -1 ? null : new WrapInfo(bestNextWrap, bestIndent));
	}

	private double getWrapPenalty(int lineStartIndex, int lineIndent, int wrapIndex, int wrapIndent,
			WrapResult wrapResult) {
		WrapPolicy wrapPolicy = null;
		Token wrapToken = null;
		if (wrapIndex < this.tm.size()) {
			wrapToken = this.tm.get(wrapIndex);
			wrapPolicy = wrapToken.getWrapPolicy();
			if (wrapIndent < 0)
				wrapIndent = getWrapIndent(this.tm.get(wrapIndex));
		}

		double penalty = wrapToken != null && wrapToken.isWrappable() ? getPenalty(wrapPolicy) : 0;

		// First parameter in method invocation has higher penalty to make wrapping more similar to the old formatter.
		// This can lead to an undesired effect like this (should wrap aaaaaa and bbbbbb, not .bar):
		// foo.foo
		// 		.bar(aaaaaa,
		// 				bbbbbbb);
		if (wrapIndent > lineIndent)
			penalty *= 1 + 3.0 / 16;

		// Avoid ugly formations like this (bar2 should be wrapped):
		// foooooo(bar1(aaaaaa,
		// 		bbb), bar2(aaa,
		// 				bbbbbb)
		// Assuming lineStartIndex is at bbb, look for unwrapped bar2 and if found,
		// add more penalty than if it was wrapped.
		Token lineStartToken = this.tm.get(lineStartIndex);
		WrapPolicy lineStartWrapPolicy = lineStartToken.getWrapPolicy();
		if (wrapToken != null && wrapToken.isWrappable() && lineStartToken.isWrappable()) {
			for (int i = lineStartIndex + 1; i < wrapIndex; i++) {
				WrapPolicy intermediatePolicy = this.tm.get(i).getWrapPolicy();
				if (intermediatePolicy != null
						&& intermediatePolicy.structureDepth < lineStartWrapPolicy.structureDepth
						&& intermediatePolicy.structureDepth < wrapPolicy.structureDepth) {
					penalty += getPenalty(intermediatePolicy) * 1.25;
				}
			}
		}

		// In the previous example, bar1 should be wrapped too, to emphasize that bar1 and bar2 are the same level.
		// Assuming wrapIndex is at bar1, check if there is a higher depth wrap (bbb) followed by
		// a wrap of the same parent (bar2). If so, then bar1 must be wrapped (so give it negative penalty).
		// Update: Actually, every token that is followed by a higher level depth wrap should be also wrapped,
		// as long as this next wrap is not the last in line and the token is not the first in its wrap group.
		WrapInfo nextWrap = wrapResult.nextWrap;
		boolean checkDepth = wrapToken != null && wrapToken.isWrappable()
				&& (lineStartWrapPolicy == null || wrapPolicy.structureDepth >= lineStartWrapPolicy.structureDepth);
		double penaltyDiff = 0;
		while (checkDepth && nextWrap != null) {
			WrapPolicy nextPolicy = this.tm.get(nextWrap.wrapTokenIndex).getWrapPolicy();
			if (nextPolicy.wrapParentIndex == wrapPolicy.wrapParentIndex
					|| (penaltyDiff != 0 && !wrapPolicy.isFirstInGroup)) {
				penalty -= penaltyDiff * (1 + 1.0 / 64);
				break;
			}
			if (nextPolicy.structureDepth <= wrapPolicy.structureDepth)
				break;
			penaltyDiff = Math.max(penaltyDiff, getPenalty(nextPolicy));
			nextWrap = this.wrapSearchResults.get(nextWrap).nextWrap;
		}

		return penalty + wrapResult.penalty;
	}

	private double getPenalty(WrapPolicy policy) {
		return Math.exp(policy.structureDepth) * policy.penaltyMultiplier;
	}

	boolean checkForceWrap(Token token, int index, int currentIndent) {
		// A token that will have smaller indent when wrapped than the current line indent,
		// should be wrapped because it's a low depth token following some complex wraps of higher depth.
		// This rule could not be implemented in getWrapPenalty() because a token's wrap indent may depend
		// on wraps in previous lines, which are not determined yet when the token's penalty is calculated.
		if (!token.isWrappable() || !this.options.wrap_outer_expressions_when_nested
				|| getWrapIndent(token) >= currentIndent)
			return false;
		WrapPolicy lineStartPolicy = this.tm.get(this.tm.findFirstTokenInLine(index, false, true)).getWrapPolicy();
		return lineStartPolicy != null && lineStartPolicy.wrapMode != WrapMode.BLOCK_INDENT;
	}

	private int[] toArray(List<Integer> list) {
		if (list.isEmpty())
			return EMPTY_ARRAY;
		int[] result = new int[list.size()];
		int i = 0;
		for (int item : list) {
			result[i++] = item;
		}
		return result;
	}

	void handleOnColumnIndent(int tokenIndex, WrapPolicy wrapPolicy) {
		if (wrapPolicy != null && wrapPolicy.indentOnColumn && !wrapPolicy.isFirstInGroup
				&& this.options.tab_char == DefaultCodeFormatterOptions.TAB
				&& !this.options.use_tabs_only_for_leading_indentations) {
			// special case: first wrap in a group should be aligned on column even if it's not wrapped
			for (int i = tokenIndex - 1; i >= 0; i--) {
				Token token = this.tm.get(i);
				WrapPolicy wrapPolicy2 = token.getWrapPolicy();
				if (wrapPolicy2 != null && wrapPolicy2.isFirstInGroup
						&& wrapPolicy2.wrapParentIndex == wrapPolicy.wrapParentIndex) {
					token.setAlign(getWrapIndent(token));
					break;
				}
			}
		}
	}

	int getWrapIndent(Token token) {
		WrapPolicy policy = token.getWrapPolicy();
		if (policy == null)
			return token.getIndent();
		if (policy == WrapPolicy.FORCE_FIRST_COLUMN)
			return 0;

		Token wrapParent = this.tm.get(policy.wrapParentIndex);
		int wrapIndent = wrapParent.getIndent();
		if (policy.indentOnColumn) {
			wrapIndent = this.tm.getPositionInLine(policy.wrapParentIndex);
			wrapIndent += this.tm.getLength(wrapParent, wrapIndent);
			Token next = this.tm.get(policy.wrapParentIndex + 1);
			if (wrapParent.isSpaceAfter() || (next.isSpaceBefore() && !next.isComment()))
				wrapIndent++;
		}
		wrapIndent += policy.extraIndent;
		return this.tm.toIndent(wrapIndent, true);
	}

	void setIndent(Token token, int indent) {
		token.setIndent(indent);

		List<Token> structure = token.getInternalStructure();
		if (token.tokenType == TokenNameTextBlock && structure != null) {
			int lineIndent;
			int indentOption = this.options.text_block_indentation;
			if (indentOption == Alignment.M_INDENT_BY_ONE) {
				lineIndent = 1 * this.options.indentation_size;
			} else if (indentOption == Alignment.M_INDENT_DEFAULT) {
				lineIndent = this.options.continuation_indentation * this.options.indentation_size;
			} else if (indentOption == Alignment.M_INDENT_ON_COLUMN) {
				lineIndent = this.tm.toIndent(this.tm.getPositionInLine(this.tm.indexOf(token)), true) - indent;
			} else {
				assert false;
				lineIndent = 0;
			}
			structure.stream().skip(1).forEach(t -> t.setIndent(lineIndent));
		}
	}
}
