// 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.util.Collection;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jp.hasc.hasctool.core.blockdiagram.model.BeanBlock;
import jp.hasc.hasctool.core.blockdiagram.model.Connection;
import jp.hasc.hasctool.core.blockdiagram.model.PortReference;
import jp.hasc.hasctool.core.runtime.annotation.PortInfo;
import jp.hasc.hasctool.ui.bdeditor2.misc.EditableComboBoxCellEditor;
import jp.hasc.hasctool.ui.bdeditor2.misc.EditableComboBoxPropertyDescriptor;

import org.eclipse.jface.dialogs.InputDialog;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.custom.CCombo;
import org.eclipse.ui.views.properties.IPropertyDescriptor;
import org.eclipse.ui.views.properties.TextPropertyDescriptor;

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

	private static final long serialVersionUID = 1;

	/** True, if the connection is attached to its endpoints. */
	private boolean isConnected;
	/** Connection's source endpoint. */
	private BlockElement source;
	/** Connection's target endpoint. */
	private BlockElement target;
	
	public ConnectionElement() {
		
	}
	
	public void init(BlockDiagramElement bdg, BlockElement source, BlockElement target) {
		init(bdg,source,target,new Connection(
				new PortReference(source.getBeanBlock().getName(), PortReference.OUTPUT_PORT),
				new PortReference(target.getBeanBlock().getName(), PortReference.INPUT_PORT)));
	}
	
	public boolean init(BlockDiagramElement bdg, Connection c) {
		BlockElement source = bdg.getBlockObjMap().get(c.getOutputPortReference().getBlockName());
		BlockElement target = bdg.getBlockObjMap().get(c.getInputPortReference().getBlockName());
		if (source==null || target==null) return false;
		init(bdg,source,target,c);
		return true;
	}
	
	public void init(BlockDiagramElement bdg, BlockElement source, BlockElement target, Connection c) {
		bdg_ = bdg;
		this.source=source;
		this.target=target;
		connection_ = c;
		reconnect();
	}

	/**
	 * Disconnect this connection from the shapes it is attached to.
	 */
	public void disconnect() {
		if (isConnected) {
			source.removeConnection(this);
			target.removeConnection(this);
			bdg_.removeConnection(this);
			isConnected = false;
		}
	}
	
	/**
	 * Returns the source endpoint of this connection.
	 * 
	 * @return a non-null Shape instance
	 */
	public BlockElement getSource() {
		return source;
	}

	/**
	 * Returns the target endpoint of this connection.
	 * 
	 * @return a non-null Shape instance
	 */
	public BlockElement getTarget() {
		return target;
	}

	/**
	 * Reconnect this connection. The connection will reconnect with the shapes
	 * it was previously attached to.
	 */
	public void reconnect() {
		if (!isConnected) {
			source.addConnection(this);
			target.addConnection(this);
			bdg_.addConnection(this);
			isConnected = true;
		}
	}

	/**
	 * Reconnect to a different source and/or target shape. The connection will
	 * disconnect from its current attachments and reconnect to the new source
	 * and target.
	 * 
	 * @param newSource
	 *            a new source endpoint for this connection (non null)
	 * @param newTarget
	 *            a new target endpoint for this connection (non null)
	 * @throws IllegalArgumentException
	 *             if any of the paramers are null or newSource == newTarget
	 */
	public void reconnect(BlockElement newSource, BlockElement newTarget) {
		disconnect();
		source=newSource;
		connection_.setOutputPortReference(new PortReference(newSource.getBeanBlock().getName(), 
				connection_.getOutputPortReference().getPortName()));
		target=newTarget;
		connection_.setInputPortReference(new PortReference(newTarget.getBeanBlock().getName(), 
				connection_.getInputPortReference().getPortName()));
		reconnect();
	}
	
	@Override
	public String toString() {
		return "Connection - " + getConnection().getOutputPortReference().getBlockName() + " to " + getConnection().getInputPortReference().getBlockName(); //$NON-NLS-1$ //$NON-NLS-2$
	}
	
	
	//////
	
	private BlockDiagramElement bdg_;
	private Connection connection_;
	
	@Override
	public Object getEditableValue() {
		return null;
	}

	private final static String P_COMMENT = "P_COMMENT"; //$NON-NLS-1$
	private final static String P_OUTPUT_PORT = "P_OUTPUT"; //$NON-NLS-1$
	private final static String P_INPUT_PORT = "P_INPUT"; //$NON-NLS-1$
	private final static String P_OUTPUT_PORT_INFO = "P_OUTPUT_INFO"; //$NON-NLS-1$
	private final static String P_INPUT_PORT_INFO = "P_INPUT_INFO"; //$NON-NLS-1$
	
	private static EditableComboBoxCellEditor.IHookComboBoxSelected hookComboBoxSelectedForArrayPortName__ =
		new EditableComboBoxCellEditor.IHookComboBoxSelected() {
			public void hookComboBoxSelected(EditableComboBoxCellEditor editor, CCombo comboBox, String previousText) {
				final String text=comboBox.getText();
				
				// hook an array port name
				if (text.endsWith("[]")) { //$NON-NLS-1$
					String initialText="0"; //$NON-NLS-1$
					Matcher m = ConnectionElement.PAT_PORT_ARRAY.matcher(previousText);
					if (m.matches()) {
						initialText=m.group(2);
					}
					//
					editor.addToLockCountOfApplyEditorValueAndDeactivate(1);
					try{
						InputDialog dialog = new InputDialog(editor.getControl().getShell(), 
								"Index", String.format(Messages.ConnectionElement_InputIndexNumberOf,text), initialText, null); //$NON-NLS-1$
						if (dialog.open() == Window.OK) {
							// insert the index to the port name
							editor.setText(text.substring(0,text.length()-1) + 
								dialog.getValue() + text.substring(text.length()-1,text.length()));
						}else{
							// cancel
							editor.setText(previousText);
						}
					}finally{
						editor.addToLockCountOfApplyEditorValueAndDeactivate(-1);
						editor.applyEditorValueAndDeactivate();
					}
				}
			}
	};

	@Override
	public IPropertyDescriptor[] getPropertyDescriptors() {
		//
		Collection<String> outs = source.getOutputPortNames();
		EditableComboBoxPropertyDescriptor outdesc = new EditableComboBoxPropertyDescriptor(P_OUTPUT_PORT,"OutputPort",outs.toArray(new String[outs.size()])); //$NON-NLS-1$
		outdesc.setHookComboBoxSelected(hookComboBoxSelectedForArrayPortName__);
		//
		Collection<String> ins = target.getInputPortNames();
		EditableComboBoxPropertyDescriptor indesc = new EditableComboBoxPropertyDescriptor(P_INPUT_PORT,"InputPort",ins.toArray(new String[ins.size()])); //$NON-NLS-1$
		indesc.setHookComboBoxSelected(hookComboBoxSelectedForArrayPortName__);
		//
		IPropertyDescriptor[] descriptor = new IPropertyDescriptor[]{
				new TextPropertyDescriptor(P_OUTPUT_PORT_INFO,"_OutputPortClasses"), //$NON-NLS-1$
				new TextPropertyDescriptor(P_INPUT_PORT_INFO,"_InputPortClasses"), //$NON-NLS-1$
				new TextPropertyDescriptor(P_COMMENT,"Comment"), //$NON-NLS-1$
				outdesc,
				indesc
		};
		return descriptor;
	}
	
	private PortInfo outputPortInfo_ = null, inputPortInfo_ = null;
	private String outputPortInfoStr_ = null, inputPortInfoStr_ = null;
	
	private void updatePortInfos() {
		boolean updated=false;
		{
			// output
			String portName = connection_.getOutputPortReference().getPortName();
			PortInfo pi=getPortInfo(source.getOutputPortInfos(), portName);
			if (outputPortInfo_!=pi) {
				outputPortInfo_=pi;
				updated=true;
			}
		}
		{
			// input
			String portName = connection_.getInputPortReference().getPortName();
			PortInfo pi=getPortInfo(target.getInputPortInfos(),portName);
			if (inputPortInfo_!=pi) {
				inputPortInfo_=pi;
				updated=true;
			}
		}
		if (updated) {
			//LOG.debug("updatePortInfoStrs");
			// type matching
			boolean[] inputMatches = new boolean[inputPortInfo_!=null ? inputPortInfo_.classes().length : 0];
			boolean[] outputMatches = new boolean[outputPortInfo_!=null ? outputPortInfo_.classes().length : 0];
			if (inputPortInfo_!=null && outputPortInfo_!=null) {
				Class<? extends Object>[] ics = inputPortInfo_.classes();
				Class<? extends Object>[] ocs = outputPortInfo_.classes();
				for(int i=0,inum=ics.length;i<inum;++i) {
					Class<? extends Object> ic = ics[i];
					for(int j=0,jnum=ocs.length;j<jnum;++j) {
						Class<? extends Object> oc = ocs[j];
						if (ic.isAssignableFrom(oc)) {
							// matches
							inputMatches[i]=true;
							outputMatches[j]=true;
						}
					}
					
				}
			}
			//
			inputPortInfoStr_ = getPortInfoStr(inputPortInfo_,inputMatches);
			outputPortInfoStr_ = getPortInfoStr(outputPortInfo_,outputMatches);
		}
	}
	
	/** "inputPort[0]" など、ポート配列の要素にマッチ */
	public static Pattern PAT_PORT_ARRAY = Pattern.compile("(.*)\\[(.*)\\]"); //$NON-NLS-1$
	
	private PortInfo getPortInfo(Map<String,PortInfo> portInfos, String portName) {
		if (portName!=null) {
			Matcher m = PAT_PORT_ARRAY.matcher(portName);
			if (m.matches()) {
				portName=m.group(1)+"[]"; //$NON-NLS-1$
			}
			return portInfos.get(portName);
		}else{
			return null;
		}
	}
	
	private String getPortInfoStr(PortInfo portInfo, boolean[] matches) {
		if (portInfo!=null) {
			StringBuilder sb = new StringBuilder();
			Class<? extends Object>[] classes = portInfo.classes();
			for(int i=0,num=matches.length;i<num;++i) {
				Class<? extends Object> c = classes[i];
				if (i!=0) sb.append(", "); //$NON-NLS-1$
				if (matches[i]) sb.append("["); //$NON-NLS-1$
				sb.append(c.getSimpleName());
				if (matches[i]) sb.append("]"); //$NON-NLS-1$
			}
			return sb.toString();
		}else{
			return null;
		}
	}
	

	@Override
	public Object getPropertyValue(Object id) {
		if(P_COMMENT.equals(id)){
			return connection_.getComment()!=null?connection_.getComment():""; //$NON-NLS-1$
		}else if(P_OUTPUT_PORT.equals(id)){
			return connection_.getOutputPortReference().getPortName();
		}else if(P_INPUT_PORT.equals(id)){
			return connection_.getInputPortReference().getPortName();
		}else if(P_OUTPUT_PORT_INFO.equals(id)){
			updatePortInfos();
			if (outputPortInfoStr_!=null) return outputPortInfoStr_; else return "-"; //$NON-NLS-1$
		}else if(P_INPUT_PORT_INFO.equals(id)){
			updatePortInfos();
			if (inputPortInfoStr_!=null) return inputPortInfoStr_; else return "-"; //$NON-NLS-1$
		}
		return null;
	}

	@Override
	public boolean isPropertySet(Object id) {
		return false;
	}

	@Override
	public void resetPropertyValue(Object id) {
	}

	@Override
	public void setPropertyValue(Object id, Object value) {
		LOG.debug("setPropertyValue: "+id+"="+value); //$NON-NLS-1$ //$NON-NLS-2$
		
		String valueStr=value!=null?value.toString():""; //$NON-NLS-1$
		if(P_COMMENT.equals(id)){
			connection_.setComment(valueStr.isEmpty()?null:valueStr);
			//bdg_.updateText();
		}else if(P_OUTPUT_PORT.equals(id)){
			if (!valueStr.isEmpty()) {
				BeanBlock blk = bdg_.getBlockObjMap().get(connection_.getOutputPortReference().getBlockName()).getBeanBlock();
				connection_.setOutputPortReference(blk.getPortByName(valueStr));
				//bdg_.updateText();
			}
		}else if(P_INPUT_PORT.equals(id)){
			if (!valueStr.isEmpty()) {
				BeanBlock blk = bdg_.getBlockObjMap().get(connection_.getInputPortReference().getBlockName()).getBeanBlock();
				connection_.setInputPortReference(blk.getPortByName(valueStr));
				//bdg_.updateText();
			}
		}
	}

	public Connection getConnection() {
		return connection_;
	}
	
}