/*
 * Copyright 2013-2017 consulo.io
 *
 * 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 consulo.csharp.lang.doc.parser;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import consulo.csharp.lang.doc.psi.CSharpDocElements;
import consulo.csharp.lang.doc.psi.CSharpDocTokenType;
import consulo.csharp.lang.doc.validation.CSharpDocAttributeInfo;
import consulo.csharp.lang.doc.validation.CSharpDocTagInfo;
import consulo.csharp.lang.doc.validation.CSharpDocTagManager;
import com.intellij.lang.PsiBuilder;
import com.intellij.psi.tree.CustomParsingType;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.ILazyParseableElementType;
import com.intellij.util.containers.Stack;

/**
 * @author max
 * @author VISTALL
 *
 * Base code from XML plugin, {@see com.intellij.psi.impl.source.parsing.xml.XmlParsing}
 */
public class CSharpDocParsing
{
	private static final int BALANCING_DEPTH_THRESHOLD = 1000;

	protected final PsiBuilder myBuilder;
	private final Stack<String> myTagNamesStack = new Stack<String>();

	public CSharpDocParsing(final PsiBuilder builder)
	{
		myBuilder = builder;
	}

	public void parse()
	{
		PsiBuilder.Marker error = null;
		while(!eof())
		{
			final IElementType tt = token();
			if(tt == CSharpDocTokenType.XML_START_TAG_START)
			{
				error = flushError(error);
				parseTag();
			}
			else if(isCommentToken(tt))
			{
				error = flushError(error);
				parseComment();
			}
			else if(tt == CSharpDocTokenType.XML_REAL_WHITE_SPACE ||
					tt == CSharpDocTokenType.XML_DATA_CHARACTERS ||
					tt == CSharpDocTokenType.DOC_LINE_START)
			{
				error = flushError(error);
				advance();
			}
			else
			{
				if(error == null)
				{
					error = mark();
				}
				advance();
			}
		}

		if(error != null)
		{
			error.error("Tag is not closed");
		}
	}

	@Nullable
	private static PsiBuilder.Marker flushError(PsiBuilder.Marker error)
	{
		if(error != null)
		{
			error.error("Unexpected tokens");
			error = null;
		}
		return error;
	}

	protected void parseTag()
	{
		assert token() == CSharpDocTokenType.XML_START_TAG_START : "Tag start expected";
		final PsiBuilder.Marker tag = mark();

		final String tagName = parseTagHeader(tag);
		if(tagName == null)
		{
			return;
		}

		final PsiBuilder.Marker content = mark();
		parseTagContent();

		if(token() == CSharpDocTokenType.XML_END_TAG_START)
		{
			final PsiBuilder.Marker footer = mark();
			advance();

			if(token() == CSharpDocTokenType.XML_NAME)
			{
				String endName = myBuilder.getTokenText();
				if(!tagName.equals(endName) && myTagNamesStack.contains(endName))
				{
					footer.rollbackTo();
					myTagNamesStack.pop();
					tag.doneBefore(CSharpDocElements.TAG, content, "Element '" + tagName + "' is not closed");
					content.drop();
					return;
				}

				advance();
			}
			footer.drop();

			while(token() != CSharpDocTokenType.XML_TAG_END && token() != CSharpDocTokenType.XML_START_TAG_START && token() != CSharpDocTokenType.XML_END_TAG_START &&
					!eof())
			{
				error("Unexpected tokens");
				advance();
			}

			if(token() == CSharpDocTokenType.XML_TAG_END)
			{
				advance();
			}
			else
			{
				error("Closing tag is not done");
			}
		}
		else
		{
			error("Unexpected end of doc");
		}

		content.drop();
		myTagNamesStack.pop();
		tag.done(CSharpDocElements.TAG);
	}

	@Nullable
	private String parseTagHeader(final PsiBuilder.Marker tag)
	{
		advance();

		final String tagName;
		if(token() != CSharpDocTokenType.XML_NAME)
		{
			error("Tag name expected");
			tagName = "";
		}
		else
		{
			tagName = myBuilder.getTokenText();
			assert tagName != null;
			advance();
		}
		myTagNamesStack.push(tagName);

		do
		{
			final IElementType tt = token();
			if(tt == CSharpDocTokenType.XML_NAME)
			{
				parseAttribute(tagName);
			}
			else
			{
				break;
			}
		}
		while(true);

		if(token() == CSharpDocTokenType.XML_EMPTY_ELEMENT_END)
		{
			advance();
			myTagNamesStack.pop();
			tag.done(CSharpDocElements.TAG);
			return null;
		}

		if(token() == CSharpDocTokenType.XML_TAG_END)
		{
			advance();
		}
		else
		{
			error("Tag is not closed");
			myTagNamesStack.pop();
			tag.done(CSharpDocElements.TAG);
			return null;
		}

		if(myTagNamesStack.size() > BALANCING_DEPTH_THRESHOLD)
		{
			error("Way too unbalanced. Stopping attempt to balance tags properly at this point");
			tag.done(CSharpDocElements.TAG);
			return null;
		}

		return tagName;
	}

	public void parseTagContent()
	{
		PsiBuilder.Marker xmlText = null;
		while(token() != CSharpDocTokenType.XML_END_TAG_START && !eof())
		{
			final IElementType tt = token();
			if(tt == CSharpDocTokenType.XML_START_TAG_START)
			{
				xmlText = terminateText(xmlText);
				parseTag();
			}
			else if(isCommentToken(tt))
			{
				xmlText = terminateText(xmlText);
				parseComment();
			}
			else if(tt instanceof CustomParsingType || tt instanceof ILazyParseableElementType)
			{
				xmlText = terminateText(xmlText);
				advance();
			}
			else
			{
				xmlText = startText(xmlText);
				advance();
			}
		}

		terminateText(xmlText);
	}

	protected boolean isCommentToken(final IElementType tt)
	{
		return tt == CSharpDocTokenType.XML_COMMENT_START;
	}

	@Nonnull
	private PsiBuilder.Marker startText(@Nullable PsiBuilder.Marker xmlText)
	{
		if(xmlText == null)
		{
			xmlText = mark();
			assert xmlText != null;
		}
		return xmlText;
	}

	protected final PsiBuilder.Marker mark()
	{
		return myBuilder.mark();
	}

	@Nullable
	private static PsiBuilder.Marker terminateText(@Nullable PsiBuilder.Marker xmlText)
	{
		if(xmlText != null)
		{
			xmlText.done(CSharpDocElements.TEXT);
			xmlText = null;
		}
		return xmlText;
	}

	protected void parseComment()
	{
		advance();
		while(true)
		{
			final IElementType tt = token();
			if(tt == CSharpDocTokenType.XML_COMMENT_CHARACTERS)
			{
				advance();
				continue;
			}
			else if(tt == CSharpDocTokenType.XML_BAD_CHARACTER)
			{
				final PsiBuilder.Marker error = mark();
				advance();
				error.error("Bad character");
				continue;
			}
			if(tt == CSharpDocTokenType.XML_COMMENT_END)
			{
				advance();
			}
			break;
		}
	}

	private void parseAttribute(String tagInfo)
	{
		String attributeName = myBuilder.getTokenText();

		CSharpDocTagInfo docTagInfo = CSharpDocTagManager.getInstance().getTag(tagInfo);
		CSharpDocAttributeInfo attributeInfo = docTagInfo == null ? null : docTagInfo.getAttribute(attributeName);

		assert token() == CSharpDocTokenType.XML_NAME;
		final PsiBuilder.Marker att = mark();
		advance();
		if(token() == CSharpDocTokenType.XML_EQ)
		{
			advance();
			parseAttributeValue(attributeInfo);
			att.done(CSharpDocElements.ATTRIBUTE);
		}
		else
		{
			error("'=' expected");
			att.done(CSharpDocElements.ATTRIBUTE);
		}
	}

	private void parseAttributeValue(@Nullable CSharpDocAttributeInfo attributeInfo)
	{
		final PsiBuilder.Marker attValue = mark();
		if(token() == CSharpDocTokenType.XML_ATTRIBUTE_VALUE_START_DELIMITER)
		{
			advance();

			if(token() == CSharpDocTokenType.XML_BAD_CHARACTER)
			{
				final PsiBuilder.Marker error = mark();
				advance();
				error.error("Unexpected token");
			}
			else if(token() == CSharpDocTokenType.XML_ATTRIBUTE_VALUE_TOKEN)
			{
				if(attributeInfo == null)
				{
					advance();
				}
				else
				{
					switch(attributeInfo.getValueType())
					{
						case REFERENCE:
							myBuilder.remapCurrentToken(CSharpDocElements.TYPE);
							break;
						case PARAMETER:
							myBuilder.remapCurrentToken(CSharpDocElements.PARAMETER_EXPRESSION);
							break;
						case TYPE_PARAMETER:
							myBuilder.remapCurrentToken(CSharpDocElements.GENERIC_PARAMETER_EXPRESSION);
							break;
					}
					advance();
				}
			}

			if(token() == CSharpDocTokenType.XML_ATTRIBUTE_VALUE_END_DELIMITER)
			{
				advance();
			}
			else
			{
				error("Attribute value is not closed");
			}
		}
		else
		{
			error("Attribute value expected");
		}

		attValue.done(CSharpDocElements.ATTRIBUTE_VALUE);
	}

	@Nullable
	protected final IElementType token()
	{
		return myBuilder.getTokenType();
	}

	protected final boolean eof()
	{
		return myBuilder.eof();
	}

	protected final void advance()
	{
		myBuilder.advanceLexer();
	}

	private void error(final String message)
	{
		myBuilder.error(message);
	}
}