package jp.sourceforge.asclipse.as3.builder;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import jp.sourceforge.asclipse.as3.AS3Lexer;
import jp.sourceforge.asclipse.as3.IAS3GlobalContext;
import jp.sourceforge.asclipse.as3.element.AS3Class;
import jp.sourceforge.asclipse.as3.element.AS3Element;
import jp.sourceforge.asclipse.as3.element.AS3Function;
import jp.sourceforge.asclipse.as3.element.AS3MxmlEmbeddedRoot;
import jp.sourceforge.asclipse.as3.element.AS3Package;
import jp.sourceforge.asclipse.as3.element.AS3Variable;
import jp.sourceforge.asclipse.as3.internal.element.AbstractAS3Element;
import jp.sourceforge.asclipse.as3.internal.element.DefaultAS3Class;
import jp.sourceforge.asclipse.as3.internal.element.DefaultAS3ConstProperty;
import jp.sourceforge.asclipse.as3.internal.element.DefaultAS3Function;
import jp.sourceforge.asclipse.as3.internal.element.DefaultAS3Import;
import jp.sourceforge.asclipse.as3.internal.element.DefaultAS3Include;
import jp.sourceforge.asclipse.as3.internal.element.DefaultAS3Interface;
import jp.sourceforge.asclipse.as3.internal.element.DefaultAS3Metadata;
import jp.sourceforge.asclipse.as3.internal.element.DefaultAS3MxmlEmbeddedRoot;
import jp.sourceforge.asclipse.as3.internal.element.DefaultAS3NamespaceDirective;
import jp.sourceforge.asclipse.as3.internal.element.DefaultAS3Package;
import jp.sourceforge.asclipse.as3.internal.element.DefaultAS3Property;
import jp.sourceforge.asclipse.as3.internal.element.DefaultAS3UseNamespaceDirective;
import jp.sourceforge.asclipse.as3.internal.element.DefaultAS3Variable;
import jp.sourceforge.asclipse.as3.internal.element.ModifiableAS3Root;
import jp.sourceforge.asclipse.as3.resolver.AS3TypeRef;
import jp.sourceforge.asclipse.as3.util.CommonTreeUtil;
import jp.sourceforge.asclipse.as3.util.ModifiersUtil;
import jp.sourceforge.asclipse.as3.util.CommonTreeUtil.Position;

import org.antlr.runtime.Token;
import org.antlr.runtime.tree.CommonTree;
import org.apache.commons.lang.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * {@link CommonTree}から{@link AS3Element}を構築するユーティリティクラス。
 * @author shin1ogawa
 */
public class TreeBuilder {

	private static final Logger LOGGER = LoggerFactory.getLogger(TreeBuilder.class);

	private List<Exception> builderExceptions = new ArrayList<Exception>(0);

	private IAS3GlobalContext globalContext;


	/**
	 * {@link CommonTree}から{@link AS3Element}を構築する
	 * @param globalContext 
	 * @param tree
	 * @return 構築したツリーのルート要素。
	 * 		構築する要素がひとつも無い場合は{@code null}
	 */
	public ModifiableAS3Root build(IAS3GlobalContext globalContext, CommonTree tree) {
		this.globalContext = globalContext;
		int childCount = tree.getChildCount();
		if (childCount == 0) {
			return null;
		}
		Token token = tree.getToken();
		ModifiableAS3Root root = null;
		if (token != null && token.getType() == AS3Lexer.PACKAGE) {
			root = buildPackage(tree);
		} else {
			if (token == null) {
				root = buildMxmlembedded(tree);
			} else {
				// packageがなく、直下にひとつだけ要素がある場合専用の処理。
				// この状況ではantlrがroot=nilを返さない。
				LOGGER.debug("build for mxmlembedded(root token is not null): " + token);
				Position endPos = CommonTreeUtil.getEndPos(tree);
				root =
						new DefaultAS3MxmlEmbeddedRoot(tree.getToken().getLine(), tree.getToken()
							.getCharPositionInLine(), endPos.line, endPos.charPositionInLine);
				build1(root, tree);
			}
		}
		if (!builderExceptions.isEmpty()) {
			root.addParserErrors(builderExceptions);
		}
		return root;
	}

	private void buildPackage(CommonTree tree, AS3Element parent) {
		AS3Element element = buildPackage(tree);
		((AbstractAS3Element) parent).addChildren(element);
		return;
	}

	private ModifiableAS3Root buildPackage(CommonTree tree) {
		int childCount = tree.getChildCount();
		List<?> children = tree.getChildren();
		int bodyStartIndex = 0;
		StringBuilder b = new StringBuilder();
		for (int i = 0; i < childCount; i++) {
			Object child = children.get(i);
			Token token = ((CommonTree) child).getToken();
			if (token.getType() == AS3Lexer.LCURLY) {
				bodyStartIndex = i + 1;
				break;
			}
			b.append(token.getText());
		}
		AS3Package root = new DefaultAS3Package(tree, b.toString());
		if (bodyStartIndex < childCount) {
			buildChildren(tree, root, bodyStartIndex);
		}
		return (ModifiableAS3Root) root;
	}

	private ModifiableAS3Root buildMxmlembedded(CommonTree tree) {
		int childCount = tree.getChildCount();
		CommonTree firstElement = (CommonTree) tree.getChild(0);
		CommonTree lastElement = (CommonTree) tree.getChild(childCount - 1);
		Position endPos = CommonTreeUtil.getEndPos(lastElement);
		AS3MxmlEmbeddedRoot root =
				new DefaultAS3MxmlEmbeddedRoot(firstElement.getToken().getLine(), firstElement
					.getToken().getCharPositionInLine(), endPos.line, endPos.charPositionInLine);
		buildChildren(tree, root, 0);
		return (ModifiableAS3Root) root;
	}

	private void buildChildren(CommonTree tree, AS3Element parent, int startIndex) {
		int childCount = tree.getChildCount();
		if (childCount == 0) {
			return;
		}
		List<?> children = tree.getChildren();
		for (int i = startIndex; i < childCount; i++) {
			Object child = children.get(i);
			build1(parent, child);
		}
	}

	private void build1(AS3Element parent, Object child) {
		Token token = ((CommonTree) child).getToken();
		switch (token.getType()) {
			case AS3Lexer.PACKAGE:
				buildPackage((CommonTree) child, parent);
				break;
			case AS3Lexer.INCLUDE:
				buildUseInclude((CommonTree) child, parent);
				break;
			case AS3Lexer.USE:
				buildUseNamespaceDirective((CommonTree) child, parent);
				break;
			case AS3Lexer.CLASS:
				buildClass((CommonTree) child, parent);
				break;
			case AS3Lexer.INTERFACE:
				buildInterface((CommonTree) child, parent);
				break;
			case AS3Lexer.IMPORT:
				buildImport((CommonTree) child, parent);
				break;
			case AS3Lexer.FUNCTION:
				buildFunction((CommonTree) child, parent);
				break;
			case AS3Lexer.VAR:
				buildProperty((CommonTree) child, parent);
				break;
			case AS3Lexer.CONST:
				buildConstProperty((CommonTree) child, parent);
				break;
			case AS3Lexer.NAMESPACE:
				buildNamespaceDirective((CommonTree) child, parent);
				break;
			case AS3Lexer.LBRACK:
				buildMetadata((CommonTree) child, parent);
				break;
			case AS3Lexer.LCURLY:
				break;
			case AS3Lexer.RCURLY:
				break;
			default:
				if (parent instanceof AS3Class) {
					// statementの可能性がある。
				} else {
					unexpectedTokenType(parent, token);
				}
		}
	}

	private void buildUseInclude(CommonTree tree, AS3Element parent) {
		Validate.isTrue(tree.getToken().getType() == AS3Lexer.INCLUDE);
		if (tree.getChildCount() == 0) {
			((AbstractAS3Element) parent).addChildren(new DefaultAS3Include(tree, ""));
			return;
		}
		CommonTree child = (CommonTree) tree.getChild(0);
		((AbstractAS3Element) parent).addChildren(new DefaultAS3Include(tree, child.getToken()
			.getText()));
		return;
	}

	private void buildUseNamespaceDirective(CommonTree tree, AS3Element parent) {
		Validate.isTrue(tree.getToken().getType() == AS3Lexer.USE);
		if (tree.getChildCount() == 0) {
			((AbstractAS3Element) parent).addChildren(new DefaultAS3UseNamespaceDirective(tree,
					new CommonTree[0]));
			return;
		}
		CommonTree tree2 = (CommonTree) tree.getChild(0); // use "namespace" identifier1, identifier2...
		int identifierCount = tree2.getChildCount();
		if (identifierCount == 0) {
			((AbstractAS3Element) parent).addChildren(new DefaultAS3UseNamespaceDirective(tree,
					new CommonTree[0]));
			return;
		}
		List<?> children = tree2.getChildren();
		CommonTree[] identifiers = new CommonTree[identifierCount];
		for (int i = 0; i < identifierCount; i++) {
			identifiers[i] = (CommonTree) children.get(i);
		}
	}

	private void buildNamespaceDirective(CommonTree tree, AS3Element parent) {
		Validate.isTrue(tree.getToken().getType() == AS3Lexer.NAMESPACE);
		if (tree.getChildCount() == 0) {
			((AbstractAS3Element) parent).addChildren(new DefaultAS3NamespaceDirective(tree,
					new CommonTree[0], "", false, ""));
			return;
		}

		List<CommonTree> modifiers = new ArrayList<CommonTree>();
		String identifier = null;
		boolean hasAssignment = false;
		String literal = null;
		List<?> children = tree.getChildren();
		boolean first = true;
		for (Object child : children) {
			Token token = ((CommonTree) child).getToken();
			if (first) {
				identifier = token.getText();
				first = false;
				continue;
			}
			if (token.getType() == AS3Lexer.ASSIGN) {
				hasAssignment = true;
				if (((CommonTree) child).getChildCount() != 0) {
					literal = ((CommonTree) ((CommonTree) child).getChild(0)).getToken().getText();
				} else {
					literal = "";
				}
			} else {
				modifiers.add((CommonTree) child);
			}
		}
		DefaultAS3NamespaceDirective element =
				new DefaultAS3NamespaceDirective(tree, modifiers.toArray(new CommonTree[0]),
						identifier, hasAssignment, literal);
		((AbstractAS3Element) parent).addChildren(element);
	}

	private void buildInterface(CommonTree tree, AS3Element parent) {
		List<CommonTree> modifiers = new ArrayList<CommonTree>(1);
		String identifier = null;
		List<AS3TypeRef> extendsTypeRefs = new ArrayList<AS3TypeRef>(0);
		int childCount = tree.getChildCount();
		List<?> children = tree.getChildren();
		CommonTree bodyTreeRoot = null;
		for (int i = 0; i < childCount; i++) {
			Object child = children.get(i);
			Token token = ((CommonTree) child).getToken();
			if (identifier == null) {
				identifier = token.getText();
				continue;
			}
			int type = token.getType();
			if (type == AS3Lexer.EXTENDS) {
				extendsTypeRefs = createTypeRefs((CommonTree) child);
			} else if (type == AS3Lexer.LCURLY) {
				bodyTreeRoot = (CommonTree) child;
				break;
			} else {
				modifiers.add((CommonTree) child);
			}
		}
		DefaultAS3Interface element =
				new DefaultAS3Interface(tree, modifiers.toArray(new CommonTree[0]), identifier,
						extendsTypeRefs);
		((AbstractAS3Element) parent).addChildren(element);
		if (bodyTreeRoot != null && bodyTreeRoot.getChildCount() > 0) {
			buildChildren(bodyTreeRoot, element, 0);
		}
	}

	private void buildClass(CommonTree tree, AS3Element parent) {
		List<CommonTree> modifiers = new ArrayList<CommonTree>(1);
		String identifier = null;
		AS3TypeRef extendsTypeRef = AS3TypeRef.OBJECT_TYPE_REF;
		List<AS3TypeRef> implementsTypeRefs = new ArrayList<AS3TypeRef>(0);
		int childCount = tree.getChildCount();
		List<?> children = tree.getChildren();
		CommonTree bodyTreeRoot = null;
		for (int i = 0; i < childCount; i++) {
			Object child = children.get(i);
			Token token = ((CommonTree) child).getToken();
			if (identifier == null) {
				identifier = token.getText();
				continue;
			}
			int type = token.getType();
			if (type == AS3Lexer.IMPLEMENTS) {
				implementsTypeRefs = createTypeRefs((CommonTree) child);
			} else if (type == AS3Lexer.EXTENDS) {
				extendsTypeRef = createTypeRef((CommonTree) child);
			} else if (type == AS3Lexer.LCURLY) {
				bodyTreeRoot = (CommonTree) child;
				break;
			} else {
				modifiers.add((CommonTree) child);
			}
		}

		DefaultAS3Class element =
				new DefaultAS3Class(tree, modifiers.toArray(new CommonTree[0]), identifier,
						extendsTypeRef, implementsTypeRefs);
		((AbstractAS3Element) parent).addChildren(element);
		if (bodyTreeRoot != null && bodyTreeRoot.getChildCount() > 0) {
			buildChildren(bodyTreeRoot, element, 0);
		}
	}

	private List<AS3TypeRef> createTypeRefs(CommonTree tree) {
		List<AS3TypeRef> refs = new ArrayList<AS3TypeRef>(1);
		if (tree.getChildCount() == 0) {
			return refs;
		}
		List<CommonTree> currentTypeTree = new ArrayList<CommonTree>(1);
		List<?> children = tree.getChildren();
		for (Object child : children) {
			Token token = ((CommonTree) child).getToken();
			if (token.getType() == AS3Lexer.COMMA) {
				if (currentTypeTree.size() > 0) {
					StringBuilder b = new StringBuilder();
					for (CommonTree type : currentTypeTree) {
						b.append(type.getToken().getText());
					}
					refs.add(globalContext.getTypeResolver().newTypeRef(b.toString()));
					currentTypeTree.clear();
				}
			} else {
				currentTypeTree.add((CommonTree) child);
			}
		}
		if (currentTypeTree.size() > 0) {
			StringBuilder b = new StringBuilder();
			for (CommonTree type : currentTypeTree) {
				b.append(type.getToken().getText());
			}
			refs.add(globalContext.getTypeResolver().newTypeRef(b.toString()));
			currentTypeTree.clear();
		}
		return refs;
	}

	private void buildImport(CommonTree tree, AS3Element parent) {
		Validate.isTrue(tree.getToken().getType() == AS3Lexer.IMPORT);
		AS3TypeRef typeRef = AS3TypeRef.NULL_TYPE_REF;
		boolean isSingleTypeRef = true;
		if (tree.getChildCount() != 0) {
			StringBuilder b = new StringBuilder();
			List<?> children = tree.getChildren();
			for (Object child : children) {
				b.append(((CommonTree) child).getToken().getText());
			}
			String typeString = b.toString();
			typeRef = globalContext.getTypeResolver().newTypeRef(typeString);
			if (typeString.endsWith("*")) {
				isSingleTypeRef = false;
			}
		}
		((AbstractAS3Element) parent).addChildren(new DefaultAS3Import(tree, typeRef,
				isSingleTypeRef));
	}

	private void buildMetadata(CommonTree tree, AS3Element parent) {
		Validate.isTrue(tree.getToken().getType() == AS3Lexer.LBRACK);
		if (tree.getChildCount() == 0) {
			((AbstractAS3Element) parent).addChildren(new DefaultAS3Metadata(tree, "",
					new CommonTree[0]));
			return;
		}
		List<CommonTree> identifierTrees = new ArrayList<CommonTree>(1);
		List<CommonTree> childTrees = new ArrayList<CommonTree>(1);
		boolean isIdentifier = true;
		for (Object child : tree.getChildren()) {
			if (isIdentifier) {
				if (((CommonTree) child).getToken().getType() == AS3Lexer.LPAREN) {
					isIdentifier = false;
					childTrees.add((CommonTree) child);
					continue;
				} else if (((CommonTree) child).getToken().getType() == AS3Lexer.RBRACK) {
					break;
				} else {
					identifierTrees.add((CommonTree) child);
				}
			} else {
				childTrees.add((CommonTree) child);
			}
		}
		if (childTrees.size() > 0) {
			// 最後の"]"を削除する。
			childTrees.remove(childTrees.size() - 1);
		}
		StringBuilder b = new StringBuilder();
		for (CommonTree i : identifierTrees) {
			b.append(i.getToken().getText());
		}
		((AbstractAS3Element) parent).addChildren(new DefaultAS3Metadata(tree, b.toString(),
				childTrees.toArray(new CommonTree[0])));
	}

	private void buildConstProperty(CommonTree tree, AS3Element parent) {
		Validate.isTrue(tree.getToken().getType() == AS3Lexer.CONST);
		if (tree.getChildCount() == 0) {
			return;
		}
		List<CommonTree> modifiers = new ArrayList<CommonTree>(1);
		List<AS3Variable> variables = new ArrayList<AS3Variable>(1);
		createPropertyInfo(tree, parent, modifiers, variables);
		((AbstractAS3Element) parent).addChildren(new DefaultAS3ConstProperty(tree, modifiers
			.toArray(new CommonTree[0]), variables));
	}

	private void buildProperty(CommonTree tree, AS3Element parent) {
		Validate.isTrue(tree.getToken().getType() == AS3Lexer.VAR);
		if (tree.getChildCount() == 0) {
			return;
		}
		List<CommonTree> modifiers = new ArrayList<CommonTree>(1);
		List<AS3Variable> variables = new ArrayList<AS3Variable>(1);
		createPropertyInfo(tree, parent, modifiers, variables);
		((AbstractAS3Element) parent).addChildren(new DefaultAS3Property(tree, modifiers
			.toArray(new CommonTree[0]), variables));
	}

	private void createPropertyInfo(CommonTree tree, AS3Element parent, List<CommonTree> modifiers,
			List<AS3Variable> variables) {
		boolean first = true;
		boolean comma = false;
		boolean modifier = false;
		List<?> children = tree.getChildren();
		for (Object child : children) {
			Token token = ((CommonTree) child).getToken();
			if (first) {
				// カンマの次か先頭ならVariable
				variables.add(buildVariable((CommonTree) child));
				first = false;
			} else if (token.getType() == AS3Lexer.COMMA) {
				comma = true;
			} else if (modifier || !comma || ModifiersUtil.isModifier(token)) {
				modifiers.add((CommonTree) child);
				comma = false;
				modifier = true;
			} else if (comma) {
				// カンマの次か先頭ならVariable
				variables.add(buildVariable((CommonTree) child));
				comma = false;
			} else {
				unexpectedTokenType(parent, token);
			}
		}
	}

	private void buildFunction(CommonTree funcTree, AS3Element parent) {
		// interfaceFunctionDeclaration
		// -> ^(FUNCTION IDENTIFIER ^(COLON type)? 
		//    accessorModifier? memberModifiers? formalParameterList)
		// functionDeclaration
		// -> ^(FUNCTION IDENTIFIER ^(COLON type)? 
		//    accessorModifier? memberModifiers? formalParameterList functionBody)
		List<?> children = funcTree.getChildren();
		String identifier = null;
		List<CommonTree> modifierList = new ArrayList<CommonTree>(1);
		CommonTree accessor = null;
		@SuppressWarnings("unchecked")
		List<AS3Variable> parameters = Collections.EMPTY_LIST;
		AS3TypeRef typeRef = AS3TypeRef.NULL_TYPE_REF;
		for (Object child : children) {
			Token token = ((CommonTree) child).getToken();
			if (identifier == null) {
				identifier = token.getText();
				continue;
			}
			switch (token.getType()) {
				case AS3Lexer.LPAREN:
					if (((CommonTree) child).getChildCount() > 0) {
						parameters = createParameters((CommonTree) child);
					}
					break;
				case AS3Lexer.COLON:
					typeRef = createTypeRef((CommonTree) child);
					break;
				case AS3Lexer.SET:
					accessor = (CommonTree) child;
					break;
				case AS3Lexer.GET:
					accessor = (CommonTree) child;
					break;
				case AS3Lexer.LCURLY:
					break;
				case AS3Lexer.RCURLY:
					break;
				default:
					modifierList.add((CommonTree) child);
					//unexpectedType(parent, token);
			}
		}
		AS3Function element =
				new DefaultAS3Function(funcTree, modifierList.toArray(new CommonTree[0]), accessor,
						identifier, parameters, typeRef);
		((AbstractAS3Element) parent).addChildren(element);

	}

	private List<AS3Variable> createParameters(CommonTree tree) {
		Validate.isTrue(tree.getChildCount() > 0);
		List<AS3Variable> variables = new ArrayList<AS3Variable>(1);
		List<?> children = tree.getChildren();
		for (Object child : children) {
			Token token = ((CommonTree) child).getToken();
			if (token == null || token.getType() == AS3Lexer.COMMA) {
				continue;
			}
			variables.add(buildVariable((CommonTree) child));
		}
		return variables;
	}

	private AS3Variable buildVariable(CommonTree tree) {
		String identifier = tree.getToken().getText();
		AS3TypeRef typeRef = AS3TypeRef.NULL_TYPE_REF;
		CommonTree[] assigmentExpression = new CommonTree[0];
		boolean hasEllipsis = false;
		if (tree.getChildCount() > 0) {
			List<?> children = tree.getChildren();
			for (Object child : children) {
				int type = ((CommonTree) child).getToken().getType();
				if (type == AS3Lexer.COLON) {
					typeRef = createTypeRef((CommonTree) child);
				} else if (type == AS3Lexer.ELLIPSIS) {
					hasEllipsis = true;
				} else if (type == AS3Lexer.ASSIGN) {
					assigmentExpression = createAssignmentExpression((CommonTree) child);
				}
			}
		}
		DefaultAS3Variable variable =
				new DefaultAS3Variable(tree, identifier, typeRef, assigmentExpression);
		variable.setEllipsis(hasEllipsis);
		return variable;
	}

	private CommonTree[] createAssignmentExpression(CommonTree tree) {
		Validate.isTrue(tree.getToken().getType() == AS3Lexer.ASSIGN);
		if (tree.getChildCount() == 0) {
			return new CommonTree[0];
		} else {
			@SuppressWarnings( {
				"unchecked",
				"cast"
			})
			List<CommonTree> list = (List<CommonTree>) tree.getChildren();
			return list.toArray(new CommonTree[0]);
		}
	}

	private AS3TypeRef createTypeRef(CommonTree tree) {
		Validate.isTrue(tree.getToken().getType() == AS3Lexer.COLON
				|| tree.getToken().getType() == AS3Lexer.EXTENDS);
		if (tree.getChildCount() == 0) {
			return AS3TypeRef.NULL_TYPE_REF;
		}
		StringBuilder b = new StringBuilder();
		List<?> children = tree.getChildren();
		for (Object child : children) {
			b.append(((CommonTree) child).getToken().getText());
		}
		return globalContext.getTypeResolver().newTypeRef(b.toString());
	}

	private void unexpectedTokenType(AS3Element currentParent, Token unexpectedToken) {
		LOGGER.warn("Unexpected type: " + unexpectedToken + " in " + currentParent.getTitle());
		builderExceptions.add(new BuilderException(currentParent, unexpectedToken));
	}


	/**
	 * ツリー構築中のエラーを保持するExceptionクラス。
	 * @author shin1ogawa
	 */
	@SuppressWarnings("serial")
	public static class BuilderException extends RuntimeException {

		private final AS3Element parentElement;

		private final Token token;


		/**
		 * Constructor.
		 * @param parentElement
		 * @param token
		 * @category constructor
		 */
		public BuilderException(AS3Element parentElement, Token token) {
			super("Unexpected type: " + token + " in " + parentElement.getTitle());
			this.parentElement = parentElement;
			this.token = token;
		}

		/**
		 * @return the parentElement
		 * @category accessor
		 */
		public AS3Element getParentElement() {
			return parentElement;
		}

		/**
		 * @return the token
		 * @category accessor
		 */
		public Token getToken() {
			return token;
		}
	}
}
