/* AbstractTag.java

	Purpose:

	Description:

	History:
		Tue Oct  4 09:15:59     2005, Created by tomyeh

Copyright (C) 2005 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
	This program is distributed under LGPL Version 2.1 in the hope that
	it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.zhtml.impl;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

import org.owasp.encoder.Encode;

import org.zkoss.html.HTMLs;
import org.zkoss.image.Images;
import org.zkoss.lang.Objects;
import org.zkoss.zk.au.DeferredValue;
import org.zkoss.zk.ui.AbstractComponent;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Execution;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.ext.DynamicPropertied;
import org.zkoss.zk.ui.ext.RawId;
import org.zkoss.zk.ui.ext.render.DirectContent;
import org.zkoss.zk.ui.sys.BooleanPropertyAccess;
import org.zkoss.zk.ui.sys.ComponentCtrl;
import org.zkoss.zk.ui.sys.ComponentsCtrl;
import org.zkoss.zk.ui.sys.HtmlPageRenders;
import org.zkoss.zk.ui.sys.PropertyAccess;
import org.zkoss.zk.ui.sys.StringPropertyAccess;

/**
 * The raw component used to generate raw HTML elements.
 *
 * <p>
 * Note: ZHTML components ignore the page listener since it handles non-deferrable event listeners
 * (see {@link org.zkoss.zk.ui.event.Deferrable}).
 *
 * @author tomyeh
 */
public class AbstractTag extends AbstractComponent implements DynamicPropertied, RawId {
	static {
		addClientEvent(AbstractTag.class, Events.ON_CLICK, 0);
	}

	/** The tag name. */
	protected String _tagnm;
	private Map<String, Object> _props;

	protected AbstractTag(String tagname) {
		if (tagname == null || tagname.length() == 0)
			throw new IllegalArgumentException("A tag name is required");
		_tagnm = tagname;
	}

	protected AbstractTag() {
	}

	/**
	 * Returns the CSS class. Due to Java's limitation, we cannot use the name called getClas.
	 * <p>
	 * Default: null (the default value depends on element).
	 */
	public String getSclass() {
		return (String) getDynamicProperty("class");
	}

	/**
	 * Sets the CSS class.
	 */
	public void setSclass(String sclass) {
		setDynamicProperty("class", sclass);
	}

	/**
	 * Returns the CSS style.
	 * <p>
	 * Default: null.
	 */
	public String getStyle() {
		return (String) getDynamicProperty("style");
	}

	/**
	 * Sets the CSS style.
	 *
	 * <p>
	 * Note: if display is not specified as part of style, the returned value of {@link #isVisible}
	 * is assumed. In other words, if not visible and display is not specified as part of style,
	 * "display:none" is appended.
	 *
	 * <p>
	 * On the other hand, if display is specified, then {@link #setVisible} is called to reflect the
	 * visibility, if necessary.
	 */
	public void setStyle(String style) {
		setDynamicProperty("style", style);
	}

	/**
	 * Returns the accesskey of this tag.
	 * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
	 * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
	 * @since 8.0.3
	 */
	public String getAccesskey() {
		return (String) getDynamicProperty("accesskey");
	}

	/**
	 * Sets the accesskey of this tag.
	 * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
	 * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
	 * @since 8.0.3
	 */
	public void setAccesskey(String accesskey) throws WrongValueException {
		setDynamicProperty("accesskey", accesskey);
	}
	/**
	 * Returns the contenteditable of this tag.
	 * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
	 * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
	 * @since 8.0.3
	 */
	public boolean isContenteditable() {
		final Boolean b = (Boolean) getDynamicProperty("contenteditable");
		return b != null && b.booleanValue();
	}

	/**
	 * Sets the contenteditable of this tag.
	 * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
	 * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
	 * @since 8.0.3
	 */
	public void setContenteditable(boolean contenteditable) throws WrongValueException {
		setDynamicProperty("contenteditable", contenteditable ? Boolean.valueOf(contenteditable) : null);
	}
	/**
	 * Returns the dir of this tag.
	 * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
	 * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
	 * @since 8.0.3
	 */
	public String getDir() {
		return (String) getDynamicProperty("dir");
	}

	/**
	 * Sets the dir of this tag.
	 * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
	 * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
	 * @since 8.0.3
	 */
	public void setDir(String dir) throws WrongValueException {
		setDynamicProperty("dir", dir);
	}
	/**
	 * Returns the draggable of this tag.
	 * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
	 * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
	 * @since 8.0.3
	 */
	public boolean isDraggable() {
		final Boolean b = (Boolean) getDynamicProperty("draggable");
		return b != null && b.booleanValue();
	}

	/**
	 * Sets the draggable of this tag.
	 * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
	 * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
	 * @since 8.0.3
	 */
	public void setDraggable(boolean draggable) throws WrongValueException {
		setDynamicProperty("draggable", draggable ? Boolean.valueOf(draggable) : null);
	}
	/**
	 * Returns the hidden of this tag.
	 * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
	 * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
	 * @since 8.0.3
	 */
	public boolean isHidden() {
		final Boolean b = (Boolean) getDynamicProperty("hidden");
		return b != null && b.booleanValue();
	}

	/**
	 * Sets the hidden of this tag.
	 * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
	 * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
	 * @since 8.0.3
	 */
	public void setHidden(boolean hidden) throws WrongValueException {
		setDynamicProperty("hidden", hidden ? Boolean.valueOf(hidden) : null);
	}
	/**
	 * Returns the lang of this tag.
	 * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
	 * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
	 * @since 8.0.3
	 */
	public String getLang() {
		return (String) getDynamicProperty("lang");
	}

	/**
	 * Sets the lang of this tag.
	 * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
	 * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
	 * @since 8.0.3
	 */
	public void setLang(String lang) throws WrongValueException {
		setDynamicProperty("lang", lang);
	}
	/**
	 * Returns the spellcheck of this tag.
	 * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
	 * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
	 * @since 8.0.3
	 */
	public boolean isSpellcheck() {
		final Boolean b = (Boolean) getDynamicProperty("spellcheck");
		return b != null && b.booleanValue();
	}

	/**
	 * Sets the spellcheck of this tag.
	 * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
	 * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
	 * @since 8.0.3
	 */
	public void setSpellcheck(boolean spellcheck) throws WrongValueException {
		setDynamicProperty("spellcheck", spellcheck ? Boolean.valueOf(spellcheck) : null);
	}
	/**
	 * Returns the tabindex of this tag.
	 * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
	 * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
	 * @since 8.0.3
	 */
	public Integer getTabindex() {
		return (Integer) getDynamicProperty("tabindex");
	}

	/**
	 * Sets the tabindex of this tag.
	 * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
	 * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
	 * @since 8.0.3
	 */
	public void setTabindex(Integer tabindex) throws WrongValueException {
		setDynamicProperty("tabindex", tabindex);
	}
	/**
	 * Returns the title of this tag.
	 * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
	 * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
	 * @since 8.0.3
	 */
	public String getTitle() {
		return (String) getDynamicProperty("title");
	}

	/**
	 * Sets the title of this tag.
	 * <p>Notice that this attribute refers to the corresponding attribute of the HTML5 specification.
	 * Hence, it would still be rendered to client-side as a DOM attribute even if the browser doesn’t support it.
	 * @since 8.0.3
	 */
	public void setTitle(String title) throws WrongValueException {
		setDynamicProperty("title", title);
	}

	/**
	 * Returns the tag name.
	 */
	public String getTag() {
		return _tagnm;
	}

	// -- DynamicPropertys --//
	public boolean hasDynamicProperty(String name) {
		return ComponentsCtrl.isReservedAttribute(name);
	}

	public Map<String, Object> getDynamicProperties() {
		return _props;
	}

	/**
	 * Returns the dynamic property, or null if not found. Note: it must be a String object or null.
	 */
	public Object getDynamicProperty(String name) {
		return _props != null ? _props.get(name) : null;
	}

	/**
	 * Sets the dynamic property. Note: it converts the value to a string object (by use of
	 * {@link Objects#toString}).
	 *
	 * <p>
	 * Note: it handles the style property specially. Refer to {@link #setStyle} for details.
	 */
	public void setDynamicProperty(String name, Object value) throws WrongValueException {
		if (name == null)
			throw new WrongValueException("name is required");
		if (!hasDynamicProperty(name))
			throw new WrongValueException("Attribute not allowed: " + name
					+ "\nSpecify the ZK namespace if you want to use special ZK attributes");

		String sval = Objects.toString(value);
		if ("style".equals(name)) {
			sval = filterStyle(sval);
			setDynaProp(name, sval);
		} else if ("src".equals(name)) {
			// ZK-3011: should defer until render
			EncodedURL url = new EncodedURL(sval);
			setDynaProp(name, url);
			sval = (String) url.getValue();
		} else if ("textContent".equals(name)) {
			setDynaProp(name, sval);
			if (!getChildren().isEmpty())
				invalidate();
		} else
			setDynaProp(name, value);
		// B80-ZK-2716: style and textContent are both dynamiccProperty
		smartUpdate("dynamicProperty", new String[] { name, sval }, true);
	}

	private String getEncodedURL(String src) {
		if (src == null)
			return Images.BASE64SPACERIMAGE;

		final Desktop dt = getDesktop(); // it might not belong to any desktop
		return dt != null ? dt.getExecution().encodeURL(src) : "";
	}

	/** Processes the style. */
	private String filterStyle(String style) {
		if (style != null) {
			final int j = HTMLs.getSubstyleIndex(style, "display");
			if (j >= 0) { // display is specified
				super.setVisible(!"none".equals(HTMLs.getSubstyleValue(style, j)));
				return style; // done
			}
		}

		if (!isVisible()) {
			int len = style != null ? style.length() : 0;
			if (len == 0)
				return "display:none;";
			if (style.charAt(len - 1) != ';')
				style += ';';
			style += "display:none;";
		}
		return style;
	}

	/** Set the dynamic property 'blindly'. */
	private void setDynaProp(String name, Object value) {
		if (value == null) {
			if (_props != null)
				_props.remove(name);
		} else {
			if (_props == null)
				_props = new LinkedHashMap<String, Object>();
			_props.put(name, value);
		}
	}

	/**
	 * Whether to hide the id attribute.
	 * <p>
	 * Default: false.
	 * <p>
	 * Some tags, such as {@link org.zkoss.zhtml.Html}, won't generate the id attribute. They shall
	 * override this method to return true.
	 */
	protected boolean shallHideId() {
		return false;
	}

	// -- Component --//
	/**
	 * Changes the visibility of this component.
	 * <p>
	 * Note: it will adjust the style ({@link #getStyle}) based on the visibility.
	 *
	 * @return the previous visibility
	 */
	public boolean setVisible(boolean visible) {
		final boolean old = super.setVisible(visible);
		if (old != visible) {
			final String style = getStyle();
			if (visible) {
				if (style != null) {
					final int j = HTMLs.getSubstyleIndex(style, "display");
					if (j >= 0) {
						final String val = HTMLs.getSubstyleValue(style, j);
						if ("none".equals(val)) {
							String newstyle = style.substring(0, j);
							final int k = style.indexOf(';', j + 7);
							if (k >= 0)
								newstyle += style.substring(k + 1);
							setDynaProp("style", newstyle);
						}
					}
				}
			} else {
				if (style == null) {
					setDynaProp("style", "display:none;");
				} else {
					final int j = HTMLs.getSubstyleIndex(style, "display");
					if (j >= 0) {
						final String val = HTMLs.getSubstyleValue(style, j);
						if (!"none".equals(val)) {
							String newstyle = style.substring(0, j) + "display:none;";
							final int k = style.indexOf(';', j + 7);
							if (k >= 0)
								newstyle += style.substring(k + 1);
							setDynaProp("style", newstyle);
						}
					} else {
						final int len = style.length();
						String newstyle = len > 0 && style.charAt(len - 1) != ';' ? style + ';'
								: style;
						setDynaProp("style", style + "display:none;");
					}
				}
			}
		}
		return old;
	}

	/**
	 * Returns the widget class, "zhtml.Widget".
	 *
	 * @since 5.0.0
	 */
	public String getWidgetClass() {
		return "zhtml.Widget";
	}

	public void redraw(java.io.Writer out) throws java.io.IOException {
		if (_tagnm == null)
			throw new UiException("The tag name is not initialized yet");

		final Execution exec = Executions.getCurrent();
		if (exec == null || exec.isAsyncUpdate(null) || !HtmlPageRenders.isDirectContent(exec)) {
			super.redraw(out); // generate JavaScript
			return;
		}

		TagRenderContext rc = PageRenderer.getTagRenderContext(exec);
		final boolean rcRequired = rc == null;
		Object ret = null;
		if (rcRequired) {
			ret = PageRenderer.beforeRenderTag(exec);
			rc = PageRenderer.getTagRenderContext(exec);
		}

		out.write(getPrologHalf(false));
		rc.renderBegin(this, getClientEvents(), getSpecialRendererOutput(this), false);

		redrawChildrenDirectly(rc, exec, out);

		out.write(getEpilogHalf());
		rc.renderEnd(this);

		if (rcRequired) {
			out.write(rc.complete());
			PageRenderer.afterRenderTag(exec, ret);
		}
	}

	/**
	 * Renders the children directly to the given output. Notice it is called only if
	 * {@link #redraw} is going to render the content (HTML tags) directly. If it is about to
	 * generate the JavaScript code {@link #redrawChildren} will be called instead.
	 * <p>
	 * You have to override this method if the deriving class has additional information to render.
	 *
	 * @since 5.0.7
	 */
	protected void redrawChildrenDirectly(TagRenderContext rc, Execution exec, java.io.Writer out)
			throws java.io.IOException {
		for (Component child = getFirstChild(); child != null;) {
			Component next = child.getNextSibling();
			if (((ComponentCtrl) child).getExtraCtrl() instanceof DirectContent) {
				((ComponentCtrl) child).redraw(out);
			} else {
				HtmlPageRenders.setDirectContent(exec, false);
				rc.renderBegin(child, null, getSpecialRendererOutput(child), true);
				HtmlPageRenders.outStandalone(exec, child, out);
				rc.renderEnd(child);
				HtmlPageRenders.setDirectContent(exec, true);
			}
			child = next;
		}
	}

	protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer)
			throws java.io.IOException {
		super.renderProperties(renderer);
		render(renderer, "prolog", getPrologHalf(false));
		render(renderer, "epilog", getEpilogHalf());
	}

	/**
	 * @param hideUuidIfNoId
	 *            whether not to generate UUID if possible
	 */
	/* package */ String getPrologHalf(boolean hideUuidIfNoId) {
		final StringBuilder sb = new StringBuilder(128).append('<').append(_tagnm);

//		Do this at client side for ZK-5270
//		if ((!hideUuidIfNoId && !shallHideId()) || getId().length() > 0)
//			sb.append(" id=\"").append(getUuid()).append('"');
		if (!getId().isEmpty()) // ZK-5841: self-defined id should be rendered
			sb.append(" id=\"").append(getUuid()).append('"');

		if (_props != null) {
			for (Iterator it = _props.entrySet().iterator(); it.hasNext();) {
				final Map.Entry me = (Map.Entry) it.next();
				if (!"textContent".equals(me.getKey())) { // ignore textContent
					// ZK-3011: should getValue if it's a deferredValue
					Object v = me.getValue();
					if (v instanceof DeferredValue) {
						v = ((DeferredValue) v).getValue();
					}
					sb.append(' ').append(me.getKey()).append("=\"")
							.append(Encode.forHtmlAttribute(Objects.toString(v))).append('"');
				}
			}
		}

		if (!isOrphanTag())
			sb.append('/');

		sb.append('>');

		Object textContent = getDynamicProperty("textContent");
		if (textContent != null)
			sb.append(Encode.forHtml((String) textContent));
		return sb.toString();
	}

	/* package */ String getEpilogHalf() {
		return isOrphanTag() ? "</" + _tagnm + '>' : "";
	}

	protected boolean isChildable() {
		return isOrphanTag();
	}

	/**
	 * Returns whether this tag is an orphan tag, i.e., it shall be in the form of &lt;tag/&gt;.
	 *
	 * @since 5.0.8
	 */
	protected boolean isOrphanTag() {
		return !HTMLs.isOrphanTag(_tagnm);
	}

	// --ComponentCtrl--//
	private static HashMap<String, PropertyAccess> _properties = new HashMap<String, PropertyAccess>(
			5);

	static {
		_properties.put("id", new StringPropertyAccess() {
			public void setValue(Component cmp, String value) {
				((AbstractTag) cmp).setId(value);
			}

			public String getValue(Component cmp) {
				return ((AbstractTag) cmp).getId();
			}
		});
		_properties.put("sclass", new StringPropertyAccess() {
			public void setValue(Component cmp, String value) {
				((AbstractTag) cmp).setSclass(value);
			}

			public String getValue(Component cmp) {
				return ((AbstractTag) cmp).getSclass();
			}
		});
		_properties.put("style", new StringPropertyAccess() {
			public void setValue(Component cmp, String value) {
				((AbstractTag) cmp).setStyle(value);
			}

			public String getValue(Component cmp) {
				return ((AbstractTag) cmp).getStyle();
			}
		});
		_properties.put("visible", new BooleanPropertyAccess() {
			public void setValue(Component cmp, Boolean value) {
				((AbstractTag) cmp).setVisible(value);
			}

			public Boolean getValue(Component cmp) {
				return ((AbstractTag) cmp).isVisible();
			}
		});
	}

	public PropertyAccess getPropertyAccess(String prop) {
		PropertyAccess pa = _properties.get(prop);
		if (pa != null)
			return pa;
		return super.getPropertyAccess(prop);
	}

	// Cloneable//
	public Object clone() {
		final AbstractTag clone = (AbstractTag) super.clone();
		if (clone._props != null)
			clone._props = new LinkedHashMap<String, Object>(clone._props);
		return clone;
	}

	// Object//
	public String toString() {
		return "[" + _tagnm + ' ' + super.toString() + ']';
	}

	public Object getExtraCtrl() {
		return new ExtraCtrl();
	}

	protected class ExtraCtrl implements DirectContent {
	}

	// ZK-3097
	private class EncodedURL implements org.zkoss.zk.au.DeferredValue, Serializable {
		private String _src;

		public EncodedURL(String src) {
			_src = src;
		}

		public Object getValue() {
			return getEncodedURL(_src);
		}
	}
}
