/* ComponentDefinitionImpl.java

	Purpose:
		
	Description:
		
	History:
		Tue May 31 17:54:45     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.zk.ui.metainfo.impl;

import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.zkoss.lang.Classes;
import org.zkoss.util.resource.Location;
import org.zkoss.web.servlet.Servlets;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.metainfo.AnnotationMap;
import org.zkoss.zk.ui.metainfo.ComponentDefinition;
import org.zkoss.zk.ui.metainfo.ComponentInfo;
import org.zkoss.zk.ui.metainfo.LanguageDefinition;
import org.zkoss.zk.ui.metainfo.PageDefinition;
import org.zkoss.zk.ui.metainfo.Property;
import org.zkoss.zk.ui.metainfo.ShadowInfo;
import org.zkoss.zk.ui.sys.ComponentsCtrl;
import org.zkoss.zk.xel.EvaluatorRef;
import org.zkoss.zk.xel.ExValue;
import org.zkoss.zk.xel.impl.Utils;

/**
 * An implementation of {@link ComponentDefinition}.
 * 
 * <p>Note: it is not thread safe. Thus, it is better to {@link #clone}
 * and then modifying the cloned instance if you want to change it
 * concurrently.
 *
 * @author tomyeh
 */
public class ComponentDefinitionImpl implements ComponentDefinition, java.io.Serializable {
	private String _name;
	private transient LanguageDefinition _langdef;
	private transient PageDefinition _pgdef;
	private EvaluatorRef _evalr;
	/** Either String or Class. */
	private Object _implcls;
	/** A map of (String mold, ExValue widgetClass). */
	private Map<String, ExValue> _molds;
	/** The default widget class. */
	private ExValue _defWgtClass;
	/** A map of custom attributes (String name, ExValue value). */
	private Map<String, ExValue> _custAttrs;
	/** A list of {@link Property}. */
	private List<Property> _props;
	/** the current directory. */
	private String _curdir;
	/** the property name to which the text within the element will be assigned. */
	private String _textAs;
	private AnnotationMap _annots;
	private URL _declURL;
	/** The parsed expressions of the apply attribute. */
	private ExValue[] _apply;
	/** Whether to preserve the blank text. */
	private boolean _blankpresv;
	/** Whether the child element is allowed within the element. */
	private boolean _childAllowedInTextAs;

	/** Constructs a native component, i.e., a component implemented by
	 * a Java class.
	 *
	 * <p>Note; if both langdef and pgdef are null, it must be a reserved
	 * component.
	 *
	 * @param langdef the language definition. It is null if it is defined
	 * as part of a page definition
	 * @param pgdef the page definition. It is null if it is defined
	 * as part of a language definition.
	 * doesn't belong to any language.
	 * @param cls the implementation class.
	 * @since 3.0.0
	 */
	public ComponentDefinitionImpl(LanguageDefinition langdef, PageDefinition pgdef, String name,
			Class<? extends Component> cls) {
		if (cls != null && !Component.class.isAssignableFrom(cls))
			throw new IllegalArgumentException(cls + " must implement " + Component.class);
		init(langdef, pgdef, name, cls);
	}

	/** Constructs a native component, i.e., a component implemented by
	 * a Java class.
	 *
	 * <p>Note; if both langdef and pgdef are null, it must be a reserved
	 * component.
	 *
	 * @param langdef the language definition. It is null if it is defined
	 * as part of a page definition
	 * @param pgdef the page definition. It is null if it is defined
	 * as part of a language definition.
	 * doesn't belong to any language.
	 * @param clsnm the implementation class.
	 * @since 3.0.8
	 */
	public ComponentDefinitionImpl(LanguageDefinition langdef, PageDefinition pgdef, String name, String clsnm) {
		init(langdef, pgdef, name, clsnm);
	}

	private void init(LanguageDefinition langdef, PageDefinition pgdef, String name, Object cls) {
		if (name == null)
			throw new IllegalArgumentException();
		if (langdef != null && pgdef != null)
			throw new IllegalArgumentException("langdef and pgdef cannot both null or both non-null");

		_langdef = langdef;
		_pgdef = pgdef;
		_name = name;
		_implcls = cls;

		_evalr = _langdef != null ? _langdef.getEvaluatorRef() : _pgdef != null ? _pgdef.getEvaluatorRef() : null;
	}

	/** Constructs a shadow component definition.
	 * It is the component definition used to implement the shadow element.
	 *
	 * @param langdef the language definition. It is null if it is defined
	 * as part of a page definition
	 * @param pgdef the page definition. It is null if it is defined
	 * as part of a language definition.
	 * @param templateURI the URI of the ZUML page to representing this shadow, like macroURI.
	 * @since 8.0.0
	 */
	public static final ComponentDefinition newShadowDefinition(LanguageDefinition langdef, PageDefinition pgdef,
			String name, Class<? extends Component> cls, String templateURI) {
		return new ShadowDefinitionImpl(langdef, pgdef, name, cls, templateURI);
	}

	/** Constructs a macro component definition.
	 * It is the component definition used to implement the macros.
	 *
	 * @param langdef the language definition. It is null if it is defined
	 * as part of a page definition
	 * @param pgdef the page definition. It is null if it is defined
	 * as part of a language definition.
	 * @param macroURI the URI of the ZUML page to representing this macro.
	 * @since 3.0.0
	 */
	public static final ComponentDefinition newMacroDefinition(LanguageDefinition langdef, PageDefinition pgdef,
			String name, Class<? extends Component> cls, String macroURI, boolean inline) {
		return new MacroDefinition(langdef, pgdef, name, cls, macroURI, inline);
	}

	/** Constructs a native component definition.
	 * It is the component definition used to implement the native namespace.
	 *
	 * @param langdef the language definition. It cannot be null.
	 * @since 3.0.0
	 */
	public static final ComponentDefinition newNativeDefinition(LanguageDefinition langdef, String name,
			Class<? extends Component> cls) {
		return new NativeDefinition(langdef, name, cls);
	}

	//extra//
	/** Adds a custom attribute.
	 */
	public void addCustomAttribute(String name, String value) {
		if (name == null || value == null || name.length() == 0 || value.length() == 0)
			throw new IllegalArgumentException();

		final ExValue ev = new ExValue(value, Object.class);
		if (_custAttrs == null)
			_custAttrs = new HashMap<String, ExValue>(4);
		_custAttrs.put(name, ev);
	}

	/** Adds an annotation to the specified property of this component
	 * definition.
	 *
	 * @param propName the property name.
	 * If null, the annotation is associated with the whole component rather than
	 * a particular property.
	 * @param annotName the annotation name (never null, nor empty).
	 * @param annotAttrs a map of attributes, or null if no attribute at all.
	 * The attribute must be in a pair of strings (String name, String value).
	 * @param loc the location information of the annotation in
	 * the document, or null if not available.
	 * @since 6.0.0
	 */
	public void addAnnotation(String propName, String annotName, Map<String, String[]> annotAttrs, Location loc) {
		if (_annots == null)
			_annots = new AnnotationMap();
		_annots.addAnnotation(propName, annotName, annotAttrs, loc);
	}

	/** Returns the current directory which is used to convert
	 * a relative URI to absolute, or null if not available.
	 */
	public String getCurrentDirectory() {
		return _curdir;
	}

	/** Sets the current directory which is used to convert
	 * a relative URI to absolute.
	 *
	 * @param curdir the current directory; null to ignore.
	 */
	public void setCurrentDirectory(String curdir) {
		if (curdir != null && curdir.length() > 0) {
			_curdir = curdir.charAt(curdir.length() - 1) != '/' ? curdir + '/' : curdir;
		} else {
			_curdir = null;
		}
	}

	/** Sets the property name to which the text enclosed within
	 * the element (associated with this component definition) is assigned to.
	 *
	 * <p>Default: null (means to create a Label component)
	 *
	 * @param propnm the property name. If empty (""), null is assumed.
	 * @see #getTextAs
	 * @since 3.0.0
	 */
	public void setTextAs(String propnm) {
		_textAs = propnm != null && propnm.length() > 0 ? propnm : null;
	}

	/** Sets whether to preserve the blank text.
	 * If false, the blank text (a non-empty string consisting of whitespaces)
	 * are ignored.
	 * If true, they are converted to a label child.
	 * <p>Default: false.
	 * @see #isBlankPreserved
	 * @since 3.5.0
	 */
	public void setBlankPreserved(boolean preserve) {
		_blankpresv = preserve;
	}

	/** Sets whether the child component is allowed within the element.
	 * For more information, please refer to {@link ComponentDefinition#getTextAs}.
	 * @since 6.0.0
	 */
	public void setChildAllowedInTextAs(boolean allowed) {
		_childAllowedInTextAs = allowed;
	}

	/** Sets the URI where this definition is declared.
	 *
	 * @param url the URL. If null, it means not available.
	 * @since 3.0.3
	 */
	public void setDeclarationURL(URL url) {
		_declURL = url;
	}

	//ComponentDefinition//
	public LanguageDefinition getLanguageDefinition() {
		return _langdef;
	}

	public String getName() {
		return _name;
	}

	public String getTextAs() {
		return _textAs;
	}

	public boolean isChildAllowedInTextAs() {
		return _childAllowedInTextAs;
	}

	public boolean isBlankPreserved() {
		return _blankpresv;
	}

	public boolean isMacro() {
		return false;
	}

	public String getMacroURI() {
		return null;
	}

	public boolean isInlineMacro() {
		return false;
	}

	public boolean isNative() {
		return false;
	}

	public Object getImplementationClass() {
		return _implcls;
	}

	public void setImplementationClass(Class<? extends Component> cls) {
		if (!Component.class.isAssignableFrom(cls))
			throw new UiException(Component.class.getName() + " must be implemented by " + cls);
		_implcls = cls;
	}

	public void setImplementationClass(String clsnm) {
		if (clsnm == null || clsnm.length() == 0)
			throw new UiException("Non-empty class name is required");
		_implcls = clsnm;
	}

	@SuppressWarnings("unchecked")
	public Component newInstance(Page page, String clsnm) {
		try {
			return newInstance((Class<? extends Component>) resolveImplementationClass(page, clsnm));
		} catch (ClassNotFoundException ex) {
			throw UiException.Aide.wrap(ex);
		}
	}

	public Component newInstance(Class<? extends Component> cls) {
		final Object curInfo = ComponentsCtrl.getCurrentInfo();
		boolean bSet = true;
		if (curInfo instanceof ComponentInfo) {
			bSet = ((ComponentInfo) curInfo).getComponentDefinition() != this;
		} else if (curInfo instanceof ShadowInfo) {
			bSet = ((ShadowInfo) curInfo).getComponentDefinition() != this;
		}
		if (bSet)
			ComponentsCtrl.setCurrentInfo(this);
		final Component comp;
		try {
			comp = cls.newInstance();
		} catch (Exception ex) {
			throw UiException.Aide.wrap(ex);
		} finally {
			if (bSet)
				ComponentsCtrl.setCurrentInfo((ComponentDefinition) null);
		}
		return comp;
	}

	public boolean isInstance(Component comp) {
		Class<?> cls;
		if (_implcls instanceof String) {
			try {
				cls = resolveImplementationClass(comp.getPage(), null);
			} catch (ClassNotFoundException ex) {
				return true; //consider as true if not resolvable
			}
		} else {
			cls = (Class<?>) _implcls;
		}
		return cls.isInstance(comp);
	}

	public Class<?> resolveImplementationClass(Page page, String clsnm) throws ClassNotFoundException {
		final Object cls = clsnm != null ? clsnm : _implcls;
		if (cls instanceof String) {
			clsnm = (String) cls;
			final Class<?> found = page != null ? page.resolveClass(clsnm) : Classes.forNameByThread(clsnm);
			if (clsnm.equals(_implcls))
				_implcls = found;
			//cache to _implcls (to improve the performance)
			return found;
		}
		return (Class) cls;
	}

	public AnnotationMap getAnnotationMap() {
		return _annots;
	}

	public String getApply() {
		if (_apply == null)
			return null;

		final StringBuffer sb = new StringBuffer();
		for (int j = 0; j < _apply.length; ++j) {
			if (j > 0)
				sb.append(',');
			sb.append(_apply[j].getRawValue());
		}
		return sb.toString();
	}

	public void setApply(String apply) {
		_apply = Utils.parseList(apply, Object.class, true);
	}

	public ExValue[] getParsedApply() {
		return _apply;
	}

	public URL getDeclarationURL() {
		return _declURL;
	}

	public void addProperty(String name, String value) {
		//Implementation Note: the reason not to have condition because
		//isEffective always assumes Executions.getCurrent, which is
		//not true if _langdef != null

		if (name == null || name.length() == 0)
			throw new IllegalArgumentException("name");

		final Property prop = new Property(_evalr, name, value, null);
		if (_props == null)
			_props = new LinkedList<Property>();
		_props.add(prop);
	}

	public void applyProperties(Component comp) {
		//Note: it doesn't apply annotations since it is done
		//by AbstractComponent's constructor
		//AbstractComponent's constructor also invokes applyAttributes automatically

		if (_props != null) {
			for (Property prop : _props) {
				prop.assign(comp);
			}
		}
	}

	public void applyAttributes(Component comp) {
		if (_custAttrs != null) {
			for (Map.Entry<String, ExValue> me : _custAttrs.entrySet()) {
				comp.setAttribute(me.getKey(), me.getValue().getValue(_evalr, comp));
			}
		}
	}

	public Map<String, Object> evalProperties(Map<String, Object> propmap, Page owner, Component parent) {
		if (propmap == null)
			propmap = new HashMap<String, Object>();

		if (_props != null) {
			for (Property prop : _props) {
				if (parent != null) {
					if (prop.isEffective(parent))
						propmap.put(prop.getName(), prop.getValue(parent));
				} else {
					if (prop.isEffective(owner))
						propmap.put(prop.getName(), prop.getValue(owner));
				}
			}
		}
		return propmap;
	}

	public void addMold(String name, String widgetClass) {
		if (name == null || name.length() == 0)
			throw new IllegalArgumentException();

		if (_molds == null)
			_molds = new HashMap<String, ExValue>(2);
		_molds.put(name, new ExValue(widgetClass, String.class));
	}

	public boolean hasMold(String name) {
		return _molds != null && _molds.containsKey(name);
	}

	public Collection<String> getMoldNames() {
		if (_molds != null)
			return _molds.keySet();
		return Collections.emptyList();
	}

	public String getWidgetClass(Component comp, String moldName) {
		if (_molds != null) {
			final ExValue wc = _molds.get(moldName);
			if (wc != null) {
				final String s = (String) wc.getValue(_evalr, comp);
				if (s != null)
					return s;
			}
		}
		return getDefaultWidgetClass(comp);
	}

	public String getDefaultWidgetClass(Component comp) {
		return _defWgtClass != null ? (String) _defWgtClass.getValue(_evalr, comp) : null;
	}

	public void setDefaultWidgetClass(String widgetClass) {
		final ExValue oldwc = _defWgtClass;
		_defWgtClass = new ExValue(widgetClass, String.class);

		//replace mold's widget class if it is the old default one
		if (oldwc != null && _molds != null)
			for (Map.Entry<String, ExValue> me : _molds.entrySet()) {
				if (oldwc.equals(me.getValue()))
					me.setValue(_defWgtClass);
			}
	}

	private String toAbsoluteURI(String uri) {
		if (_curdir != null && uri != null && uri.length() > 0) {
			final char cc = uri.charAt(0);
			if (cc != '/' && cc != '~' && !Servlets.isUniversalURL(uri))
				return _curdir + uri;
		}
		return uri;
	}

	public boolean isShadowElement() {
		return false;
	}

	//Serializable//
	//NOTE: they must be declared as private
	private synchronized void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
		s.defaultWriteObject();

		s.writeObject(_langdef != null ? _langdef.getName() : null);
	}

	private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
		s.defaultReadObject();

		final String langnm = (String) s.readObject();
		if (langnm != null)
			_langdef = LanguageDefinition.lookup(langnm);
	}

	//Object//
	public String toString() {
		return "[ComponentDefinition: " + _name + ']';
	}

	//Cloneable/
	public Object clone() {
		final ComponentDefinitionImpl compdef;
		try {
			compdef = (ComponentDefinitionImpl) super.clone();
		} catch (CloneNotSupportedException ex) {
			throw new InternalError();
		}

		if (_annots != null)
			compdef._annots = (AnnotationMap) _annots.clone();
		if (_props != null)
			compdef._props = new LinkedList<Property>(_props);
		if (_molds != null)
			compdef._molds = new HashMap<String, ExValue>(_molds);
		if (_custAttrs != null)
			compdef._custAttrs = new HashMap<String, ExValue>(_custAttrs);
		return compdef;
	}

	public ComponentDefinition clone(LanguageDefinition langdef, String name) {
		if (name == null || name.length() == 0)
			throw new IllegalArgumentException("empty");

		ComponentDefinitionImpl cd = (ComponentDefinitionImpl) clone();
		cd._name = name;
		cd._langdef = langdef;
		return cd;
	}
}
