/*
 * 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.markup.html.form;

import java.io.Serializable;

import org.apache.wicket.Component;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.MetaDataKey;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.core.request.handler.ComponentNotFoundException;
import org.apache.wicket.core.util.string.CssUtils;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.MarkupStream;
import org.apache.wicket.markup.html.TransparentWebMarkupContainer;
import org.apache.wicket.markup.resolver.IComponentResolver;
import org.apache.wicket.util.visit.IVisit;
import org.apache.wicket.util.visit.IVisitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Resolver that implements the {@code wicket:for} attribute functionality. The attribute makes it
 * easy to set up {@code <label>} tags for form components by providing the following features
 * without having to add any additional components in code:
 * <ul>
 * <li>Outputs the {@code for} attribute with the value equivalent to the markup id of the
 * referenced form component</li>
 * <li>Appends {@code required} css class to the {@code <label>} tag if the referenced form
 * component is required. Name of the css class can be overwritten by having a i18n property defined
 * for key AutoLabel.CSS.required</li>
 * <li>Appends {@code error} css class to the {@code <label>} tag if the referenced form component
 * has failed validation. Name of the css class can be overwritten by having a i18n property defined
 * for key AutoLabel.CSS.error</li>
 * <li>Appends {@code disabled} css class to the {@code <label>} tag if the referenced form
 * component has is not enabled in hierarchy. Name of the css class can be overwritten by having a i18n property defined
 * for key AutoLabel.CSS.disabled</li>
 * </ul>
 * 
 * <p>
 * The value of the {@code wicket:for} attribute can either contain an id of the form component or a
 * path to it using the standard {@code :} path separator. Note that {@code ..} can be used as part
 * of the path to construct a reference to the parent container, eg {@code ..:..:foo:bar}. First the
 * value of the attribute will be treated as a path and the {@code <label>} tag's closest parent
 * container will be queried for the form component. If the form component cannot be resolved the
 * value of the {@code wicket:for} attribute will be treated as an id and all containers will be
 * searched from the closest parent to the page.
 * </p>
 * 
 * @author igor
 * @author Carl-Eric Menzel <cmenzel@wicketbuch.de>
 */
public class AutoLabelResolver implements IComponentResolver
{
	private static final long serialVersionUID = 1L;

	private static final Logger logger = LoggerFactory.getLogger(AutoLabelResolver.class);

	static final String WICKET_FOR = ":for";
	
	public static final String LABEL_ATTR = "label_attr";
	
	public static final String CSS_REQUIRED_KEY = CssUtils.key(AutoLabel.class, "required");
	public static final String CSS_DISABLED_KEY = CssUtils.key(AutoLabel.class, "disabled");
	public static final String CSS_ERROR_KEY = CssUtils.key(AutoLabel.class, "error");
	private static final String CSS_DISABLED_DEFAULT = "disabled";
	private static final String CSS_REQUIRED_DEFAULT = "required";
	private static final String CSS_ERROR_DEFAULT = "error";
	


	@Override
	public Component resolve(final MarkupContainer container, final MarkupStream markupStream,
		final ComponentTag tag)
	{
		if (!tag.getId().startsWith(LABEL_ATTR))
		{
			return null;
		}

		final String id = tag.getAttribute(getWicketNamespace(markupStream) + WICKET_FOR).trim();

		Component component = findRelatedComponent(container, id);
		if (component == null)
		{
			throw new ComponentNotFoundException("Could not find form component with id '" + id +
				"' while trying to resolve wicket:for attribute");
		}

		if (!(component instanceof ILabelProvider))
		{
			throw new WicketRuntimeException("Component pointed to by wicket:for attribute '" + id +
				"' does not implement " + ILabelProvider.class.getName());
		}

		if (!component.getOutputMarkupId())
		{
			component.setOutputMarkupId(true);
			if (component.hasBeenRendered())
			{
				logger.warn(
					"Component: {} is referenced via a wicket:for attribute but does not have its outputMarkupId property set to true",
					component.toString(false));
			}
		}

		if (component instanceof FormComponent)
		{
			component.setMetaData(MARKER_KEY, new AutoLabelMarker((FormComponent<?>)component));
		}

		return new AutoLabel(tag.getId(), component);
	}

	private String getWicketNamespace(MarkupStream markupStream)
	{
		return markupStream.getWicketNamespace();
	}

	/**
	 * 
	 * @param container
	 * @param id
	 * @return Component
	 */
	static Component findRelatedComponent(MarkupContainer container, final String id)
	{
		// try the quick and easy route first

		Component component = container.get(id);
		if (component != null)
		{
			return component;
		}

		// try the long way, search the hierarchy from the closest container up to the page

		final Component[] searched = new Component[] { null };
		while (container != null)
		{
			component = container.visitChildren(Component.class,
				new IVisitor<Component, Component>()
				{
					@Override
					public void component(Component child, IVisit<Component> visit)
					{
						if (child == searched[0])
						{
							// this container was already searched
							visit.dontGoDeeper();
							return;
						}
						if (id.equals(child.getId()))
						{
							visit.stop(child);
							return;
						}
					}
				});

			if (component != null)
			{
				return component;
			}

			// remember the container so we dont search it again, and search the parent
			searched[0] = container;
			container = container.getParent();
		}

		return null;
	}

	public static String getLabelIdFor(Component component)
	{
		return component.getMarkupId() + "-w-lbl";
	}

	public static final MetaDataKey<AutoLabelMarker> MARKER_KEY = new MetaDataKey<AutoLabelMarker>()
	{
	};

	/**
	 * Marker used to track whether or not a form component has an associated auto label by its mere
	 * presense as well as some attributes of the component across requests.
	 * 
	 * @author igor
	 * 
	 */
	public static final class AutoLabelMarker implements Serializable
	{
		public static final short VALID = 0x01;
		public static final short REQUIRED = 0x02;
		public static final short ENABLED = 0x04;

		private short flags;

		public AutoLabelMarker(FormComponent<?> component)
		{
			setFlag(VALID, component.isValid());
			setFlag(REQUIRED, component.isRequired());
			setFlag(ENABLED, component.isEnabledInHierarchy());
		}

		public void updateFrom(FormComponent<?> component, AjaxRequestTarget target)
		{
			boolean valid = component.isValid(), required = component.isRequired(), enabled = component.isEnabledInHierarchy();

			if (isValid() != valid)
			{
				target.appendJavaScript(String.format("Wicket.DOM.toggleClass('%s', '%s', %s);",
					getLabelIdFor(component), component.getString(CSS_ERROR_KEY, null, CSS_ERROR_DEFAULT),
					!valid));
			}

			if (isRequired() != required)
			{
				target.appendJavaScript(String.format("Wicket.DOM.toggleClass('%s', '%s', %s);",
					getLabelIdFor(component), component.getString(CSS_REQUIRED_KEY, null, CSS_REQUIRED_DEFAULT),
					required));
			}

			if (isEnabled() != enabled)
			{
				target.appendJavaScript(String.format("Wicket.DOM.toggleClass('%s', '%s', %s);",
					getLabelIdFor(component), component.getString(CSS_DISABLED_KEY, null, CSS_DISABLED_DEFAULT),
					!enabled));
			}

			setFlag(VALID, valid);
			setFlag(REQUIRED, required);
			setFlag(ENABLED, enabled);
		}

		public boolean isValid()
		{
			return getFlag(VALID);
		}

		public boolean isEnabled()
		{
			return getFlag(ENABLED);
		}

		public boolean isRequired()
		{
			return getFlag(REQUIRED);
		}

		private boolean getFlag(final int flag)
		{
			return (flags & flag) != 0;
		}

		private void setFlag(final short flag, final boolean set)
		{
			if (set)
			{
				flags |= flag;
			}
			else
			{
				flags &= ~flag;
			}
		}
	}

	/**
	 * Component that is attached to the {@code <label>} tag and takes care of writing out the label
	 * text as well as setting classes on the {@code <label>} tag
	 * 
	 * @author igor
	 */
	protected static class AutoLabel extends TransparentWebMarkupContainer
	{
		private static final long serialVersionUID = 1L;

		private final Component component;

		public AutoLabel(String id, Component fc)
		{
			super(id);
			component = fc;
			
			setMarkupId(getLabelIdFor(component));
			setOutputMarkupId(true);
		}

		@Override
		protected void onComponentTag(ComponentTag tag)
		{
			super.onComponentTag(tag);
			tag.put("for", component.getMarkupId());

			if (component instanceof FormComponent)
			{
				FormComponent<?> fc = (FormComponent<?>)component;
				if (fc.isRequired())
				{
					tag.append("class", component.getString(CSS_REQUIRED_KEY, null, CSS_REQUIRED_DEFAULT), " ");
				}
				if (!fc.isValid())
				{
					tag.append("class", component.getString(CSS_ERROR_KEY, null, CSS_ERROR_DEFAULT), " ");
				}
			}

			if (!component.isEnabledInHierarchy())
			{
				tag.append("class", component.getString(CSS_DISABLED_KEY, null, CSS_DISABLED_DEFAULT), " ");
			}
		}


		/**
		 * @return the component this label points to, if any.
		 */
		public Component getRelatedComponent()
		{
			return component;
		}
	}
}
