package vgp.tutor.loader;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StreamTokenizer;
import java.io.Writer;
import java.util.BitSet;
import java.util.Date;

import jv.loader.PgLoader;
import jv.loader.PgLoaderIf;
import jv.loader.PvDisplayOption;
import jv.number.PuString;
import jv.object.PsConfig;
import jv.object.PsDebug;
import jv.project.PgGeometryIf;
import jv.project.PgJvxSrc;
import jv.project.PvGeometryIf;
import jv.vecmath.PdVector;
import jv.vecmath.PiVector;

/**
 * Loader for geometry files given in a user defined file format.
 * <p>
 * In JavaView the file name convention is that Abc is replaced
 * with a user defined file name extension of three characters.
 * 
 * @author		Konrad Polthier
 * @version		07.03.03, 1.10 revised (kp) Index of first vertex may now be changed.<br>
 *					09.11.01, 1.00 created (kp)
 */
public class PgAbcLoader implements PgLoaderIf {
	/** Index of first vertex. */
	protected		int		m_indexFirstVertex = 0;
	
	/**
	 * Parse a <code>.abc</code> file and fill instance variables of this class.
	 * This is a helper routine of {@link PgAbcLoader#read(BufferedReader) read(BufferedReader)}.
	 *
	 * @see			PgAbcLoader#read(BufferedReader)
	 * @author		Konrad Polthier
	 * @version		07.03.03, 1.10 revised (kp) Index of first vertex is used.<br>
	 *					09.11.01, 1.00 created (kp)
	 */
	protected boolean parse(BufferedReader bufReader, PgJvxSrc geom) {
		StreamTokenizer st = new StreamTokenizer(bufReader);
		st.ordinaryChars('/', '/');
		st.ordinaryChars('_', '_');

		// In the vertex section do not care about end-of-line
		// since all vertices are assumed to have 3 coordinates
		// but later in the element section.
		st.eolIsSignificant(false);
		st.ordinaryChar('-');
		st.ordinaryChar('+');
		st.ordinaryChar('.');
		st.ordinaryChars('0', '0');
		st.ordinaryChars('1', '9');
		st.wordChars('^', '^');
		st.wordChars('&', '&');
		st.wordChars('$', '$');
		st.wordChars('(', '(');
		st.wordChars(')', ')');
		st.wordChars('~', '~');
		st.wordChars('@', '@');
		st.wordChars('-', '-');
		st.wordChars('+', '+');
		st.wordChars('*', '*');
		st.wordChars('0', '0');
		st.wordChars('1', '9');
		st.wordChars('.', '.');
		st.wordChars(',', ',');
		st.wordChars(';', ';');
		st.wordChars('_', '_');
		st.wordChars(':', ':');
		
		st.wordChars('_', '_');
		st.commentChar('#');

		try {
scan:
			// Parse list of vertices
			st.nextToken();
			int nov = Integer.parseInt(st.sval);
			PdVector [] vertex	= new PdVector[nov];
			for (int i=0; i<nov; i++) {
				vertex[i] = new PdVector(3);
				for (int j=0; j<3; j++) {
					st.nextToken();
					double val = PgLoader.parseDouble(st.sval);
					vertex[i].setEntry(j, val);
				}
			}
			
			// Parse list of elements
			int noe					= 0;
			PiVector [] element	= null;
			st.nextToken(); // Parse number of elements
			if (st.sval != null) {
				noe		= Integer.parseInt(st.sval);
				element	= new PiVector[noe];
			
				// maximal number of edges of a face
				int maxFaceSize		= 200;
				int [] face				= new int[maxFaceSize];

				// In the element section enable end-of-line significance
				// since elements may have different number of vertices
				st.eolIsSignificant(true);
				// Parse end-of-line of previous line
				st.nextToken();

				st.parseNumbers();
				for (int i=0; i<noe; i++) {
					int numVerticesPerFace = 0;
					boolean bEOL = false;
					while (!bEOL) {
						switch (st.nextToken()) {
						default:
							if (PsDebug.WARNING) PsDebug.warning("parsing broke abnormally in line="+st.lineno()+" reading st.sval="+st.sval+", st.ttype="+st.ttype);
							return false;
						case StreamTokenizer.TT_EOL:
							bEOL = true;
							break;
						case StreamTokenizer.TT_NUMBER:
							face[numVerticesPerFace++] = (int) st.nval - m_indexFirstVertex;
							break;
						}
					}
					element[i] = new PiVector(numVerticesPerFace);
					element[i].set(face, numVerticesPerFace);
				}
			}
			
			// Assign parsed data to geometry:
			
			// Assign vertex array
			if (nov > 0) {
				geom.setNumVertices(nov);
				geom.setVertices(vertex);

				// HACK: Use size of first vertex as dimension,
				// instead we should check size of all vertices.
				geom.setDimOfVertices(vertex[0].getSize());
			}

			// Assign element array
			if (noe > 0) {
				geom.setNumElements(noe);
				geom.setElements(element);
				int dimOfElements = element[0].getSize();
				for(int i=1; i<noe; i++) {
					if (element[i].getSize() != dimOfElements) {
						dimOfElements = -1;
						break;
					}
				}
				geom.setDimOfElements(dimOfElements);
			}
		} catch (IOException e) {
			if (PsDebug.WARNING) PsDebug.warning("Exception thrown = "+e.toString()+
															 "\n\tparsing broke abnormally in line="+st.lineno()+
															 ",\n\treading st.sval="+st.sval+
															 ",\n\tst.ttype="+st.ttype);
			return false;
		}

		return true;
	}
	/**
	 * Write an array of geometries into an output stream writer.
	 * 
	 * @see			#read(BufferedReader)
	 * @param		writer	Write all data to this stream
	 * @param		geomArr	Array with geometries to save
	 * @return		<code>true</code> on success.
	 * @version		07.03.03, 1.10 revised (kp) Index of first vertex is used.<br>
	 *					09.11.01, 1.00 created (kp)
	 */
	public boolean write(Writer writer, PgJvxSrc [] geomArr) throws IOException {
		if (geomArr==null || geomArr.length==0 || geomArr[0]==null) {
			if (PsDebug.WARNING) PsDebug.warning("missing geometry");	
			return false;
		}
		PgJvxSrc geom = geomArr[0];
		writer.write("# Produced with JavaView v."+PsConfig.getVersion()+"\n");
		writer.write("# JavaView is "+PsConfig.getCopyright()+", "+PsConfig.getHomepage()+"\n");
		writer.write("# by "+PsConfig.getAuthors()+"\n");
		writer.write("#     File Format = ABC (tutorial demonstration)\n");
		writer.write("#     Geometry    = "+geom.getName()+"\n");
		writer.write("#     Date        = "+(new Date()).toString()+"\n#\n");
		int numOfVertices = geom.getNumVertices();
		writer.write("#     Number of Vertices    = "+numOfVertices+"\n");
		int numOfElements = geom.getNumElements();
		writer.write("#     Number of Elements	   = "+numOfElements+"\n");
		writer.write("#     Index of First Vertex = "+m_indexFirstVertex+"\n");
		writer.write("#\n# End of Header\n");

		// Write vertices
		PdVector [] vertex	= geom.getVertices();
		writer.write(String.valueOf(numOfVertices)+"\n");
		for (int i=0; i<numOfVertices; i++)
			writer.write(PuString.toString(vertex[i].m_data)+"\n");

		// Write elements
		PiVector [] element	= geom.getElements();
		writer.write(String.valueOf(numOfElements)+"\n");
		for (int i=0; i<numOfElements; i++) {
			int len = element[i].m_data.length;
			if (len == 0) {
				if (PsDebug.WARNING) PsDebug.warning("Missing vertex references in element["+i+"]");
				return false;
			}
			String data = String.valueOf(element[i].m_data[0]+m_indexFirstVertex);
			for (int k=1; k<len; k++)
				data += " "+String.valueOf(element[i].m_data[k]+m_indexFirstVertex);
			writer.write(data+"\n");
		}

		return true;
	}
	/**
	 * Determines if the loaded geometry requires some optimization.
	 * The method in this class prohibits optimization and always returns false.
	 * <p>
	 * For example, some file formats do not store a connected mesh
	 * but provide individual vertices for each face every time the face is menetioned.
	 * In this case, a loader manager may identify equal vertices.
	 * <p>
	 * A similar method in the loader manager PgLoader may override the individual settings
	 * of each loader.
	 * 
	 * @see			jv.loader.PgLoader#isEnabledOptimization()
	 * @see			jv.loader.PgLoader#setEnabledOptimization(boolean)
	 * @return		<code>true</code> if loader is enabled for optimization.
	 * @version		06.10.03, 1.00 created (kp)
	 * @since		JavaView 2.99.018
	 */
	public			boolean				isEnabledOptimization() { return false; }
	/**
	 * Set the geometry options which indicate which parts of a geometry shall be saved.
	 * This method must be called before writing the geometry.
	 * Note, not all geometry file formats support geometry options.
	 * @version		19.04.03, 1.00 created (kp)
	 * @since		JavaView 2.68.000
	 */
	public			void					setGeometryOption(BitSet option) { return; }
	/**
	 * Get display options which some parsers and geometry file formats supply.
	 * This geometry file format does not support loading/saving of display options.
	 */
	public			PvDisplayOption	getDisplayOption()			{ return null; }
	/**
	 * Set the display options to be saved within the geometry file.
	 * This method must be called before writing the geometry.
	 * This geometry file format does not support loading/saving of display options.
	 */
	public			void					setDisplayOption(PvDisplayOption option) { return; }
	/**
	 * Determine if currently loaded data is an animation.
	 * <p>
	 * This method may be called after successful parsing an input stream
	 * with the method {@link #load(BufferedReader) load(BufferedReader)}.
	 * @see			#load(BufferedReader)
	 * @return		<code>true</code> if loaded data is an animation.
	 */
	public			boolean				isAnimation() {return false;}
	/** Set flag whether current geometry data is an animation. */
	public			void					setAnimation(boolean flag) { return; }
	/** A list of loaded geometries. */
	protected		PgJvxSrc []			m_geomArr;
	/**
	 * Retrieve the previously loaded animations.
	 * <p>
	 * This method may be called after successful parsing an input stream
	 * with the method {@link #load(BufferedReader) load(BufferedReader)} and
	 * if {@link #isAnimation() isAnimation()} returns <code>true</code>.
	 * @see			#load(BufferedReader)
	 * @return		Double array of JVX geometries.
	 * @author		Konrad Polthier
	 *	@version		21.08.02, 1.00 created (kp)
	 */
	public PgJvxSrc[][] getAnimations() { return null; }
	/**
	 * Retrieve the previously loaded geometries.
	 * <p>
	 * This method may be called after successful parsing an input stream
	 * with the method {@link #load(BufferedReader) load(BufferedReader)} and
	 * if {@link #isAnimation() isAnimation()} returns <code>false</code>.
	 * @see			#load(BufferedReader)
	 * @return		Array of JVX geometries.
	 * @author		Konrad Polthier
	 *	@version		21.08.02, 1.00 created (kp)
	 */
	public PgJvxSrc[] getGeometries() { return m_geomArr; }
	/**
	 * Currently, this method always returns <code>null</code>.
	 * <p>
	 * Return the loaded geometry in a more specialized class. This method
	 * allows a loader to return a geometry class which best fits the
	 * data found in a geometry file.
	 * <p>
	 * The loader manager {@link jv.loader.PgLoader jv.loader.PgLoader} in JavaView
	 * will first check if a loader returns specialized geometies, and if
	 * not, check for a data supplied in a container class PgJvxSrc.
	 * <p>
	 * This method may be called after successful parsing an input stream
	 * with the method {@link #load(BufferedReader) load(BufferedReader)} and
	 * if {@link #isAnimation() isAnimation()} returns <code>false</code>.
	 * @see			#getGeometries()
	 * @return		Array of geometries whose actual class type may vary between loaders.
	 * @author		Konrad Polthier
	 *	@version		06.03.03, 1.00 created (kp)
	 * @since		JavaView 2.53
	 */
	public			PgGeometryIf []			getSpecializedGeometries() { return null; }
	/**
	 * Read an ABC geometry file and return an array of new geometries.
	 * <p>
	 * After successful parsing retrieve the loaded geometries or animations
	 * using the methods {@link #getGeometries() getGeometries()} respectively
	 * {@link #getAnimations() getAnimations()} depending on the return value
	 * of the method {@link #isAnimation() isAnimation()}.
	 * @see			#getAnimations()
	 * @see			#getGeometries()
	 * @see			#isAnimation()
	 * @param		in			BufferedReader to read textual data from.
	 * @return		<code>true</code> if parsing was successfull.
	 * @author		Konrad Polthier
	 *	@version		21.08.02, 1.00 created (kp)
	 */
	public boolean load(BufferedReader in) {
		if (in == null) {
			if (PsDebug.WARNING) PsDebug.warning("missing reader");
			return false;
		}
		PgJvxSrc geom = new PgJvxSrc();
		if (!parse(in, geom)) {
			if (PsDebug.WARNING) PsDebug.warning("error during parsing of reader");
			return false;
		}
		if (geom.getNumElements() == 0) {
			geom.setType(PvGeometryIf.GEOM_POINT_SET);
			geom.showVertices(true);
			geom.setGlobalVertexSize(1.);
		} else {
			geom.setType(PvGeometryIf.GEOM_ELEMENT_SET);
			geom.assureNeighbours();
			geom.showEdges(true);
			geom.showElements(true);
		}
		m_geomArr = new PgJvxSrc[] { geom };
		return true;
	}
	/**
	 * Read an ABC geometry file and return an array of new geometries.
	 * <p>
	 * The returned array of geometry may contain keyframes of
	 * an animation. This is checked with the method {@link #isAnimation() isAnimation()}.
	 * <p>
	 * Alternatively, one may call load(BuffereredReader) followed by getGeometries()
	 * resp. getAnimations() to have a finer control about which information one
	 * wants to use from a source. For example, optional display information must
	 * be retrieve by a subsequent call getDisplayOption() if available.
	 * 
	 * @see			#write(Writer, PgJvxSrc [])
	 * @param		in			BufferedReader to read textual data from.
	 * @return		Array of JVX geometries.
	 * @author		Konrad Polthier
	 *	@version		10.06.01, 1.10 revised (kp) Return an array instead a single geometry.<br>
	 *					26.02.01, 1.00 created (kp)
	 */
	public PgJvxSrc [] read(BufferedReader in) {
		if (!load(in))
			return null;
		return getGeometries();
	}
	
	/**
	 * Vertices are assigned an index starting at the given value.
	 * Default index counting starts at 0. For example, OBJ, OFF and JVX
	 * use index counting [0, numOfVertices-1] while BUY use indices
	 * in the range [1, numOfVertices].
	 * <p>
	 * The faces of a mesh are defined by referencing the vertices of
	 * a face by their index.
	 * 
	 * @return		index of first vertex
	 * @version		07.06.03, 1.00 created (kp)
	 * @since		JavaView 2.81.000
	 */
	public int getFirstVertexIndex() { return m_indexFirstVertex; }
	/**
	 * Vertices are assigned an index starting at the given value.
	 * Default index counting starts at 0. For example, OBJ, OFF and JVX
	 * use index counting [0, numOfVertices-1] while BUY use indices
	 * in the range [1, numOfVertices].
	 * <p>
	 * The faces of a mesh are defined by referencing the vertices of
	 * a face by their index.
	 * 
	 * @param		index		index of first vertex
	 * @version		07.06.03, 1.00 created (kp)
	 * @since		JavaView 2.81.000
	 */
	public void setFirstVertexIndex(int index) {
		m_indexFirstVertex = index;
	}
}
