package adecWatt.model;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.Vector;
import javax.swing.ImageIcon;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import misc.DimensionDouble;
import misc.ScaledImage;
import misc.Story;
import misc.Util;

import adecWatt.model.unit.Building;
import adecWatt.model.xml.*;

public abstract class Editable<P extends XmlPermanent<T, A>,
			       T extends Enum<?>,
			       A extends Enum<?>,
			       E extends Embedded<?,?,?,?,?>>
    extends Permanent<P, T, A> {
    // ========================================
    static public final String
	StoryAddAcc		= "AddAcc",
	StoryAddItem		= "AddItem",
	StoryAddProp		= "AddProp",
	StoryAddSpinProp	= "AddSpinProp",
	StoryChangeBuilding	= "ChangeBuilding",
	StoryChangeEnum		= "ChangeEnum",
	StoryChangeLabels	= "ChangeLabels",
	StoryChangeModifiers	= "ChangeModifiers",
	StoryChangeName		= "ChangeName",
	StoryChangeParent	= "ChangeParent",
	StoryChangeSpin		= "ChangeSpin",
	StoryChangeType		= "ChangeType",
	StoryChangeValue	= "ChangeValue",
	StoryClearProp		= "ClearProp",
	StoryCloneItem		= "CloneItem",
	StoryConnectAcc		= "ConnectAcc",
	StoryEdit		= "Edit",
	StoryHideItem		= "HideItem",
	StoryLinkAcc		= "LinkAcc",
	StoryLinkEmb		= "LinkEmb",
	StoryLinkItem		= "LinkItem",
	StoryMoveItem		= "MoveItem",
	StoryOrderedProp	= "OrderedProp",
	StoryPasteItem		= "PasteItem",
	StoryRemoveAcc		= "RemoveAcc",
	StoryRemoveEnum		= "RemoveEnum",
	StoryRemoveItem		= "RemoveItem",
	StoryRemoveLabels	= "RemoveLabels",
	StoryRemoveProp		= "RemoveProp",
	StoryRotItem		= "RotItem",
	StoryRotResizeItem	= "RotResizeItem",
	StorySelectItem		= "SelectItem",
	StoryTransform		= "Transform";

    // ========================================
    protected Hashtable<String, Prop> ownProps = new Hashtable<String, Prop> ();
    protected String embeddedPrefix = "c"; // XXX dans Unit et pas dans comp
    private int embIdCount;
    protected Hashtable<String, E> embedded;

    // ========================================
    public abstract AdecWatt			getAdecWatt ();
    public abstract String			getId ();
    public abstract Editable<P, T, A, E>	getParent ();
    public abstract Unit<?>			getDirectUnit ();
    public abstract Unit<?>			getParentUnit ();
    public abstract Unit<?>			getStoryUnit ();
    public abstract Editable<?, ?, ?, ?>	getModel ();
    public Editable<?, ?, ?, ?>			getContainer ()	{ return null; }

    // public boolean	hasChild (String childName)	{ return false; }
    public E		getLocalEmbedded (String id)	{ return embedded == null ? null : embedded.get (id); }
    public Hashtable<String, E>	getLocalEmbedded ()	{ return embedded; }

    public abstract void	setParentUnit (Unit<?> parent);

    public boolean match (String text) {
	return
	    Util.removeAccent (getName ()).toLowerCase ().indexOf (text) >= 0 ||
	    Util.removeAccent (getLocalName ()).toLowerCase ().indexOf (text) >= 0 ||
	    getId ().indexOf (text) >= 0;
    }

    public boolean parentMatch (String text) {
	for (Editable<?, ?, ?, ?> editable = this; editable != null; editable = editable.getParentUnit ())
	    if (editable.match (text))
		return true;
	return false;
    }

    // ========================================
    private ArrayList<String> reservedEmbName;
    private boolean isReservedEmbName (String name) {
	return reservedEmbName != null && reservedEmbName.contains (name);
    }
    private void reservedEmbName (String name) {
	if (reservedEmbName == null)
	    reservedEmbName = new ArrayList<String> ();
	reservedEmbName.add (name);
    }
    private void freeEmbName (String name) {
	if (reservedEmbName == null)
	    return;
	reservedEmbName.remove (name);
	if (reservedEmbName.size () == 0)
	    reservedEmbName = null;
    }
    public String getUniqName () {
	String thisId = getId ();
	for (; ; embIdCount++) {
	    String embeddedName = embeddedPrefix+embIdCount;
	    if (embedded != null && embedded.containsKey (thisId+":"+embeddedName))
		continue;
	    if (isReservedEmbName (embeddedName))
		continue;
	    reservedEmbName (embeddedName);
	    return embeddedName;
	}
    }
    @SuppressWarnings ("unchecked")
    public E getCloseEmbedded (E emb) {
	if (embedded == null)
	    embedded = new Hashtable<String, E> ();
	E result = embedded.get (emb.getId ());
	if (result == null)
	    // attention emb doit exister !
	    result = (E) emb.getLink (this);
	return result;
    }
    @SuppressWarnings ("unchecked")
    public E findEmbedded (String id) {
    	for (Editable<P, T, A, E> editable = this;
	     editable != null;
	     editable = editable.getParent ()) {
    	    E emb = editable.getLocalEmbedded (id);
    	    if (emb != null)
    		return emb;
    	}
 	Editable<?, ?, ?, ?> model = getModel ();
	if (model != null)
	    for (Editable<?, ?, ?, E> editable = (Editable<?, ?, ?, E>) model;
		 editable != null;
		 editable = editable.getParent ()) {
		E emb = editable.getLocalEmbedded (id);
		if (emb != null)
		    return emb;
	    }
   	return null;
    }
    public E findEmbeddedRootOf (Unit<?> model) {
	for (String embId : getEmbeddedIds ()) {
	    E emb = findEmbedded (embId);
	    if (emb == null || !model.isDescendingFrom (emb.getModel ().getId ()))
		continue;
	    return emb;
	}
	return null;
    }

    public E findEmbeddedBaseOn (String parentId) {
	for (String embId : getEmbeddedIds ()) {
	    E emb = findEmbedded (embId);
	    if (emb == null || !emb.getDirectUnit ().isDescendingFrom (parentId))
		continue;
	    return emb;
	}
	return null;
    }

    public TreeSet<String> getEmbeddedIds () {
	TreeSet<String> result = new TreeSet<String> ();
	for (Editable<P, T, A, E> editable = this; editable != null; editable = editable.getParent ()) {
	    if (editable.embedded == null)
		continue;
	    result.addAll (editable.embedded.keySet ());
	}
	Editable<?, ?, ?, ?> model = getModel ();
	if (model != null)
	    for (Editable<?, ?, ?, ?> editable = model; editable != null; editable = editable.getParent ()) {
		if (editable.embedded == null)
		    continue;
		result.addAll (editable.embedded.keySet ());
	    }
	return result;
    }

    // ========================================
    public void addEmbedded (E emb) {
	if (embedded == null)
	    embedded = new Hashtable<String, E> ();
	String embId = emb.getId ();
	if (embedded.get (embId) != null)
	    System.err.println ("Already embedded name recorded ("+embId+" = "+emb.getName ()+" in "+name+").");
	embedded.put (embId, emb);
	freeEmbName (embId);
    }
    public void removeEmbedded (E emb) {
	if (embedded == null)
	    return;
	embedded.remove (emb.getId ());
    }
    public Collection<E> getInheritedEmbeddedValues () {
	Hashtable<String, E> inherited = getInheritedEmbedded ();
	if (inherited == null)
	    return new Vector<E> ();
	return inherited.values ();
    }

    @SuppressWarnings ("unchecked")
    public Hashtable<String, E> getInheritedEmbedded () {
	Editable<P, T, A, E> parent = getParent ();
	if (parent == null)
	    return embedded;
	Hashtable<String, E> result = parent.getInheritedEmbedded ();
	if (result == null)
	    return embedded;
	if (embedded == null)
	    return result;
	result = (Hashtable<String, E>) result.clone ();
	// for (String key : embedded.keySet ())
	//     result.put (key, embedded.get (key));
	result.putAll (embedded);
	return result;
    }
    @SuppressWarnings ("unchecked")
    public Hashtable<String, E> getAllEmbedded () {
	// +
	// si workspace le dernier building dans les getparentunit
	// si comp le dernier modèle dans le 
	Editable<?, ?, ?, ?> pattern = getModel ();
	Hashtable<String, E> result = getInheritedEmbedded ();
	if (pattern == null)
	    return result;
	Hashtable<String, E> resultP = (Hashtable<String, E>) pattern.getInheritedEmbedded ();
	if (result == null)
	    return resultP;
	if (resultP == null)
	    return result;
	resultP = (Hashtable<String, E>) resultP.clone ();
	resultP.putAll (result);
	return resultP;
    }

    public abstract void storyChange (Story.Commands commands, ArrayList<Prop[]> changeProps);
    public abstract void storyTransform (Story.Commands commands, Collection<String> modifiers, Unit<?> permanent, String newName, ArrayList<Prop[]> transProps);
    public void storyTransform (Collection<String> modifiers, Unit<?> permanent, String newName, ArrayList<Prop[]> transProps) {
	Story story = getStoryUnit ().story;
	Story.Commands commands = story.new Commands (StoryTransform) {
		public void display () { updateView (); }
	    };
	storyTransform (commands, modifiers, permanent, newName, transProps);
	story.add (commands);
    }

    // ========================================
    protected Editable () {
    }
    protected Editable (P xmlPermanent) {
	super (xmlPermanent);
	collectOwnProps ();
    }
    protected void initXml (P xmlPermanent) {
	super.initXmlAvantOwnProp (xmlPermanent);
	collectOwnProps ();
    }
    public void collectEmbedded () {
	if (xmlPermanent == null)
	    return;
	for (XmlAcc xmlAcc : xmlPermanent.getAccs ()) {
	    Acc acc = new Acc (this, xmlAcc);
	    acc.fixName (false);
	}
    }

    // ========================================
    public void importFrom (Editable<P, T, A, E> from) {
	super.importFrom (from);
	for (String propName : from.ownProps.keySet ())
	    ownProps.putIfAbsent (propName, from.ownProps.get (propName).clone ());
	if (from.embedded == null)
	    return;
	if (embedded == null)
	    embedded = new Hashtable<String, E> ();
	// XXX ??? n'y a-t-il pas partage des emb ?
	for (String embId : from.embedded.keySet ())
	    embedded.putIfAbsent (embId, from.embedded.get (embId));
    }

    public void newNamePlan (HashSet<Editable<?, ?, ?, ?>> visited, TreeMap<String, String> iconMap, TreeMap<String, String> imageMap) {
	if (visited.contains (this))
	    return;
	visited.add (this);
	if (embedded != null)
	    for (E emb : embedded.values ())
		emb.newNamePlan (visited, iconMap, imageMap);
	Story story = getStoryUnit ().story;
	Story.Commands commands = story.new Commands (StoryEdit) {
		public void display () { }
	    };
	for (String propName : ownProps.keySet ()) {
	    final Prop prop = getPropVal (propName);
	    if (prop == null)
		continue;
	    TreeMap<String, String> map = null;
	    switch (prop.getTypeToken ()) {
	    case Icon:
		map = iconMap;
		break;
	    case Image:
		map = imageMap;
		break;
	    default:
		continue;
	    }
	    final String newValue = map.get (prop.sValue);
	    if (newValue == null)
		continue;
	    commands.add (story.new Command (StoryChangeValue+" "+prop.name) {
		    String oldValue = prop.sValue;
		    public void exec () { prop.setValue (newValue); }
		    public void undo () { prop.setValue (oldValue); }
		});
	}
	story.add (commands);
    }
    public boolean renameUnits (TreeMap<String, String> translateMap) {
	Editable<P, T, A, E> parent = getParent ();
	boolean result = parent != null && translateMap.containsValue (parent.getId ());
	if (embedded != null)
	    for (E emb : embedded.values ())
		result |= emb.renameUnits (translateMap);
	for (String propName : ownProps.keySet ()) {
	    Prop prop = getPropVal (propName);
	    if (prop == null)
		continue;
	    switch (prop.getTypeToken ()) {
	    case Text:
	    case Building:
		String newVal = translateMap.get (prop.sValue);
		if (newVal != null) {
		    prop.sValue = newVal;
		    result = true;
		    continue;
		}
		int idx = prop.sValue.indexOf (":");
		if (idx < 0)
		    continue;
		newVal = translateMap.get (prop.sValue.substring (0, idx));
		if (newVal == null)
		    continue;
		prop.sValue = newVal+prop.sValue.substring (idx);
		result = true;
	    }
	}
	return result;
    }
    public boolean renameImages (TreeMap<String, String> translateMap) {
	boolean result = false;
	if (embedded != null)
	    for (E emb : embedded.values ())
		result |= emb.renameImages (translateMap);
	for (String propName : ownProps.keySet ()) {
	    Prop prop = getPropVal (propName);
	    if (prop == null)
		continue;
	    switch (prop.getTypeToken ()) {
	    case Icon:
	    case Image:
		String newVal = translateMap.get (Util.getBase (prop.sValue));
		if (newVal == null)
		    continue;
		prop.sValue = newVal;
		result = true;
		break;
	    }
	}
	return result;
    }
    public void getLocalLink (HashSet<Editable<?, ?, ?, ?>> visited,
			      HashSet<Unit<?>> unitLinks, TreeSet<String> iconsLinks, TreeSet<String> imagesLinks) {
	if (visited.contains (this))
	    return;
	visited.add (this);
	if (embedded != null)
	    for (E emb : embedded.values ())
		emb.getLocalLink (visited, unitLinks, iconsLinks, imagesLinks);
	for (String propName : ownProps.keySet ()) {
	    Prop prop = getPropVal (propName);
	    if (prop == null)
		continue;
	    switch (prop.getTypeToken ()) {
	    case Icon:
		if (!getAdecWatt ().getIconDB ().isRemote (prop.sValue))
		    iconsLinks.add (prop.sValue);
		break;
	    case Image:
		if (!getAdecWatt ().getImageDB ().isRemote (prop.sValue))
		    imagesLinks.add (prop.sValue);
		break;
	    case Building:
		Building building = getBuilding ();
		if (building == null)
		    continue;
		building.getLocalLink (visited, unitLinks, iconsLinks, imagesLinks);
		break;
	    }
	}
    }

    // ========================================
    public void removeOwnProp (String propName) {
	ownProps.remove (propName);
    }

    public void collectOwnProps () {
	for (XmlProp xmlProp : xmlPermanent.getProps ()) {
	    Prop prop = new Prop (xmlProp);
	    ownProps.put (prop.name, prop);
	}
    }
    public Prop getOwnProp (String propName) { 
	return ownProps.get (propName);
    }
    public ArrayList<Prop> getOrderedOwnProps () {
	return Prop.getOrderedProps (ownProps.values ());
    }
    public ArrayList<String> getOrderedPropsName () {
	Unit<?> parent = getParentUnit ();
	ArrayList<String> result = parent == null ? new ArrayList<String> () : parent.getOrderedPropsName ();
	for (Prop prop : getOrderedOwnProps ()) {
	    if (result.contains (prop.name))
		continue;
	    result.add (prop.name);
	}
	return result;
    }

    public Element getXml (Node parent, Document document) {
	Element child = super.getXml (parent, document);
	ArrayList<Prop> tmp = new ArrayList<Prop> (ownProps.values ());
	tmp.sort (Prop.propComparator);
	for (Prop prop : tmp)
	    prop.getXml (child, document);
	if (embedded != null)
	    for (String embId : embedded.keySet ())
		embedded.get (embId).getXml (child, document);
	return child;
    }

    // ========================================
    public ScaledImage getIconFromFileName (String fileName, boolean horizontalSpin) {
	return getAdecWatt ().getIconDB ().getImage (fileName, horizontalSpin);
    }
    public ScaledImage getImageFromFileName (String fileName, boolean horizontalSpin) {
	return getAdecWatt ().getImageDB ().getImage (fileName, horizontalSpin);
    }
    public ImageIcon getIcon (int max) {
	ScaledImage scaledIcon = getIcon ();
	return scaledIcon == null ? null : scaledIcon.getSide (max);
    }

    // ========================================
    public interface ComputeProp<R> {
	public R applyOnProp (Prop prop);
    }
    public<R> R parentWalk (String propName, ComputeProp<R> computeProp, boolean skipFirst) {
	for (Editable<P, T, A, E> editable = skipFirst ? getParent () : this;
	     editable != null;
	     editable = editable.getParent ())
	    try {
		R result = computeProp.applyOnProp (editable.ownProps.get (propName));
		if (result != null)
		    return result;
	    } catch (Exception e) {
	    }
	return null;
    }
    public<R> R parentAndModelWalk (String propName, ComputeProp<R> computeProp, boolean skipFirst) {
	R result = parentWalk (propName, computeProp, skipFirst);
	if (result != null)
	    return result;
	try {
	    result = getContainer ().getModel ().findEmbedded (getId ()).parentWalk (propName, computeProp, false);
	    if (result != null)
		return result;
	} catch (Exception e) {
	}
	try {
	    result = getModel ().parentWalk (propName, computeProp, false);
	    if (result != null)
		return result;
	} catch (Exception e) {
	}
	return null;
    }

    // ========================================
    public Collection<String> getPropModifiers (String propName) {
	return getPropModifiers (propName, false);
    }
    public Collection<String> getParentPropModifiers (String propName) {
	return getPropModifiers (propName, true);
    }
    public Collection<String> getPropModifiers (String propName, boolean skipFirst) {
	return parentAndModelWalk (propName, new ComputeProp<Collection<String>> () {
		public Collection<String> applyOnProp (Prop prop) {
		    return prop.modifiers;
		}
	    }, skipFirst);
    }
    public String getPropModifiers (final String modifier, String propName) {
	return parentAndModelWalk (propName, new ComputeProp<String> () {
		public String applyOnProp (Prop prop) {
		    if (prop.modifiers.contains (modifier))
			return modifier;
		    return null;
		}
	    }, false);
    }
    public Collection<String> getPropEnumChoice (String propName) {
	return parentAndModelWalk (propName, new ComputeProp<Collection<String>> () {
		public Collection<String> applyOnProp (Prop prop) {
		    if (prop != null && prop.enumChoice != null)
			return prop.enumChoice;
		    return null;
		}
	    }, false);
    }
    public List<String> getPropLabel (String propName) {
	return parentAndModelWalk (propName, new ComputeProp<List<String>> () {
		public List<String> applyOnProp (Prop prop) {
		    return prop.multiLabel;
		}
	    }, false);
    }
    public Prop getPropVal (String propName) {
	return getPropVal (propName, false);
    }
    public Prop getParentPropVal (String propName) {
	return getPropVal (propName, true);
    }
    public Prop getPropVal (String propName, boolean skipFirst) {
	return parentAndModelWalk (propName, new ComputeProp<Prop> () {
		public Prop applyOnProp (Prop prop) {
		    if (prop != null && prop.sValue != null) // XXX et "".equals...
			return prop;
		    return null;
		}
	    }, skipFirst);
    }
    public Prop getProp (String propName) {
	return getProp (propName, false);
    }
    public Prop getParentProp (String propName) {
	return getProp (propName, true);
    }
    public Prop getProp (String propName, boolean skipFirst) {
	return parentAndModelWalk (propName, new ComputeProp<Prop> () {
		public Prop applyOnProp (Prop prop) {
		    return prop;
		}
	    }, skipFirst);
    }
    public Double[] getPartialProp (String propName, Double[] result) {
	return getPartialProp (propName, result, false);
    }
    public Double[] getParentPartialProp (String propName, Double[] result) {
	return getPartialProp (propName, result, true);
    }
    public Double[] getPartialProp (String propName, final Double[] result, boolean skipFirst) {
	return parentAndModelWalk (propName, new ComputeProp<Double[]> () {
		public Double[] applyOnProp (Prop prop) {
		    // XXX test nbVal >= result.length
		    if (prop != null && prop.values != null) {
			boolean complete = true;
			for (int i = 0; i < result.length; i++)
			    if (result [i] == null) {
				if (prop.values [i] != null) {
				    result [i] = prop.values [i];
				    continue;
				}
				complete = false;
			    }
			if (complete)
			    return result;
		    }
		    return null;
		}
	    }, skipFirst);
    }
    // ========================================
    public interface ComputeSpin {
	public boolean getSpin (Prop prop);
    }
    public boolean parentSpinWalk (String propName, ComputeSpin computeSpin, boolean skipFirst) {
	boolean spin = false;
	for (Editable<P, T, A, E> editable = skipFirst ? getParent () : this;
	     editable != null;
	     editable = editable.getParent ())
	    try {
		spin ^= computeSpin.getSpin (editable.ownProps.get (propName));
	    } catch (Exception e) {
	    }
	return spin;
    }
    public boolean parentAndModelSpinWalk (String propName, ComputeSpin computeSpin, boolean skipFirst) {
	boolean spin = parentSpinWalk (propName, computeSpin, skipFirst);
	try {
	    spin ^= getContainer ().getModel ().findEmbedded (getId ()).parentSpinWalk (propName, computeSpin, false);
	} catch (Exception e) {
	}
	try {
	    spin ^= getModel ().parentSpinWalk (propName, computeSpin, false);
	} catch (Exception e) {
	}
	return spin;
    }
    public boolean getPropHorizontalSpin (String propName) {
	return getPropHorizontalSpin (propName, false);
    }
    public boolean getParentPropHorizontalSpin (String propName) {
	return getPropHorizontalSpin (propName, true);
    }
    public boolean getPropHorizontalSpin (String propName, boolean skipFirst) {
	return parentSpinWalk (propName, new ComputeSpin () {
		public boolean getSpin (Prop prop) {
		    return prop.horizontalSpin;
		}
	    }, skipFirst);
    }

    // ========================================
    public boolean getPropHidden (String propName) {
	return getPropModifiers (Permanent.Hidden, propName) != null;
    }
    public boolean getPropLock (String propName) {
	return getPropModifiers (Permanent.Lock, propName) != null;
    }
    public boolean getPropLocalized (String propName) {
	return getPropModifiers (Permanent.Localized, propName) != null;
    }
    public boolean getPropNoMouse (String propName) {
	return getPropModifiers (Permanent.NoMouse, propName) != null;
    }
    public ScaledImage getIcon () {
	return getIconImage (Prop.PropIcon);
    }
    public ScaledImage getLowIcon () {
	return getIconImage (Prop.PropLowIcon);
    }
    public ScaledImage getBlueprint () {
	return getIconImage (Prop.PropBlueprint);
    }
    public Double getBlueprintVisibility () {
	try {
	    return getPropVal (Prop.PropBlueprintVisibility).values [0];
	} catch (Exception e) {
	    return null;
	}
    }
    public DimensionDouble getBlueprintSize () {
	return getSize (Prop.PropBlueprintSize);
    }
    public Point2D.Double getBlueprintPos () {
	return getPos (Prop.PropBlueprintPos);
    }
    public ScaledImage getIconImage (String propName) {
	return getIconImage (getPropVal (propName), getPropHorizontalSpin (propName));
    }
    public ScaledImage getIconImage (Prop iconImageProp, boolean horizontalSpin) {
	if (iconImageProp == null)
	    return null;
	switch (iconImageProp.getTypeToken ()) {
	case Icon:
	    return getAdecWatt ().getIconDB ().getImage (iconImageProp.sValue, horizontalSpin);
	case Image:
	    return getAdecWatt ().getImageDB ().getImage (iconImageProp.sValue, horizontalSpin);
	default:
	    throw new IllegalArgumentException ("Bad type ("+iconImageProp.getTypeToken ()+") for icon or image!");
	}
    }
    private Double dummy;
    protected Double []
	seek12 = new Double[] {null, null},
	seek3 = new Double[] {dummy, dummy, null};
    public DimensionDouble getRealSize () {
	return getSize ();
    }

    // ========================================
    public DimensionDouble getSize () {
	return getSize (Prop.PropSize);
    }
    public Double[] getTileSize () {
	Double [] seek = seek12.clone ();
	getPartialProp (Prop.PropTileSize, seek);
	if (seek[0] == null && seek[1] == null)
	    return null;
	return seek;
    }
    static public Dimension getTile (Double [] tileSize, DimensionDouble realSize) {
	return tileSize == null ? ScaledImage.ONE_TILE :
	    new Dimension (tileSize[0] == null ? 1 : (int) Math.max (1, Math.round (realSize.width/tileSize[0])),
			   tileSize[1] == null ? 1 : (int) Math.max (1, Math.round (realSize.height/tileSize[1])));
    }
    public DimensionDouble getSize (String propName) {
	Double [] seek = seek12.clone ();
	getPartialProp (propName, seek);
	if (seek[0] == null || seek[1] == null)
	    return null;
	return new DimensionDouble (seek [0], seek [1]);
    }
    public Point2D.Double getPos () {
	return getPos (Prop.PropPos);
    }
    public Point2D.Double getPos (String propName) {
	Double [] seek = seek12.clone ();
	getPartialProp (propName, seek);
	if (seek[0] == null || seek[1] == null)
	    return null;
	return new Point2D.Double (seek [0], seek [1]);
    }
    public double getThetaDegree () {
	try {
	    Double [] seek = seek3.clone ();
	    getPartialProp (Prop.PropRot, seek);
	    return seek [2];
	} catch (Exception e) {
	    return 0;
	}
    }
    public double getLevel () {
	try {
	    Double [] seek = seek3.clone ();
	    getPartialProp (Prop.PropPos, seek);
	    return seek [2];
	} catch (Exception e) {
	    return 0;
	}
    }
    public double getLength () {
	try {
	    return getPropVal (Prop.PropLength).values[0];
	} catch (Exception e) {
	    return 0;
	}
    }
    public double getWidth () {
	try {
	    return getPropVal (Prop.PropWidth).values[0];
	} catch (Exception e) {
	    return 0;
	}
    }
    public double getAngle () {
	try {
	    return getPropVal (Prop.PropAngle).values[0];
	} catch (Exception e) {
	    return 0;
	}
    }
    public String getStyle () {
	try {
	    return getPropVal (Prop.PropStyle).sValue;
	} catch (Exception e) {
	    return null;
	}
    }
    public String getBegin () {
	try {
	    return getPropVal (Prop.PropBegin).sValue;
	} catch (Exception e) {
	    return null;
	}
    }
    public String getEnd () {
	try {
	    return getPropVal (Prop.PropEnd).sValue;
	} catch (Exception e) {
	    return null;
	}
    }
    public boolean isLow () {
	return getLevel () <= AdecWatt.lowLevel;
    }
    public Building getBuilding () {
	try {
	    return (Building) getAdecWatt ().getPermanentDB ().getUnitById (getProp (Prop.PropBuilding).sValue);
	} catch (Exception e) {
	    return null;
	}
    }
    public Color getColor () {
    	try {
    	    return LPColor.getColor (getPropVal (Prop.PropColor).sValue);
    	} catch (Exception e) {
    	    return Color.black;
    	}
    }
    public String getConnectedTo () {
	try {
    	    return getPropVal (Prop.PropConnectedTo).sValue;
    	} catch (Exception e) {
    	    return null;
    	}
    }
    public String getConnectedOn () {
	try {
    	    return getPropVal (Prop.PropConnectedOn).sValue;
    	} catch (Exception e) {
    	    return null;
    	}
    }

    // ========================================
    public abstract void updateView ();

    // ========================================
    public void validateContainer (Story.Commands commands, String commandName) {
	// unit has no container
    }

    // ========================================
    protected void storyChange (Story.Commands commands, boolean comp, final Hashtable<String, Prop> ownProps, ArrayList<Prop[]> changeProps) {
	Story story = commands.getStory ();
	validateContainer (commands, StoryLinkEmb);
	for (Prop[] changeProp : changeProps) {
	    final Prop type = changeProp[0];
		Prop parentVal = changeProp[1];
		final Prop src = changeProp [2];
		final Prop dst = changeProp [3] == null ? null : changeProp [3].getApplyMixProp (src, parentVal);
	    if (dst != null && dst.sValue == Prop.PropMix)
		continue;
	    boolean isParentValue = Prop.isSValue (parentVal);
	    boolean isDstValue = Prop.isSValue (dst);
	    boolean changeParentValue =
		isParentValue != isDstValue || (isDstValue && !dst.sValue.equals (parentVal.sValue));
	    if (src == dst && changeParentValue)
		continue;
	    boolean dstSpin = dst != null && dst.horizontalSpin;
	    // add
	    if (src == null) {
		if (isDstValue && changeParentValue) {
		    commands.add (story.new Command (StoryAddProp+" "+dst.name) {
			    public void exec () { ownProps.put (dst.name, dst); }
			    public void undo () { ownProps.remove (dst.name); }
			});
		    continue;
		}
		if (!dstSpin)
		    continue;
		final Prop newSpin = new Prop (dst.name, dst.getTypeToken (), dst.horizontalSpin);
		commands.add (story.new Command (StoryAddSpinProp+" "+newSpin.name) {
			public void exec () { ownProps.put (newSpin.name, newSpin); }
			public void undo () { ownProps.remove (newSpin.name); }
		    });
		continue;
	    }
	    boolean srcSpin = src != null && (src.horizontalSpin);
	    boolean changeSpin = (srcSpin != dstSpin) ||
		(srcSpin && dstSpin && src.horizontalSpin != dst.horizontalSpin);
	    // change
	    if (changeSpin) {
		final boolean oldHorizontalSpin = src.horizontalSpin;
		commands.add (story.new Command (StoryChangeSpin+" "+src.name) {
			public void exec () {
			    src.horizontalSpin = dst.horizontalSpin;
			}
			public void undo () {
			    src.horizontalSpin = oldHorizontalSpin;
			}
		    });
	    }
	    if (isDstValue && changeParentValue) {
		if (dst.sValue.equals (src.sValue))
		    continue;
		// XXX a tester si remove partiel de Prop.PropSize ou Prop.PropPos et rien chez parent
		// XXX faire modif partiel de multilabel
		// XXX peut impliquer un remove
		commands.add (story.new Command (StoryChangeValue+" "+dst.name+"<"+src.sValue+","+dst.sValue+">") {
			String newValue = dst.sValue;
			String oldValue = src.sValue;
			public void exec () { src.setValue (newValue); }
			public void undo () { src.setValue (oldValue); }
		    });
	    }
	    // XXX si unit && "name"
	    //     => faire un backup du fichier... mais quand ?
	    //     => refaire héritage => marquer les héritier direct a sauver (boolean changement parent)
	    // XXX si comp && si size = 0 ou pos hors du Workspace => remove
	    // XXX si unit ou dérivé dans des workspace && si size = 0 ou pos hors du Workspace => remove

	    if (dstSpin || (isDstValue && changeParentValue))
		continue;
	    // remove
	    if (comp && (type.name.equals (Prop.PropSize) || type.name.equals (Prop.PropPos))) {
		if (!isParentValue)
		    continue;
		Double [] seek = seek12.clone ();
		getParentPartialProp (type.name, seek);
		if (seek[0] == null || seek[1] == null)
		    continue;
	    }
	    if (getParentProp (type.name) == null ||
		src.unknownSpin != null ||
		(src.enumChoice != null && (parentVal == null || !parentVal.getEnumString ().equals (src.getEnumString ()))) ||
		(src.multiLabel != null && (parentVal == null || !parentVal.getLabelString ().equals (src.getLabelString ()))) ||
		src.getModifiersString ().equals (getModifiersString (getPropModifiers (src.name))) ) {
		final String oldValue = src.sValue;
		if (oldValue == null || oldValue.isEmpty ())
		    continue;
		commands.add (story.new Command (StoryClearProp+" "+src.name) {
			public void exec () { src.setValue (null); }
			public void undo () { src.setValue (oldValue); }
		    });
		continue;
	    }
	    commands.add (story.new Command (StoryRemoveProp+" "+type.name) {
		    public void exec () { ownProps.remove (type.name); }
		    public void undo () { ownProps.put (type.name, src); }
		});
	}
    }

    // ========================================
    public void storyTransform (Story.Commands commands, final String newName, final Collection<String> newModifiers,
				final Unit<?> permanent, final Hashtable<String, Prop> ownProps, ArrayList<Prop[]> transProps) {
	Story story = commands.getStory ();
	validateContainer (commands, StoryLinkEmb);
	if (newName != null) {
	    final String oldName = getName ();
	    if (!newName.equals (oldName)) {
		commands.add (story.new Command (StoryChangeName) {
			// XXX incidance sur PermanentDB si root ou powerSocket ou powerPlug
			public void exec () { Editable.this.name = newName; }
			public void undo () { Editable.this.name = oldName; }
			public void display () { getDirectUnit ().updateTree (); }
		    });
	    }
	}
	if (newModifiers != null && !getModifiersString ().equals (getModifiersString (newModifiers))) {
	    final Collection<String> oldModifiers = getModifiers ();
	    commands.add (story.new Command (StoryChangeModifiers) {
		    public void exec () { setModifiers (newModifiers); }
		    public void undo () { setModifiers (oldModifiers); }
		});
	}
	if (permanent != null) {
	    //System.err.println ("coucou change parent:"+getName ()+" => "+permanent.getName ());
	    final Unit<?> oldPermanent = getParentUnit ();
	    if (!(oldPermanent == permanent ||
		  permanent == this ||
		  getParent () == null))
		commands.add (story.new Command (StoryChangeParent) {
			public void exec () { setParentUnit (permanent); }
			public void undo () { setParentUnit (oldPermanent); }
			public void display () { permanent.updateTree (); oldPermanent.updateTree (); getDirectUnit ().updateTree (); }
		    });
	}
	if (transProps != null) {
	    ArrayList<Prop> oldProps = new ArrayList<Prop> (ownProps.values ());
	    // change
	    for (Prop[] transProp : transProps) {
		final Prop src = transProp [0];
		final Prop dst = transProp [1];
		if (src == null)
		    continue;
		oldProps.remove (src);
		if (src.rank == dst.rank)
		    continue;
		if (src.rank == null || !src.rank.equals (dst.rank)) {
		    final Integer oldRank = src.rank;
		    commands.add (story.new Command (StoryOrderedProp+" "+src.name) {
			    public void exec () { src.rank = dst.rank; }
			    public void undo () { src.rank = oldRank; }
			});
		}
		if (!src.getModifiersString ().equals (dst.getModifiersString ())) {
		    final Collection<String> oldModifiers = src.getModifiers ();
		    commands.add (story.new Command (StoryChangeModifiers+" "+src.name) {
			    public void exec () { src.setModifiers (dst.getModifiers ()); }
			    public void undo () { src.setModifiers (oldModifiers); }
			});
		}
		if (!src.name.equals (dst.name)) {
		    final String oldPropName = src.name;
			final String newPropName = dst.name;
		    commands.add (story.new Command (StoryChangeName+" "+oldPropName+" "+newPropName) {
			    public void exec () {
				if (ownProps.get (oldPropName) == src)
				    ownProps.remove (oldPropName);
				src.name = newPropName;
				ownProps.put (newPropName, src);
			    }
			    public void undo () {
				if (ownProps.get (newPropName) == src)
				    ownProps.remove (newPropName);
				src.name = oldPropName;
				ownProps.put (oldPropName, src);
			    }
			});
		}
		if (src.getTypeToken () != dst.getTypeToken ()) {
		    final PropTypeEnum oldType = src.getTypeToken ();
		    final PropTypeEnum newType = dst.getTypeToken ();
		    commands.add (story.new Command (StoryChangeType+" "+src.name) {
			    public void exec () { src.setType (newType); }
			    public void undo () { src.setType (oldType); }
			});
		}
		if (!src.getEnumString ().equals (dst.getEnumString ())) {
		    final Collection<String> oldEnum = src.enumChoice;
		    final Collection<String> newEnum = dst.enumChoice;
		    commands.add (story.new Command (StoryChangeEnum+" "+src.name) {
			    public void exec () { src.enumChoice = newEnum; }
			    public void undo () { src.enumChoice = oldEnum; }
			});
		}
		if (!src.getLabelString ().equals (dst.getLabelString ())) {
		    final List<String> oldLabels = src.multiLabel;
		    final List<String> newLabels = dst.multiLabel;
		    commands.add (story.new Command (StoryChangeLabels+" "+src.name) {
			    public void exec () { src.multiLabel = newLabels; }
			    public void undo () { src.multiLabel = oldLabels; }
			});
		}
	    }
	    // add
	    for (Prop[] transProp : transProps) {
		Prop src = transProp [0];
		final Prop dst = transProp [1];
		if (src != null)
		    continue;
		final Prop oldSrc = ownProps.get (dst.name);
		if (oldSrc != null) {
		    oldProps.remove (oldSrc);
		    commands.add (story.new Command (StoryRemoveProp+" "+dst.name) {
			    public void exec () { ownProps.remove (dst.name); }
			    public void undo () { ownProps.put (dst.name, oldSrc); }
			});
		}
		commands.add (story.new Command (StoryAddProp+" "+dst.name) {
			public void exec () { ownProps.put (dst.name, dst); }
			public void undo () { ownProps.remove (dst.name); }
		    });
	    }
	    // clean
	    Unit<?> parent = getParentUnit ();
	    for (final Prop prop : oldProps) {
		// if (prop.enumChoice == null && (parent != null && parent.getProp (prop.name) != null))
		// 	// XXX enum # parent aussi ?
		// 	continue;
		// XXX Et si plus de prop (masquée) ?
		if (prop.sValue != null || prop.horizontalSpin) {
		    final Collection<String> enumChoice = prop.enumChoice;
		    if (enumChoice != null)
			commands.add (story.new Command (StoryRemoveEnum+" "+prop.name) {
				public void exec () { prop.enumChoice = null; }
				public void undo () { prop.enumChoice = enumChoice; }
			    });
		    final List<String> multiLabel = prop.multiLabel;
		    if (multiLabel != null)
			commands.add (story.new Command (StoryRemoveLabels+" "+prop.name) {
				public void exec () { prop.multiLabel = null; }
				public void undo () { prop.multiLabel = multiLabel; }
			    });
		    continue;
		}
		commands.add (story.new Command (StoryRemoveProp+" "+prop.name) {
			String oldPropName = prop.name;
			public void exec () {
			    if (ownProps.get (oldPropName) == prop)
				ownProps.remove (prop.name);
			}
			public void undo () { ownProps.put (oldPropName, prop); }
		    });
	    }
	}
    }

    // ========================================
}
