//==============================================================================
//	
//	Copyright (c) 2002-
//	Authors:
//	* Dave Parker <d.a.parker@cs.bham.ac.uk> (University of Birmingham/Oxford)
//	
//------------------------------------------------------------------------------
//	
//	This file is part of PRISM.
//	
//	PRISM is free software; you can redistribute it and/or modify
//	it under the terms of the GNU General Public License as published by
//	the Free Software Foundation; either version 2 of the License, or
//	(at your option) any later version.
//	
//	PRISM is distributed in the hope that it will be useful,
//	but WITHOUT ANY WARRANTY; without even the implied warranty of
//	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//	GNU General Public License for more details.
//	
//	You should have received a copy of the GNU General Public License
//	along with PRISM; if not, write to the Free Software Foundation,
//	Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//	
//==============================================================================

package prism;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

public class SBML2Prism extends Reactions2Prism implements EntityResolver
{
	/**
	 * Calling point for command-line script:
	 * e.g. java -cp classes prism.SBML2Prism myfile.sbml 100
	 * (100 denotes (integer) maximum for species population sizes, default is 100)
	 * (also used to compute amounts from (real-valued) concentrations)
	 */
	public static void main(String args[])
	{
		PrismLog errLog = new PrismPrintStreamLog(System.err);
		try {
			if (args.length < 1) {
				System.err.println("Usage: java -cp classes prism.SBML2Prism <sbml_file> [max_amount]");
				System.exit(1);
			}
			SBML2Prism sbml2prism = new SBML2Prism(errLog);
			try {
				if (args.length > 1)
					sbml2prism.setMaxAmount(Integer.parseInt(args[1]));
			} catch (NumberFormatException e) {
				throw new PrismException("Invalid max amount \"" + args[1] + "\"");
			}
			sbml2prism.translate(new File(args[0]));
		} catch (PrismException e) {
			errLog.println("Error: " + e.getMessage() + ".");
		}
	}

	// Constructors

	public SBML2Prism()
	{
		super();
	}

	public SBML2Prism(PrismLog mainLog)
	{
		super(mainLog);
	}

	/**
	 * Main method: load SBML file, process and send resulting PRISM file to stdout
	 */
	public void translate(File file) throws PrismException
	{
		// Read in SBML
		Document doc = parseSBML(file);
		checkSBMLVersion(doc);
		extractModelFromSBML(doc);
		// Generate PRISM code
		prismCodeHeader = "// File generated by automatic SBML-to-PRISM conversion\n";
		prismCodeHeader += "// Original SBML file: " + file.getPath() + "\n\n";
		convertToPRISMCode(System.out);
	}

	/**
	 * Parse SBML file {@code file} and return as an XML Document object.
	 */
	private Document parseSBML(File file) throws PrismException
	{
		DocumentBuilderFactory factory;
		DocumentBuilder builder;
		Document doc = null;

		// Create XML parser
		factory = DocumentBuilderFactory.newInstance();
		factory.setValidating(false);
		factory.setIgnoringElementContentWhitespace(true);
		try {
			builder = factory.newDocumentBuilder();
			builder.setEntityResolver(this);
			builder.setErrorHandler(new ErrorHandler()
			{
				public void fatalError(SAXParseException e) throws SAXException
				{
					throw e;
				}

				public void error(SAXParseException e) throws SAXException
				{
					throw e;
				}

				public void warning(SAXParseException e)
				{
				}
			});
		} catch (ParserConfigurationException e) {
			throw new PrismException("Couldn't create XML parser");
		}

		// Parse
		try {
			doc = builder.parse(file);
		} catch (IOException e) {
			throw new PrismException("Couldn't load file \"" + file.getPath() + "\": " + e.getMessage());
		} catch (SAXException e) {
			throw new PrismException("Invalid XML file:\n" + e.getMessage());
		}

		return doc;
	}

	// Function used by parseSBML() above to find the SBML DTD
	// (this currently unused since we do not validate the SBML file when reading)
	// (and since the DTD specified in the SBML files is not local)
	// (if validation is enabled, put the DTD file "sbml.dtd" in PRISM's "dtds" directory)

	public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException
	{
		InputSource inputSource = null;

		// override the resolve method for the dtd
		if (systemId.endsWith("dtd")) {
			// get appropriate dtd from classpath
			InputStream inputStream = this.getClass().getResourceAsStream("dtds/sbml.dtd");
			if (inputStream != null)
				inputSource = new InputSource(inputStream);
		}

		return inputSource;
	}

	/**
	 * Check that we can handle whatever level/version this file is.
	 */
	private void checkSBMLVersion(Document doc) throws PrismException
	{
		String level, s;

		level = doc.getDocumentElement().getAttribute("level");
		if (!("2".equals(level))) {
			s = "The translator only handles Level 2 SBML files - this is a Level " + level + " file";
			throw new PrismException(s);
		}
	}

	/**
	 * Build the reaction set model from a parsed SBML file.
	 */
	private void extractModelFromSBML(Document doc) throws PrismException
	{
		Element e, e_model, e_list;
		Element e_comp, e_species, e_parameter, e_reaction, e_kinetics, e_mathml;
		NodeList nodes, nodes2;
		Node node = null;
		Species species;
		Parameter parameter;
		Reaction reaction;
		int i, j, k, n, m;
		double d;
		String s, s2;
		boolean found;

		// Get "model" element of SBML file
		nodes = doc.getDocumentElement().getElementsByTagName("model");
		e_model = (Element) nodes.item(0);

		// Process compartment info (if present)
		// Just need to extract the size in case used
		e_list = (Element) e_model.getElementsByTagName("listOfCompartments").item(0);
		nodes = e_list.getElementsByTagName("compartment");
		n = nodes.getLength();
		// Make sure there is at most one
		if (n > 1)
			throw new PrismException("Only one compartment is permitted");
		// And if present, store size info
		compartmentName = null;
		if (n == 1) {
			e_comp = (Element) nodes.item(0);
			if (!"".equals(e_comp.getAttribute("size"))) {
				compartmentName = e_comp.getAttribute("id");
				if ("".equals(compartmentName))
					throw new PrismException("Missing compartment name");
				compartmentSize = Double.parseDouble(e_comp.getAttribute("size"));
			}
		}

		// Process list of species
		speciesList = new ArrayList<Species>();
		e_list = (Element) e_model.getElementsByTagName("listOfSpecies").item(0);
		nodes = e_list.getElementsByTagName("species");
		n = nodes.getLength();
		for (i = 0; i < n; i++) {
			e_species = (Element) nodes.item(i);
			speciesId = e_species.getAttribute("id");
			initialAmountString = e_species.getAttribute("initialAmount");
			if ("".equals(initialAmountString))
				throw new PrismException("Missing initial amount for species " + speciesId);
			try {
				d = Double.parseDouble(initialAmountString);
			} catch (NumberFormatException nfe) {
				String msg = "Badly formatted initialAmount \"" + initialAmountString + "\" for species " + speciesId;
				throw new PrismException(msg);
			}
			species = new Species(speciesId, e_species.getAttribute("name"), d);
			s = e_species.getAttribute("boundaryCondition");
			if (s.equals("true"))
				species.boundaryCondition = true;
			speciesList.add(species);
		}

		// Process list of parameters (if present)
		parameterList = new ArrayList<Parameter>();
		// Look at direct children only (there might be listOfParameters nodes lower in the tree)
		nodes = e_model.getChildNodes();
		n = nodes.getLength();
		found = false;
		for (i = 0; i < n; i++) {
			node = nodes.item(i);
			if ("listOfParameters".equals(node.getNodeName())) {
				found = true;
				break;
			}
		}
		if (found) {
			e_list = (Element) node;
			nodes = e_list.getElementsByTagName("parameter");
			n = nodes.getLength();
			for (i = 0; i < n; i++) {
				e_parameter = (Element) nodes.item(i);
				parameter = new Parameter(e_parameter.getAttribute("id"), e_parameter.getAttribute("value"));
				parameterList.add(parameter);
			}
		}

		// Process list of reactions
		reactionList = new ArrayList<Reaction>();
		e_list = (Element) e_model.getElementsByTagName("listOfReactions").item(0);
		nodes = e_list.getElementsByTagName("reaction");
		n = nodes.getLength();
		for (i = 0; i < n; i++) {

			// Process a single reaction...
			e_reaction = (Element) nodes.item(i);
			reaction = new Reaction(e_reaction.getAttribute("id"), e_reaction.getAttribute("name"));

			// Reactant list
			e_list = (Element) e_reaction.getElementsByTagName("listOfReactants").item(0);
			if (e_list != null) {
				nodes2 = e_list.getElementsByTagName("speciesReference");
				m = nodes2.getLength();
				for (j = 0; j < m; j++) {
					e = (Element) nodes2.item(j);
					// Get species name of product
					s = e.getAttribute("species");
					// Get stoichiometry if present
					s2 = e.getAttribute("stoichiometry");
					k = 1;
					if (s2.length() > 0)
						try {
							k = Integer.parseInt(s2);
						} catch (NumberFormatException ex) {
							throw new PrismException("Invalid stoichiometry value \"" + s2 + "\"");
						}
					// Add reactant to reaction
					reaction.addReactant(s, k);
				}
			}

			// Product list
			e_list = (Element) e_reaction.getElementsByTagName("listOfProducts").item(0);
			if (e_list != null) {
				nodes2 = e_list.getElementsByTagName("speciesReference");
				m = nodes2.getLength();
				for (j = 0; j < m; j++) {
					e = (Element) nodes2.item(j);
					// Get species name of product
					s = e.getAttribute("species");
					// Get stoichiometry if present
					s2 = e.getAttribute("stoichiometry");
					k = 1;
					if (s2.length() > 0)
						try {
							k = Integer.parseInt(s2);
						} catch (NumberFormatException ex) {
							throw new PrismException("Invalid stoichiometry value \"" + s2 + "\"");
						}
					// Add product to reaction
					reaction.addProduct(s, k);
				}
			}

			// Kinetic law
			e_kinetics = (Element) e_reaction.getElementsByTagName("kineticLaw").item(0);
			e_mathml = (Element) e_kinetics.getElementsByTagName("math").item(0);
			reaction.setKineticLaw(e_mathml);
			e_list = (Element) e_kinetics.getElementsByTagName("listOfParameters").item(0);
			if (e_list != null) {
				nodes2 = e_list.getElementsByTagName("parameter");
				m = nodes2.getLength();
				for (j = 0; j < m; j++) {
					e = (Element) nodes2.item(j);
					reaction.addParameter(e.getAttribute("id"), e.getAttribute("value"));
				}
			}

			// Add reaction to list
			reactionList.add(reaction);
		}
	}
}
