package adecWatt.model;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;

import misc.Config;
import misc.Log;
import misc.Util;
import static misc.Config.FS;

import adecWatt.control.AdecWattManager;
import adecWatt.model.unit.Building;
import adecWatt.model.xml.XmlUnit;

public class PermanentDB {

    // ========================================
    static public final File dataDir = Config.findDataDir ();
    static public enum UnitLocation { Server, Local, Zip };
    static public final String localToken = "local";
    static public final String serverToken = "server";
    static public final String cacheToken = "cache";
    static public final List<String> localRemoteDir = Arrays.asList (localToken, serverToken);
    static public final String lptExtention = "lpt";
    static public final String lpzExtention = "lpz";
    static public final String backExtention = "back";
    static public final String[] exportExtention = { lpzExtention };

    // ========================================
    private AdecWatt adecWatt;
    private ImageDB iconDB;
    private ImageDB imageDB;
    private String powerPlugId, powerSocketId;

    public String	getPowerPlugId ()	{ return powerPlugId; }
    public String	getPowerSocketId ()	{ return powerSocketId; }

    public ArrayList<Building> getAllBuildings () {
	ArrayList<Building> result = new ArrayList<Building> ();
	for (Enumeration<?> enumUnitNode = getRootByName (Prop.PropBuilding).getUnitNode ().breadthFirstEnumeration ();
	     enumUnitNode.hasMoreElements();
	     ) {
	    Building building = (Building) ((UnitNode<?>) enumUnitNode.nextElement ()).getUnit ();
	    if (building.isUncompleted (null))
		continue;
	    result.add (building);
	}
	return result;
    }

    public PermanentDB (AdecWatt adecWatt) {
	this.adecWatt = adecWatt;
	iconDB = adecWatt.getIconDB ();
	imageDB = adecWatt.getImageDB ();
    }

    // ========================================
    // Unit
    // ========================================
    HashSet<Unit<?>> allUnit = new HashSet<Unit<?>> ();
    Hashtable<String, Unit<?>> idUnit = new Hashtable<String, Unit<?>> ();
    Hashtable<String, Unit<?>> namedRoots = new Hashtable<String, Unit<?>> ();

    public boolean	isReferedUnit (String unitId)	{ return idUnit.containsKey (unitId); }
    public Unit<?>	getUnitById (String unitId)	{ return idUnit.get (unitId); }
    public Unit<?>	getRootByName (String unitName)	{ return namedRoots.get (unitName); }
    public Set<String>	getReferedIds ()		{ return idUnit.keySet (); }

    public boolean isModifiedUnit () {
	for (Unit<?> unit : allUnit)
	    if (unit.story.isModified ())
		return true;
	return false;
    }

    // ========================================
    public void reload () {
	powerSocketId = powerPlugId = null;
	allUnit.clear ();
	idUnit.clear ();
	namedRoots.clear ();
	for (String srcDir : localRemoteDir)
	    for (File file : getAllXmlFiles (srcDir))
		try {
		    if (file.length () == 0)
			continue;
		    FileInputStream fileInputStream = new FileInputStream (file);
		    Unit<?> unit = Unit.getInstanceFromStream (adecWatt, fileInputStream, file,
							       serverToken.equals (srcDir) ? UnitLocation.Server : UnitLocation.Local);
		    fileInputStream.close ();
		    if (isReferedUnit (unit.getId ())) {
			System.err.println (unit.getName ()+" already recorded : skip "+file);
			// ignore
			continue;
		    }
		    add (unit);
		} catch (Exception e) {
		    System.err.println ("XXX: "+file+" error: "+e);
		    e.printStackTrace ();
		}
	for (Unit<?> unit : allUnit)
	    unit.updateXmlParent ();
    }

    public void newNamePlan (TreeMap<String, String> iconMap, TreeMap<String, String> imageMap) {
	HashSet<Editable<?, ?, ?, ?>> visited = new HashSet<Editable<?, ?, ?, ?>> ();
	for (Unit<?> unit : allUnit)
	    unit.newNamePlan (visited, iconMap, imageMap);
    }

    public void add (Unit<?> unit) {
	String unitId = unit.getId ();
	try {
	    allUnit.remove (idUnit.get (unitId));
	} catch (Exception e) {
	}
	allUnit.add (unit);
	idUnit.put (unitId, unit);
	adecWatt.getUser ().updateDataId (unitId);
	if ("powerPlug".equals (unit.getName ()))
	    powerPlugId = unitId;
	if ("powerSocket".equals (unit.getName ()))
	    powerSocketId = unitId;
	unit.enableStory ();
    }
    public void forget (Unit<?> unit) {
	adecWatt.broadcastDisplay (AdecWattManager.actionClose, unit);
	idUnit.remove (unit.getId ());
	allUnit.remove (unit);
	unit.disableStory ();
	// XXX et si "powerPlug" ou "powerSocket" ?
    }
    public Unit<?> reload (Unit<?> unit) {
	for (String srcDir : localRemoteDir) {
	    try  {
		File file = getXmlFile (new File (dataDir, srcDir), unit.getFileName ());
		FileInputStream fileInputStream = new FileInputStream (file);
		Unit<?> newUnit = Unit.getInstanceFromStream (adecWatt, fileInputStream, file,
							      serverToken.equals (srcDir) ? UnitLocation.Server : UnitLocation.Local);
		fileInputStream.close ();
		return newUnit;
	    } catch (Exception e) {
	    }
	}
	return null;
    }
    public void remove (Unit<?> unit) {
	if (unit.file != null)
	    Util.backup (unit.file, Util.getExtention (unit.file), backExtention);
	forget (unit);
    }
    public void renameVisitor (HashSet<Unit<?>> units) {
	TreeMap<String, String> translateMap = new TreeMap<String, String> ();
	String pattern = String.format ("%03d-", User.visitorId);
	User user = adecWatt.getUser ();
	for (Unit<?> unit : units) {
	    String unitId = unit.getId ();
	    if (!unitId.startsWith (pattern))
		continue;
	    translateMap.put (unitId, user.getDataId ());
	}
	if (translateMap.size () < 1)
	    return;
	for (String unitId : translateMap.keySet ()) {
	    Unit<?> unit = idUnit.get (unitId);
	    if (unit.file != null)
		Util.backup (unit.file, Util.getExtention (unit.file), backExtention);
	    idUnit.remove (unitId);
	    String newUnitId = translateMap.get (unitId);
	    unit.changeId (newUnitId);
	    idUnit.put (newUnitId, unit);
	}
	for (Unit<?> unit : allUnit)
	    if (unit.renameUnits (translateMap))
		// YYY voir save ?
		unit.story.markNotSaved ();
    }
    public void promote (HashSet<Unit<?>> units) {
	if (units.size () == 0)
	    return;
	for (Unit<?> unit : units)
	    unit.promote ();
	adecWatt.broadcastUpdate (AdecWatt.BroadcastUnitRoots);
    }
    public void renameImages (TreeMap<String, String> translateMap) {
	for (Unit<?> unit : allUnit)
	    if (unit.renameImages (translateMap))
		// YYY voir save ?
		unit.story.markNotSaved ();
    }

    // ========================================
    static public final File getXmlFile (String name) {
	for (String srcDir : localRemoteDir)
	    try {
		File file = getXmlFile (new File (dataDir, srcDir), name);
		if (file != null)
		    return file;
	    } catch (Exception e) {
	    }
	return null;
    }

    static public final File getXmlFile (File dir, String name) {
	File test = new File (dir, name);
	if (test.exists ())
	    return test;
	for (File subDir : getSubDirs (dir)) {
	    test = getXmlFile (subDir, name);
	    if (test != null)
		return test;

	}
	return null;
    }

    static public final File[] getXmlFiles (File dir) {
	return dir.listFiles (new FileFilter () {
		public boolean accept (File file) {
		    return file.isFile () && !file.isHidden () &&
			lptExtention.equals (Util.getExtention (file));
		}
	    });
    }
    static public final File[] getSubDirs (File dir) {
	return dir.listFiles (new FileFilter () {
		public boolean accept (File file) {
		    return file.isDirectory () && ! file.isHidden ();
		}
	    });
    }
    public boolean isFileNameExists (String baseName) {
	for (String srcDir : localRemoteDir)
	    try {
		if (isFileExists (new File (dataDir, srcDir), baseName))
		    return true;
	    } catch (Exception e) {
	    }
	return false;
    }
    public boolean isFileExists (File dir, String baseName) {
	for (File file : getXmlFiles (dir)) {
	    String fileName = file.getName ();
	    if (fileName.startsWith (baseName) &&
		lptExtention.equals (fileName.substring (1+baseName.length ())))
		return true;
	}
	for (File subDir : getSubDirs (dir))
	    if (isFileExists (subDir, baseName))
		return true;
	return false;
    }

    // ========================================
    static public ArrayList<File> getAllXmlFiles (String srcDir) {
	ArrayList<File> result = new ArrayList<File> ();
	try {
	    getAllXmlFiles (new File (dataDir, srcDir), result);
	} catch (Exception e) {
	}
	return result;
    }
    static public void getAllXmlFiles (File dir, ArrayList<File> result) {
	for (File file : getXmlFiles (dir))
	    result.add (file);
	for (File subDir : getSubDirs (dir))
	    getAllXmlFiles (subDir, result);
    }

    // ========================================
    static public File getInputFile (String name) {
	for (String srcDir : localRemoteDir) {
	    File file = new File (dataDir, srcDir+FS+name);
	    if (file.exists () && file.isFile ())
		return file;
	}
	throw new IllegalArgumentException ("Can't find input "+name+"!");
    }

    // ========================================
    static public File getLocalFile (String name) {
	File file = (new File (dataDir, localToken+FS+name));
	file.getParentFile ().mkdirs ();
	return file;
    }
    static public File getRemoteFile (String name) {
	File file = (new File (dataDir, serverToken+FS+name));
	file.getParentFile ().mkdirs ();
	return file;
    }

    // ========================================
    static public XmlUnit readXmlUnit (InputStream stream) {
	try {
	    DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance ().newDocumentBuilder ();
	    Document document = documentBuilder.parse (stream);
	    document.normalizeDocument ();
	    return new XmlUnit (document.getFirstChild ());
	} catch (Exception e) {
	    Log.keepLastException ("PermanentDB::readDocument", e);
	    throw new IllegalArgumentException ("Not XML file input.");
	}
    }

    // ========================================
    static public Document newDocument () {
	try {
	    DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance ().newDocumentBuilder ();
	    Document document = documentBuilder.newDocument ();
	    document.setXmlStandalone (true);
	    return document;
	} catch (Exception e) {
	    return null;
	}
    }

    // ========================================
    static public void writeUnit (Unit<?> unit, File file) {
	try {
	    FileOutputStream fileOutputStream = new FileOutputStream (file);
	    writeUnit (unit, fileOutputStream);
	    fileOutputStream.close ();
	} catch (Exception e) {
	    Log.keepLastException ("PermanentDB::writeDocument", e);
	}
    }

    static public void writeUnit (Unit<?> unit, OutputStream stream)
	throws IOException, TransformerException {
	Document document = newDocument ();
	unit.getXml (document, document);
	Source source = new DOMSource (document);
	Result result = new StreamResult (stream);
	Transformer xformer = TransformerFactory.newInstance ().newTransformer ();
	xformer.setOutputProperty (OutputKeys.INDENT, "yes");
	xformer.setOutputProperty ("{http://xml.apache.org/xslt}indent-amount", "2");
	xformer.transform (source, result);
	stream.flush ();
    }

    public void addUnitZip (ZipOutputStream out, Unit<?> unit)
	throws IOException, TransformerException {
	if (unit == null)
	    return;
	ZipEntry ze = new ZipEntry (unit.getSaveDir ()+"/"+unit.getFileName ());
	out.putNextEntry (ze);
	ByteArrayOutputStream stream = new ByteArrayOutputStream ();
	writeUnit (unit, stream);
	out.write (stream.toByteArray ());
	out.closeEntry();
	out.flush ();
    }

    public File getLastExport () { return Config.getFile ("LastExport"); }

    public File checkExportExtention (File file) {
	if (Util.getExtention (file) != null)
	    return file;
	return new File (file.getParentFile (), Util.changeExtention (file.getName (), lpzExtention));
    }

    public void exportFile (Unit<?> unit, File file) {
	if (file == null)
	    return;
	try {
	    file = checkExportExtention (file);
	    HashSet<Unit<?>> unitLinks = new HashSet<Unit<?>> ();
	    TreeSet<String> iconsLinks = new TreeSet<String> ();
	    TreeSet<String> imagesLinks = new TreeSet<String> ();
	    unitLinks.add (unit);
	    unit.getLocalLink (new HashSet<Editable<?, ?, ?, ?>> (), unitLinks, iconsLinks, imagesLinks);
	    FileOutputStream fileOutputStream = new FileOutputStream (file);
	    ZipOutputStream out = new ZipOutputStream (fileOutputStream);
	    for (Unit<?> unit2 : unitLinks)
		addUnitZip (out, unit2);
	    // XXX prendre aussi les étiquettes dans icon/"user".lpi filté avec "^imageName^:.*" dans iconsLinks
	    for (String iconName : iconsLinks)
		iconDB.addImageZip (out, iconName);
	    // XXX prendre aussi les étiquettes dans image/"user".lpi filté avec "^imageName^:.*" dans imagesLinks
	    for (String imageName : imagesLinks)
		imageDB.addImageZip (out, imageName);
	    out.close();
	    fileOutputStream.close ();
	    Config.setFile ("LastExport", file);
	} catch (Exception e) {
	    e.printStackTrace ();
	}
    }

    public void importFile (File file) {
	if (file == null)
	    return;
	try {
	    file = checkExportExtention (file);
	    ArrayList<Unit<?>> newUnit = new ArrayList<Unit<?>> ();
	    byte[] tmp = new byte[1024*1024];
	    FileInputStream fileInputStream = new FileInputStream (file);
	    ZipInputStream in = new ZipInputStream (fileInputStream);
	    for (;;) {
		ZipEntry ze = in.getNextEntry ();
		if (ze == null)
		    break;
		ByteArrayOutputStream out = new ByteArrayOutputStream ();
		for (;;) {
		    int nb = in.read (tmp, 0, tmp.length);
		    if (nb < 0)
			break;
		    out.write (tmp, 0, nb);
		}
		out.flush ();
		in.closeEntry ();
		String[] path = ze.getName ().split ("/");
		String dirName = path[0];
		String fileName = path [1];
		String fileExtention = Util.getExtention (fileName);
		ByteArrayInputStream dataIn = new ByteArrayInputStream (out.toByteArray ());
		if (!iconDB.importFile (dirName, fileName, dataIn) &&
		    !imageDB.importFile (dirName, fileName, dataIn)) {
		    if (lptExtention.equals (fileExtention))
			newUnit.add (Unit.getInstanceFromStream (adecWatt, dataIn, null, UnitLocation.Zip));
		    else
			System.err.println ("XXX pb: "+fileName);
		}
	    }
	    fileInputStream.close ();
	    for (Unit<?> unit : newUnit) {
		String unitId = unit.getId ();
		if (isReferedUnit (unitId)) {
		    Unit<?> oldUnit = idUnit.get (unitId);
		    forget (oldUnit);
		    oldUnit.replace (unit);
		    continue;
		}
		add (unit);
	    }
	    for (Unit<?> unit : newUnit) {
		unit.updateXmlParent ();
		unit.story.markNotSaved ();
	    }
	    adecWatt.broadcastUpdate (AdecWatt.BroadcastUnitRoots);
	    Config.setFile ("LastExport", file);
	} catch (Exception e) {
	    e.printStackTrace ();
	}
    }

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