/**
 * A .rfo file is a compressed archive containing multiple objects that are all to
 * be built in a RepRap machine at once.  See this web page:
 * 
 * http://reprap.org/bin/view/Main/MultipleMaterialsFiles
 * 
 * for details.
 * 
 * This is the class that handles .rfo files.
 */
package org.reprap;
// http://www.devx.com/tips/Tip/14049

import java.io.*;
import java.nio.channels.*;
import java.util.zip.*;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;


import org.xml.sax.XMLReader;
import org.xml.sax.InputSource;
import org.xml.sax.helpers.XMLReaderFactory;
import org.xml.sax.helpers.DefaultHandler;


import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.swing.JOptionPane;
import javax.vecmath.Matrix4d;
//import javax.vecmath.Matrix3d;
//import javax.vecmath.Point3d;
//import javax.vecmath.Tuple3d;
//import javax.vecmath.Vector3d;

import org.reprap.geometry.polyhedra.AllSTLsToBuild;
import org.reprap.geometry.polyhedra.CSG3D;
import org.reprap.geometry.polyhedra.CSGReader;
import org.reprap.geometry.polyhedra.STLObject;
import org.reprap.utilities.Debug;

public class RFO 
{
	/**
	 * XML stack top.  If it gets 100 deep we're in trouble...
	 */
	static final int top = 100;
	
	/**
	 * Names of STL files in the compressed .rfo file
	 */
	static final String stlPrefix = "rfo-";
	static final String stlSuffix = ".stl";
	
	//**************************************************************************************
	//
	// XML file writing.  The legend file that ties everything together is XML.  This writes
	// it.
	
	class XMLOut
	{
		PrintStream XMLStream;
		String[] stack;
		int sp;
		
		/**
		 * Create an XML file called LegendFile starting with XML entry start.
		 * @param LegendFile
		 * @param start
		 */
		XMLOut(String LegendFile, String start)
		{
			FileOutputStream fileStream = null;
			try
			{
				fileStream = new FileOutputStream(LegendFile);
			} catch (Exception e)
			{
				Debug.e("XMLOut(): " + e);
			}
			XMLStream = new PrintStream(fileStream);
			stack = new String[top];
			sp = 0;
			push(start);
		}
		
		/**
		 * Start item s
		 * @param s
		 */
		void push(String s)
		{
			for(int i = 0; i < sp; i++)
				XMLStream.print(" ");
			XMLStream.println("<" + s + ">");
			int end = s.indexOf(" ");
			if(end < 0)
				stack[sp] = s;
			else
				stack[sp] = s.substring(0, end);
			sp++;
			if(sp >= top)
				Debug.e("RFO: XMLOut stack overflow on " + s);
		}
		
		/**
		 * Output a complete item s all in one go.
		 * @param s
		 */
		void write(String s)
		{
			for(int i = 0; i < sp; i++)
				XMLStream.print(" ");
			XMLStream.println("<" + s + "/>");
		}
		
		/**
		 * End the current item.
		 *
		 */
		void pop()
		{
			sp--;
			for(int i = 0; i < sp; i++)
				XMLStream.print(" ");
			if(sp < 0)
				Debug.e("RFO: XMLOut stack underflow.");
			XMLStream.println("</" + stack[sp] + ">");
		}
		
		/**
		 * Wind it up.
		 *
		 */
		void close()
		{
			while(sp > 0)
				pop();
			XMLStream.close();
		}
	}
	
	//**************************************************************************************
	//
	// XML file reading.  This reads the legend file.
	
	class XMLIn extends DefaultHandler
	{
		/**
		 * The rfo that we are reading in
		 */
		private RFO rfo;
		
		/**
		 * The STL being read
		 */
		private STLObject stl;
		
		/**
		 * The first of a list of STLs being read.
		 */
		private STLObject firstSTL;
		/**
		 * The current XML item
		 */
		private String element;
		
		/**
		 * File location for reading (eg for an input STL file).
		 */
		private String location;
		
		/**
		 * What type of file (Only STLs supported at the moment).
		 */
		private String filetype;
		
		/**
		 * The name of the material (i.e. extruder) that this item is made from.
		 */
		private String material;
		
		/**
		 * Transfom matrix to get an item in the right place.
		 */
		private double[] mElements;
		private Transform3D transform;
		
		private int rowNumber = 0;
		
		/**
		 * Open up legendFile and use it to build RFO rfo.
		 * @param legendFile
		 * @param r
		 */
		XMLIn(String legendFile, RFO r)
		{
			super();
			rfo = r;
			element = "";
			location = "";
			filetype = "";
			material = "";
			mElements = new double[16];
			setMToIdentity();			
			
			XMLReader xr = null;
			try
			{
				xr = XMLReaderFactory.createXMLReader();
			} catch (Exception e)
			{
				Debug.e("XMLIn() 1: " + e);
			}
			
			xr.setContentHandler(this);
			xr.setErrorHandler(this);
			try
			{
				xr.parse(new InputSource(legendFile));
			} catch (Exception e)
			{
				Debug.e("XMLIn() 2: " + e);
			}

		}

		/**
		 * Initialise the matrix to the identity matrix.
		 *
		 */
		private void setMToIdentity()
		{
			for(rowNumber = 0; rowNumber < 4; rowNumber++)
				for(int column = 0; column < 4; column++)
				{
					if(rowNumber == column)
						mElements[rowNumber*4 + column] = 1;
					else
						mElements[rowNumber*4 + column] = 0;
				}
			transform = new Transform3D(mElements);
			rowNumber = 0;
		}
		////////////////////////////////////////////////////////////////////
		// Event handlers.  These are callbacks for the XML parser.
		////////////////////////////////////////////////////////////////////


		/**
		 * Begin the XML document - no action needed.
		 */
		public void startDocument ()
		{
			//Debug.a("Start document");
		}


		/**
		 * End the XML document - no action needed.
		 */
		public void endDocument ()
		{
			//Debug.a("End document");
		}


		/**
		 * Start an element
		 */
		public void startElement (String uri, String name,
				String qName, org.xml.sax.Attributes atts)
		{
			if (uri.equals(""))
				element = qName;
			else
				element = name;
			//System.out.print(element);
			
			// What element is it?
			
			if(element.equalsIgnoreCase("reprap-fab-at-home-build"))
			{
				
			} else if(element.equalsIgnoreCase("object"))
			{
				stl = new STLObject();
				firstSTL = null;
			} else  if(element.equalsIgnoreCase("files"))
			{
				
			} else if(element.equalsIgnoreCase("file"))
			{
				location = atts.getValue("location");
				filetype = atts.getValue("filetype");
				material = atts.getValue("material");
				if(!filetype.equalsIgnoreCase("application/sla"))
					Debug.e("XMLIn.startElement(): unreconised object file type (should be \"application/sla\"): " + filetype);
			} else if(element.equalsIgnoreCase("transform3D"))
			{
				setMToIdentity();
			} else if(element.equalsIgnoreCase("row"))
			{
				for(int column = 0; column < 4; column++)
					mElements[rowNumber*4 + column] = Double.parseDouble(atts.getValue("m" + rowNumber + column));
			} else
			{
				Debug.e("XMLIn.startElement(): unreconised RFO element: " + element);
			}
		}

		/**
		 * End an element
		 */
		public void endElement (String uri, String name, String qName)
		{
			if (uri.equals(""))
				element = qName;
			else
				element = name;
			if(element.equalsIgnoreCase("reprap-fab-at-home-build"))
			{
				
			} else if(element.equalsIgnoreCase("object"))
			{
				stl.setTransform(transform);
				rfo.astl.add(stl);
			} else  if(element.equalsIgnoreCase("files"))
			{
				
			} else if(element.equalsIgnoreCase("file"))
			{
				org.reprap.Attributes att = stl.addSTL("file:" + rfoDir + location, null, Preferences.unselectedApp(), firstSTL);
				if(firstSTL == null)
					firstSTL = stl;
				att.setMaterial(material);
				location = "";
				filetype = "";
				material = "";

			} else if(element.equalsIgnoreCase("transform3D"))
			{
				if(rowNumber != 4)
					Debug.e("XMLIn.endElement(): incomplete Transform3D matrix - last row number is not 4: " + rowNumber);
				transform = new Transform3D(mElements);
			} else if(element.equalsIgnoreCase("row"))
			{
				rowNumber++;
			} else
			{
				Debug.e("XMLIn.endElement(): unreconised RFO element: " + element);
			}
		}

		/**
		 * Nothing to do for characters in between.
		 */
		public void characters (char ch[], int start, int length)
		{
//			for (int i = start; i < start + length; i++) 
//				System.out.print(ch[i]);
//			Debug.a();
		}

	}
	
	//**************************************************************************************
	//
	// Start of RFO handling
	
	private static final String legendName = "legend.xml";
	
	/**
	 * The name of the RFO file.
	 */
	private String fileName;
	
	/**
	 * The unique file names;
	 */
	private List<String> uNames;
	
	/**
	 * The directory in which it is.
	 */
	private String path;
	
	/**
	 * The temporary directory
	 */
	private String tempDir;
	
	/**
	 * The location of the temporary RFO directory
	 */
	private String  rfoDir;
	
	/**
	 * The unique temporary directory name
	 */
	private String uniqueName;
	
	/**
	 * The collection of objects being written out or read in.
	 */
	private AllSTLsToBuild astl;
	
	/**
	 * The XML output for the legend file.
	 */
	private XMLOut xml;
	
	/**
	 * The constructor is the same whether we're reading or writing.  fn is where to put or get the
	 * rfo file from.  as is all the things to write; set that null when reading.
	 * @param fn
	 * @param as
	 */
	private RFO(String fn, AllSTLsToBuild as)
	{
		astl = as;
		uNames = null;
		int sepIndex = fn.lastIndexOf(File.separator);
		int fIndex = fn.indexOf("file:");
		fileName = fn.substring(sepIndex + 1, fn.length());
		if(sepIndex >= 0)
		{
			if(fIndex >= 0)
				path = fn.substring(fIndex + 5, sepIndex + 1);
			else
				path = fn.substring(0, sepIndex + 1);
		} else
			path = "";
		
		uniqueName = "rfo" + Long.toString(System.nanoTime());

		tempDir = System.getProperty("java.io.tmpdir") + File.separator + uniqueName;
		
		File rfod = new File(tempDir);
		if(!rfod.mkdir())
			throw new RuntimeException(tempDir);
		tempDir += File.separator;
		rfoDir = tempDir + "rfo";
		rfod = new File(rfoDir);
		if(!rfod.mkdir())
			throw new RuntimeException(rfoDir);
		rfoDir += File.separator;
	}
	

	public static boolean recursiveDelete(File fileOrDir)
	{
	    if(fileOrDir.isDirectory())
	    {
	        // recursively delete contents
	        for(File innerFile: fileOrDir.listFiles())
	        {
	            if(!recursiveDelete(innerFile))
	            {
	                return false;
	            }
	        }
	    }

	    return fileOrDir.delete();
	}



	//****************************************************************************
	//
	// .rfo writing
	
	/**
	 * Copy a file from one place to another
	 */
	private static void copyFile(File in, File out)
	{
		try
		{
			FileChannel inChannel = new	FileInputStream(in).getChannel();
			FileChannel outChannel = new FileOutputStream(out).getChannel();
			inChannel.transferTo(0, inChannel.size(), outChannel);
			inChannel.close();
			outChannel.close();			
		} catch (Exception e)
		{
			Debug.e("RFO.copyFile(): " + e);
		}

	}		
	
	/**
	 * Copy a file from one place to another.
	 * @param from
	 * @param to
	 */
	private static void copyFile(String from, String to)
	{
		File inputFile;
	    File outputFile;
		int fIndex = from.indexOf("file:");
		int tIndex = to.indexOf("file:");
		if(fIndex < 0)
			inputFile = new File(from);
		else
			inputFile = new File(from.substring(fIndex + 5, from.length()));
		if(tIndex < 0)
			outputFile = new File(to);
		else
			outputFile = new File(to.substring(tIndex + 5, to.length())); 
		copyFile(inputFile, outputFile);		
	}
	
//	/**
//	 * Create the name of STL file number i
//	 * @param i
//	 * @return
//	 */
//	private static String stlName(int i)
//	{
//	if (uNames == null)
//	{
//		Debug.e("RFO.createLegend(): no list of unique names saved.");
//		return;
//	}
//		return stlPrefix + i + stlSuffix;
//	}
	
	/**
	 * 
	 * Copy each unique STL file to a directory.  Files used more
	 * than once are only copied once.
	 *
	 * @param astltb
	 * @param rfod
	 */
	public static List<String> copySTLs(AllSTLsToBuild astltb, String rfod)
	{
		int u = 0;
		List<String> uniqueNames = new ArrayList<String>();
		for(int i = 0; i < astltb.size(); i++)
		{
			for(int subMod1 = 0; subMod1 < astltb.get(i).size(); subMod1++)
			{
				String s = astltb.get(i).fileAndDirectioryItCameFrom(subMod1);
				astltb.get(i).setUnique(subMod1, u);
				for(int j = 0; j < i; j++)
				{
					for(int subMod2 = 0; subMod2 < astltb.get(j).size(); subMod2++)
					{
						if(s.equals(astltb.get(j).fileAndDirectioryItCameFrom(subMod2)))
						{
							astltb.get(i).setUnique(subMod1, astltb.get(j).getUnique(subMod2));
							break;
						}
					}
				}
				if(astltb.get(i).getUnique(subMod1) == u)
				{
					String un = astltb.get(i).fileItCameFrom(subMod1);
					copyFile(s, rfod + un);
					uniqueNames.add(un);

					String csgFile = CSGReader.CSGFileExists(s);
					if(csgFile != null)
					{
						int sepIndex = csgFile.lastIndexOf(File.separator);
						String justFile = csgFile.substring(sepIndex + 1, csgFile.length());
						copyFile(csgFile, rfod + justFile);
					}

					u++;
				}
			}
		}
		return uniqueNames;
	}
	
	/**
	 * Write a 4x4 homogeneous transform in XML format.
	 * @param trans
	 */
	private void writeTransform(TransformGroup trans)
	{
		Transform3D t = new Transform3D();
		Matrix4d m = new Matrix4d();
		trans.getTransform(t);
		t.get(m);
		xml.push("transform3D");
		 xml.write("row m00=\"" + m.m00 + "\" m01=\"" + m.m01 + "\" m02=\"" + m.m02 + "\" m03=\"" + m.m03 + "\"");
		 xml.write("row m10=\"" + m.m10 + "\" m11=\"" + m.m11 + "\" m12=\"" + m.m12 + "\" m13=\"" + m.m13 + "\"");
		 xml.write("row m20=\"" + m.m20 + "\" m21=\"" + m.m21 + "\" m22=\"" + m.m22 + "\" m23=\"" + m.m23 + "\"");
		 xml.write("row m30=\"" + m.m30 + "\" m31=\"" + m.m31 + "\" m32=\"" + m.m32 + "\" m33=\"" + m.m33 + "\"");
		xml.pop();
	}
	
	/**
	 * Create the legend file
	 *
	 */
	private void createLegend()
	{
		if (uNames == null)
		{
			Debug.e("RFO.createLegend(): no list of unique names saved.");
			return;
		}
		xml = new XMLOut(rfoDir + legendName, "reprap-fab-at-home-build version=\"0.1\"");
		for(int i = 0; i < astl.size(); i++)
		{
			xml.push("object name=\"object-" + i + "\"");
			 xml.push("files");
			  STLObject stlo = astl.get(i);
			  for(int subObj = 0; subObj < stlo.size(); subObj++)
			  {
				  xml.push("file location=\"" + uNames.get(stlo.getUnique(subObj)) + "\" filetype=\"application/sla\" material=\"" + 
						  stlo.attributes(subObj).getMaterial() + "\"");
				  xml.pop();
			  }
			 xml.pop();
			 writeTransform(stlo.trans());
			xml.pop();
		}
		xml.close();
	}
	
	/**
	 * The entire temporary directory with the legend file and ann the STLs is complete.
	 * Compress it into the required rfo file using zip.  Note we delete the temporary files as we
	 * go along, ending up by deleting the directory containing them.
	 *
	 */
	private void compress()
	{
		try
		{
			ZipOutputStream rfoFile = new ZipOutputStream(new FileOutputStream(path + fileName)); 
			File dirToZip = new File(rfoDir); 
			String[] fileList = dirToZip.list(); 
			byte[] buffer = new byte[4096]; 
			int bytesIn = 0; 

			for(int i=0; i<fileList.length; i++) 
			{ 
				File f = new File(dirToZip, fileList[i]); 
				FileInputStream fis = new FileInputStream(f); 
				String zEntry = f.getPath();
				//Debug.a("\n" + zEntry);
				int start = zEntry.indexOf(uniqueName);
				zEntry = zEntry.substring(start + uniqueName.length() + 1, zEntry.length());
				//Debug.a(tempDir);
				//Debug.a(zEntry + "\n");
				ZipEntry entry = new ZipEntry(zEntry); 
				rfoFile.putNextEntry(entry); 
				while((bytesIn = fis.read(buffer)) != -1) 
					rfoFile.write(buffer, 0, bytesIn); 
				fis.close();
			}
			rfoFile.close();
		} catch (Exception e)
		{
			Debug.e("RFO.compress(): " + e);
		}
	}
	
	/**
	 * Warn the user of an overwrite
	 * @return
	 */
	public static boolean checkFile(String pth, String nm)
	{
		File file= new File(pth + nm);
		if(file.exists())
		{
			String[] options = { "OK", "Cancel" };
			int r = JOptionPane.showOptionDialog(null, "The file " + nm + " exists.  Overwrite it?", "Warning",
			        JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE,
			        null, options, options[0]);
			return r == 0;
		}
		return true;
	}
	
	/**
	 * This is what gets called to write an rfo file.  It saves all the parts of allSTL in rfo file fn.
	 * @param fn
	 * @param allSTL
	 */
	public static void save(String fn, AllSTLsToBuild allSTL)
	{
		if(!fn.endsWith(".rfo"))
			fn += ".rfo";
		RFO rfo = new RFO(fn, allSTL);
		if(!RFO.checkFile(rfo.path, rfo.fileName))
			return;
		rfo.uNames = RFO.copySTLs(allSTL, rfo.rfoDir);
		rfo.createLegend();
		rfo.compress();
		File t = new File(rfo.tempDir);
		recursiveDelete(t);
	}
	
	//******************************************************************************************
	//
	// .rfo reading
	
	/**
	 * Arrghhh!!!!
	 */
	private String processSeparators(String is)
	{
		String result = "";
		for(int i = 0; i < is.length(); i++)
		{
			if(is.charAt(i) == '\\')
			{
				if(File.separator.charAt(0) == '/')
					result += '/';
				else
					result += '\\';
			} else if(is.charAt(i) == '/')
			{
				if(File.separator.charAt(0) == '\\')
					result += '\\';
				else
					result += '/';
			} else
				result += is.charAt(i);
		}
		
		return result;
	}
	
	/**
	 * This uncompresses the zip that is the rfo file into the temporary directory.
	 */
	private void unCompress()
	{
		try
		{
			byte[] buffer = new byte[4096];
			int bytesIn;
			ZipFile rfoFile = new ZipFile(path + fileName);
			Enumeration<? extends ZipEntry> allFiles = rfoFile.entries();
			while(allFiles.hasMoreElements())
			{
				ZipEntry ze = (ZipEntry)allFiles.nextElement();
				InputStream is = rfoFile.getInputStream(ze);
				String fName = processSeparators(ze.getName());
				File element = new File(tempDir + fName);
				org.reprap.Main.ftd.add(element);
				FileOutputStream os = new FileOutputStream(element);
				while((bytesIn = is.read(buffer)) != -1) 
					os.write(buffer, 0, bytesIn);
				os.close();
			}
		} catch (Exception e)
		{
			Debug.e("RFO.unCompress(): " + e);
		}
	}
	
	/**
	 * This reads the legend file and does what it says.
	 *
	 */
	private void interpretLegend()
	{
		@SuppressWarnings("unused")
		XMLIn xi = new XMLIn(rfoDir + legendName, this);
	}
	
	/**
	 * This is what gets called to read an rfo file from filename fn.
	 * @param fn
	 * @return
	 */
	public static AllSTLsToBuild load(String fn)
	{
		if(!fn.endsWith(".rfo"))
			fn += ".rfo";
		RFO rfo = new RFO(fn, null);
		File rfod = new File(rfo.tempDir);
		org.reprap.Main.ftd.add(rfod);
		rfod = new File(rfo.rfoDir);
		org.reprap.Main.ftd.add(rfod);
		
		rfo.astl = new AllSTLsToBuild();
		rfo.unCompress();
		try
		{
			rfo.interpretLegend();
		} catch (Exception e)
		{
			Debug.e("RFO.load(): exception - " + e.toString());
		}
		
		// Tidy up - delete the temporary files and the directory
		// containing them.
		
//		File td = new File(rfo.rfoDir);
//		String[] fileList = td.list(); 
//		for(int i=0; i<fileList.length; i++) 
//		{ 
//			File f = new File(rfo.rfoDir, fileList[i]);
//			if(!f.delete())
//				Debug.e("RFO.AllSTLsToBuild(): Can't delete file: " + fileList[i]);
//		}
//		if(!td.delete())
//			Debug.e("RFO.AllSTLsToBuild(): Can't delete file: " + rfo.rfoDir);
		return rfo.astl;
	}
}
