/*******************************************************************************
 * Copyright (c) 2000, 2010 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.formatter.align;

import org.eclipse.core.runtime.Assert;
import org.eclipse.jdt.internal.formatter.Location;
import org.eclipse.jdt.internal.formatter.Scribe;

/**
 * Alignment management
 *
 * @since 2.1
 */
public class Alignment {

	// Kind of alignment
	public int kind;
	public static final int ALLOCATION = 1;
	public static final int ANNOTATION_MEMBERS_VALUE_PAIRS = 2;
	public static final int ARRAY_INITIALIZER = 3;
	public static final int ASSIGNMENT = 4;
	public static final int BINARY_EXPRESSION = 5;
	public static final int CASCADING_MESSAGE_SEND = 6;
	public static final int COMPACT_IF = 7;
	public static final int COMPOUND_ASSIGNMENT = 8;
	public static final int CONDITIONAL_EXPRESSION = 9;
	public static final int ENUM_CONSTANTS = 10;
	public static final int ENUM_CONSTANTS_ARGUMENTS = 11;
	public static final int EXPLICIT_CONSTRUCTOR_CALL = 12;
	public static final int FIELD_DECLARATION_ASSIGNMENT = 13;
	public static final int LOCAL_DECLARATION_ASSIGNMENT = 14;
	public static final int MESSAGE_ARGUMENTS = 15;
	public static final int MESSAGE_SEND = 16;
	public static final int METHOD_ARGUMENTS = 17;
	public static final int METHOD_DECLARATION = 18;
	public static final int MULTIPLE_FIELD = 19;
	public static final int SUPER_CLASS = 20;
	public static final int SUPER_INTERFACES = 21;
	public static final int THROWS = 22;
	public static final int TYPE_MEMBERS = 23;
	public static final int STRING_CONCATENATION = 24;

	// name of alignment
	public String name;
	public static final String[] NAMES = {
		"", //$NON-NLS-1$
		"allocation", //$NON-NLS-1$
		"annotationMemberValuePairs", //$NON-NLS-1$
		"array_initializer", //$NON-NLS-1$
		"assignmentAlignment", //$NON-NLS-1$
		"binaryExpressionAlignment", //$NON-NLS-1$
		"cascadingMessageSendAlignment", //$NON-NLS-1$
		"compactIf", //$NON-NLS-1$
		"compoundAssignmentAlignment", //$NON-NLS-1$
		"conditionalExpression", //$NON-NLS-1$
		"enumConstants", //$NON-NLS-1$
		"enumConstantArguments", //$NON-NLS-1$
		"explicit_constructor_call", //$NON-NLS-1$
		"fieldDeclarationAssignmentAlignment", //$NON-NLS-1$
		"localDeclarationAssignmentAlignment", //$NON-NLS-1$
		"messageArguments", //$NON-NLS-1$
		"messageAlignment", //$NON-NLS-1$
		"methodArguments", //$NON-NLS-1$
		"methodDeclaration", //$NON-NLS-1$
		"multiple_field", //$NON-NLS-1$
		"superclass", //$NON-NLS-1$
		"superInterfaces", //$NON-NLS-1$
		"throws", //$NON-NLS-1$
		"typeMembers", //$NON-NLS-1$
		"stringConcatenation", //$NON-NLS-1$
	};

	// link to enclosing alignment
	public Alignment enclosing;

	// start location of this alignment
	public Location location;

	// indentation management
	public int fragmentIndex;
	public int fragmentCount;
	public int[] fragmentIndentations;
	public boolean needRedoColumnAlignment;

	// chunk management
	public int chunkStartIndex;
	public int chunkKind;

	// break management
	public int originalIndentationLevel;
	public int breakIndentationLevel;
	public int shiftBreakIndentationLevel;
	public int[] fragmentBreaks;
	public boolean wasSplit;
	public boolean blockAlign = false;
	public boolean tooLong = false;

	public Scribe scribe;

	// reset
	private boolean reset = false;

	/*
	 * Alignment modes
	 */
	public static final int M_FORCE = 1; // if bit set, then alignment will be non-optional (default is optional)
	public static final int M_INDENT_ON_COLUMN = 2; // if bit set, broken fragments will be aligned on current location column (default is to break at current indentation level)
	public static final int	M_INDENT_BY_ONE = 4; // if bit set, broken fragments will be indented one level below current (not using continuation indentation)

	// split modes can be combined either with M_FORCE or M_INDENT_ON_COLUMN

	/** foobar(#fragment1, #fragment2, <ul>
	 *  <li>    #fragment3, #fragment4 </li>
	 * </ul>
	 */
	public static final int M_COMPACT_SPLIT = 16; // fill each line with all possible fragments

	/** foobar(<ul>
	 * <li>    #fragment1, #fragment2,  </li>
	 * <li>     #fragment5, #fragment4, </li>
	 * </ul>
	 */
	public static final int M_COMPACT_FIRST_BREAK_SPLIT = 32; //  compact mode, but will first try to break before first fragment

	/** foobar(<ul>
	 * <li>     #fragment1,  </li>
	 * <li>     #fragment2,  </li>
	 * <li>     #fragment3 </li>
	 * <li>     #fragment4,  </li>
	 * </ul>
	 */
	public static final int M_ONE_PER_LINE_SPLIT = 32+16; // one fragment per line

	/**
	 * foobar(<ul>
	 * <li>     #fragment1,  </li>
	 * <li>        #fragment2,  </li>
	 * <li>        #fragment3 </li>
	 * <li>        #fragment4,  </li>
	 * </ul>
	 */
	public static final int M_NEXT_SHIFTED_SPLIT = 64; // one fragment per line, subsequent are indented further

	/** foobar(#fragment1, <ul>
	 * <li>      #fragment2,  </li>
	 * <li>      #fragment3 </li>
	 * <li>      #fragment4,  </li>
	 * </ul>
	 */
	public static final int M_NEXT_PER_LINE_SPLIT = 64+16; // one per line, except first fragment (if possible)

	//64+32
	//64+32+16

	// mode controlling column alignments
	/**
	 * <table BORDER COLS=4 WIDTH="100%" >
	 * <tr><td>#fragment1A</td>            <td>#fragment2A</td>       <td>#fragment3A</td>  <td>#very-long-fragment4A</td></tr>
	 * <tr><td>#fragment1B</td>            <td>#long-fragment2B</td>  <td>#fragment3B</td>  <td>#fragment4B</td></tr>
	 * <tr><td>#very-long-fragment1C</td>  <td>#fragment2C</td>       <td>#fragment3C</td>  <td>#fragment4C</td></tr>
	 * </table>
	 */
	public static final int M_MULTICOLUMN = 256; // fragments are on same line, but multiple line of fragments will be aligned vertically

	public static final int M_NO_ALIGNMENT = 0;

	public int mode;

	public static final int SPLIT_MASK = M_ONE_PER_LINE_SPLIT | M_NEXT_SHIFTED_SPLIT | M_COMPACT_SPLIT | M_COMPACT_FIRST_BREAK_SPLIT | M_NEXT_PER_LINE_SPLIT;

	// alignment tie-break rules - when split is needed, will decide whether innermost/outermost alignment is to be chosen
	public static final int R_OUTERMOST = 1;
	public static final int R_INNERMOST = 2;
	public int tieBreakRule;
	public int startingColumn = -1;

	// alignment effects on a per fragment basis
	public static final int NONE = 0;
	public static final int BREAK = 1;

	// chunk kind
	public static final int CHUNK_FIELD = 1;
	public static final int CHUNK_METHOD = 2;
	public static final int CHUNK_TYPE = 3;
	public static final int CHUNK_ENUM = 4;

	// location to align and break on.
	public Alignment(int kind, int mode, int tieBreakRule, Scribe scribe, int fragmentCount, int sourceRestart, int continuationIndent){

		Assert.isTrue(kind >=ALLOCATION && kind <=STRING_CONCATENATION);
		this.kind = kind;
		this.name = NAMES[kind];
		this.location = new Location(scribe, sourceRestart);
		this.mode = mode;
		this.tieBreakRule = tieBreakRule;
		this.fragmentCount = fragmentCount;
		this.scribe = scribe;
		this.originalIndentationLevel = this.scribe.indentationLevel;
		this.wasSplit = false;

		// initialize the break indentation level, using modes and continuationIndentationLevel preference
		final int indentSize = this.scribe.indentationSize;
		int currentColumn = this.location.outputColumn;
		if (currentColumn == 1) {
		    currentColumn = this.location.outputIndentationLevel + 1;
		}

		if ((mode & M_INDENT_ON_COLUMN) != 0) {
			// indent broken fragments at next indentation level, based on current column
			this.breakIndentationLevel = this.scribe.getNextIndentationLevel(currentColumn);
			if (this.breakIndentationLevel == this.location.outputIndentationLevel) {
				this.breakIndentationLevel += (continuationIndent * indentSize);
			}
		} else if ((mode & M_INDENT_BY_ONE) != 0) {
			// indent broken fragments exactly one level deeper than current indentation
			this.breakIndentationLevel = this.location.outputIndentationLevel + indentSize;
		} else {
			this.breakIndentationLevel = this.location.outputIndentationLevel + continuationIndent * indentSize;
		}
		this.shiftBreakIndentationLevel = this.breakIndentationLevel + indentSize;

		this.fragmentIndentations = new int[this.fragmentCount];
		this.fragmentBreaks = new int[this.fragmentCount];

		// check for forced alignments
		if ((this.mode & M_FORCE) != 0) {
			couldBreak();
		}
	}

	public boolean checkChunkStart(int chunk, int startIndex, int sourceRestart) {
		if (this.chunkKind != chunk) {
			this.chunkKind = chunk;

			// when redoing same chunk alignment, must not reset
			if (startIndex != this.chunkStartIndex) {
				this.chunkStartIndex = startIndex;
				this.location.update(this.scribe, sourceRestart);
				reset();
			}
			return true;
		}
		return false;
	}

	public void checkColumn() {
		if ((this.mode & M_MULTICOLUMN) != 0 && this.fragmentCount > 0) {
			int currentIndentation = this.scribe.getNextIndentationLevel(this.scribe.column+(this.scribe.needSpace ? 1 : 0));
			int fragmentIndentation = this.fragmentIndentations[this.fragmentIndex];
			if (currentIndentation > fragmentIndentation) {
				this.fragmentIndentations[this.fragmentIndex] =  currentIndentation;
				if (fragmentIndentation != 0) {
					for (int i = this.fragmentIndex+1; i < this.fragmentCount; i++) {
						this.fragmentIndentations[i] = 0;
					}
					this.needRedoColumnAlignment = true;
				}
			}
			// backtrack only once all fragments got checked
			if (this.needRedoColumnAlignment && this.fragmentIndex == this.fragmentCount-1) { // alignment too small

//				if (CodeFormatterVisitor.DEBUG){
//					System.out.println("ALIGNMENT TOO SMALL");
//					System.out.println(this);
//				}
				this.needRedoColumnAlignment = false;
				int relativeDepth = 0;
				Alignment targetAlignment = this.scribe.memberAlignment;
				while (targetAlignment != null){
					if (targetAlignment == this){
						throw new AlignmentException(AlignmentException.ALIGN_TOO_SMALL, relativeDepth);
					}
					targetAlignment = targetAlignment.enclosing;
					relativeDepth++;
				}
			}
		}
	}

	public int depth() {
		int depth = 0;
		Alignment current = this.enclosing;
		while (current != null) {
			depth++;
			current = current.enclosing;
		}
		return depth;
	}
	
	/**
	 * Returns whether the alignment can be aligned or not.
	 * Only used for message send alignment, it currently blocks its alignment
	 * when it's at the first nesting level of a message send. It allow to save
	 * space on the argument broken line by reducing the number of indentations.
	 */
	public boolean canAlign() {
		if (this.tooLong) {
			return true;
		}
		boolean canAlign = true;
		Alignment enclosingAlignment = this.enclosing;
		while (enclosingAlignment != null) {
			switch (enclosingAlignment.kind) {
				case Alignment.ALLOCATION:
				case Alignment.MESSAGE_ARGUMENTS:
					// message send inside arguments, avoid to align
					if (enclosingAlignment.isWrapped() && 
							(enclosingAlignment.fragmentIndex > 0 || enclosingAlignment.fragmentCount < 2)) {
						return !this.blockAlign;
					}
					if (enclosingAlignment.tooLong) {
						return true;
					}
					canAlign = false;
					break;
				case Alignment.MESSAGE_SEND:
					// multiple depth of message send, hence allow current to align
					switch (this.kind) {
						case Alignment.ALLOCATION:
						case Alignment.MESSAGE_ARGUMENTS:
						case Alignment.MESSAGE_SEND:
							Alignment superEnclosingAlignment = enclosingAlignment.enclosing;
							while (superEnclosingAlignment != null) {
								switch (superEnclosingAlignment.kind) {
									case Alignment.ALLOCATION:
									case Alignment.MESSAGE_ARGUMENTS:
									case Alignment.MESSAGE_SEND:
										// block the alignment of the intermediate message send
										if (this.scribe.nlsTagCounter == 0) {
											enclosingAlignment.blockAlign = true;
										}
										return !this.blockAlign;
								}
								superEnclosingAlignment = superEnclosingAlignment.enclosing;
							}
							break;
					}
					return !this.blockAlign;
			}
			enclosingAlignment = enclosingAlignment.enclosing;
		}
		return canAlign && !this.blockAlign;
	}

	public boolean couldBreak(){
		if (this.fragmentCount == 0) return false;
		int i;
		switch(this.mode & SPLIT_MASK){

			/*  # aligned fragment
			 *  foo(
			 *     #AAAAA, #BBBBB,
			 *     #CCCC);
			 */
			case M_COMPACT_FIRST_BREAK_SPLIT :
				if (this.fragmentBreaks[0] == NONE) {
					this.fragmentBreaks[0] = BREAK;
					this.fragmentIndentations[0] = this.breakIndentationLevel;
					return this.wasSplit = true;
				}
				i = this.fragmentIndex;
				do {
					if (this.fragmentBreaks[i] == NONE) {
						this.fragmentBreaks[i] = BREAK;
						this.fragmentIndentations[i] = this.breakIndentationLevel;
						return this.wasSplit = true;
					}
				} while (--i >= 0);
				break;
			/*  # aligned fragment
			 *  foo(#AAAAA, #BBBBB,
			 *     #CCCC);
			 */
			case M_COMPACT_SPLIT :
				i = this.fragmentIndex;
				do {
					if (this.fragmentBreaks[i] == NONE) {
						this.fragmentBreaks[i] = BREAK;
						this.fragmentIndentations[i] = this.breakIndentationLevel;
						return this.wasSplit = true;
					}
				} while (--i >= 0);
				break;

			/*  # aligned fragment
			 *  foo(
			 *      #AAAAA,
			 *          #BBBBB,
			 *          #CCCC);
			 */
			case M_NEXT_SHIFTED_SPLIT :
				if (this.fragmentBreaks[0] == NONE) {
					this.fragmentBreaks[0] = BREAK;
					this.fragmentIndentations[0] = this.breakIndentationLevel;
					for (i = 1; i < this.fragmentCount; i++){
						this.fragmentBreaks[i] = BREAK;
						this.fragmentIndentations[i] = this.shiftBreakIndentationLevel;
					}
					return this.wasSplit = true;
				}
				break;

			/*  # aligned fragment
			 *  foo(
			 *      #AAAAA,
			 *      #BBBBB,
			 *      #CCCC);
			 */
			case M_ONE_PER_LINE_SPLIT :
				if (this.fragmentBreaks[0] == NONE) {
					for (i = 0; i < this.fragmentCount; i++){
						this.fragmentBreaks[i] = BREAK;
						this.fragmentIndentations[i] = this.breakIndentationLevel;
					}
					return this.wasSplit = true;
				}
				break;
			/*  # aligned fragment
			 *  foo(#AAAAA,
			 *      #BBBBB,
			 *      #CCCC);
			 */
			case M_NEXT_PER_LINE_SPLIT :
				if (this.fragmentBreaks[0] == NONE) {
					if (this.fragmentCount > 1
							&& this.fragmentBreaks[1] == NONE) {
						if ((this.mode & M_INDENT_ON_COLUMN) != 0) {
							this.fragmentIndentations[0] = this.breakIndentationLevel;
						}
						for (i = 1; i < this.fragmentCount; i++) {
							this.fragmentBreaks[i] = BREAK;
							this.fragmentIndentations[i] = this.breakIndentationLevel;
						}
						return this.wasSplit = true;
					}
				}
				break;
		}
		return false; // cannot split better
	}
	
	public boolean isWrapped() {
		if (this.fragmentCount == 0) return false;
		return this.fragmentBreaks[this.fragmentIndex] == BREAK;
	}

	public int wrappedIndex() {
		for (int i = 0, max = this.fragmentCount; i < max; i++) {
			if (this.fragmentBreaks[i] == BREAK) {
				return i;
			}
		}
		return -1;
	}

	// perform alignment effect for current fragment
	public void performFragmentEffect(){
		if (this.fragmentCount == 0) return;
		if ((this.mode & M_MULTICOLUMN) == 0) {
			switch(this.mode & SPLIT_MASK) {
				case Alignment.M_COMPACT_SPLIT :
				case Alignment.M_COMPACT_FIRST_BREAK_SPLIT :
				case Alignment.M_NEXT_PER_LINE_SPLIT :
				case Alignment.M_NEXT_SHIFTED_SPLIT :
				case Alignment.M_ONE_PER_LINE_SPLIT :
					break;
				default:
					return;
			}
		}

		int fragmentIndentation = this.fragmentIndentations[this.fragmentIndex];
		if (this.startingColumn < 0 || (fragmentIndentation+1) < this.startingColumn) {
			if (this.fragmentBreaks[this.fragmentIndex] == BREAK) {
				this.scribe.printNewLine();
			}
			if (fragmentIndentation > 0) {
				this.scribe.indentationLevel = fragmentIndentation;
			}
		}
	}

	// reset fragment indentation/break status
	public void reset() {

		this.wasSplit = false;
		if (this.fragmentCount > 0){
			this.fragmentIndentations = new int[this.fragmentCount];
			this.fragmentBreaks = new int[this.fragmentCount];
		}

		// check for forced alignments
		if ((this.mode & M_FORCE) != 0) {
			couldBreak();
		}
		this.reset = true;
	}

	public void toFragmentsString(StringBuffer buffer){
		// default implementation
	}

	public String toString() {
		StringBuffer buffer = new StringBuffer(10);
		return toString(buffer, -1);
	}

	public String toString(StringBuffer buffer, int level) {
		
		// Compute the indentation at the given level
		StringBuffer indentation = new StringBuffer();
		for (int i=0; i<level; i++) {
			indentation.append('\t');
		}
		
		// First line is for class and name
		buffer.append(indentation);
		buffer
			.append("<kind: ")	//$NON-NLS-1$
			.append(this.kind)
			.append("> ");	//$NON-NLS-1$
		buffer
			.append("<name: ")	//$NON-NLS-1$
			.append(this.name)
			.append(">\n");	//$NON-NLS-1$
		
		// Line for depth and break indentation
		buffer.append(indentation);
		buffer
			.append("<depth=")	//$NON-NLS-1$
			.append(depth())
			.append("><breakIndent=")	//$NON-NLS-1$
			.append(this.breakIndentationLevel)
			.append("><shiftBreakIndent=")	//$NON-NLS-1$
			.append(this.shiftBreakIndentationLevel)
			.append(">\n"); //$NON-NLS-1$

		// Line to display the location
		buffer.append(indentation);
		buffer
			.append("<location=")	//$NON-NLS-1$
			.append(this.location.toString())
			.append(">\n");	//$NON-NLS-1$

		// Lines for fragments
		buffer
			.append(indentation)
			.append("<fragments:\n");	//$NON-NLS-1$
		for (int i = 0; i < this.fragmentCount; i++){
			buffer
				.append(indentation)
				.append(" - ")	//$NON-NLS-1$
				.append(i)
				.append(": ")	//$NON-NLS-1$
				.append("<break: ")	//$NON-NLS-1$
				.append(this.fragmentBreaks[i] > 0 ? "YES" : "NO")	//$NON-NLS-1$	//$NON-NLS-2$
				.append(">")	//$NON-NLS-1$
				.append("<indent: ")	//$NON-NLS-1$
				.append(this.fragmentIndentations[i])
				.append(">\n");	//$NON-NLS-1$
		}
		buffer
			.append(indentation)
			.append(">\n"); //$NON-NLS-1$
		
		// Display enclosing
		if (this.enclosing != null && level >= 0) {
			buffer
				.append(indentation)
				.append("<enclosing assignement:\n");	//$NON-NLS-1$
			this.enclosing.toString(buffer, level+1);
			buffer
				.append(indentation)
				.append(">\n"); //$NON-NLS-1$
		}
		
		// Return the result
		return buffer.toString();
	}

	public void update() {
		for (int i = 1; i < this.fragmentCount; i++){
		    if (this.fragmentBreaks[i] == BREAK) {
		        this.fragmentIndentations[i] = this.breakIndentationLevel;
		    }
		}
	}

	public boolean wasReset() {
		return this.reset;
	}
}
