// PathVisio,
// a tool for data visualization and analysis using Biological Pathways
// Copyright 2006-2011 BiGCaT Bioinformatics
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// $Id: MappToGmml.java,v 1.5 2005/10/21 12:33:27 gontran Exp $
package org.pathvisio.core.model;

import java.awt.Color;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.bridgedb.DataSource;
import org.bridgedb.bio.Organism;
import org.pathvisio.core.debug.Logger;
import org.pathvisio.core.util.Utils;
import org.pathvisio.core.view.ShapeRegistry;

/**
 * The class MappFormat is responsible for all interaction with
 * .mapp files (GenMapp pathway format). Here is also codified all the
 * assumptions about the .mapp format.
 *
 *
 * Certain aspects of the Mapp <-> GPML conversion are getting quite hairy,
 * as there are certain inconsistencies within GenMAPP. We intend to isolate
 * all these inconsistencies just within this class, so they don't propagate
 * to the rest of PathVisio.
 *
 * For example:
 *
 * Color: The "Color" column in the Objects table sometimes corresponds to
 * fillColor (for Rect and Oval) and sometimes to LineColor (for Label, Line,
 * Arc, Brace, etc.). This gets confusing especially for Arc and Brace, because
 * they all get mapped to ObjectType.Shape. For those objects that can have
 * a fill color, the value of the color column can be -1, which indicates
 * transparency.
 *
 * Size: certain objects (i.e. Polygons) always maintain equal width and height.
 * other objects always have a fixed size and even a fixed rotation in the case
 * of CELLB.
 */
public class MappFormat extends AbstractPathwayFormat
{
	private static final String SQL_INFO_INSERT =
		"INSERT INTO INFO (Title, MAPP, GeneDB, Version, Author, " +
		"Maint, Email, Copyright, Modify, Remarks, BoardWidth, BoardHeight, " +
		"WindowWidth, WindowHeight, Notes) " +
		"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
	// note: column GeneDBVersion is not in all mapps.
	// Notably the mapps converted from kegg are different from the rest.
	private static final String SQL_OBJECTS_INSERT =
		"INSERT INTO OBJECTS (ObjKey, ID, SystemCode, Type, CenterX, " +
		"CenterY, SecondX, SecondY, Width, Height, Rotation, " +
		"Color, Label, Head, Remarks, Image, Links, Notes) " +
		"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";

	private static final String SQL_INFO_SELECT =
		"SELECT Title, MAPP, GeneDB, Version, Author, " +
		"Maint, Email, Copyright, Modify, Remarks, BoardWidth, BoardHeight, " +
		"WindowWidth, WindowHeight, Notes " +
		"FROM INFO";
	private static final String SQL_OBJECTS_SELECT =
		"SELECT ObjKey, ID, SystemCode, Type, CenterX, " +
		"CenterY, SecondX, SecondY, Width, Height, Rotation, " +
		"Color, Label, Head, Remarks, Image, Links, Notes " +
		"FROM OBJECTS";

    private static final String DATABASE_AFTER = ";DriverID=22;READONLY=true";
    private static final String DATABASE_BEFORE =
            "jdbc:odbc:Driver={Microsoft Access Driver (*.mdb)};DBQ=";

    private static final int PIXELS_TO_MAPP = 15;

    //  These constants below define columns in the info table.
    //  they are linked to the order of columns in the sqlInfoSelect
    //  statement above
	private static final int ICOL_TITLE = 0;
	private static final int ICOL_MAPP = 1;
	private static final int ICOL_GENEDB = 2;
	private static final int ICOL_VERSION = 3;
	private static final int ICOL_AUTHOR = 4;
	private static final int ICOL_MAINT = 5;
	private static final int ICOL_EMAIL = 6;
	private static final int ICOL_COPYRIGHT = 7;
	private static final int ICOL_MODIFY = 8;
	private static final int ICOL_REMARKS = 9;
	private static final int ICOL_BOARDWIDTH = 10;
	private static final int ICOL_BOARDHEIGHT = 11;
	private static final int ICOL_WINDOWWIDTH = 12;
	private static final int ICOL_WINDOWHEIGHT = 13;
	private static final int ICOL_NOTES = 14;

	// these constants define the columns in the Objects table.
	// they are linked to the order of columns in the SQL_OBJECTS_SELECT
	// statement above.
	private static final int COL_OBJKEY = 0;
	private static final int COL_ID = 1;
	private static final int COL_SYSTEMCODE = 2;
	private static final int COL_TYPE = 3;
	private static final int COL_CENTERX = 4;
	private static final int COL_CENTERY = 5;
	private static final int COL_SECONDX = 6;
	private static final int COL_SECONDY = 7;
	private static final int COL_WIDTH = 8;
	private static final int COL_HEIGHT = 9;
	private static final int COL_ROTATION = 10;
	private static final int COL_COLOR = 11;
	private static final int COL_LABEL = 12;
	private static final int COL_HEAD = 13;
	private static final int COL_REMARKS = 14;
	private static final int COL_IMAGE = 15;
	private static final int COL_LINKS = 16;
	private static final int COL_NOTES = 17;

    /**
     * MAPPTmpl.gtp is a template access database for newly generated
     * mapp's. This file should be
     * in the classpath, normally in resources.jar.
     */
	private static String mappTemplateFile = "MAPPTmpl.gtp";

    static void readFromMapp (String filename, Pathway data)
    	throws ConverterException
    {
    	String database = DATABASE_BEFORE + filename + DATABASE_AFTER;

    	try
    	{
	    	// Load Sun's jdbc-odbc driver
	        Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
    	} catch (ClassNotFoundException cnfe)
    	{
    		// decoupling: wrap classnotfoundexception into converterexception
    		ConverterException ce = new ConverterException("Class not found exception in converter");
    		ce.setStackTrace(cnfe.getStackTrace());
    		throw ce;
    	}
        Logger.log.debug ("Connection string: " + database);

		// Create the connection to the database

        try
        {
	        Connection con = DriverManager.getConnection(database, "", "");

	        Statement s = con.createStatement();

	        Logger.log.trace ("READING INFO TABLE");
	        // first do the INFO table, only one row.
		    {
		        ResultSet r = s.executeQuery(SQL_INFO_SELECT);
		        r.next();
		        int cCol = r.getMetaData().getColumnCount();
		        String[] row = new String[cCol];
		        for (int i = 0; i < cCol; ++i) row[i] = r.getString(i + 1);

		        copyMappInfo(row, data, filename);
	    	}

		    Logger.log.trace ("READING OBJECTS TABLE");
	        // now do the OBJECTS table, multiple rows
	        {
		        ResultSet r = s.executeQuery(SQL_OBJECTS_SELECT);
		        int cCol = r.getMetaData().getColumnCount();
		        String[] row = new String[cCol];
		        while (r.next())
		        {
		        	for (int i = 0; i < cCol; ++i) row[i] = r.getString(i + 1);
		        	copyMappObjects(row, data);
		        }
	        }
        }
        catch (SQLException sqle)
        {
        	// decoupling: wrap sqlexception into converterexception
    		ConverterException ce = new ConverterException("SQLException while converting");
    		ce.setStackTrace(sqle.getStackTrace());
    		throw ce;
        }
    }

    private static void copyResource(String resource, java.io.File destination) throws IOException
    {
		try {
			ClassLoader cl = MappFormat.class.getClassLoader();
			InputStream inStream = cl.getResourceAsStream(resource);

			java.io.FileOutputStream outStream=new java.io.FileOutputStream(destination);

			int len;
			byte[] buf=new byte[2048];

			while ((len=inStream.read(buf))!=-1) {
				outStream.write(buf,0,len);
			}
		} catch (Exception e) {
			throw new IOException("Can't copy resource "+mappTemplateFile+" -> "+destination+".\n" + e.getMessage());
		}
	}

    public static void exportMapp (String filename,
    		String[] mappInfo, List<String[]> mappObjects) throws ConverterException
    {
        String database = DATABASE_BEFORE + filename + ";DriverID=22";

        try {
        	copyResource (mappTemplateFile, new File(filename));

            // Load Sun's jdbc-odbc driver
            Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");

            // Create the connection to the database
            Connection con = DriverManager.getConnection(database, "", "");

            // Create a new sql statement

    		PreparedStatement sInfo = con.prepareStatement(SQL_INFO_INSERT);
            PreparedStatement sObjects = con.prepareStatement(SQL_OBJECTS_INSERT);


            int k = 1;
            for (String[] row : mappObjects)
            {
    			sObjects.setInt (1, k);
    			for (int j = 1; j < row.length; ++j)
    			{
    				Logger.log.trace ("[" + (j + 1) + "] " + row[j]);
//    				System.err.println ("[" + (j + 1) + "] " + row[j]);
    				if (j >= COL_REMARKS && j <= COL_LINKS)
    				{
    					if (row[j] != null && row[j].equals("")) row[j] = null;
    					sObjects.setObject(j + 1, row[j], Types.LONGVARCHAR);
    					// bug workaround, see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4401822
    				}
    				else if (j >= COL_CENTERX && j <= COL_ROTATION)
    				{
    					//NOTE: by using setDouble, we prevent period <-> comma digit symbol issues.
    					//NOTE: could be optimized, now we convert
    					// double to string and back to double again. but at least this works.
    					sObjects.setDouble (j + 1, Double.parseDouble(row[j]));
    				}
    				else
    				{
    					// the line below is a bugfix for
    					// Hs_Contributed_20060824/cellular_process-GenMAPP/Hs_Signaling_of_Hepatocyte_Growth_Factor_Receptor_Biocarta.mapp.
    					// No idea why this is necessary
    					if (row[j] == null) row[j] = "";
    					sObjects.setString(j + 1, row[j]);
    				}
    			}

    			sObjects.executeUpdate();
    			k++;
            }

			for (int j = 0; j < mappInfo.length; ++j)
			{
				Logger.log.trace("[" + (j + 1) + "] " + mappInfo[j]);

				if (j >= ICOL_BOARDWIDTH && j <= ICOL_WINDOWHEIGHT)
				{
					sInfo.setDouble (j + 1, Double.parseDouble (mappInfo[j]));
				}
				else
				{
					sInfo.setString (j + 1, mappInfo[j]);
				}
			}
			sInfo.executeUpdate();
            con.close();

        } catch (Exception e) {
        	throw new ConverterException(e);
        }
    }

	public static String[] uncopyMappInfo (Pathway data)
	{
		String[] mappInfo = new String[15];

		PathwayElement mi = null;
		for (PathwayElement o : data.getDataObjects())
		{
			if (o.getObjectType() == ObjectType.MAPPINFO)
				mi = o;
		}
		
		String mapInfoName = mi.getMapInfoName();
		// MAPP format imposes a limit of 50 characters.
		if (mapInfoName.length() > 50) mapInfoName = mapInfoName.substring(0, 50);
		Logger.log.warn("Truncated MAPP INFO name to 50 characters");

		mappInfo[ICOL_TITLE] = mapInfoName;
		mappInfo[ICOL_VERSION] = mi.getVersion();
		mappInfo[ICOL_AUTHOR] = mi.getAuthor();
		mappInfo[ICOL_MAINT] = mi.getMaintainer();
		mappInfo[ICOL_EMAIL] = mi.getEmail();
		mappInfo[ICOL_COPYRIGHT] = mi.getCopyright();
		mappInfo[ICOL_MODIFY] = mi.getLastModified();

		mappInfo[ICOL_NOTES] = mi.findComment("GenMAPP notes");
		mappInfo[ICOL_REMARKS] = mi.findComment("GenMAPP remarks");

		double[] size = mi.getMBoardSize();
		mappInfo[ICOL_BOARDWIDTH] = "" + size[0] * PIXELS_TO_MAPP;
		mappInfo[ICOL_BOARDHEIGHT] = "" + size[1] * PIXELS_TO_MAPP;

		String val;
		val = mi.getDynamicProperty("org.pathvisio.model.WindowWidth");
		mappInfo[ICOL_WINDOWWIDTH] = (val == null) ? "0" : val;
		
		val = mi.getDynamicProperty("org.pathvisio.model.WindowWidth");
    	if (val == null) val = "0";
		mappInfo[ICOL_WINDOWHEIGHT] = (val == null) ? "0" : val;

		return mappInfo;
	}

	// This method copies the Info table of the genmapp mapp to a new gpml
	// pathway
	public static void copyMappInfo(String[] row, Pathway data, String filename)
	{

		/* Data is lost when converting from GenMAPP to GPML:
		*
		* GenMAPP:
		*		"Title", "MAPP", "Version", "Author",
		* 		"Maint", "Email", "Copyright","Modify",
		*		"Remarks", "BoardWidth", "BoardHeight","WindowWidth",
		*		"WindowHeight", "GeneDB", "Notes"
		* GPML:
		*		"Name", NONE, Version, "Author",
		*		"MaintainedBy", "Email", "Availability", "LastModified",
		*		"Comment", "BoardWidth", "BoardHeight", NONE,
		*		NONE, NONE, "Notes"
		*
		*/

		Logger.log.trace ("CONVERTING INFO TABLE TO GPML");

		PathwayElement o = data.getMappInfo();

		o.setMapInfoName(row[ICOL_TITLE]);
		o.setMapInfoDataSource("GenMAPP 2.0");
		o.setVersion(row[ICOL_VERSION]);
		o.setAuthor(row[ICOL_AUTHOR]);
		o.setMaintainer(row[ICOL_MAINT]);
		o.setEmail(row[ICOL_EMAIL]);
		o.setCopyright(row[ICOL_COPYRIGHT]);
		o.setLastModified(row[ICOL_MODIFY]);

		o.addComment(row[ICOL_NOTES], "GenMAPP notes");
		o.addComment(row[ICOL_REMARKS], "GenMAPP remarks");

		//Board size will be calculated
//		o.setMBoardWidth(Double.parseDouble(row[icolBoardWidth]));
//		o.setMBoardHeight(Double.parseDouble(row[icolBoardHeight]));

		o.setDynamicProperty("org.pathvisio.model.WindowWidth", row[ICOL_WINDOWWIDTH]);
		o.setDynamicProperty("org.pathvisio.model.WindowHeight", row[ICOL_WINDOWHEIGHT]);

		// guess organism based on first two characters of filename
		String shortCode = new File (filename).getName().substring(0, 2);
		Organism org = Organism.fromCode(shortCode);
		if (org != null)
		{
			o.setOrganism(org.latinName());
		}
	}

	public static List<String[]> uncopyMappObjects(Pathway data) throws ConverterException
	{
		List<String[]> result = new ArrayList<String[]>();

		for (PathwayElement o : data.getDataObjects())
		{
			ObjectType objectType = o.getObjectType();
			String[] row = new String[18];

			// init:
			row[COL_CENTERX] = "0.0";
			row[COL_CENTERY] = "0.0";
			row[COL_SECONDX] = "0.0";
			row[COL_SECONDY] = "0.0";
			row[COL_WIDTH] = "0.0";
			row[COL_HEIGHT] = "0.0";
			row[COL_ROTATION] = "0.0";
			row[COL_COLOR] = "-1";

			switch (objectType)
			{
				case LINE:
					unmapNotesAndComments (o, row);
					unmapLineType(o, row);
					result.add(row);
					break;
				case GRAPHLINE:
					unmapNotesAndComments (o, row);
					unmapLineType(o, row);
					result.add(row);
					break;	
				case DATANODE:
					unmapNotesAndComments (o, row);
					unmapGeneProductType(o, row);
					result.add(row);
					break;
				case INFOBOX:
					unmapInfoBoxType(o, row);
					result.add(row);
					break;
				case LABEL:
					unmapNotesAndComments (o, row);
					unmapLabelType(o, row);
					result.add(row);
					break;
				case LEGEND:
					unmapLegendType(o, row);
					result.add(row);
					break;
				case SHAPE:
					unmapNotesAndComments (o, row);
					IShape s = o.getShapeType();
					if (s == ShapeType.BRACE)
					{
						unmapBraceType(o, row);
					}
					else if (s == ShapeType.OVAL ||
							 s == ShapeType.ARC ||
							 s == ShapeType.RECTANGLE)
					{
						unmapShapeType(o, row);
					}
					else if (s == ShapeType.CELLA ||
							 s == ShapeType.RIBOSOME ||
							 s == ShapeType.ORGANA ||
							 s == ShapeType.ORGANB ||
							 s == ShapeType.ORGANC)
					{
						unmapFixedShapeType(o, row);
					}
					else if (s == ShapeType.PENTAGON ||
							 s == ShapeType.HEXAGON ||
							 s == ShapeType.TRIANGLE ||
							 s == ShapeType.PROTEINB ||
							 s == ShapeType.VESICLE)
					{
						unmapComplexShapeType(o, row);
					}
					else
					{
						Logger.log.warn("This Pathway uses Shapes not supported by GenMAPP");
						unmapUnknownShapeType (o, row);
					}
					result.add(row);
					break;
			}

		}

		return result;
	}

	/**
	 * Unmapp a gpml shape type that is not defined in GenMAPP.
	 * This will create an oval shape with the same properties
	 * (rotation, location, size and color)
	 */
	private static void unmapUnknownShapeType(PathwayElement o, String[] mappObject)
	{
		mappObject[COL_TYPE] = ShapeType.OVAL.getMappName();
		unmapShapeHalf (o, mappObject);
		mappObject[COL_COLOR] = toMappColor(o.getFillColor(), o.isTransparent());
		unmapRotation (o, mappObject);
	}

	private static void unmapNotesAndComments(PathwayElement o, String[] row)
	{
		row[COL_NOTES] = o.findComment("GenMAPP notes");
		row[COL_REMARKS] = o.findComment("GenMAPP remarks");
	}

	private static void mapNotesAndComments(PathwayElement o, String[] row)
	{
        if (row[COL_NOTES] != null &&
        		!row[COL_NOTES].equals(""))
        {
    		o.addComment(row[COL_NOTES], "GenMAPP notes");
        }

        if (row[COL_REMARKS] != null &&
        		!row[COL_REMARKS].equals(""))
        {
    		o.addComment(row[COL_REMARKS], "GenMAPP remarks");
        }
	}

	// This list adds the elements from the OBJECTS table to the new gpml
	// pathway
    public static void copyMappObjects(String[] row, Pathway data) throws ConverterException
    {

		// Create the GenMAPP --> GPML mappings list for use in the switch
		// statement

		List<String> typeslist = Arrays.asList(new String[] {
				"Arrow", "DottedArrow", "DottedLine", "Line",
				"Brace", "Gene", "InfoBox", "Label", "Legend", "Oval",
				"Rectangle", "TBar", "Receptor", "LigandSq",  "ReceptorSq",
				"LigandRd", "ReceptorRd", "CellA", "Arc", "Ribosome",
				"OrganA", "OrganB", "OrganC", "ProteinB", "Poly", "Vesicle"
		});
		PathwayElement o = null;
		int index = typeslist.indexOf(row[COL_TYPE]);
		switch(index) {

				case 0: /*Arrow*/
				case 1: /*DottedArrow*/
				case 2: /*DottedLine"*/
				case 3: /*Line*/
				case 11: /*TBar*/
				case 12: /*Receptor*/
				case 13: /*LigandSq*/
				case 14: /*ReceptorSq*/
				case 15: /*LigandRd*/
				case 16: /*ReceptorRd*/
						o = mapLineType(row);
						mapNotesAndComments (o, row);
						break;
				case 4: /*Brace*/
						o = mapBraceType(row);
						mapNotesAndComments (o, row);
						o.setGraphId(data.getUniqueGraphId());
						break;
				case 5: /*Gene*/
						o = mapGeneProductType(row);
						mapNotesAndComments (o, row);
						o.setGraphId(data.getUniqueGraphId());
						break;
				case 6: /*InfoBox*/
						o = mapInfoBoxType (row, data);
						break;
				case 7: /*Label*/
						o = mapLabelType(row);
						mapNotesAndComments (o, row);
						o.setGraphId(data.getUniqueGraphId());
						break;
				case 8: /*Legend*/
						o = mapLegendType(row);
						break;
				case 9: /*Oval*/
				case 10: /*Rectangle*/
				case 18: /*Arc*/
						o = mapShapeType(row);
						mapNotesAndComments (o, row);
						o.setGraphId(data.getUniqueGraphId());
						break;
				case 17: /*CellA*/
				case 19: /*Ribosome*/
				case 20: /*OrganA*/
				case 21: /*OrganB*/
				case 22: /*OrganC*/
						o = mapFixedShapeType(row);
						mapNotesAndComments (o, row);
						o.setGraphId(data.getUniqueGraphId());
						break;
				case 23: /*ProteinB*/
				case 24: /*Poly*/
				case 25: /*Vesicle*/
						o = mapComplexShapeType(row);
						mapNotesAndComments (o, row);
						o.setGraphId(data.getUniqueGraphId());
						break;
				default:
						throw new ConverterException (
							"-> Type '"
							+ row[COL_TYPE]
							+ "' is not recognised as a GenMAPP type "
							+ "and is therefore not processed.\n");
		}
		data.add(o);
    }


    private static void unmapLineType (PathwayElement o, String[] mappObject)
    {
    	int lineStyle = o.getLineStyle();
		LineType lineType = o.getEndLineType();
		String style = lineType.getMappName();
		if(style == null) {
			style = LineType.LINE.getMappName();
		}
		if (lineStyle == LineStyle.DASHED && (lineType == LineType.ARROW || lineType == LineType.LINE))
			style = "Dotted" + style;

		mappObject[COL_TYPE] = style;
		mappObject[COL_CENTERX] = "" + o.getMStartX() * PIXELS_TO_MAPP;
    	mappObject[COL_CENTERY] = "" + o.getMStartY() * PIXELS_TO_MAPP;
    	mappObject[COL_SECONDX] = "" + o.getMEndX() * PIXELS_TO_MAPP;
    	mappObject[COL_SECONDY] = "" + o.getMEndY() * PIXELS_TO_MAPP;
		mappObject[COL_COLOR] = toMappColor(o.getColor(), false);
    }

	private static Map<String,LineType> mappLineTypes = initMappLineTypes();

	static private Map<String,LineType> initMappLineTypes()
	{
		Map<String,LineType> result = new HashMap<String,LineType>();
		result.put ("DottedLine", LineType.LINE);
		result.put ("DottedArrow", LineType.ARROW);
		for (LineType l : LineType.getValues())
		{
			result.put (l.getMappName(), l);
		}
		return result;
	}

	private static PathwayElement mapLineType(String [] mappObject) throws ConverterException
	{
    	PathwayElement o = PathwayElement.createPathwayElement(ObjectType.LINE);

		String type = mappObject[COL_TYPE];
    	if(type.startsWith("Dotted"))
    	{
			o.setLineStyle(LineStyle.DASHED);
    	}
    	else
    	{
    		o.setLineStyle(LineStyle.SOLID);
    	}

    	o.setEndLineType(mappLineTypes.get(type));
        o.setMStartX(Double.parseDouble(mappObject[COL_CENTERX]) / PIXELS_TO_MAPP);
        o.setMStartY(Double.parseDouble(mappObject[COL_CENTERY]) / PIXELS_TO_MAPP);
        o.setMEndX(Double.parseDouble(mappObject[COL_SECONDX]) / PIXELS_TO_MAPP);
        o.setMEndY(Double.parseDouble(mappObject[COL_SECONDY]) / PIXELS_TO_MAPP);
		o.setColor(fromMappColor(mappObject[COL_COLOR]));
        return o;
	}

	private static void unmapCenter (PathwayElement o, String[] mappObject)
	{
		mappObject[COL_CENTERX] = "" + o.getMCenterX() * PIXELS_TO_MAPP;
    	mappObject[COL_CENTERY] = "" + o.getMCenterY() * PIXELS_TO_MAPP;
	}

	private static void mapCenter (PathwayElement o, String[] mappObject)
	{
		o.setMCenterX(Double.parseDouble(mappObject[COL_CENTERX]) / PIXELS_TO_MAPP);
		o.setMCenterY(Double.parseDouble(mappObject[COL_CENTERY]) / PIXELS_TO_MAPP);
	}

	private static void unmapRotation (PathwayElement o, String[] mappObject)
	{
		mappObject[COL_ROTATION] = "" + o.getRotation();
	}

	private static void mapRotation (PathwayElement o, String[] mappObject)
	{
		o.setRotation(Double.parseDouble(mappObject[COL_ROTATION]));
	}

	private static void unmapShape (PathwayElement o, String[] mappObject)
	{
    	unmapCenter(o, mappObject);
    	mappObject[COL_WIDTH] = "" + o.getMWidth() * PIXELS_TO_MAPP;
    	mappObject[COL_HEIGHT] = "" + o.getMHeight() * PIXELS_TO_MAPP;
	}

	private static void mapShape (PathwayElement o, String[] mappObject)
	{
    	mapCenter(o, mappObject);
    	o.setMWidth(Double.parseDouble(mappObject[COL_WIDTH]) / PIXELS_TO_MAPP);
    	o.setMHeight(Double.parseDouble(mappObject[COL_HEIGHT]) / PIXELS_TO_MAPP);
	}

	private static void unmapShapeHalf (PathwayElement o, String[] mappObject)
	{
    	unmapCenter(o, mappObject);
    	mappObject[COL_WIDTH] = "" + o.getMWidth() / 2 * PIXELS_TO_MAPP;
    	mappObject[COL_HEIGHT] = "" + o.getMHeight() / 2 * PIXELS_TO_MAPP;
	}

	private static void mapShapeHalf (PathwayElement o, String[] mappObject)
	{
    	mapCenter(o, mappObject);
    	o.setMWidth(Double.parseDouble(mappObject[COL_WIDTH]) * 2 / PIXELS_TO_MAPP);
    	o.setMHeight(Double.parseDouble(mappObject[COL_HEIGHT]) * 2 / PIXELS_TO_MAPP);
	}

	private static void unmapBraceType (PathwayElement o, String[] mappObject) throws ConverterException
    {
    	mappObject[COL_TYPE] = "Brace";
    	mappObject[COL_ROTATION] = "" + o.getOrientation();
    	unmapShape (o, mappObject);
		mappObject[COL_COLOR] = toMappColor(o.getColor(), false);
    }

    private static PathwayElement mapBraceType(String[] mappObject) throws ConverterException
    {
    	PathwayElement o = PathwayElement.createPathwayElement(ObjectType.SHAPE);
    	o.setShapeType (ShapeType.BRACE);
    	mapShape(o, mappObject);
    	o.setColor(fromMappColor(mappObject[COL_COLOR]));
    	o.setTransparent (true);
    	o.setOrientation((int)Double.parseDouble(mappObject[COL_ROTATION]));
        return o;
    }

    private static String getSafeDynamicProperty(PathwayElement o, String key)
    {
    	String val = o.getDynamicProperty(key);
    	if (val == null) return ""; else return val;
    }
    
    private static void unmapGeneProductType (PathwayElement o, String[] mappObject) throws ConverterException
    {
    	mappObject[COL_TYPE] = "Gene";
    	mappObject[COL_SYSTEMCODE] = o.getDataSource() != null ? o.getDataSource().getSystemCode() : "";
		mappObject[COL_HEAD] = getSafeDynamicProperty (o, "org.pathvisio.model.BackpageHead");
		mappObject[COL_ID] = o.getGeneID();
		mappObject[COL_LABEL] = o.getTextLabel();
		mappObject[COL_LINKS] = getSafeDynamicProperty (o, "org.pathvisio.model.GenMAPP-Xref");
		unmapShape(o, mappObject);
    }

    private static PathwayElement mapGeneProductType(String[] mappObject) throws ConverterException
	{
    	PathwayElement o = PathwayElement.createPathwayElement(ObjectType.DATANODE);

    	String syscode = mappObject[COL_SYSTEMCODE];
    	if (syscode == null) syscode = "";
    	syscode = syscode.trim();

        o.setDataSource(DataSource.getBySystemCode(syscode));

        o.setDynamicProperty ("org.pathvisio.model.BackpageHead", mappObject[COL_HEAD]);
        if (mappObject[COL_ID] == null)
        {
        	o.setGeneID("");
        }
        else
        {
        	o.setGeneID(mappObject[COL_ID]);
        }
        o.setTextLabel(mappObject[COL_LABEL]);

        o.setDataNodeType("GeneProduct");
        String xrefv = mappObject[COL_LINKS];
        if (xrefv == null) { xrefv = ""; }
        o.setDynamicProperty("org.pathvisio.model.GenMAPP-Xref", xrefv);

        mapShape(o, mappObject);
        return o;
	}

	private static PathwayElement mapInfoBoxType (String[] mappObject, Pathway data)
	{
    	PathwayElement o = data.getInfoBox();

    	mapCenter (o, mappObject);
        return o;
	}

	private static void unmapInfoBoxType (PathwayElement o, String[] mappObject)
    {
    	mappObject[COL_TYPE] = "InfoBox";

    	unmapCenter (o, mappObject);
    }

	private static PathwayElement mapLegendType (String[] mappObject)
	{
    	PathwayElement o = PathwayElement.createPathwayElement(ObjectType.LEGEND);

    	mapCenter (o, mappObject);

        return o;
	}

	private static void unmapLegendType (PathwayElement o, String[] mappObject)
    {
    	mappObject[COL_TYPE] = "Legend";

    	unmapCenter (o, mappObject);
    }

	private final static int STYLE_BOLD = 1;
	private final static int STYLE_ITALIC = 2;
	private final static int STYLE_UNDERLINE = 4;
	private final static int STYLE_STRIKETHRU = 8;

    private static PathwayElement mapLabelType(String[] mappObject)
    {
    	PathwayElement o = PathwayElement.createPathwayElement(ObjectType.LABEL);

    	mapShape(o, mappObject);
		o.setColor(fromMappColor(mappObject[COL_COLOR]));

    	o.setTextLabel(mappObject[COL_LABEL]);

    	if (mappObject[COL_ID] == null)
    	{
    		o.setFontName("");
    	}
    	else
    	{
    		o.setFontName(mappObject[COL_ID]);
    	}


        o.setMFontSize(Double.parseDouble(mappObject[COL_SECONDX]));

        String styleString = mappObject[COL_SYSTEMCODE];
        int style = styleString == null ? 0 : (int)(styleString.charAt(0));

        o.setBold((style & STYLE_BOLD) > 0);
        o.setItalic((style & STYLE_ITALIC) > 0);
        o.setUnderline((style & STYLE_UNDERLINE) > 0);
        o.setStrikethru((style & STYLE_STRIKETHRU) > 0);


        String xrefv = mappObject[COL_LINKS];
        if (xrefv == null) { xrefv = ""; }
        o.setDynamicProperty("org.pathvisio.model.GenMAPP-Xref", xrefv);
        return o;
    }

    private static void unmapLabelType (PathwayElement o, String[] mappObject)
    {
    	mappObject[COL_TYPE] = "Label";
    	String text = o.getTextLabel();
    	if(text != null) {
    		text = text.replace("\n", " ");
    	}
    	mappObject[COL_LABEL] = text;

    	unmapShape(o, mappObject);
		mappObject[COL_COLOR] = toMappColor(o.getColor(), false);

    	mappObject[COL_ID] = o.getFontName();
    	mappObject[COL_SECONDX] = "" + (o.getMFontSize());

    	int style = 16;
    	// note: from VB source I learned that 16 is added to prevent field from becoming 0,
    	// as this can't be stored in a text field in the database
    	if (o.isBold()) style |= STYLE_BOLD;
    	if (o.isItalic()) style |= STYLE_ITALIC;
    	if (o.isUnderline()) style |= STYLE_UNDERLINE;
    	if (o.isStrikethru()) style |= STYLE_STRIKETHRU;

    	char stylechars[] = new char[1];
    	stylechars[0] = (char)style;

    	mappObject[COL_SYSTEMCODE] = new String (stylechars);
		mappObject[COL_LINKS] = getSafeDynamicProperty(o, "org.pathvisio.model.GenMAPP-Xref");
    }

	private static PathwayElement mapShapeType(String[] mappObject)
    {
    	PathwayElement o = PathwayElement.createPathwayElement(ObjectType.SHAPE);
    	IShape shapeType = ShapeRegistry.fromMappName(mappObject[COL_TYPE]);
    	o.setShapeType(shapeType);
    	if (shapeType == ShapeType.ARC || shapeType == ShapeType.OVAL)
    		mapShapeHalf (o, mappObject);
    	else
    		mapShape (o, mappObject);

        if (shapeType == ShapeType.ARC)
        {
        	o.setColor(fromMappColor(mappObject[COL_COLOR]));
        	o.setTransparent (true);
        }
        else
        {
            int i = Integer.parseInt(mappObject[COL_COLOR]);
            if (i < 0)
            {
            	o.setTransparent(true); // automatically sets fillColor to null
            }
            else
            {
            	o.setFillColor(fromMappColor(mappObject[COL_COLOR]));
            }
        }

        mapRotation (o, mappObject);
        return o;
    }

    private static void unmapShapeType (PathwayElement o, String[] mappObject)
    {
    	IShape shapeType = o.getShapeType();
    	mappObject[COL_TYPE] = shapeType.getMappName();
    	if (shapeType == ShapeType.ARC || shapeType == ShapeType.OVAL)
    		unmapShapeHalf (o, mappObject);
    	else
    		unmapShape (o, mappObject);

		// note: when converting gpml to mapp,
		// line color is discarded for oval and rect
    	if (shapeType == ShapeType.ARC)
    	{
    		mappObject[COL_COLOR] = toMappColor(o.getColor(), false);
    	}
    	else
    	{
    		mappObject[COL_COLOR] = toMappColor(o.getFillColor(), o.isTransparent());
    	}
		unmapRotation (o, mappObject);
    }

    private static PathwayElement mapFixedShapeType(String[] mappObject)
    {
    	PathwayElement o = PathwayElement.createPathwayElement(ObjectType.SHAPE);
        o.setShapeType(ShapeRegistry.fromMappName(mappObject[COL_TYPE]));
        mapCenter (o, mappObject);

        if (o.shapeType == ShapeType.CELLA)
		{
        	o.setRotation (-1.308997);
        	o.setMWidth(1500 / PIXELS_TO_MAPP);
        	o.setMHeight(375 / PIXELS_TO_MAPP);
        }
		else if (o.shapeType == ShapeType.RIBOSOME)
		{
        	o.setMWidth (600 / PIXELS_TO_MAPP);
        	o.setMHeight (600 / PIXELS_TO_MAPP);
		}
		else if (o.shapeType == ShapeType.ORGANA)
		{
        	o.setMWidth (500 / PIXELS_TO_MAPP);
        	o.setMHeight (2000 / PIXELS_TO_MAPP);
		}
		else if (o.shapeType == ShapeType.ORGANB)
		{
        	o.setMWidth (500 / PIXELS_TO_MAPP);
        	o.setMHeight (2000 / PIXELS_TO_MAPP);
		}
		else if (o.shapeType == ShapeType.ORGANC)
		{
        	o.setMWidth (600 / PIXELS_TO_MAPP);
        	o.setMHeight (600 / PIXELS_TO_MAPP);
        }
        return o;
    }

    private static void unmapFixedShapeType (PathwayElement o, String[] mappObject)
    {
    	IShape shapeType = o.getShapeType();
    	mappObject[COL_TYPE] = shapeType.getMappName();

    	if (shapeType == ShapeType.CELLA)
    	{
    		mappObject[COL_ROTATION] = "-1.308997";
    		mappObject[COL_COLOR] = "0";
    		mappObject[COL_WIDTH] = "1500";
    		mappObject[COL_HEIGHT] = "375";
    	}
    	unmapShape (o, mappObject);
    }

    private static PathwayElement mapComplexShapeType(String[] mappObject) throws ConverterException
	{
    	PathwayElement o = PathwayElement.createPathwayElement(ObjectType.SHAPE);

    	if (mappObject[COL_TYPE].equals("Poly"))
        {
        	switch ((int)Double.parseDouble(mappObject[COL_SECONDY]))
        	{
        	case 3: o.setShapeType(ShapeType.TRIANGLE); break;
        	case 5: o.setShapeType(ShapeType.PENTAGON); break;
        	case 6: o.setShapeType(ShapeType.HEXAGON); break;
        	default: throw
        		new ConverterException ("Found polygon with unexpectec edge count: " +
        				mappObject[COL_SECONDY]);
        	}
        }
    	else
    	{
    		o.setShapeType(ShapeRegistry.fromMappName(mappObject[COL_TYPE]));
    	}

    	mapCenter(o, mappObject);
    	double size = Double.parseDouble(mappObject[COL_WIDTH]);
    	o.setMWidth(size / PIXELS_TO_MAPP);
    	o.setMHeight(size / PIXELS_TO_MAPP);
        mapRotation (o, mappObject);
        return o;
    }

    private static void unmapComplexShapeType (PathwayElement o, String[] mappObject)
    {
    	IShape shapeType = o.getShapeType();
    	mappObject[COL_TYPE] = shapeType.getMappName();

    	if (shapeType == ShapeType.TRIANGLE)
    	{
    		mappObject[COL_SECONDY] = "3";
    	} else if (shapeType == ShapeType.PENTAGON)
    	{
    		mappObject[COL_SECONDY] = "5";
		} else if (shapeType == ShapeType.HEXAGON)
		{
			mappObject[COL_SECONDY] = "6";
		}

    	unmapCenter (o, mappObject);
    	double size = o.getMWidth();
    	mappObject[COL_WIDTH] = "" + size;
    	if (shapeType == ShapeType.PROTEINB)
    	{
    		mappObject[COL_HEIGHT] = "0";
    	}
    	else
    	{
    		mappObject[COL_HEIGHT] = "400";
    	}
        unmapRotation (o, mappObject);
    }

	private static String[] extensions = new String[] { "mapp" };

	public String getName() {
		return "GenMAPP";
	}

	public String[] getExtensions() {
		return extensions;
	}

	public void doExport(File file, Pathway pathway) throws ConverterException 
	{
		if (Utils.getOS() != Utils.OS_WINDOWS)
		{
			throw new ConverterException ("MAPP format is only available on Windows operating systems");
		}

		String[] mappInfo = MappFormat.uncopyMappInfo(pathway);
		List<String[]> mappObjects = MappFormat.uncopyMappObjects(pathway);
		MappFormat.exportMapp (file.getAbsolutePath(), mappInfo, mappObjects);
	}

	public Pathway doImport(File file) throws ConverterException {
        Pathway pathway = new Pathway();
		String inputString = file.getAbsolutePath();

        MappFormat.readFromMapp (inputString, pathway);
        pathway.setSourceFile(null); //Don't save back to mapp file
        return pathway;
	}

    private static Color fromMappColor(String s)
    {

    	int i = Integer.parseInt(s);

    	Color result = new Color(
    			i & 0xFF,
    			(i & 0xFF00) >> 8,
    			(i & 0xFF0000) >> 16
    	);

    	return result;
    }

    private static String toMappColor(Color rgb, boolean fTransparent)
    {
    	if (fTransparent)
    		return "-1";
    	else
    	{
	    	int c = (rgb.getRed()) + (rgb.getGreen() << 8) + (rgb.getBlue() << 16);
	    	return "" + c;
    	}
    }

	@Override
	public boolean isCorrectType(File f)
	{
		return true;
	}
}
