// based on GEF ShapesEditor example
/*******************************************************************************
 * Copyright (c) 2004, 2005 Elias Volanakis and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Elias Volanakis - initial API and implementation
 *******************************************************************************/
package jp.hasc.hasctool.ui.bdeditor2.model;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import jp.hasc.hasctool.core.blockdiagram.model.AbstractBlock;
import jp.hasc.hasctool.core.blockdiagram.model.BeanBlock;
import jp.hasc.hasctool.core.blockdiagram.model.BlockDiagram;
import jp.hasc.hasctool.core.blockdiagram.model.Connection;
import jp.hasc.hasctool.core.messaging.MessageConnector;
import jp.hasc.hasctool.core.messaging.MessageProcessor;
import jp.hasc.hasctool.core.runtime.annotation.PortInfo;
import jp.hasc.hasctool.core.runtime.annotation.RuntimeBeanInfo;
import jp.hasc.hasctool.ui.bdeditor2.misc.EditableComboBoxPropertyDescriptor;
import jp.hasc.hasctool.ui.views.beanslist.BeanClass;

import org.apache.commons.beanutils.PropertyUtils;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.jface.viewers.ICellEditorValidator;
import org.eclipse.ui.views.properties.IPropertyDescriptor;
import org.eclipse.ui.views.properties.PropertyDescriptor;
import org.eclipse.ui.views.properties.TextPropertyDescriptor;

/**
 * @author iwasaki
 */
public class BlockElement extends ModelElement {
	/** logger for this class */
	private final static org.apache.commons.logging.Log LOG = org.apache.commons.logging.LogFactory
			.getLog(BlockElement.class);

	private static final long serialVersionUID = 1;

	@Override
	public String toString() {
		return "Block - " + getBeanBlock().getName();
	}

	private BlockDiagramElement bdg_;
	private BeanBlock block_;
	private HashMap<String,PropertyInfo> propertyInfos_=new HashMap<String, PropertyInfo>();
	
	private Class<?> class_;
	private String classDescription_;
	private ArrayList<String> inputPortNames_ = new ArrayList<String>();
	private HashMap<String,PortInfo> inputPortInfos_ = new HashMap<String, PortInfo>();
	private ArrayList<String> outputPortNames_ = new ArrayList<String>();
	private HashMap<String,PortInfo> outputPortInfos_ = new HashMap<String, PortInfo>();
	
	class PropertyInfo {
		private String name_;
		private Class<?> valueClass_;
		
		public PropertyInfo(String name, Class<?> valueClass) {
			super();
			name_ = name;
			valueClass_ = valueClass;
		}
		
		public String getName() {
			return name_;
		}
		
		public Class<?> getValueClass() {
			return valueClass_;
		}
		
		public Object getValue() {
			return block_.getProperty(name_);
		}
		
		public void setValue(Object value) {
			block_.setProperty(name_, value);
		}
	}

	public BlockElement() {
	}
	
	public void init(BlockDiagramElement bdg, BeanBlock block) {
		bdg_ = bdg;
		block_ = block;
		buildPropertyInfos();
	}
	
	private void buildPropertyInfos() {
		propertyInfos_.clear();
		
		// class info
		class_=null;
		classDescription_=null;
		inputPortNames_.clear();
		inputPortInfos_.clear();
		outputPortNames_.clear();
		outputPortInfos_.clear();
		try{
			class_=Class.forName(block_.getRuntimeClassName());
			classDescription_=BeanClass.getBeanClassDescriptionOrNull(class_);
			RuntimeBeanInfo rbi = class_.getAnnotation(RuntimeBeanInfo.class);
			if (rbi!=null) {
				for(PortInfo pi:rbi.inputPorts()) {
					inputPortNames_.add(pi.name());
					inputPortInfos_.put(pi.name(), pi);
				}
				for(PortInfo pi:rbi.outputPorts()) {
					outputPortNames_.add(pi.name());
					outputPortInfos_.put(pi.name(), pi);
				}
			}else{
				// get port names by reflection
				final String getter_prefix="get";
				for(Method m : class_.getMethods()) {
					String mname = m.getName();
					if (!Modifier.isStatic(m.getModifiers()) && mname.startsWith(getter_prefix) && mname.length()>getter_prefix.length()) {
						String name = mname.substring(getter_prefix.length(),getter_prefix.length()+1).toLowerCase() + mname.substring(getter_prefix.length()+1);
						if (m.getParameterTypes().length==0) {
							if (MessageConnector.class.isAssignableFrom(m.getReturnType())) {
								outputPortNames_.add(name);
							}else if (MessageProcessor.class.isAssignableFrom(m.getReturnType())) {
								inputPortNames_.add(name);
							}
						}else if (m.getParameterTypes().length==1 && m.getParameterTypes()[0].equals(Integer.TYPE)) {
							if (MessageConnector.class.isAssignableFrom(m.getReturnType())) {
								outputPortNames_.add(name+"[]");
							}else if (MessageProcessor.class.isAssignableFrom(m.getReturnType())) {
								inputPortNames_.add(name+"[]");
							}
						}
					}
				}
			}
		}catch(Exception ex) {
			ex.printStackTrace();
		}
		
		// from block diagram properties
		for(String key: block_.properties().keySet()) {
			Object value=block_.getProperty(key);
			if (value!=null) {
				if (isPropetyValueClassSupported(value.getClass())) {
					PropertyInfo pi = new PropertyInfo(key, value.getClass());
					propertyInfos_.put(key, pi);
				}
			}
		}
		
		// from class reflection
		try{
			Class<?> c = Class.forName(block_.getRuntimeClassName());
			buildPropertyInfosFromClass(c);
		}catch(ClassNotFoundException ex) {
			LOG.info("ClassNotFoundException",ex);
		}
	}

	private void buildPropertyInfosFromClass(Class<?> c) {
		for(java.beans.PropertyDescriptor pd: PropertyUtils.getPropertyDescriptors(c)) {
			if (pd.getWriteMethod()!=null) {
				String key=pd.getName();
				Class<?> vt = covertPrimitiveToClass(pd.getPropertyType());
				//LOG.debug(key+":"+vt.getName());
				if (isPropetyValueClassSupported(vt)) {
					PropertyInfo pi = new PropertyInfo(key, vt);
					propertyInfos_.put(key, pi);
				}
			}
		}
		Class<?> sc = c.getSuperclass();
		if (sc!=null && !sc.equals(Object.class)) {
			buildPropertyInfosFromClass(sc);
		}
	}

	public static Class<?> covertPrimitiveToClass(Class<?> propertyType) {
		if (propertyType==null || !propertyType.isPrimitive())
			return propertyType;
		if (propertyType==Boolean.TYPE) return Boolean.class;
		if (propertyType==Character.TYPE) return Character.class;
		if (propertyType==Byte.TYPE) return Byte.class;
		if (propertyType==Short.TYPE) return Short.class;
		if (propertyType==Integer.TYPE) return Integer.class;
		if (propertyType==Long.TYPE) return Long.class;
		if (propertyType==Float.TYPE) return Float.class;
		if (propertyType==Double.TYPE) return Double.class;
		if (propertyType==Void.TYPE) return Void.class;
		throw new RuntimeException("Unknown class: "+propertyType.getName());
	}

	public BeanBlock getBeanBlock() {
		return block_;
	}
	
	private static final String CAT_TYPE = "_block";
	private static final String CAT_PROPERTIES = "bean properties";

	public final static String P_NAME = "P_NAME";
	//private final static String P_TYPE = "P_TYPE";
	public final static String P_CLASS = "P_CLASS";
	private final static String P_COMMENT = "P_COMMENT";
	public final static String P_ENABLED = "P_ENABLED";
	private final static String P_PROPERTIES_PRIFIX = "P_PROPERTIES_";
	public final static String P_DESCRIPTION = "P_DESCRIPTION";
	
	
	@Override
	public IPropertyDescriptor[] getPropertyDescriptors() {
		ArrayList<IPropertyDescriptor> descriptors=new ArrayList<IPropertyDescriptor>();
		
		descriptors.add(new TextPropertyDescriptor(P_DESCRIPTION,"_ClassDescription"));
		//descriptors.add(new TextPropertyDescriptor(P_TYPE,"_Type"));
		descriptors.add(new TextPropertyDescriptor(P_NAME,"Name"));
		descriptors.add(new TextPropertyDescriptor(P_CLASS,"Class"));
		descriptors.add(new EditableComboBoxPropertyDescriptor(P_ENABLED,"Enabled",new String[]{"false","true"}));
		//descriptors.add(new TextPropertyDescriptor(P_COMMENT,"Comment"));
		descriptors.add(new TextPropertyDescriptor(XPOS_PROP, "X"));
		descriptors.add(new TextPropertyDescriptor(YPOS_PROP, "Y"));

		for(IPropertyDescriptor pd : descriptors) {
			PropertyDescriptor pd2 = (PropertyDescriptor)pd;
			if (pd2.getId().equals(XPOS_PROP) || pd2.getId().equals(YPOS_PROP)) {
				pd2.setValidator(intValidator_);
			}
			pd2.setCategory(CAT_TYPE);
		}
		
		//
		for(PropertyInfo pi: propertyInfos_.values()) {
			TextPropertyDescriptor pd = new TextPropertyDescriptor(P_PROPERTIES_PRIFIX+pi.getName(),pi.getName());
			pd.setCategory(CAT_PROPERTIES);
			descriptors.add(pd);
		}
		
		return descriptors.toArray(new IPropertyDescriptor[descriptors.size()]);
	}
	
	private String getPropertyKey(Object id) {
		if (id instanceof String) {
			String idStr = (String) id;
			if (idStr.startsWith(P_PROPERTIES_PRIFIX) && block_.properties()!=null) {
				String key=idStr.substring(P_PROPERTIES_PRIFIX.length());
				return key;
			}
		}
		return null;
	}

	@Override
	public Object getPropertyValue(Object id) {
		/*if(P_TYPE.equals(id)){
			return "BeanBlock";
		}else*/
		if(P_NAME.equals(id)){
			return block_.getName();
		}else if(P_DESCRIPTION.equals(id)){
			return classDescription_==null?"-":classDescription_;
		}else if(P_CLASS.equals(id)){
			return block_.getRuntimeClassName();
		}else if(P_COMMENT.equals(id)){
			return block_.getComment()!=null?block_.getComment():"";
		}else if(P_ENABLED.equals(id)){
			return Boolean.toString(block_.isEnabled());
		}else if (XPOS_PROP.equals(id)) {
			return Integer.toString(getLocation().x);
		}else if (YPOS_PROP.equals(id)) {
			return Integer.toString(getLocation().y);
		}else{
			String key=getPropertyKey(id);
			if (key!=null) {
				PropertyInfo pi = propertyInfos_.get(key);
				return propertyValueToString(pi.getValue());
			}
		}
		return super.getPropertyValue(id);
	}

	@Override
	public Object getEditableValue() {
		return null;
	}

	@Override
	public boolean isPropertySet(Object id) {
		String key=getPropertyKey(id);
		if (key!=null) {
			PropertyInfo pi = propertyInfos_.get(key);
			return pi.getValue()!=null;
		}
		return false;
	}


	@Override
	public void resetPropertyValue(Object id) {
		String key=getPropertyKey(id);
		if (key!=null) {
			PropertyInfo pi = propertyInfos_.get(key);
			pi.setValue(null);
			//bdg_.updateText();
			//
			firePropertyChange(BLOCK_PROP, null, block_);
		}
	}


	@Override
	public void setPropertyValue(Object id, Object value) {
//		if(P_NAME.equals(id)){
//			setName((String)value); // is it possible?
//		}
		LOG.debug("setPropertyValue: "+id+"="+value);
		
		String valueStr=value!=null?value.toString():"";
		if(P_COMMENT.equals(id)){
			block_.setComment(valueStr.isEmpty()?null:valueStr);
			//bdg_.updateText();
			//
			firePropertyChange(P_COMMENT, null, block_);
		}else if(P_NAME.equals(id)){
			if (!valueStr.isEmpty()) {
				// refactor
				String oldName=block_.getName();
				String newName=valueStr;
				BlockDiagram bd = bdg_.getBlockDiagram();
				for(AbstractBlock blk:bd.getBlocks()) {
					if (blk.getName().equals(newName)) {
						LOG.info("block name duplicated: "+newName);
						return;
					}
				}
				
				// update connections
				block_.setName(newName);
				for(Connection c:bd.getConnections()) {
					if (c.getInputPortReference().getBlockName().equals(oldName)) {
						c.setInputPortReference(
							block_.getPortByName(c.getInputPortReference().getPortName())
						);
					}
					if (c.getOutputPortReference().getBlockName().equals(oldName)) {
						c.setOutputPortReference(
							block_.getPortByName(c.getOutputPortReference().getPortName())
						);
					}
				}
				//
				//bdg_.updateText();
				//bdg_.updateDiagram();
				//
				firePropertyChange(P_NAME, null, block_);
			}
		}else if(P_CLASS.equals(id)){
			try{
				Class.forName(valueStr);
				block_.setRuntimeClassName(valueStr);
				buildPropertyInfos();
			}catch(ClassNotFoundException ex) {
				LOG.info("ClassNotFoundException",ex);
				return;
			}
			//bdg_.updateText();
			//
			firePropertyChange(P_CLASS, null, block_);
		} else if (P_ENABLED.equals(id)) {
			boolean b = parseBoolean((String) value);
			block_.setEnabled(b);
			firePropertyChange(P_ENABLED, null, block_);
		} else if (XPOS_PROP.equals(id)) {
			int x = Integer.parseInt((String) value);
			setLocation(new Point(x, getLocation().y),false);
		} else if (YPOS_PROP.equals(id)) {
			int y = Integer.parseInt((String) value);
			setLocation(new Point(getLocation().x, y),false);
		}else{
			String key=getPropertyKey(id);
			if (key!=null) {
				PropertyInfo pi = propertyInfos_.get(key);
				try{
					Class<?> c = pi.getValueClass();
					Object v = stringToPropertyValue(valueStr, c);
					pi.setValue(v);
				}catch(Exception ex) {
					LOG.info("Exception",ex);
					return;
				}
			}else{
				LOG.warn("Unknown property id:"+id);
				//
				super.setPropertyValue(id, value);
			}
			//bdg_.updateText();
			//
			firePropertyChange(BLOCK_PROP, null, block_);
		}
	}
	
	private boolean isPropetyValueClassSupported(Class<?> c) {
		return c.equals(String.class) || c.equals(Integer.class) || c.equals(Long.class) || c.equals(Float.class) || c.equals(Double.class)
			|| c.equals(Boolean.class) || c.equals(Character.class) || c.equals(Byte.class) || c.equals(Short.class);
	}
	
	private String propertyValueToString(Object value) {
		return value==null?"":value.toString();
	}

	private Object stringToPropertyValue(String valueStr, Class<?> c) throws Exception {
		if (valueStr.isEmpty()) {
			return null;
		} else if (c.equals(String.class)) {
			return valueStr;
		}else if (c.equals(Integer.class)) {
			return Integer.parseInt(valueStr);
		}else if (c.equals(Long.class)) {
			return Long.parseLong(valueStr);
		}else if (c.equals(Float.class)) {
			return Float.parseFloat(valueStr);
		}else if (c.equals(Double.class)) {
			return Double.parseDouble(valueStr);
		}else if (c.equals(Boolean.class)) {
			return parseBoolean(valueStr);
		}else if (c.equals(Character.class)) {
			return new Character((valueStr.length()>0)?valueStr.charAt(0):'\0');
		}else if (c.equals(Byte.class)) {
			return Byte.parseByte(valueStr);
		}else if (c.equals(Short.class)) {
			return Short.parseShort(valueStr);
		}else throw new RuntimeException("Value class not supported: "+c.getName());
	}
	
	// Shape
	
	private boolean parseBoolean(String valueStr) {
		return valueStr.startsWith("t") || valueStr.startsWith("T") || valueStr.equals("1");
	}

	/**
	 * ID for the Height property value (used for by the corresponding property
	 * descriptor).
	 */
	/** Property ID to use when the location of this shape is modified. */
	public static final String LOCATION_PROP = "Shape.Location";
	/** Property ID to use when the list of outgoing connections is modified. */
	public static final String SOURCE_CONNECTIONS_PROP = "Shape.SourceConn";
	/** Property ID to use when the list of incoming connections is modified. */
	public static final String TARGET_CONNECTIONS_PROP = "Shape.TargetConn";

	/**
	 * ID for the X property value (used for by the corresponding property
	 * descriptor).
	 */
	private static final String XPOS_PROP = "Shape.xPos";
	/**
	 * ID for the Y property value (used for by the corresponding property
	 * descriptor).
	 */
	private static final String YPOS_PROP = "Shape.yPos";
	
	public static final String BLOCK_PROP = "Block";


	/** List of outgoing Connections. */
	private List<ConnectionElement> sourceConnections = new ArrayList<ConnectionElement>();
	/** List of incoming Connections. */
	private List<ConnectionElement> targetConnections = new ArrayList<ConnectionElement>();

	private ICellEditorValidator intValidator_ = new ICellEditorValidator() {
		public String isValid(Object value) {
			int intValue = -1;
			try {
				intValue = Integer.parseInt((String) value);
			} catch (NumberFormatException exc) {
				return "Not a number";
			}
			return (intValue >= 0) ? null : "Value must be >=  0";
		}
	};

	/**
	 * Add an incoming or outgoing connection to this shape.
	 * 
	 * @param conn
	 *            a non-null connection instance
	 * @throws IllegalArgumentException
	 *             if the connection is null or has not distinct endpoints
	 */
	void addConnection(ConnectionElement conn) {
		if (conn == null || conn.getSource() == conn.getTarget()) {
			throw new IllegalArgumentException();
		}
		if (conn.getSource() == this) {
			sourceConnections.add(conn);
			firePropertyChange(SOURCE_CONNECTIONS_PROP, null, conn);
		} else if (conn.getTarget() == this) {
			targetConnections.add(conn);
			firePropertyChange(TARGET_CONNECTIONS_PROP, null, conn);
		}
	}

	/*
	private Dimension size=new Dimension(96,32);
	
	private Dimension getSize() {
		return size;
	}
	*/

	/**
	 * Return a List of outgoing Connections.
	 */
	public List<ConnectionElement> getSourceConnections() {
		return new ArrayList<ConnectionElement>(sourceConnections);
	}

	/**
	 * Return a List of incoming Connections.
	 */
	public List<ConnectionElement> getTargetConnections() {
		return new ArrayList<ConnectionElement>(targetConnections);
	}

	/**
	 * Remove an incoming or outgoing connection from this shape.
	 * 
	 * @param conn
	 *            a non-null connection instance
	 * @throws IllegalArgumentException
	 *             if the parameter is null
	 */
	void removeConnection(ConnectionElement conn) {
		if (conn == null) {
			throw new IllegalArgumentException();
		}
		if (conn.getSource() == this) {
			sourceConnections.remove(conn);
			firePropertyChange(SOURCE_CONNECTIONS_PROP, null, conn);
		} else if (conn.getTarget() == this) {
			targetConnections.remove(conn);
			firePropertyChange(TARGET_CONNECTIONS_PROP, null, conn);
		}
	}

	public Point getLocation() {
		return new Point(block_.getViewPosition().getX(), block_.getViewPosition().getY());
	}
	
	public void setLocation(Point newLocation, boolean snapToGrid) {
		block_.setViewPosition(BlockDiagramElement.toModelPosition(newLocation, snapToGrid));
		firePropertyChange(LOCATION_PROP, null, getLocation());
	}

	public Map<String, PortInfo> getInputPortInfos() {
		return inputPortInfos_;
	}

	public Map<String, PortInfo> getOutputPortInfos() {
		return outputPortInfos_;
	}

	public Collection<String> getInputPortNames() {
		return inputPortNames_;
	}

	public Collection<String> getOutputPortNames() {
		return outputPortNames_;
	}
	
	/*
	private void setBounds(Rectangle newBounds) {
		if (newBounds == null) {
			throw new IllegalArgumentException();
		}
		block_.setViewPosition(new jp.hasc.hasctool.core.blockdiagram.model.Point(newBounds.x+newBounds.width/2, newBounds.y+newBounds.height/2));
		firePropertyChange(LOCATION_PROP, null, getLocation());
	}
	
	public Rectangle getBounds() {
		Dimension asize = getSize();
		return new Rectangle(block_.getViewPosition().getX()-asize.width/2, block_.getViewPosition().getY()-asize.height/2,asize.width,asize.height);
	}
	*/

}
