/*******************************************************************************
 * Copyright (c) 2006, 2013 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.core.formatter;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;

import org.eclipse.jdt.core.formatter.DefaultCodeFormatterConstants;
import org.eclipse.jdt.internal.compiler.parser.ScannerHelper;
import org.eclipse.jdt.internal.compiler.util.Util;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DefaultLineTracker;
import org.eclipse.jface.text.ILineTracker;
import org.eclipse.jface.text.IRegion;

import org.eclipse.text.edits.ReplaceEdit;

/**
 * Helper class to provide String manipulation functions dealing with indentations.
 *
 * @since 3.2
 * @noinstantiate This class is not intended to be instantiated by clients.
 */
@SuppressWarnings({ "rawtypes", "unchecked" })
public final class IndentManipulation {

	private IndentManipulation() {
		// don't instantiate
	}

	/**
	 * Returns <code>true</code> if the given character is an indentation character. Indentation character are all whitespace characters
	 * except the line delimiter characters.
	 *
	 * @param ch the given character
	 * @return Returns <code>true</code> if this the character is a indent character, <code>false</code> otherwise
	 */
	public static boolean isIndentChar(char ch) {
		return ScannerHelper.isWhitespace(ch) && !isLineDelimiterChar(ch);
	}

	/**
	 * Returns <code>true</code> if the given character is a line delimiter character.
	 *
	 * @param ch the given character
	 * @return Returns <code>true</code> if this the character is a line delimiter character, <code>false</code> otherwise
	 */
	public static boolean isLineDelimiterChar(char ch) {
		return ch == '\n' || ch == '\r';
	}

	/**
	 * Returns the indentation of the given line in indentation units. Odd spaces are
	 * not counted. This method only analyzes the content of <code>line</code> up to the first
	 * non-whitespace character.
	 *
	 * @param line the string to measure the indent of
	 * @param tabWidth the width of one tab character in space equivalents
	 * @param indentWidth the width of one indentation unit in space equivalents
	 * @return the number of indentation units that line is indented by
	 * @exception IllegalArgumentException if:
	 * <ul>
	 * <li>the given <code>indentWidth</code> is lower than zero</li>
	 * <li>the given <code>tabWidth</code> is lower than zero</li>
	 * <li>the given <code>line</code> is null</li>
	 * </ul>
	 */
	public static int measureIndentUnits(CharSequence line, int tabWidth, int indentWidth) {
		if (indentWidth < 0 || tabWidth < 0 || line == null) {
			throw new IllegalArgumentException();
		}

		if (indentWidth == 0) return 0;
		int visualLength= measureIndentInSpaces(line, tabWidth);
		return visualLength / indentWidth;
	}

	/**
	 * Returns the indentation of the given line in space equivalents.
	 *
	 * <p>Tab characters are counted using the given <code>tabWidth</code> and every other indent
	 * character as one. This method analyzes the content of <code>line</code> up to the first
	 * non-whitespace character.</p>
	 *
	 * @param line the string to measure the indent of
	 * @param tabWidth the width of one tab in space equivalents
	 * @return the measured indent width in space equivalents
	 * @exception IllegalArgumentException if:
	 * <ul>
	 * <li>the given <code>line</code> is null</li>
	 * <li>the given <code>tabWidth</code> is lower than zero</li>
	 * </ul>
	 */
	public static int measureIndentInSpaces(CharSequence line, int tabWidth) {
		if (tabWidth < 0 || line == null) {
			throw new IllegalArgumentException();
		}

		int length= 0;
		int max= line.length();
		for (int i= 0; i < max; i++) {
			char ch= line.charAt(i);
			if (ch == '\t') {
				length = calculateSpaceEquivalents(tabWidth, length);
			} else if (isIndentChar(ch)) {
				length++;
			} else {
				return length;
			}
		}
		return length;
	}

	/**
	 * Returns the leading indentation string of the given line. Note that the returned string
	 * need not be equal to the leading whitespace as odd spaces are not considered part of the
	 * indentation.
	 *
	 * @param line the line to scan
	 * @param tabWidth the size of one tab in space equivalents
	 * @param indentWidth the width of one indentation unit in space equivalents
	 * @return the indent part of <code>line</code>, but no odd spaces
	 * @exception IllegalArgumentException if:
	 * <ul>
	 * <li>the given <code>indentWidth</code> is lower than zero</li>
	 * <li>the given <code>tabWidth</code> is lower than zero</li>
	 * <li>the given <code>line</code> is null</li>
	 * </ul>
	 */
	public static String extractIndentString(String line, int tabWidth, int indentWidth) {
		if (tabWidth < 0 || indentWidth < 0 || line == null) {
			throw new IllegalArgumentException();
		}

		int size = line.length();
		int end = 0;

		int spaceEquivs = 0;
		int characters = 0;
		for (int i = 0; i < size; i++) {
			char c = line.charAt(i);
			if (c == '\t') {
				spaceEquivs = calculateSpaceEquivalents(tabWidth, spaceEquivs);
				characters++;
			} else if (isIndentChar(c)) {
				spaceEquivs++;
				characters++;
			} else {
				break;
			}
			if (spaceEquivs >= indentWidth) {
				end += characters;
				characters = 0;
				if(indentWidth == 0) {
					spaceEquivs = 0;
				} else {
					spaceEquivs = spaceEquivs % indentWidth;
				}
			}
		}
		if (end == 0) {
			return Util.EMPTY_STRING;
		} else if (end == size) {
			return line;
		} else {
			return line.substring(0, end);
		}
	}


	/**
	 * Removes the given number of indentation units from a given line. If the line
	 * has less indent than the given indentUnitsToRemove, all the available indentation is removed.
	 * If <code>indentsToRemove <= 0 or indent == 0</code> the line is returned.
	 *
	 * @param line the line to trim
	 * @param tabWidth the width of one tab in space equivalents
	 * @param indentWidth the width of one indentation unit in space equivalents
	 * @return the trimmed string
	 * @exception IllegalArgumentException if:
	 * <ul>
	 * <li>the given <code>indentWidth</code> is lower than zero</li>
	 * <li>the given <code>tabWidth</code> is lower than zero</li>
	 * <li>the given <code>line</code> is null</li>
	 * </ul>
	 */
	public static String trimIndent(String line, int indentUnitsToRemove, int tabWidth, int indentWidth) {
		if (tabWidth < 0 || indentWidth < 0 || line == null) {
			throw new IllegalArgumentException();
		}

		if (indentUnitsToRemove <= 0 || indentWidth == 0) {
			return line;
		}
		final int spaceEquivalentsToRemove= indentUnitsToRemove * indentWidth;

		int start= 0;
		int spaceEquivalents= 0;
		int size= line.length();
		String prefix= null;
		for (int i= 0; i < size; i++) {
			char c= line.charAt(i);
			if (c == '\t') {
				spaceEquivalents = calculateSpaceEquivalents(tabWidth, spaceEquivalents);
			} else if (isIndentChar(c)) {
				spaceEquivalents++;
			} else {
				// Assert.isTrue(false, "Line does not have requested number of indents");
				start= i;
				break;
			}
			if (spaceEquivalents == spaceEquivalentsToRemove) {
				start= i + 1;
				break;
			}
			if (spaceEquivalents > spaceEquivalentsToRemove) {
				// can happen if tabSize > indentSize, e.g tabsize==8, indent==4, indentsToRemove==1, line prefixed with one tab
				// this implements the third option
				start= i + 1; // remove the tab
				// and add the missing spaces
				char[] missing= new char[spaceEquivalents - spaceEquivalentsToRemove];
				Arrays.fill(missing, ' ');
				prefix= new String(missing);
				break;
			}
		}
		String trimmed;
		if (start == size)
			trimmed= Util.EMPTY_STRING;
		else
			trimmed= line.substring(start);

		if (prefix == null)
			return trimmed;
		return prefix + trimmed;
	}

	/**
	 * Change the indent of a, possible multiple line, code string. The given number of indent units is removed,
	 * and a new indent string is added.
	 * <p>The first line of the code will not be changed (It is considered to have no indent as it might start in
	 * the middle of a line).</p>
	 *
	 * @param code the code to change the indent of
	 * @param indentUnitsToRemove the number of indent units to remove from each line (except the first) of the given code
	 * @param tabWidth the size of one tab in space equivalents
	 * @param indentWidth the width of one indentation unit in space equivalents
	 * @param newIndentString the new indent string to be added to all lines (except the first)
	 * @param lineDelim the new line delimiter to be used. The returned code will contain only this line delimiter.
	 * @return the newly indent code, containing only the given line delimiters.
	 * @exception IllegalArgumentException if:
	 * <ul>
	 * <li>the given <code>indentWidth</code> is lower than zero</li>
	 * <li>the given <code>tabWidth</code> is lower than zero</li>
	 * <li>the given <code>code</code> is null</li>
	 * <li>the given <code>indentUnitsToRemove</code> is lower than zero</li>
	 * <li>the given <code>newIndentString</code> is null</li>
	 * <li>the given <code>lineDelim</code> is null</li>
	 * </ul>
	 */
	public static String changeIndent(String code, int indentUnitsToRemove, int tabWidth, int indentWidth, String newIndentString, String lineDelim) {
		if (tabWidth < 0 || indentWidth < 0 || code == null || indentUnitsToRemove < 0 || newIndentString == null || lineDelim == null) {
			throw new IllegalArgumentException();
		}

		try {
			ILineTracker tracker= new DefaultLineTracker();
			tracker.set(code);
			int nLines= tracker.getNumberOfLines();
			if (nLines == 1) {
				return code;
			}

			StringBuffer buf= new StringBuffer();

			for (int i= 0; i < nLines; i++) {
				IRegion region= tracker.getLineInformation(i);
				int start= region.getOffset();
				int end= start + region.getLength();
				String line= code.substring(start, end);

				if (i == 0) {  // no indent for first line (contained in the formatted string)
					buf.append(line);
				} else { // no new line after last line
					buf.append(lineDelim);
					buf.append(newIndentString);
					if(indentWidth != 0) {
						buf.append(trimIndent(line, indentUnitsToRemove, tabWidth, indentWidth));
					} else {
						buf.append(line);
					}
				}
			}
			return buf.toString();
		} catch (BadLocationException e) {
			// can not happen
			return code;
		}
	}

	/**
	 * Returns the text edits retrieved after changing the indentation of a, possible multi-line, code string.
	 *
	 * <p>The given number of indent units is removed, and a new indent string is added.</p>
	 * <p>The first line of the code will not be changed (It is considered to have no indent as it might start in
	 * the middle of a line).</p>
	 *
	 * @param source The code to change the indent of
	 * @param indentUnitsToRemove the number of indent units to remove from each line (except the first) of the given code
	 * @param tabWidth the size of one tab in space equivalents
	 * @param indentWidth the width of one indentation unit in space equivalents
	 * @param newIndentString the new indent string to be added to all lines (except the first)
	 * @return returns the resulting text edits
	 * @exception IllegalArgumentException if:
	 * <ul>
	 * <li>the given <code>indentWidth</code> is lower than zero</li>
	 * <li>the given <code>tabWidth</code> is lower than zero</li>
	 * <li>the given <code>source</code> is null</li>
	 * <li>the given <code>indentUnitsToRemove</code> is lower than zero</li>
	 * <li>the given <code>newIndentString</code> is null</li>
	 * </ul>
	 */
	public static ReplaceEdit[] getChangeIndentEdits(String source, int indentUnitsToRemove, int tabWidth, int indentWidth, String newIndentString) {
		if (tabWidth < 0 || indentWidth < 0 || source == null || indentUnitsToRemove < 0 || newIndentString == null) {
			throw new IllegalArgumentException();
		}

		ArrayList result= new ArrayList();
		try {
			ILineTracker tracker= new DefaultLineTracker();
			tracker.set(source);
			int nLines= tracker.getNumberOfLines();
			if (nLines == 1)
				return (ReplaceEdit[])result.toArray(new ReplaceEdit[result.size()]);
			for (int i= 1; i < nLines; i++) {
				IRegion region= tracker.getLineInformation(i);
				int offset= region.getOffset();
				String line= source.substring(offset, offset + region.getLength());
				int length= indexOfIndent(line, indentUnitsToRemove, tabWidth, indentWidth);
				if (length >= 0) {
					result.add(new ReplaceEdit(offset, length, newIndentString));
				} else {
					length= measureIndentUnits(line, tabWidth, indentWidth);
					result.add(new ReplaceEdit(offset, length, "")); //$NON-NLS-1$
				}
			}
		} catch (BadLocationException cannotHappen) {
			// can not happen
		}
		return (ReplaceEdit[])result.toArray(new ReplaceEdit[result.size()]);
	}

	/*
	 * Returns the index where the indent of the given size ends.
	 * Returns <code>-1<code> if the line isn't prefixed with an indent of
	 * the given number of indents.
	 */
	private static int indexOfIndent(CharSequence line, int numberOfIndentUnits, int tabWidth, int indentWidth) {

		int spaceEquivalents= numberOfIndentUnits * indentWidth;

		int size= line.length();
		int result= -1;
		int blanks= 0;
		for (int i= 0; i < size && blanks < spaceEquivalents; i++) {
			char c= line.charAt(i);
			if (c == '\t') {
				blanks = calculateSpaceEquivalents(tabWidth, blanks);
			} else if (isIndentChar(c)) {
				blanks++;
			} else {
				break;
			}
			result= i;
		}
		if (blanks < spaceEquivalents)
			return -1;
		return result + 1;
	}

	/*
	 * Calculates space equivalents up to the next tab stop
	 */
	private static int calculateSpaceEquivalents(int tabWidth, int spaceEquivalents) {
		if (tabWidth == 0){
			return spaceEquivalents;
		}
		int remainder = spaceEquivalents % tabWidth;
		spaceEquivalents += tabWidth - remainder;
		return spaceEquivalents;
	}

	/**
	 * Returns the tab width as configured in the given map.
	 * <p>Use {@link org.eclipse.jdt.core.IJavaProject#getOptions(boolean)} to get the most current project options.</p>
	 *
	 * @param options the map to get the formatter settings from.
	 *
	 * @return the tab width
	 * @exception IllegalArgumentException if the given <code>options</code> is null
	 */
	public static int getTabWidth(Map options) {
		if (options == null) {
			throw new IllegalArgumentException();
		}
		return getIntValue(options, DefaultCodeFormatterConstants.FORMATTER_TAB_SIZE, 4);
	}

	/**
	 * Returns the tab width as configured in the given map.
	 * <p>Use {@link org.eclipse.jdt.core.IJavaProject#getOptions(boolean)} to get the most current project options.</p>
	 *
	 * @param options the map to get the formatter settings from
	 *
	 * @return the indent width
	 * @exception IllegalArgumentException if the given <code>options</code> is null
	 */
	public static int getIndentWidth(Map options) {
		if (options == null) {
			throw new IllegalArgumentException();
		}
		int tabWidth=getTabWidth(options);
		boolean isMixedMode= DefaultCodeFormatterConstants.MIXED.equals(options.get(DefaultCodeFormatterConstants.FORMATTER_TAB_CHAR));
		if (isMixedMode) {
			return getIntValue(options, DefaultCodeFormatterConstants.FORMATTER_INDENTATION_SIZE, tabWidth);
		}
		return tabWidth;
	}

	private static int getIntValue(Map options, String key, int def) {
		try {
			return Integer.parseInt((String) options.get(key));
		} catch (NumberFormatException e) {
			return def;
		}
	}
}

