/*
 * 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.extensions.markup.html.repeater.util;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;

import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.extensions.markup.html.repeater.tree.AbstractTree;
import org.apache.wicket.extensions.markup.html.repeater.tree.ITreeProvider;
import org.apache.wicket.model.IModel;

/**
 * A provider wrapping a Swing {@link TreeModel}.
 * <br>
 * EXPERIMENTAL !
 * 
 * @author svenmeier
 * @param <T>
 *            model object type
 */
public abstract class TreeModelProvider<T> implements ITreeProvider<T>
{

	private static final long serialVersionUID = 1L;

	private final TreeModel treeModel;

	private final boolean rootVisible;

	protected boolean completeUpdate;

	protected List<T> nodeUpdates;

	protected List<T> branchUpdates;

	/**
	 * Wrap the given {@link TreeModel}.
	 * 
	 * @param treeModel
	 *            model to wrap
	 */
	public TreeModelProvider(TreeModel treeModel)
	{
		this(treeModel, true);
	}

	/**
	 * Wrap the given {@link TreeModel}.
	 * 
	 * @param treeModel
	 *            the wrapped model
	 * @param rootVisible
	 *            should the root be visible
	 */
	public TreeModelProvider(TreeModel treeModel, boolean rootVisible)
	{
		this.treeModel = treeModel;
		this.rootVisible = rootVisible;

		treeModel.addTreeModelListener(new Listener());
	}

	@Override
	public Iterator<T> getRoots()
	{
		if (rootVisible)
		{
			return new Iterator<T>()
			{
				boolean next = true;

				@Override
				public boolean hasNext()
				{
					return next;
				}

				@Override
				public T next()
				{
					next = false;
					return cast(treeModel.getRoot());
				}

				@Override
				public void remove()
				{
					throw new UnsupportedOperationException();
				}
			};
		}
		else
		{
			return getChildren(cast(treeModel.getRoot()));
		}
	}

	@Override
	public boolean hasChildren(T object)
	{
		return !treeModel.isLeaf(object);
	}

	@Override
	public Iterator<T> getChildren(final T object)
	{
		return new Iterator<T>()
		{
			private int size = treeModel.getChildCount(object);
			private int index = -1;

			@Override
			public boolean hasNext()
			{
				return index < size - 1;
			}

			@Override
			public T next()
			{
				index++;
				return cast(treeModel.getChild(object, index));
			}

			@Override
			public void remove()
			{
				throw new UnsupportedOperationException();
			}
		};
	}

	@SuppressWarnings("unchecked")
	protected T cast(Object object)
	{
		return (T)object;
	}

	@Override
	public abstract IModel<T> model(T object);

	@Override
	public void detach()
	{
		completeUpdate = false;
		nodeUpdates = null;
		branchUpdates = null;
	}

	/**
	 * Call this method after all change to the wrapped {@link TreeModel} being initiated via
	 * {@link AjaxRequestTarget}.
	 * 
	 * @param tree
	 *            the tree utilizing this {@link ITreeProvider}
	 * @param target
	 *            the {@link AjaxRequestTarget} which initiated the changes
	 */
	public void update(AbstractTree<T> tree, AjaxRequestTarget target)
	{
		if (completeUpdate)
		{
			target.add(tree);
		}
		else
		{
			if (nodeUpdates != null)
			{
				for (T object : nodeUpdates)
				{
					tree.updateNode(object, target);
				}
			}

			if (branchUpdates != null)
			{
				for (T object : branchUpdates)
				{
					tree.updateBranch(object, target);
				}
			}
		}

		detach();
	}

	protected void nodeUpdate(Object[] nodes)
	{
		if (nodeUpdates == null)
		{
			nodeUpdates = new ArrayList<>();
		}

		for (Object node : nodes)
		{
			nodeUpdates.add(cast(node));
		}
	}

	protected void branchUpdate(Object branch)
	{
		if (branchUpdates == null)
		{
			branchUpdates = new ArrayList<>();
		}

		branchUpdates.add(cast(branch));
	}

	private class Listener implements TreeModelListener, Serializable
	{
		private static final long serialVersionUID = 1L;

		@Override
		public void treeNodesChanged(TreeModelEvent e)
		{
			if (e.getChildIndices() == null)
			{
				completeUpdate = true;
			}
			else
			{
				nodeUpdate(e.getChildren());
			}
		}

		@Override
		public void treeNodesInserted(TreeModelEvent e)
		{
			branchUpdate(e.getTreePath().getLastPathComponent());
		}

		@Override
		public void treeNodesRemoved(TreeModelEvent e)
		{
			branchUpdate(e.getTreePath().getLastPathComponent());
		}

		@Override
		public void treeStructureChanged(TreeModelEvent e)
		{
			if (e.getTreePath().getPathCount() == 1)
			{
				completeUpdate = true;
			}
			else
			{
				branchUpdate(e.getTreePath().getLastPathComponent());
			}
		}
	}
}
