/*
 *  Draco Econometrics
 *  Copyright (C) 2006-2007 Approximatrix, LLC
 *  http://www.approximatrix.com
 *  
 *  This program 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 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.approximatrix.draco.io.XMLio;

import java.io.IOException;
import java.io.InputStream;

import java.util.Map;
import java.util.ArrayList;
import java.util.Iterator;
import java.awt.Dimension;

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.NodeList;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import com.approximatrix.charting.render.AbstractChartRenderer;
import com.approximatrix.charting.render.BarChartRenderer;
import com.approximatrix.charting.render.MultiScatterChartRenderer;
import com.approximatrix.charting.swing.AbstractChartPanel;
import com.approximatrix.draco.main.ui.MainWindow;
import com.approximatrix.draco.plot.ui.DataPlot;
import com.approximatrix.draco.plot.ui.GenericPlotInterface;
import com.approximatrix.draco.plot.ui.HistogramPlot;
import com.approximatrix.draco.plot.ui.PlotDialog;

public class ODSChartDatabase {
	
	private static final double PIXEL_PER_CM = 28.0;
	
	private GenericPlotInterface plot = null;
	private Document result = null;
	private String window_title = null;
	
	public ODSChartDatabase(GenericPlotInterface plot) {
		this.plot = plot;
	}
	
	public ODSChartDatabase() {
		// Just a dummy
	}
	
	public boolean loadDOMrepresentation(InputStream in) {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		
		try {
			DocumentBuilder builder = factory.newDocumentBuilder();
			result = builder.parse(in);
		} catch (SAXParseException spe) {
			return false;
		} catch (SAXException sxe) {
			// Error generated during parsing
			Exception  x = sxe;
			if (sxe.getException() != null)
				x = sxe.getException();
			x.printStackTrace();
			return false;
		} catch (ParserConfigurationException pce) {
			// Parser with specified options can't be built
			pce.printStackTrace();
			return false;
		} catch (IOException ioe) {
			ioe.printStackTrace();
			return false;
	    }
		return true;
	}
	
	public Document getDOMrepresentation() {
    	
    	if(result != null) return result;
    	
        result = null;
        try {
            DocumentBuilder dbuild = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            result = dbuild.newDocument();
            
            Element collection = result.createElement("office:document-content");
            
            collection.setAttribute("xmlns:office","urn:oasis:names:tc:opendocument:xmlns:office:1.0");
            collection.setAttribute("xmlns:style","urn:oasis:names:tc:opendocument:xmlns:style:1.0");
            collection.setAttribute("xmlns:text","urn:oasis:names:tc:opendocument:xmlns:text:1.0");
            collection.setAttribute("xmlns:table","urn:oasis:names:tc:opendocument:xmlns:table:1.0");
            collection.setAttribute("xmlns:draw","urn:oasis:names:tc:opendocument:xmlns:drawing:1.0");
            collection.setAttribute("xmlns:fo","urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0");
            collection.setAttribute("xmlns:xlink","http://www.w3.org/1999/xlink");
            collection.setAttribute("xmlns:dc","http://purl.org/dc/elements/1.1/");
            collection.setAttribute("xmlns:meta","urn:oasis:names:tc:opendocument:xmlns:meta:1.0");
            collection.setAttribute("xmlns:number","urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0");
            collection.setAttribute("xmlns:svg","urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0");
            collection.setAttribute("xmlns:chart","urn:oasis:names:tc:opendocument:xmlns:chart:1.0" );
            collection.setAttribute("xmlns:dr3d","urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0");
            collection.setAttribute("xmlns:math","http://www.w3.org/1998/Math/MathML");
            collection.setAttribute("xmlns:form","urn:oasis:names:tc:opendocument:xmlns:form:1.0");
            collection.setAttribute("xmlns:script","urn:oasis:names:tc:opendocument:xmlns:script:1.0");
            collection.setAttribute("xmlns:ooo","http://openoffice.org/2004/office");
            collection.setAttribute("xmlns:ooow","http://openoffice.org/2004/writer");
            collection.setAttribute("xmlns:oooc","http://openoffice.org/2004/calc");
            collection.setAttribute("xmlns:dom","http://www.w3.org/2001/xml-events");
            collection.setAttribute("xmlns:xforms","http://www.w3.org/2002/xforms");
            collection.setAttribute("xmlns:xsd","http://www.w3.org/2001/XMLSchema");
            collection.setAttribute("xmlns:xsi","http://www.w3.org/2001/XMLSchema-instance");
            collection.setAttribute("office:version","1.0");
            
            Element body = result.createElement(ODSChartTags.CHT_BODY);
            Element chart = result.createElement(ODSChartTags.CHT_CHART);
            AbstractChartPanel panel = plot.getPlotPanel();
            
            //Save the chart size arrume 28 px per cm
            Dimension size = panel.getSize();
            double width = ((double)size.getWidth())/PIXEL_PER_CM;
            double height = ((double)size.getHeight())/PIXEL_PER_CM;
            
            chart.setAttribute(ODSChartTags.CHT_WIDTH_SUBELEMENT,new Double(width).toString());
            chart.setAttribute(ODSChartTags.CHT_HEIGHT_SUBELEMENT,new Double(height).toString());
            
            // Save the chart type
            String ch_class = null;
            Map renderers = panel.getChartRenderer();
            AbstractChartRenderer renderer = null;
            for(Iterator<AbstractChartRenderer>iter = renderers.values().iterator();iter.hasNext();) {
            	AbstractChartRenderer now = iter.next();
            	if(now != null) {
            		renderer = now;
            		break;
            	}
            }
            
            ArrayList values = null;
            if(renderer == null) 
            	ch_class = ODSChartTags.CHT_SCATTER_TYPE;
            else if(renderer.getClass().isAssignableFrom(MultiScatterChartRenderer.class)) {
            	ch_class = ODSChartTags.CHT_SCATTER_TYPE;
            	values = get_scatter_data(panel);
            } else if(renderer.getClass().isAssignableFrom(BarChartRenderer.class)) {
            	ch_class = ODSChartTags.CHT_BAR_TYPE;
            	values = get_bar_data(panel);
            }
            chart.setAttribute(ODSChartTags.CHT_CLASS_SUBELEMENT,ch_class);
            
            // Save the title
            Element title = result.createElement(ODSChartTags.CHT_TITLE);
            title.appendChild(result.createTextNode(panel.getTitle().getText()));
            chart.appendChild(title);
            
            // Save the legend
            Element legend = result.createElement(ODSChartTags.CHT_LEGEND);
            legend.setAttribute(ODSChartTags.CHT_LEGEND_SUBELEMENT, "end"); // For now
            chart.appendChild(legend);
            
            // The plot area
            Element plot_area = result.createElement(ODSChartTags.CHT_PLOT);
            plot_area.setAttribute(ODSChartTags.CHT_PLOT_LABELS_SUBELEMENT, "row");
            
            Element x_axis = result.createElement(ODSChartTags.CHT_PLOT_AXIS);
            x_axis.setAttribute(ODSChartTags.CHT_PLOT_AXIS_DIM_SUBELEMENT, "x");
            x_axis.setAttribute(ODSChartTags.CHT_PLOT_AXIS_NAME_SUBELEMENT, "primary-x");
            Element x_label = result.createElement(ODSChartTags.CHT_TITLE);
            x_label.appendChild(result.createTextNode(panel.getCoordSystem().getXAxisUnit()));
            x_axis.appendChild(x_label);
            Element categories = result.createElement(ODSChartTags.CHT_CATEGORIES);
            StringBuffer range = new StringBuffer(ODSChartTags.CHT_LOCAL_TABLE);
            range.append(".A2:A");
            if(values == null)
            	range.append("2");
            else
            	range.append(values.size());
            categories.setAttribute(ODSChartTags.CHT_CATEGORIES_RANGE_SUBELEMENT,range.toString());
            x_axis.appendChild(categories);
            plot_area.appendChild(x_axis);
            
            Element y_axis = result.createElement(ODSChartTags.CHT_PLOT_AXIS);
            y_axis.setAttribute(ODSChartTags.CHT_PLOT_AXIS_DIM_SUBELEMENT, "y");
            y_axis.setAttribute(ODSChartTags.CHT_PLOT_AXIS_NAME_SUBELEMENT, "primary-y");
            Element y_label = result.createElement(ODSChartTags.CHT_TITLE);
            y_label.appendChild(result.createTextNode(panel.getCoordSystem().getYAxisUnit()));
            y_axis.appendChild(y_label);
            plot_area.appendChild(y_axis);
            
            chart.appendChild(plot_area);
            
            // Place the local data here
            Element table = result.createElement(TableTags.TBL_TABLE);
            table.setAttribute(TableTags.TBL_TABLE_NAME_SUBELEMENT, "local_table");
            
            Element header_cols = result.createElement(TableTags.TBL_HEADER_COLS);
            header_cols.appendChild(result.createElement(TableTags.TBL_COL));
            table.appendChild(header_cols);
            
            Element columns = result.createElement(TableTags.TBL_COLS);
            Element one_col = result.createElement(TableTags.TBL_COL);
            one_col.setAttribute(TableTags.TBL_COL_REPEATED_SUBELEMENT, Integer.toString(panel.getChartDataModel().getDataSetNumber()+1));
            columns.appendChild(one_col);
            table.appendChild(columns);
            
            
            Element headers = result.createElement(TableTags.TBL_HEADER_ROWS);
            Element one_row = result.createElement(TableTags.TBL_ROW);
            
            // Blank
            Element cell = result.createElement(TableTags.TBL_CELL);
            cell.appendChild(result.createTextNode(""));
            one_row.appendChild(cell);
            
            // X data
            cell = result.createElement(TableTags.TBL_CELL);
            cell.appendChild(result.createTextNode(panel.getCoordSystem().getXAxisUnit()));
            one_row.appendChild(cell);
            
            // Cycle through all the Y data sets - we're assuming X always has the same unit
            for(int i=0;i<panel.getChartDataModel().getDataSetNumber();i++) {
            	String this_name = panel.getChartDataModel().getDataSetName(i);
            	cell = result.createElement(TableTags.TBL_CELL);
            	cell.appendChild(result.createTextNode(this_name));
                one_row.appendChild(cell);
            }
            
            headers.appendChild(one_row);
            table.appendChild(headers);
            
            Element all_rows = result.createElement(TableTags.TBL_ROWS);
            
            // Store tha actual data
            if(values != null) {
            	for(int row = 0;row<values.size();row++) {
            		
            		one_row = result.createElement(TableTags.TBL_ROW);
            		
            		// Row Designator
            		cell = result.createElement(TableTags.TBL_CELL);
                    cell.appendChild(result.createTextNode("Row "+Integer.toString(row+1)));
                    one_row.appendChild(cell);
                    
                    // Access the row from values array
                    Object[] to_save = (Object[])values.get(row);
                    
                    // X value
                    cell = result.createElement(TableTags.TBL_CELL);
                    //if(to_save[0].getClass().isAssignableFrom(Number.class)) {
                    if(to_save[0] instanceof Number) {
                    	cell.setAttribute(TableTags.TBL_CELL_TYPE_SUBELEMENT,TableTags.TBL_CELL_TYPE_FLOAT_ATTR);
                    	cell.setAttribute(TableTags.TBL_CELL_VALUE_SUBELEMENT,((Number)to_save[0]).toString());
                    	cell.appendChild(result.createTextNode(((Number)to_save[0]).toString()));
                    //} else if(to_save[0].getClass().isAssignableFrom(String.class)) {
                    } else if(to_save[0] instanceof String) {
                    	cell.appendChild(result.createTextNode(((String)to_save[0])));
                    }
                    one_row.appendChild(cell);
                    
                    // Y values
                    for(int vals = 1;vals<to_save.length;vals++) {
                    	cell = result.createElement(TableTags.TBL_CELL);
                    	if(to_save[vals] == null) 
                    		cell.appendChild(result.createTextNode(""));
                    	else {
                    		//if(//to_save[vals].getClass().i.isAssignableFrom(Number.class)) {
                    		if(to_save[vals] instanceof Number) {
                            	cell.setAttribute(TableTags.TBL_CELL_TYPE_SUBELEMENT,TableTags.TBL_CELL_TYPE_FLOAT_ATTR);
                            	cell.setAttribute(TableTags.TBL_CELL_VALUE_SUBELEMENT,((Number)to_save[vals]).toString());
                            	cell.appendChild(result.createTextNode(((Number)to_save[vals]).toString()));
                            //} else if(to_save[vals].getClass().isAssignableFrom(String.class)) {
                            } else if(to_save[vals] instanceof String) {
                            	cell.appendChild(result.createTextNode(((String)to_save[vals])));
                            }
                    	}
                    	one_row.appendChild(cell);
                    }
                    all_rows.appendChild(one_row);
            	}
            }
            table.appendChild(all_rows);

            // Finish assigning children
            chart.appendChild(table);
            body.appendChild(chart);
            collection.appendChild(body);
            result.appendChild(collection);
            
        } catch (Exception e) {
            System.out.println("Exception caught..." + e);
            e.printStackTrace();
        }
        return result;
	}

	
	private ArrayList get_scatter_data(AbstractChartPanel panel) {
			
		ArrayList values = new ArrayList();
		int ds_total = panel.getChartDataModel().getDataSetNumber();
		for(int ds = 0; ds < ds_total;ds++) {
			int last_index = -1;
			for(int cols = 0;cols < panel.getChartDataModel().getDataSetLength(ds);cols++) {
				Object value = panel.getChartDataModel().getColumnValueAt(ds, cols);

				// Search for a duplicate
				int index = -1;
				for(int al = 0;al<values.size();al++) {
					Object[] one_set = (Object[])values.get(al);
					if(one_set[0].equals(value)) {
						index = al;
						break;
					}
				}

				// Add the value at the appropriate index
				if(index > -1) {
					Object[] one_set = (Object[])values.get(index);
					one_set[ds+1] = panel.getChartDataModel().getValueAt(ds, cols);
				} else {
					Object[] new_set = new Object[ds_total+1];
					new_set[0] = value;
					new_set[ds+1] = panel.getChartDataModel().getValueAt(ds, cols);
					if(value.getClass().isAssignableFrom(Number.class)) {
						index = values.size();
						for(int search = 1; search<values.size(); search++) {
							Object[] s_row = (Object[])values.get(search);

							if(! s_row[0].getClass().isAssignableFrom(Number.class))
								continue;

							Number s_x = (Number)s_row[0];
							if(s_x.doubleValue()>((Number)value).doubleValue()) {
								index = search;
								break;
							}
						}
						values.add(index,new_set);
					} else {
						values.add(last_index+1,new_set);
						index = last_index+1;
					}
				}
				last_index = index;
			}
		}
		return values;
		
	}
	
	private ArrayList get_bar_data(AbstractChartPanel panel) {
		return get_scatter_data(panel);
	}
	
	public void setWindowTitle(String title) {
		window_title = title;
	}
	
	public PlotDialog createPlot(String window_title, MainWindow parent) {
		this.setWindowTitle(window_title);
		return this.createPlot(parent);
	}
	
	public PlotDialog createPlot(MainWindow parent) {
		
		Element chart = this.getSingleElementByName(result.getDocumentElement(), ODSChartTags.CHT_CHART);
		if(chart == null) return null;
		
		// Dialog size
		int width = (int)(new Double(chart.getAttribute(ODSChartTags.CHT_WIDTH_SUBELEMENT)).doubleValue()*PIXEL_PER_CM);
		int height = (int)(new Double(chart.getAttribute(ODSChartTags.CHT_HEIGHT_SUBELEMENT)).doubleValue()*PIXEL_PER_CM);
		
		// Chart type
		String chart_type = chart.getAttribute(ODSChartTags.CHT_CLASS_SUBELEMENT);
		
		// Chart title
		Element el_title = this.getSingleElementByName(chart, ODSChartTags.CHT_TITLE);
		String title = el_title.getTextContent();
		
		// Check for a legend
		boolean legend = (this.getSingleElementByName(chart, ODSChartTags.CHT_LEGEND) != null);
		
		// Axes
		Element plot_area = this.getSingleElementByName(chart, ODSChartTags.CHT_PLOT);
		NodeList axes =plot_area.getElementsByTagName(ODSChartTags.CHT_PLOT_AXIS);
		String[] labels = new String[2];
		for(int i=0;i<axes.getLength();i++) {
			Element axis = (Element)axes.item(i);
			String label = this.getSingleElementByName(axis, ODSChartTags.CHT_TITLE).getTextContent();
			if(axis.getAttribute(ODSChartTags.CHT_PLOT_AXIS_DIM_SUBELEMENT).compareToIgnoreCase("x")==0)
				labels[0] = label;
			else
				labels[1] = label;
		}
		
		// Finally, the data table must be processed
		Element table = this.getSingleElementByName(chart, TableTags.TBL_TABLE);
		
		// Determine the number of columns
		Element columns = this.getSingleElementByName(table, TableTags.TBL_COLS);
		Element column = this.getSingleElementByName(columns, TableTags.TBL_COL);
		int number_columns = new Integer(column.getAttribute(TableTags.TBL_COL_REPEATED_SUBELEMENT)).intValue();
		
		// Get the column headers
		String[] data_series = new String[number_columns-1];
		Element header_rows = this.getSingleElementByName(table, TableTags.TBL_HEADER_ROWS);
		Element header_row = this.getSingleElementByName(header_rows, TableTags.TBL_ROW);
		NodeList header_cells = header_row.getElementsByTagName(TableTags.TBL_CELL);
		for(int i=2;i<header_cells.getLength();i++) {
			Element cell = (Element)header_cells.item(i);
			data_series[i-2] = cell.getTextContent();
		}
		
		// Get the data row by row
		Element all_rows = this.getSingleElementByName(table, TableTags.TBL_ROWS);
		ArrayList unprocessed_data = new ArrayList();
		NodeList rows = all_rows.getElementsByTagName(TableTags.TBL_ROW);
		for(int i=0;i<rows.getLength();i++) {
			Element one_row = (Element)rows.item(i);
			NodeList cells = one_row.getElementsByTagName(TableTags.TBL_CELL);
			
			// The cell data will be stored here
			Object[] row_values = new Object[number_columns];
			
			// Fill in the vector
			for(int j=1;j<cells.getLength();j++) {
				Element cell = (Element)cells.item(j);
				String type = cell.getAttribute(TableTags.TBL_CELL_TYPE_SUBELEMENT);
				if(type.compareToIgnoreCase(TableTags.TBL_CELL_TYPE_FLOAT_ATTR)==0) {
					row_values[j-1] = new Double(cell.getAttribute(TableTags.TBL_CELL_VALUE_SUBELEMENT));
				} else {
					String text = cell.getTextContent();
					if(text == null)
						row_values[j-1] = null;
					else if(text.length() == 0) 
						row_values[j-1] = null;
					else
						row_values[j-1] = text;
				}
			}
			
			// Add the vector to the master arraylist
			unprocessed_data.add(row_values);
		}
		
		// Need to separate the data into a plot-comatible format
		GenericPlotInterface gpi = null;
		if(chart_type.compareToIgnoreCase(ODSChartTags.CHT_SCATTER_TYPE) == 0) {
			
			ArrayList plot_data = prepareScatterData(unprocessed_data, number_columns);
			gpi = new DataPlot(plot_data,labels[0],data_series);
			
		} else if(chart_type.compareToIgnoreCase(ODSChartTags.CHT_BAR_TYPE) == 0) {
			// Bar chart only supports one series - easy!
			int[] nonnull = this.getNonNullRows(unprocessed_data, 1, true);
			
			String[] categories = new String[nonnull.length];
			int[] values = new int[nonnull.length];
			for(int i=0;i<nonnull.length;i++) {
				Object[] row = (Object[])unprocessed_data.get(nonnull[i]);
				categories[i] = row[0].toString();
				if(row[1] instanceof Number)
					values[i] = ((Number)row[1]).intValue();
				else
					values[i] = 0;
			}

			// Create the bar chart (Histogram at the moment)
			gpi = new HistogramPlot(data_series[0],categories,values);
			
		}
		
		// Set up the labels
		gpi.getPlotPanel().getCoordSystem().setXAxisUnit(labels[0]);
		gpi.getPlotPanel().getCoordSystem().setYAxisUnit(labels[1]);
		gpi.getPlotPanel().getTitle().setText(title);
		gpi.getPlotPanel().setSize(width, height);
		
		PlotDialog return_val = (PlotDialog)parent.openWindow(new PlotDialog(window_title,gpi));
		//manager.updateWindowNames();
		
		return return_val;
	}
	
	
	private Element getSingleElementByName(Element parent, String name) {
		NodeList elements = parent.getElementsByTagName(name);
		if(elements == null) {
				return null;
		}
		return (Element)elements.item(0);
	}
	
	private int[] getNonNullRows(ArrayList data, int column, boolean check_index_0) {
		ArrayList buffer = new ArrayList();
		for(int i=0;i<data.size();i++) {
			Object[] row = (Object[])data.get(i);
			
			if(row == null)
				continue;
			
			if(row.length < column)
				continue;
			
			//if(row[i] == null || (row[0] == null && check_index_0))
			if(row[0] == null && check_index_0)
				continue;
			
			buffer.add(new Integer(i));
		}
		
		// Build a simple vector from the ArrayList
		int[] return_val = new int[buffer.size()];
		for(int i=0;i<buffer.size();i++)
			return_val[i] = ((Integer)buffer.get(i)).intValue();
		
		return return_val;
	}
	
	private int[] getNonNullRows(ArrayList data, int column) {
		return getNonNullRows(data,column,false);
	}
	
	private ArrayList prepareScatterData(ArrayList original, int columns) {
		ArrayList datasets = new ArrayList();

		int y_columns = columns-1;
		
		for(int i=0;i<y_columns;i++) {
			int[] nonnull = getNonNullRows(original,i+1,true);
			
			double[][] dataset = extractDataset(original,i+1,nonnull);
			
			datasets.add(dataset);
		}
		
		return datasets;
	}
	
	private double[][] extractDataset(ArrayList original, int y_column, int[] use_these) {
		// Size the return array
		double[][] return_val = new double[use_these.length][2];
		
		for(int i=0;i<use_these.length;i++) {
			Object[] one_row = (Object[])original.get(use_these[i]);
			
			return_val[i][0] = toDouble(one_row[0]);
			if(y_column<one_row.length)
				return_val[i][1] = toDouble(one_row[y_column]);
			else
				return_val[i][1] = Double.NaN;
			
		}
		return return_val;
	}
	
	private double toDouble(Object value) {
		if(value == null)
			return Double.NaN;
		else if(value.getClass().isAssignableFrom(Number.class))
			return ((Number)value).doubleValue();
		else {
			double return_val = Double.NaN;
			try {
				return_val = new Double(value.toString()).doubleValue();
			} catch(NumberFormatException nfe) {
				// Nothing
			}
			return return_val;
		}
	}
}

