package adecWatt.model;

import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.TreeSet;
import javax.swing.tree.TreePath;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import misc.Bundle;
import misc.StateNotifier;
import misc.Story;
import misc.Util;
import static misc.Config.FS;

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

@SuppressWarnings ({"unchecked"})
public abstract class Unit<E extends Embedded<?,?,?,?,?>>
    extends Editable<XmlUnit, Permanent.UnitTypeEnum, Permanent.UnitAttrEnum, E> {

    static public final String
	BroadcastChangeBuilding		= "ChangeBuilding",
	BroadcastChangePos		= "ChangePos",
	BroadcastChangeRot		= "ChangeRot",
	BroadcastChangeRotSize		= "ChangeRotSize",
	BroadcastConnection		= "Connection",
	BroadcastNewItem		= "NewItem",
	BroadcastRemoveItem		= "RemoveItem",
	BroadcastSetSelectionItems	= "SetSelectionItems",
	BroadcastUpdateItem		= "UpdateItem",
	BroadcastUpdatePermItem		= "UpdatePermItem";

    // ========================================
    PermanentDB.UnitLocation	location;
    protected String		id;
    protected File		file;
    protected AdecWatt		adecWatt;
    protected UnitNode<E>	unitNode;

    private UnitTypeEnum type;

    public StateNotifier stateNotifier = new StateNotifier ();
    public Story story = new Story (stateNotifier);

    // ========================================
    public Collection<String>		getModifiersSet ()	{ return UnitModifiersSet; }
    public UnitTypeEnum			getTypeToken ()		{ return type; }
    static public UnitAttrEnum		getIdToken ()		{ return UnitAttrEnum.Id; }
    public UnitAttrEnum			getNameToken ()		{ return UnitAttrEnum.Name; }
    public UnitAttrEnum			getModifierToken ()	{ return UnitAttrEnum.Modifier; }

    public String			getId ()		{ return id; }
    public String			getFileName ()		{ return id+"."+PermanentDB.lptExtention; }
    public String			getSaveDir ()		{ return getClass ().getSimpleName ().toLowerCase (); }
    public PermanentDB.UnitLocation	getLocation ()		{ return location; }
    public boolean			getRemote ()		{ return location == PermanentDB.UnitLocation.Server; }

    public AdecWatt			getAdecWatt ()		{ return adecWatt; }
    public Unit<E>			getUnitRoot ()		{ return ((UnitNode<E>) unitNode.getRoot ()).getUnit (); }
    public UnitNode<E>			getUnitNode ()		{ return unitNode; }
    public Unit<E>			getDirectUnit ()	{ return this; }
    public Unit<E>			getParentUnit ()	{ return getParent (); }
    public Unit<E>			getStoryUnit ()		{ return this; }
    public Editable<?, ?, ?, ?>		getModel ()		{ return null; }

    // ========================================
    public Unit<E> getParent () {
	UnitNode<E> parentNode = (UnitNode<E>) unitNode.getParent ();
	return parentNode == null ? null : parentNode.getUnit ();
    }

    // public boolean hasChild (String childName) {
    // 	for (Enumeration<?> e = unitNode.children (); e.hasMoreElements (); )
    // 	    if (((UnitNode<E>) e.nextElement ()).getUnit ().getName ().equals (childName))
    // 		return true;
    // 	return false;
    // }

    public ArrayList<Unit<E>> getChildren () {
	ArrayList<Unit<E>> result = new ArrayList<Unit<E>> ();
	for (Enumeration<?> e = unitNode.children (); e.hasMoreElements (); )
	    result.add (((UnitNode<E>) e.nextElement ()).getUnit ());
	return result;
    }

    public boolean isDescendingFrom (String unitId) {
	if (unitId == null || unitId.isEmpty ())
	    return false;
	for (Unit<E> current = this; ; ) {
	    if (unitId.equals (current.getId ()))
		return true;
	    UnitNode<E> parent = (UnitNode<E>) current.unitNode.getParent ();
	    if (parent == null)
		return false;
	    current = parent.getUnit ();
	}
    }

    public ArrayList<Unit<E>> getAvailablePermanents (boolean isEmbedded, Editable<?, ?, ?, ?> editable) {
	ArrayList<Unit<E>> result = new ArrayList<Unit<E>> ();
	for (Enumeration<?> e = ((UnitNode<E>) unitNode.getRoot ()).breadthFirstEnumeration (); e.hasMoreElements (); ) {
	    Unit<E> unit = ((UnitNode<E>) e.nextElement ()).getUnit ();
	    if (isEmbedded) {
		if (unit.isUncompleted (editable))
		    continue;
	    } else {
		if (unit.getUnitNode ().isNodeAncestor (unitNode) || unit == editable)
		    continue;
	    }
	    result.add (unit);
	}
	return result;
    }

    public boolean isUncompleted (Editable<?, ?, ?, ?> editable) {
	for (String propName : getOrderedPropsName ()) {
	    if (!Permanent.isModifier (Permanent.Mandatory, getPropModifiers (propName)))
		continue;
	    if (getPropVal (propName) != null)
		continue;
	    if (editable == null)
		return true;
	    Prop prop = editable.getOwnProp (propName);
	    if (prop != null && prop.sValue != null)
		continue;
	    return true;
	}
	return false;
    }

    // ========================================
    static final Unit<?> getInstanceFromStream (AdecWatt adecWatt, InputStream dataIn, File file, PermanentDB.UnitLocation location) {
	XmlUnit xmlUnit = PermanentDB.readXmlUnit (dataIn);
	String id = xmlUnit.getFacet (getIdToken ());
	adecWatt.getUser ().updateDataId (id);
	Unit<?> result = getInstance (adecWatt, xmlUnit.type, id);
	result.location = location;
	result.file = file;
	result.initXml (xmlUnit);
	return result;
    }
    public void getCopy (String name) {
	if (getParent () == null)
	    return;
	Unit<E> result = (Unit<E>) getInstance (adecWatt, xmlPermanent.type, null);
	result.name = name;
	result.importFrom (this);
	adecWatt.getPermanentDB ().add (result);
	adecWatt.broadcastDisplay (AdecWatt.BroadcastUpdateUnitTree, result.getParent ());
    }
    public void getNewChild (String name) {
	Unit<E> result = (Unit<E>) getInstance (adecWatt, type, null);
	result.initChild (this, name);
	adecWatt.getPermanentDB ().add (result);
	updateTree ();
    }

    private static final Unit<?> getInstance (AdecWatt adecWatt, UnitTypeEnum type, String id) {
	switch (type) {
	case Furniture:
	    return new Furniture (adecWatt, id);
	case Information:
	    return new Information (adecWatt, id);
	case Line:
	    return new Line (adecWatt, id);
	case Accessory:
	    return new Accessory (adecWatt, id);
	case Building:
	    return new Building (adecWatt, id);
	case Lightplot:
	    return new Lightplot (adecWatt, id);
	}
	throw new IllegalArgumentException ("Can't find constructor for type ("+type+").");
    }

    // ========================================
    protected Unit (AdecWatt adecWatt, String id) {
	this.adecWatt = adecWatt;
	if (id == null)
	    id = adecWatt.getUser ().getDataId ();
	this.id = id;
	unitNode = new UnitNode<E> (this);
	Bundle.addBundleObserver (this);
    }

    protected void initXml (XmlUnit xmlUnit) {
	super.initXml (xmlUnit);
	if (name == null)
	    // XXX c'est plus un gros problème
	    throw new IllegalArgumentException ("Unknown XML unit has no name ("+xmlPermanent+").");
	type = xmlUnit.type;
    }
    protected void importFrom (Unit<E> from) {
	super.importFrom (from);
	location = PermanentDB.UnitLocation.Local;
	type = from.type;
	story.markNotSaved ();
	Unit<E> parent = from.getParent ();
	if (parent != null)
	    parent.unitNode.add (unitNode);
    }
    protected void initChild (Unit<?> unit, String name) {
	location = PermanentDB.UnitLocation.Local;
	type = unit.type;
	modifiers = unit.modifiers == null || unit.modifiers.size () < 1 ? null : new TreeSet<String> (unit.modifiers);
	this.name = name;
	story.markNotSaved ();
	unit.unitNode.add (unitNode);
    }

    public void updateXmlParent () {
	if (getParent () != null)
	    return;
	String parentId = xmlPermanent.getFacet (UnitAttrEnum.Parent);
	if (parentId == null) {
	    adecWatt.getPermanentDB ().namedRoots.put (getName (), this);
	    return;
	}
	Unit<?> parent = adecWatt.getPermanentDB ().getUnitById (parentId);
	if (parent == null) {
	    System.err.println ("XXX Unit:updateXmlParent: "+parentId+" not loaded !");
	    return;
	}
	parent.unitNode.add (unitNode);
	collectEmbedded ();
    }

    // ========================================
    public TreePath getPath () {
	ArrayList<UnitNode<E>> path = new ArrayList<UnitNode<E>> ();
	for (Unit<E> unit = this; unit != null; unit = unit.getParent ())
	    path.add (0, unit.unitNode);
	return new TreePath (path.toArray ());
    }

    public ArrayList<Unit<E>> search (String text) {
	ArrayList<Unit<E>> result = new ArrayList<Unit<E>> ();
	for (Enumeration<?> e = ((UnitNode<E>) unitNode.getRoot ()).breadthFirstEnumeration (); e.hasMoreElements (); ) {
	    Unit<E> unit = ((UnitNode<E>) e.nextElement ()).getUnit ();
	    if (unit.match (text))
		result.add (unit);
	}
	return result;
    }

    // ========================================
    public void enableStory () {
	stateNotifier.addUpdateObserver (adecWatt, AdecWatt.BroadcastStory);
    }
    public void disableStory () {
	stateNotifier.removeObserver (adecWatt);
    }
    public void replace (Unit<?> newUnit) {
	Unit<E> parent = getParent ();
	adecWatt.getPermanentDB ().add (newUnit);
	if (parent != null)
	    parent.unitNode.remove (unitNode);
	for (Unit<?> child : getChildren ())
	    newUnit.unitNode.add (child.unitNode);
	adecWatt.broadcastDisplay (AdecWatt.BroadcastUnitStory, newUnit);
    }
    public void findPromote (HashSet<Unit<?>> unitLinks, TreeSet<String> iconsLinks, TreeSet<String> imagesLinks) {
	unitLinks.add (this);
	getLocalLink (new HashSet<Editable<?, ?, ?, ?>> (), unitLinks, iconsLinks, imagesLinks);
    }
    public void changeId (String newId) {
	id = newId;
	story.markNotSaved ();
    }

    public void remove () {
	PermanentDB permanentDB = adecWatt.getPermanentDB ();
	Unit<?> newUnit = null;
	Unit<E> parent = getParent ();
	switch (location) {
	case Server:
	    return;
	case Local:
	    permanentDB.remove (this);
	    break;
	case Zip:
	    permanentDB.forget (this);
	    break;
	}
	newUnit = permanentDB.reload (this);
	if (newUnit != null) {
	    replace (newUnit);
	    newUnit.updateXmlParent ();
	    if (parent != null)
		adecWatt.broadcastDisplay (AdecWatt.BroadcastUpdateUnitTree, parent);
	    else
		adecWatt.broadcastUpdate (AdecWatt.BroadcastUnitRoots);
	    updateView ();
	    return;
	}
	parent.unitNode.remove (unitNode);
	adecWatt.broadcastDisplay (AdecWatt.BroadcastUpdateUnitTree, parent);
    }

    // ========================================   
    public void updateBundle () {
	//XXX setUserObject (getLocalName ());
    }

    // ========================================
    public void getLocalLink (HashSet<Editable<?, ?, ?, ?>> visited, HashSet<Unit<?>> unitLinks, TreeSet<String> iconsLinks, TreeSet<String> imagesLinks) {
	if (visited.contains (this))
	    return;
	super.getLocalLink (visited, unitLinks, iconsLinks, imagesLinks);
	if (!getRemote () || story.isModified ())
	    unitLinks.add (this);
	Unit<E> parent = getParent ();
	if (parent == null)
	    return;
	parent.getLocalLink (visited, unitLinks, iconsLinks, imagesLinks);
    }

    public void save () {
	File file = PermanentDB.getLocalFile (getSaveDir ()+FS+getFileName ());
	PermanentDB.writeUnit (this, file);
	location = PermanentDB.UnitLocation.Local;
	this.file = file;
	story.markSaved ();
    }

    public void promote () {
	String fileName = getSaveDir ()+FS+getFileName ();
	Util.backup (PermanentDB.getLocalFile (fileName), PermanentDB.lptExtention, PermanentDB.backExtention);
	File remotefile = PermanentDB.getRemoteFile (fileName);
	PermanentDB.writeUnit (this, remotefile);
	this.file = remotefile;
	location = PermanentDB.UnitLocation.Server;
	story.markSaved ();
    }

    public Element getXml (Node parentXml, Document document) {
	Element child = super.getXml (parentXml, document);
	XmlUnit.putFacet (child, UnitAttrEnum.Id, id);
	Unit<E> parent = getParent ();
	if (parent != null)
	    XmlUnit.putFacet (child, UnitAttrEnum.Parent, parent.id);
	return child;
    }

    // ========================================
    public void setParentUnit (Unit<?> parent) {
	UnitNode<E> oldParentNode = (UnitNode<E>) unitNode.getParent ();
	if (oldParentNode == null || parent == null || oldParentNode == parent.unitNode)
	    return;
	oldParentNode.remove (unitNode);
	((Unit<E>)parent).unitNode.add (unitNode);
    }
    public void updateTree () {
	adecWatt.broadcastDisplay (AdecWatt.BroadcastUpdateUnitTree, this);
    }
    public void storyChange (Story.Commands commands, ArrayList<Prop[]> changeProps) {
	storyChange (commands, false, ownProps, changeProps);
    }
    public void storyTransform (Story.Commands commands, Collection<String> modifiers, Unit<?> parent, String newName, ArrayList<Prop[]> transProps) {
	storyTransform (commands, newName, modifiers, parent, ownProps, transProps);
    }
    public String toString () {
	return getLocalName ();
    }

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