package adecWatt.model;

import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.geom.Dimension2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.Hashtable;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import misc.DimensionDouble;
import misc.Story;

import adecWatt.model.unit.NonWorkspace;
import adecWatt.model.unit.Workspace;
import adecWatt.model.xml.XmlPermanent;

public abstract class Item<P extends XmlPermanent<T, A>, T extends Enum<?>, A extends Enum<?>>
    extends Embedded<P, T, A, Workspace, Acc> {

    // ========================================
    protected Workspace placeUnit;

    // ========================================
    public Workspace getStoryUnit ()				{ return container; }

    // ========================================
    public Item (Workspace workspace, P xml) {
	super (workspace, xml);
	collectEmbedded ();
    }
    public Item (String name, String placeName, Workspace workspace, NonWorkspace model) {
	super (name, placeName, workspace, model);
    }
    public Item (Workspace workspace, NonWorkspace model) {
	super (null, null, workspace, model);
    }
    abstract public Item clone (Workspace workspace);
    protected void updateId () {
	super.updateId ();
	try {
	    boolean localItem = container.getId ().equals (placeId);
	    if (!localItem)
		placeUnit = (Workspace) getAdecWatt ().getPermanentDB ().getUnitById (placeId);
	} catch (Exception e) {
	}
    }

    // ========================================
    public ArrayList<Acc> findAccs (Point2D.Double realPos, double close) {
	ArrayList<Acc> result = new ArrayList<Acc> ();
	double theta = Math.toRadians (-getThetaDegree ());
	double [] coord = new double [] {realPos.x, realPos.y};
	for (String accId : getEmbeddedIds ())
	    try {
		coord[0] = realPos.x;
		coord[1] = realPos.y;
		Point2D.Double accPos = accCenter.get (accId);
		DimensionDouble accSize = this.accSize.get (accId);
		AffineTransform at = new AffineTransform ();
		at.rotate (theta);
		at.translate (-accPos.x, -accPos.y);
		at.transform (coord, 0, coord, 0, 1);
		double halfWidth = accSize.width/2+close, halfHeight = accSize.height/2+close;
		if (coord[0] >= -halfWidth && coord[0] <= halfWidth &&
		    coord[1] >= -halfHeight && coord[1] <= halfHeight)
		    result.add (findEmbedded (accId));
	    } catch (Exception e) {
	    }
	return result;
    }

    public<R> R parentAndModelWalk (String propName, ComputeProp<R> computeProp, boolean skipFirst) {
	R result = parentWalk (propName, computeProp, skipFirst);
	if (result != null)
	    return result;
	try {
	    boolean localItem = placeId.equals (getContainer ().getId ());
	    result = placeUnit.findEmbedded (getId ()).parentWalk (propName, computeProp, localItem);
	    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 Element getXml (Node parent, Document document) {
	return super.getXml (parent, document);
    }

    // ========================================
    public boolean match (String text) {
	return
	    super.match (text) ||
	    model.parentMatch (text);
	// if (container.search (text).size () > 0)
	//     return true;
	// if (embedded != null)
	//     for (Acc acc: embedded.values ())
	// 	if (acc.search (text))
	// 	    return true;    
	// XXX ou accéssoire
    }

    // ========================================
    public void validateContainer (Story.Commands commands, String commandName) {
	if (container.getLocalEmbedded (getId ()) == this)
	    return;
	commands.add (container.story.new Command (commandName) {
		public void exec () { container.addEmbedded (Item.this); }
		public void undo () { container.removeEmbedded (Item.this); }
		public void displayExec () { updateView (Unit.BroadcastNewItem); }
		public void displayUndo () { updateView (Unit.BroadcastRemoveItem); }
	    });
    }

    public void storyChange (Story.Commands commands, ArrayList<Prop[]> changeProps) {
	storyChange (commands, true, ownProps, changeProps);
    }
    public void storyTransform (Story.Commands commands, Collection<String> modifiers, Unit<?> permanent, String newName, ArrayList<Prop[]> transProps) {
	storyTransform (commands, null, modifiers, permanent, ownProps, transProps);
    }
    public void updateView () {
	updateView (Unit.BroadcastUpdateItem);
    }
    @SuppressWarnings ("unchecked")
    public void updateView (String msg) {
	for (Enumeration<?> e = container.unitNode.breadthFirstEnumeration (); e.hasMoreElements (); ) {
	    Unit<Item<P, T, A>> unit = ((UnitNode<Item<P, T, A>>) e.nextElement ()).getUnit ();
	    unit.stateNotifier.broadcastDisplay (msg, this);
	}
    }

    // ========================================
    public boolean inside (Rectangle2D.Double selection) {
	try {
	    Point2D.Double center = getPos ();
	    DimensionDouble size = getRotSize (getSize (), getThetaDegree ());
	    double halfWidth = size.width/2;
	    double halfHeight = size.height/2;
	    return
		selection.x <= center.x-halfWidth && selection.x+selection.width >= center.x+halfWidth &&
		selection.y <= center.y-halfHeight && selection.y+selection.height >= center.y+halfHeight;
	} catch (Exception e) {
	    return false;
	}
    }

    public boolean isPosOutside (DimensionDouble inside) {
	Point2D.Double pos = getPos ();
	DimensionDouble size = getRotSize (getSize (), getThetaDegree ());
	double halfWidth = size.width/2;
	double halfHeight = size.height/2;
	return
	    pos.x < halfWidth || pos.y < halfHeight ||
	    pos.x > inside.width-halfWidth || pos.y > inside.height-halfHeight;
    }

    static public DimensionDouble getRotSize (DimensionDouble size, double thetaDegree) {
	if (thetaDegree == 0)
	    return size;
	double [] bounds = getShapeBounds (size, thetaDegree);
	return new DimensionDouble (Math.max (Math.abs (bounds[4]-bounds[0]),
					      Math.abs (bounds[6]-bounds[2])),
				    Math.max (Math.abs (bounds[5]-bounds[1]),
					      Math.abs (bounds[7]-bounds[3])));
    }
    public double getCloseBound (Point2D.Double pos, double close, Point2D.Double result) {
	return getCloseBound (getBounds (), pos, close, result);
    }
    public double[] getBounds () {
	return getBounds (getPos (), getSize (), getThetaDegree ());
    }

    static public Point2D.Double getPosInside (DimensionDouble inside, Point2D.Double pos, double thetaDegree, DimensionDouble refSize) {
	DimensionDouble size = getRotSize (refSize, thetaDegree);
	double halfWidth = size.width/2;
	double halfHeight = size.height/2;
	return new
	    Point2D.Double (Math.min (Math.max (pos.x, halfWidth), inside.width-halfWidth),
			    Math.min (Math.max (pos.y, halfHeight), inside.height-halfHeight));
    }
    static public double getCloseBound (double[] bounds, Point2D.Double pos, double close, Point2D.Double result) {
	for (int x = 0, y = 1; x < 18; x+=2, y+=2) {
	    double d = pos.distance (bounds[x], bounds[y]);
	    if (d < close) {
		close = d;
		result.x = bounds[x];
		result.y = bounds[y];
	    }
	}
	return close;
    }
    static public double[] getBounds (Point2D.Double pos, Dimension2D size, double thetaDegree) {
	double halfWidth = size.getWidth ()/2, halfHeight = size.getHeight ()/2;
	AffineTransform at = AffineTransform.getTranslateInstance (pos.x, pos.y);
	at.rotate (Math.toRadians (thetaDegree));
	double [] bounds = new double [] {
	    -halfWidth, -halfHeight,
	    -halfWidth, halfHeight,
	    halfWidth, halfHeight,
	    halfWidth, -halfHeight,
	    -halfWidth, 0,
	    0, halfHeight,
	    halfWidth, 0,
	    0, -halfHeight,
	    0, 0
	};
	//System.err.println ("coucou A:"+java.util.Arrays.toString (bounds));
	at.transform (bounds, 0, bounds, 0, 9);
	//System.err.println ("coucou B:"+java.util.Arrays.toString (bounds));
	return bounds;
    }
    static public double[] getShapeBounds (Dimension2D size, double thetaDegree) {
	double halfWidth = size.getWidth ()/2, halfHeight = size.getHeight ()/2;
	AffineTransform at = AffineTransform.getRotateInstance (Math.toRadians (thetaDegree));
	double [] bounds = new double [] {-halfWidth, -halfHeight, -halfWidth, halfHeight, halfWidth, halfHeight, halfWidth, -halfHeight};
	at.transform (bounds, 0, bounds, 0, 4);
	return bounds;
    }
    public boolean containsClose (double [] coord, double close) {
	try {
	    Point2D.Double center = getPos ();
	    DimensionDouble size = getSize ();
	    AffineTransform at = new AffineTransform ();
	    at.rotate (Math.toRadians (-getThetaDegree ()));
	    at.translate (-center.x, -center.y);
	    at.transform (coord, 0, coord, 0, 1);
	    double halfWidth = size.width/2+close, halfHeight = size.height/2+close;
	    return
		coord[0] >= -halfWidth && coord[0] <= halfWidth &&
		coord[1] >= -halfHeight && coord[1] <= halfHeight;
	} catch (Exception e) {
	    return false;
	}
    }
    // ========================================
    abstract public void changeThetaDegree (double thetaDegree);
    abstract public void changeSize (DimensionDouble size);

    public void changeRotSize (Point2D.Double pos, double thetaDegree, DimensionDouble size) {
	if (isSticky ())
	    return;
	// XXX ? si parent valeur identique ?
	getLocalProp (Prop.PropPos, Prop.PropTypeEnum.Cube).setPosition (pos);
	changeThetaDegree (thetaDegree);
	changeSize (size);
    }

    public void changePos (Point2D.Double pos) {
	if (isSticky ())
	    return;
	// XXX ? si parent valeur identique ?
	getLocalProp (Prop.PropPos, Prop.PropTypeEnum.Cube).setPosition (pos);
    }

    // ========================================
    static public Comparator<Item> xPosComparator = new AxeItemComparator (0, 1, 2);
    static public Comparator<Item> yPosComparator = new AxeItemComparator (1, 0, 2);
    static public Comparator<Item> zPosComparator = new AxeItemComparator (2, 0, 1);

    static class AxeItemComparator implements Comparator<Item> {
	int a, b, c;
	public AxeItemComparator (int a, int b, int c) {
	    this.a = a;
	    this.b = b;
	    this.c = c;
	}
	public int compare (Item o1, Item o2) {
	    if (o1 == o2)
		return 0;
	    Double [] seekProp1 = new Double[] {null, null, null};
	    Double [] seekProp2 = new Double[] {null, null, null};
	    o1.getPartialProp (Prop.PropPos, seekProp1);
	    o2.getPartialProp (Prop.PropPos, seekProp2);
	    if (seekProp1[a] != seekProp2[a]) {
		if (seekProp1[a] == null)
		    return 1;
		if (seekProp2[a] == null)
		    return -1;
		double diff = seekProp1[a] - seekProp2[a];
		if (diff != 0)
		    return diff > 0 ? 1 : -1;
	    }
	    if (seekProp1[b] != seekProp2[b]) {
		if (seekProp1[b] == null)
		    return 1;
		if (seekProp2[b] == null)
		    return -1;
		double diff = seekProp1[b] - seekProp2[b];
		if (diff != 0)
		    return diff > 0 ? 1 : -1;
	    }
	    if (seekProp1[c] != seekProp2[c]) {
		if (seekProp1[c] == null)
		    return 1;
		if (seekProp2[c] == null)
		    return -1;
		double diff = seekProp1[c] - seekProp2[c];
		if (diff != 0)
		    return diff > 0 ? 1 : -1;
	    }
	    return 0;
	}
    };

    // ========================================
    public String toString () {
	return getName ();
    }

    // ========================================
    public Hashtable<String, Point2D.Double> accCenter = new Hashtable<String, Point2D.Double> ();
    public Hashtable<String, DimensionDouble> accSize = new Hashtable<String, DimensionDouble> ();
    public Point2D.Double getAccCenter (String accId) {
	return accCenter.get (accId);
    }
    public DimensionDouble getAccSize (String accId) {
	return accSize.get (accId);
    }
    abstract public void print (Graphics2D printGraphics, Workspace workspace, String plugId);
    abstract public void print (Graphics2D printGraphics, Workspace workspace, Point2D pos, DimensionDouble size, double thetaDegree);

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