/*
 * 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 java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.wicket.behavior.Behavior;
import org.apache.wicket.behavior.InvalidBehaviorIdException;
import org.apache.wicket.model.IDetachable;
import org.apache.wicket.util.lang.Args;

/**
 * Manages behaviors in a {@link Component} instance
 * 
 * @author igor
 */
final class Behaviors implements IDetachable
{
	private static final long serialVersionUID = 1L;
	private final Component component;

	public Behaviors(Component component)
	{
		this.component = component;
	}

	public void add(Behavior... behaviors)
	{
		Args.notNull(behaviors, "behaviors");

		for (Behavior behavior : behaviors)
		{
			Args.notNull(behavior, "behavior");

			internalAdd(behavior);

			if (!behavior.isTemporary(component))
			{
				component.addStateChange();
			}

			// Give handler the opportunity to bind this component
			behavior.bind(component);
		}
	}

	private void internalAdd(final Behavior behavior)
	{
		component.data_add(behavior);
		if (behavior.getStatelessHint(component) == false)
		{
			getBehaviorId(behavior);
		}
	}

	@SuppressWarnings("unchecked")
	public <M extends Behavior> List<M> getBehaviors(Class<M> type)
	{
		final int len = component.data_length();
		final int start = component.data_start();
		if (len < start)
		{
			return Collections.emptyList();
		}

		List<M> subset = new ArrayList<>(len);
		for (int i = component.data_start(); i < len; i++)
		{
			Object obj = component.data_get(i);
			if (obj != null && obj instanceof Behavior)
			{
				if (type == null || type.isAssignableFrom(obj.getClass()))
				{
					subset.add((M)obj);
				}
			}
		}
		return Collections.unmodifiableList(subset);
	}


	public void remove(Behavior behavior)
	{
		Args.notNull(behavior, "behavior");

		if (internalRemove(behavior))
		{
			if (!behavior.isTemporary(component))
			{
				component.addStateChange();
			}
			behavior.detach(component);
		}
		else
		{
			throw new IllegalStateException(
				"Tried to remove a behavior that was not added to the component. Behavior: " +
					behavior.toString());
		}
	}

	/**
	 * THIS IS WICKET INTERNAL ONLY. DO NOT USE IT.
	 * 
	 * Traverses all behaviors and calls detachModel() on them. This is needed to cleanup behavior
	 * after render. This method is necessary for {@link org.apache.wicket.ajax.AjaxRequestTarget} to be able to cleanup
	 * component's behaviors after header contribution has been done (which is separated from
	 * component render).
	 */
	@Override
	public final void detach()
	{
		int len = component.data_length();
		for (int i = component.data_start(); i < len; i++)
		{
			Object obj = component.data_get(i);
			if (obj != null && obj instanceof Behavior)
			{
				final Behavior behavior = (Behavior)obj;

				behavior.detach(component);

				if (behavior.isTemporary(component))
				{
					internalRemove(behavior);
					i--;
					len--;
				}
			}
		}
	}

	private boolean internalRemove(final Behavior behavior)
	{
		final int len = component.data_length();
		for (int i = component.data_start(); i < len; i++)
		{
			Object o = component.data_get(i);
			if (o != null && o.equals(behavior))
			{
				component.data_remove(i);
				behavior.unbind(component);

				// remove behavior from behavior-ids
				ArrayList<Behavior> ids = getBehaviorsIdList(false);
				if (ids != null)
				{
					int idx = ids.indexOf(behavior);
					if (idx == ids.size() - 1)
					{
						ids.remove(idx);
					}
					else if (idx >= 0)
					{
						ids.set(idx, null);
					}
					ids.trimToSize();

					if (ids.isEmpty())
					{
						removeBehaviorsIdList();
					}

				}
				return true;
			}
		}
		return false;
	}

	private void removeBehaviorsIdList()
	{
		for (int i = component.data_start(); i < component.data_length(); i++)
		{
			Object obj = component.data_get(i);
			if (obj != null && obj instanceof BehaviorIdList)
			{
				component.data_remove(i);
				return;
			}
		}
	}

	private BehaviorIdList getBehaviorsIdList(boolean createIfNotFound)
	{
		int len = component.data_length();
		for (int i = component.data_start(); i < len; i++)
		{
			Object obj = component.data_get(i);
			if (obj != null && obj instanceof BehaviorIdList)
			{
				return (BehaviorIdList)obj;
			}
		}
		if (createIfNotFound)
		{
			BehaviorIdList list = new BehaviorIdList();
			component.data_add(list);
			return list;
		}
		return null;
	}

	/**
	 * Called when the component is going to be removed. Notifies all
	 * behaviors assigned to this component.
	 *
	 * @param component
	 *      the component that will be removed from its parent
	 */
	public void onRemove(Component component)
	{
		final int len = component.data_length();
		for (int i = component.data_start(); i < len; i++)
		{
			Object obj = component.data_get(i);
			if (obj != null && obj instanceof Behavior)
			{
				final Behavior behavior = (Behavior)obj;

				behavior.onRemove(component);
			}
		}
	}

	private static class BehaviorIdList extends ArrayList<Behavior>
	{
		private static final long serialVersionUID = 1L;

		public BehaviorIdList()
		{
			super(1);
		}
	}

	public final int getBehaviorId(Behavior behavior)
	{
		Args.notNull(behavior, "behavior");

		boolean found = false;
		for (int i = component.data_start(); i < component.data_length(); i++)
		{
			if (behavior == component.data_get(i))
			{
				found = true;
				break;
			}
		}
		if (!found)
		{
			throw new IllegalStateException(
				"Behavior must be added to component before its id can be generated. Behavior: " +
					behavior + ", Component: " + this);
		}

		ArrayList<Behavior> ids = getBehaviorsIdList(true);

		int id = ids.indexOf(behavior);

		if (id < 0)
		{
			// try to find an unused slot
			for (int i = 0; i < ids.size(); i++)
			{
				if (ids.get(i) == null)
				{
					ids.set(i, behavior);
					id = i;
					break;
				}
			}
		}

		if (id < 0)
		{
			// no unused slots, add to the end
			id = ids.size();
			ids.add(behavior);
			ids.trimToSize();
		}

		return id;
	}

	public final Behavior getBehaviorById(int id)
	{
		Behavior behavior = null;

		ArrayList<Behavior> ids = getBehaviorsIdList(false);
		if (ids != null)
		{
			if (id >= 0 && id < ids.size())
			{
				behavior = ids.get(id);
			}
		}

		if (behavior != null)
		{
			return behavior;
		}
		throw new InvalidBehaviorIdException(component, id);
	}


}
