/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.wicket;

import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.IMarkupFragment;
import org.apache.wicket.markup.MarkupElement;
import org.apache.wicket.util.collections.ArrayListStack;

/**
 * Context for component dequeueing. Keeps track of markup position and container stack.
 * 
 * @author igor
 *
 */
public final class DequeueContext
{
	private final IMarkupFragment markup;
	private int index;
	private ComponentTag next;
	private ArrayListStack<ComponentTag> tags = new ArrayListStack<>();
	private final boolean skipFirst;
	private ComponentTag first;

	private ArrayListStack<MarkupContainer> containers = new ArrayListStack<>();

	/** A bookmark for the DequeueContext stack */
	public static final class Bookmark
	{
		private final int index;
		private final ComponentTag next;
		private final ArrayListStack<ComponentTag> tags;
		private final ArrayListStack<MarkupContainer> containers;

		private Bookmark(DequeueContext parser)
		{
			this.index = parser.index;
			this.next = parser.next;
			this.tags = new ArrayListStack<>(parser.tags);
			this.containers = new ArrayListStack<>(parser.containers);
		}

		private void restore(DequeueContext parser)
		{
			parser.index = index;
			parser.next = next;
			parser.tags = new ArrayListStack<>(tags);
			parser.containers = new ArrayListStack<>(containers);
		}
	}

	public DequeueContext(IMarkupFragment markup, MarkupContainer root, boolean skipFirst)
	{
		this.markup = markup;
		this.skipFirst = skipFirst;
		this.containers.push(root);
		this.next = nextTag();
	}

	/**
	 * Saves the state of the context into a bookmark which can later be used to restore it.
	 */
	public Bookmark save()
	{
		return new Bookmark(this);
	}

	/**
	 * Restores the state of the context from the bookmark
	 * 
	 * @param bookmark
	 */
	public void restore(Bookmark bookmark)
	{
		bookmark.restore(this);
	}

	/**
	 * Peeks markup tag that would be retrieved by call to {@link #takeTag()}
	 * 
	 * @return
	 */
	public ComponentTag peekTag()
	{
		return next;
	}

	/**
	 * Retrieves the next markup tag
	 * 
	 * @return
	 */
	public ComponentTag takeTag()
	{
		ComponentTag taken = next;

		if (taken == null)
		{
			return null;
		}

		if (taken.isOpen() && !taken.hasNoCloseTag())
		{
			tags.push(taken);
		}
		else if (tags.size() > 0 && taken.closes(tags.peek()))
		{
			tags.pop();
		}
		next = nextTag();
		return taken;
	}

	/**
	 * Skips to the closing tag of the tag retrieved from last call to {@link #takeTag()}
	 */
	public void skipToCloseTag()
	{
		while (!next.closes(tags.peek()))
		{
			next = nextTag();
		}
	}

	private ComponentTag nextTag()
	{
		if (skipFirst && first == null)
		{
			for (; index < markup.size(); index++)
			{
				MarkupElement element = markup.get(index);
				if (element instanceof ComponentTag)
				{
					first = (ComponentTag)element;
					index++;
					break;
				}
			}
		}

		for (; index < markup.size(); index++)
		{
			MarkupElement element = markup.get(index);
			if (element instanceof ComponentTag)
			{
				ComponentTag tag = (ComponentTag)element;

				if (tag.isOpen() || tag.isOpenClose())
				{
					DequeueTagAction action = canDequeueTag(tag);
					switch (action)
					{
						case IGNORE :
							continue;
						case DEQUEUE :
							index++;
							return tag;
						case SKIP : // skip to close tag
							boolean found = false;
							for (; index < markup.size(); index++)
							{
								if ((markup.get(index) instanceof ComponentTag)
									&& markup.get(index).closes(tag))
								{
									found = true;
									break;
								}
							}
							if (!found)
							{
								throw new IllegalStateException(String.format(
									"Could not find close tag for tag '%s' in markup: %s ", tag,
									markup));
							}

					}
				}
				else
				{
					// closed tag
					ComponentTag open = tag.isClose() ? tag.getOpenTag() : tag;

					if (skipFirst && first != null && open == first)
					{
						continue;
					}

					switch (canDequeueTag(open))
					{
						case DEQUEUE :
							index++;
							return tag;
						case IGNORE :
							continue;
						case SKIP :
							throw new IllegalStateException(String.format(
								"Should not see closed tag of skipped open tag '%s' in markup:%s",
								tag, markup));
					}
				}
			}
		}
		return null;
	}

	private DequeueTagAction canDequeueTag(ComponentTag open)
	{
		if (containers.size() < 1)
		{
			// TODO queueing message: called too early
			throw new IllegalStateException();
		}

		DequeueTagAction action;
		for (int i = containers.size() - 1; i >= 0; i--)
		{
			action = containers.get(i).canDequeueTag((open));
			if (action != null)
			{
				return action;
			}
		}
		return DequeueTagAction.IGNORE;
	}

	/**
	 * Checks if the tag returned by {@link #peekTag()} is either open or open-close.
	 * 
	 * @return
	 */
	public boolean isAtOpenOrOpenCloseTag()
	{
		ComponentTag tag = peekTag();
		return tag != null && (tag.isOpen() || tag.isOpenClose());
	}

	/**
	 * Retrieves the container on the top of the containers stack
	 * 
	 * @return
	 */
	public MarkupContainer peekContainer()
	{
		return containers.peek();
	}

	/**
	 * Pushes a container onto the container stack
	 * 
	 * @param container
	 */
	public void pushContainer(MarkupContainer container)
	{
		containers.push(container);
	}

	/**
	 * Pops a container from the container stack
	 * 
	 * @return
	 */
	public MarkupContainer popContainer()
	{
		return containers.pop();
	}

	/**
	 * Searches the container stack for a component that can be dequeude
	 * 
	 * @param tag
	 * @return
	 */
	public Component findComponentToDequeue(ComponentTag tag)
	{
		for (int j = containers.size() - 1; j >= 0; j--)
		{
			MarkupContainer container = containers.get(j);
			Component child = container.findComponentToDequeue(tag);
			if (child != null)
			{
				return child;
			}
		}
		return null;
	}

}
