package ca.ucalgary.seahawk.gui;

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.Semaphore;
import java.util.logging.*;
import javax.swing.ImageIcon;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import com.artofsolving.jodconverter.*;
import com.artofsolving.jodconverter.openoffice.connection.*;
import com.artofsolving.jodconverter.openoffice.converter.*;

import ca.ucalgary.seahawk.util.*;

/**
 * A utility to save the current document as XML or HTML, or the tab's history as a workflow.
 *
 * @author Paul Gordon
 */
public class MobySaveDialog implements WorkflowPreviewListener{

    public static final String FILE_CHOOSER_SAVE_TITLE = "Save Current Tab To File";
    public static final String XML_DESC = "XML (MOBY data envelope format)";
    public static final String TXT_DESC = "TXT (Plain Text Format)";
    public static final String RTF_DESC = "RTF (Rich Text Format)";
    public static final String CSV_DESC = "CSV (Comma Separated Values)";
    public static final String TSV_DESC = "TSV (Tab Separated Values)";
    public static final String PPT_DESC = "PPT (Microsoft PowerPoint Format)";
    public static final String ODT_DESC = "ODT (Open Document Text)";
    public static final String WPD_DESC = "WPD (WordPerfect Format)";
    public static final String ODS_DESC = "ODS (Open Document Spreadsheet)";
    public static final String ODP_DESC = "ODP (Open Document Presentation)";
    public static final String DOC_DESC = "DOC (Microsoft Word Format)";
    public static final String XLS_DESC = "XLS (Microsoft Excel Format)";
    public static final String TEX_DESC = "TEX (TeX Typesetting File)";
    public static final String PDF_DESC = "PDF (Portable Document Format)";
    public static final String HTML_DESC = "HTML (Web Page)";
    public static final String T2FLOW_DESC = "Workflow Macro (Taverna format)";
    public static final String T2FLOW_EXT = ".t2flow";
    public static final String XML_EXT = ".xml";
    public static final String TXT_EXT = ".txt";
    public static final String CSV_EXT = ".csv";
    public static final String RTF_EXT = ".rtf";
    public static final String PPT_EXT = ".ppt";
    public static final String DOC_EXT = ".doc";
    public static final String XLS_EXT = ".xls";
    public static final String PDF_EXT = ".pdf";
    public static final String ODP_EXT = ".odp";
    public static final String ODS_EXT = ".ods";
    public static final String ODT_EXT = ".odt";
    public static final String TSV_EXT = ".tsv";
    public static final String TEX_EXT = ".tex";
    public static final String WPD_EXT = ".wpd";
    public static final String HTML_EXT = ".html";
    public static final String XLS_MIME = "application/msexcel";
    public static final String DOC_MIME = "application/msword";
    public static final String PPT_MIME = "application/mspowerpoint";
    public static final String XLS_MIME2 = "application/vnd.ms-excel";
    public static final String DOC_MIME2 = "application/vnd.ms-word";
    public static final String PPT_MIME2 = "application/vnd.ms-powerpoint";
    public static final String ODT_MIME = "application/vnd.oasis.opendocument.text";
    public static final String ODS_MIME = "application/vnd.oasis.opendocument.spreadsheet";
    public static final String ODP_MIME = "application/vnd.oasis.opendocument.presentation";
    public static final String CSV_MIME = "text/csv";  //RFC4180
    public static final String CSV_MIME2 = "text/comma-separated-values";//RFC4180

    private static Logger logger = Logger.getLogger(MobySaveDialog.class.getName());

    private static javax.swing.filechooser.FileFilter lastFilterUsed = null;  //not java.io.FileFilter
    private static File lastDirOpened = null;
    private static File lastFileOpened = null;

    private final Semaphore available = new Semaphore(0, true);
    private File saveFile;
    private boolean exportConfirmed = false;

    protected MobySaveDialog(File fileToSave){
	saveFile = fileToSave;
    }

    public static void exportDocument(final MobyContentPane document) {

        if (document != null) {
            new Thread() {
		public void run(){
			
		    JFileChooser fileChooser = new JFileChooser();
        
		    fileChooser.setDialogTitle(FILE_CHOOSER_SAVE_TITLE);
		    fileChooser.setName(FILE_CHOOSER_SAVE_TITLE);
		    fileChooser.setFileHidingEnabled(false);
		    fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
		    fileChooser.addChoosableFileFilter(new DescriptiveFileFilter(HTML_EXT, HTML_DESC));
		    if(document.hasXMLSource()){
			fileChooser.addChoosableFileFilter(new DescriptiveFileFilter(XML_EXT, XML_DESC));
			fileChooser.addChoosableFileFilter(new DescriptiveFileFilter(T2FLOW_EXT, T2FLOW_DESC));
		    }
		    fileChooser.setAcceptAllFileFilterUsed(false);
		    if(lastDirOpened != null){
			fileChooser.setCurrentDirectory(lastDirOpened);
		    }
		    if(lastFileOpened != null){
			fileChooser.setSelectedFile(lastFileOpened);
		    }
		    if(lastFilterUsed != null){
			fileChooser.setFileFilter(lastFilterUsed);
		    }

		    int choice = fileChooser.showSaveDialog(null);
		    if (choice == JFileChooser.APPROVE_OPTION) {

			File f = fileChooser.getSelectedFile();
			String filename = f.getName();
			javax.swing.filechooser.FileFilter filter = fileChooser.getFileFilter();
			if(filename.indexOf(XML_EXT) != -1 && 
			   filename.indexOf(XML_EXT) == filename.length()-XML_EXT.length()){
			    // ends in xml suffix
			    exportXML(document, f);
			}
			else if(filename.indexOf(HTML_EXT) != -1 &&
				filename.indexOf(HTML_EXT) == filename.length()-HTML_EXT.length()){
			    // ends in html suffix
			    exportHTML(document, f);
			}
			else if(filename.indexOf(T2FLOW_EXT) != -1 &&
				filename.indexOf(T2FLOW_EXT) == filename.length()-T2FLOW_EXT.length()){
			    // ends in workflow suffix
			    if(!exportWorkflow(document, f)){
				lastDirOpened = fileChooser.getCurrentDirectory();
				lastFileOpened = f;
				lastFilterUsed = new DescriptiveFileFilter(T2FLOW_EXT, T2FLOW_DESC);
				exportDocument(document);
			    }
			}
			else if(filter.getDescription().equals(XML_DESC)){
			    // Add the xml suffix automatically
			    logger.log(Level.INFO, "Adding xml suffix automatically to get "+ (new File(f.getParent(), filename+XML_EXT))); 
			    exportXML(document, new File(f.getParent(), filename+XML_EXT));
			}
			else if(filter.getDescription().equals(HTML_DESC)){
			    // Add the html suffix automatically
			    logger.log(Level.INFO, "Adding html suffix automatically to get "+ (new File(f.getParent(), filename+HTML_EXT))); 
			    exportHTML(document, new File(f.getParent(), filename+HTML_EXT));
			}
			else if(filter.getDescription().equals(T2FLOW_DESC)){
			    // Add the html suffix automatically
			    logger.log(Level.INFO, "Adding T2Flow suffix automatically to get "+ (new File(f.getParent(), filename+T2FLOW_EXT))); 
			    File fs = new File(f.getParent(), filename+T2FLOW_EXT);
			    if(!exportWorkflow(document, fs)){
				lastDirOpened = fileChooser.getCurrentDirectory();
				lastFileOpened = fs;
				lastFilterUsed = new DescriptiveFileFilter(T2FLOW_EXT, T2FLOW_DESC);
				exportWorkflow(document, fs);
			    }
			}
			else{
			    logger.log(Level.WARNING, "Could not determine file type for " + filename);
			}
                    }
                }
            }.start();
        }
	else{
	    logger.log(Level.WARNING, "Document handed to MobySaveDialog was null, ignoring");
	}
    }

    public static void exportHTML(final MobyContentPane document, final File f){
	
	new Thread() {
		public void run() {
		    try {
			OutputStream ostream = new FileOutputStream(f);
			String html = document.getHTMLSource();
			ostream.write(html.getBytes());
			ostream.flush();
			ostream.close();
		    } catch (Exception ex) {
			JOptionPane.showMessageDialog(null, "Cannot save the MOBY document: "+ex, 
						      "Error saving", JOptionPane.ERROR_MESSAGE);
		    }
		}
	    }.start();
    }

    /**
     * @return true if the workflow was exported, otherwise false
     */
    public static boolean exportWorkflow(final MobyContentPane tab, final File f){
	String workflowName = f.getName().replaceFirst("\\.[^.]+$",""); //chop off .suffix

	try {
	    // See if there is more than one result tab open
	    DataFlowRecorder dataRecorder = null;
	    Map<URL,FilterSearch> resultsToProduce = new LinkedHashMap<URL,FilterSearch>();
	    boolean currentTabHasResults = false;
	    javax.swing.JTabbedPane tabbedPane = tab.getTabbedPaneParent();
	    for(int tabIndex = 0; tabIndex < tabbedPane.getTabCount(); tabIndex++){
		MobyContentPane document = (MobyContentPane) tabbedPane.getComponentAt(tabIndex);
		if(document.hasXMLSource()){
		    if(dataRecorder == null){
			dataRecorder = document.getDataFlowRecorder();
		    }
		    if(document == tab){
			currentTabHasResults = true;
		    }
		    // Don't include the clipboard in the list of results tabs,
		    // it's only saved if it's the current showing tab.
		    if(!(document instanceof MobyContentClipboard) &&
		       !(document instanceof MobyContentHelpPane)){
			resultsToProduce.put(document.getCurrentURL(), document.getFilter());
		    }
		}
	    }
	    if(dataRecorder == null){
		JOptionPane.showMessageDialog(null, "Could not find the DataFlowRecorder needed for " +
					      "workflow export.  Please contact gordonp@ucalgary.ca " +
					      "with this error message.", 
					      "Internal Error", 
					      JOptionPane.ERROR_MESSAGE);
		return false;
	    }
	    int choice = 0;  // 0 = all tabs, 1 = this tab only
	    if(tab instanceof MobyContentClipboard){
		if(((MobyContentClipboard) tab).getCollection().size() > 0){
		    choice = 1;	//the clipboard is exported alone if it has any contents
		}
		else{
		    currentTabHasResults = false;  //empty clipboard
		}
	    }
	    else if(resultsToProduce.size() == 0){
		JOptionPane.showMessageDialog(null, "There do not appear to be any service results " +
					      "to save as a workflow (in this tab or any other).  Sorry!", 
					      "No Service Results Found", 
					      JOptionPane.WARNING_MESSAGE);
		return false;
	    }
	    // Not the clipboard...
	    if(choice != 1){
		// ... the current tab can't produce a workflow (empty clipboard, for example)
		if(!currentTabHasResults){
		    Object[] options = {"Export results from the other tabs", "Cancel workflow export"};
		    choice = JOptionPane.showOptionDialog(null, "The current tab doesn't seem to have " +
							  "service results, but other tabs do.", 
							  "Workflow Export",
							  JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE,
							  null, options, options[1]);
		    if(choice == 1){
			return false;
		    }
		}
		// ... the current tab can produce a workflow, and more than one result tab available
		else if(resultsToProduce.size() > 1){
		    Object[] options = {"All tabs' results", "Just this tab's results", "Cancel workflow export"};
		    choice = JOptionPane.showOptionDialog(null, "What results would you like the workflow to produce?", 
							  "Workflow Export",
							  JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE,
							  null, options, options[1]);
		    if(choice == 2){
			return false;
		    }
		}
	    }
	    // clipboard, or only one tab available anyway
	    if(choice == 1){
		resultsToProduce.clear();
		resultsToProduce.put(tab.getCurrentURL(), tab.getFilter());
	    }
	    
	    // Preview the workflow before saving to file, 
	    // so the user can see if that's what they really want...
	    MobySaveDialog dialog = new MobySaveDialog(f);
	    new WorkflowPreviewDialog(null, resultsToProduce, dataRecorder, workflowName, dialog);

	    // Waits until workflowConfirmed or workflowCanceled is called by the preview dialog
	    dialog.available.acquire();
	    // Check choice, because if the user cancelled the dialog, don't show it again regardless
	    return choice == 1 ? true : dialog.exportConfirmed;
	} catch (Exception ex) {
	    ex.printStackTrace();
	    JOptionPane.showMessageDialog(null, "Cannot save the Seahawk tab history as a Taverna workflow: "+ex, 
					  "Error saving", JOptionPane.ERROR_MESSAGE); 
	}

	return false;
    }

    public void workflowCanceled(WorkflowPreviewDialog dialog){
	available.release();//lets exportWorkflow() return false 
    }

    /**
     * Callback for when the user has approved the workflow preview and entered metadata.
     */
    public void workflowConfirmed(WorkflowPreviewDialog dialog, byte[] confirmedWorkflow){
	try{
	    OutputStream ostream = new FileOutputStream(saveFile);
	    ostream.write(confirmedWorkflow);
	    ostream.flush();
	    exportConfirmed = true;
	    ostream.close();
	}catch(Exception e){
	    e.printStackTrace();
	    JOptionPane.showMessageDialog(null, "Cannot save the Seahawk tab history as a Taverna workflow: "+e, 
					  "Error saving", JOptionPane.ERROR_MESSAGE); 	    
	}
	finally{
	    available.release();
	}
    }
	
    public static void exportXML(final MobyContentPane document, final File f){

	new Thread() {
		public void run() {
		    try {
			OutputStream ostream = new FileOutputStream(f);
			String xml = document.getXMLSource();
			// Before saving, strip out the Seahawk processing instructions, which will 
			// confuse Seahawk if we reload the XML doc later.
			ostream.write(xml.replaceAll("<\\?"+DataUtils.PI_TARGET+".*?\\?>","").getBytes());
			ostream.flush();
			ostream.close();
		    } catch (Exception ex) {
			JOptionPane.showMessageDialog(null, "Cannot save the MOBY document in XML format: "+ex, 
						      "Error saving", JOptionPane.ERROR_MESSAGE); 
		    }
		}
	    }.start();
    }

    public static void showFileOpenDialog(MobyContentGUI mcg, boolean useDefaultHandler){
	JFileChooser fileChooser = new JFileChooser();
	fileChooser.setDialogTitle(mcg.FILE_CHOOSER_OPEN_TITLE);
	fileChooser.setName(mcg.FILE_CHOOSER_OPEN_TITLE);

	// Remember the last dir explored (if we have one)
	if(lastDirOpened != null){
	    fileChooser.setCurrentDirectory(lastDirOpened);
	}

	fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);

	// Loadable only if a document converter is available
	if(SeahawkOptions.getDocConverterHost() != null){
	    fileChooser.addChoosableFileFilter(new DescriptiveFileFilter(CSV_EXT, 
									 CSV_DESC));
	    fileChooser.addChoosableFileFilter(new DescriptiveFileFilter(DOC_EXT, 
									 DOC_DESC));
	    fileChooser.addChoosableFileFilter(new DescriptiveFileFilter(ODP_EXT, 
									 ODP_DESC));
	    fileChooser.addChoosableFileFilter(new DescriptiveFileFilter(ODS_EXT, 
									 ODS_DESC));
	    fileChooser.addChoosableFileFilter(new DescriptiveFileFilter(ODT_EXT, 
									 ODT_DESC));
	    fileChooser.addChoosableFileFilter(new DescriptiveFileFilter(PPT_EXT, 
									 PPT_DESC));
	    fileChooser.addChoosableFileFilter(new DescriptiveFileFilter(TSV_EXT, 
									 TSV_DESC));
	    fileChooser.addChoosableFileFilter(new DescriptiveFileFilter(WPD_EXT, 
									 WPD_DESC));
	    fileChooser.addChoosableFileFilter(new DescriptiveFileFilter(XLS_EXT, 
									 XLS_DESC));
	}

	// Handled natively by Java or Seahawk
	fileChooser.addChoosableFileFilter(new DescriptiveFileFilter(HTML_EXT, 
								     HTML_DESC));
	fileChooser.addChoosableFileFilter(new DescriptiveFileFilter(RTF_EXT, 
								     RTF_DESC));
	fileChooser.addChoosableFileFilter(new DescriptiveFileFilter(TEX_EXT, 
								     TEX_DESC));
	fileChooser.addChoosableFileFilter(new DescriptiveFileFilter(TXT_EXT, 
								     TXT_DESC));
	DescriptiveFileFilter xmlFilter = new DescriptiveFileFilter(XML_EXT, 
								    XML_DESC);
	fileChooser.addChoosableFileFilter(xmlFilter);
	fileChooser.setAcceptAllFileFilterUsed(true);
	if(lastFilterUsed != null){
	    fileChooser.setFileFilter(lastFilterUsed); // remember last thing the user did
	}
	else{
	    fileChooser.setFileFilter(xmlFilter);  // show xml only by default
	}

	for(;;){
	    int choice = fileChooser.showOpenDialog(mcg);
	    if (choice == JFileChooser.APPROVE_OPTION) {
		if(!fileChooser.getSelectedFile().exists()){
		    Object[] options = {"Try again", "Cancel"};
		    int response = JOptionPane.showOptionDialog(mcg,
								"Sorry, the specified file does not exist",
								"Invalid File Name",
								JOptionPane.OK_CANCEL_OPTION,
								JOptionPane.QUESTION_MESSAGE,
								null,
								options,
								options[0]);
		    if(response == 1){  //Cancel
			return;
		    }
		    else{
			continue;  // Try again
		    }
		}
		
		try{
		    // Remember the directory and filter for future reference
		    lastDirOpened = fileChooser.getCurrentDirectory();
		    lastFilterUsed = fileChooser.getFileFilter();

		    // If it's a microsoft format, convert it to RTF for display 
		    // via OpenOffice, as Java can't handle displaying it natively
		    File fileToOpen = fileChooser.getSelectedFile();
		    
		    mcg.loadPaneFromURL(fileToOpen.toURI().toURL(), useDefaultHandler);
		    return;
		}
		catch(Exception e){
		    e.printStackTrace();
		    JOptionPane.showMessageDialog(mcg,
						  "Could not load the data:\n"+e,
						  "Error Loading Data From File",
						  JOptionPane.ERROR_MESSAGE);
		}
	    }
	    else{
		return;  // Cancel
	    }
	}  //end infinite for loop
    }

    public static URL convertToDisplayable(URL u) throws Exception{
	URLConnection uc = convertToDisplayable(u.openConnection());
	return uc == null ? null : uc.getURL();
    }

    /**
     * A method for converting Microsoft Office documents (and perhaps other
     * formats in the future) into text that Seahawk can load.  Uses a remote
     * OpenOffice server to do the conversion.  The converted file will be 
     * deleted on exit.
     */
    public static URLConnection convertToDisplayable(URLConnection uc) throws Exception{
	String conversionHost = SeahawkOptions.getDocConverterHost();
	if(conversionHost == null){
	    return null;  // do not do conversions if there is no host to perform them
	}

	URL u = uc.getURL();
	String filePrefix = u.getPath();
	filePrefix = filePrefix.substring(0, filePrefix.length()-4);  // take off .doc or .xls
	// take off any leading directory structure
	if(filePrefix.indexOf("/") != -1 && filePrefix.indexOf("/") < filePrefix.length()-1){
	    filePrefix = filePrefix.substring(filePrefix.lastIndexOf("/")+1);
	}
	File outputFile = File.createTempFile(filePrefix,
                                              getPreferredExtension(uc),
					      SeahawkOptions.getTempDir());
	outputFile.delete(); // OpenOffice won't overwrite the file
	outputFile.deleteOnExit();
	
	// Create a local file for conversion if it's not one 
	// already (this is what JODConverter wants)
	File fileToOpen = null;
	if(u.getProtocol().equals("file")){
	    try{
		fileToOpen = new File(u.toURI());
	    } catch(Exception e){
		JOptionPane.showMessageDialog(null,
					      "Could not convert the file URL into a system file path:\n"+e,
					      "Error Converting URL to File",
					      JOptionPane.ERROR_MESSAGE);
		throw e;
	    }
	} else{
	    // Download the URL's data to a local temporary file for conversion
	    fileToOpen = File.createTempFile(filePrefix,
					     u.getFile().substring(u.getFile().length()-4), 
					     SeahawkOptions.getTempDir());
	    fileToOpen.deleteOnExit();
	    OutputStream os = new FileOutputStream(fileToOpen);
	    os.write(HTMLUtils.getURLContentBytes(u));
	    os.close();
	}
	

	//System.err.println("Converting " +  fileToOpen + " to " + outputFile);
	// Connect to an OpenOffice.org instance running on some machine
	OpenOfficeConnection connection = new SocketOpenOfficeConnection(conversionHost, 
									 SeahawkOptions.getDocConverterPort());
	connection.connect();
	
	// convert, and if it's the localhost, use the more efficient converter
	DocumentConverter converter = conversionHost.equals("localhost") ?
	    new OpenOfficeDocumentConverter(connection) :
	    new StreamOpenOfficeDocumentConverter(connection);
	converter.convert(fileToOpen, outputFile);
	
	// close the connection
	connection.disconnect();
	return outputFile.toURI().toURL().openConnection();
	
	// TODO, when the conversion is to HTML for a presentation, many 
	// HTML files are created, and need to be downloaded
    }
    
    /**
     * Given a file name to import, returns the file extension that Seahawk would
     * prefer to convert the document to.
     */
    public static String getPreferredExtension(URLConnection uc){
	String fileName = uc.getURL().getPath().toLowerCase();
	String mimeType = uc.getContentType();
	
	if(fileName.endsWith(XLS_EXT) || mimeType.equals(XLS_MIME) || mimeType.equals(XLS_MIME2) ||
	   fileName.endsWith(ODS_EXT) || mimeType.equals(ODS_MIME) ||
	   fileName.endsWith(TSV_EXT) || mimeType.equals(CSV_MIME) ||
	   fileName.endsWith(CSV_EXT) || mimeType.equals(CSV_MIME2)){
	    return HTML_EXT;
	}
	else if(fileName.endsWith(PPT_EXT) || mimeType.equals(PPT_MIME) || mimeType.equals(PPT_MIME2) ||
		fileName.endsWith(ODP_EXT) || mimeType.equals(ODP_MIME)){
	    return HTML_EXT;
	}
	else{
	    return HTML_EXT; 
	}
    }

    public static boolean isConvertible(URLConnection uc){
	if(SeahawkOptions.getDocConverterHost() == null){
	    return false;  // do not do conversions if there is no host to perform them
	}
	String fileName = uc.getURL().getPath().toLowerCase();
	String mimeType = uc.getContentType();
	return fileName.endsWith(XLS_EXT) || mimeType.equals(XLS_MIME) || mimeType.equals(XLS_MIME2) ||
	    fileName.endsWith(ODS_EXT) || fileName.endsWith(CSV_EXT) || 
	    fileName.endsWith(PPT_EXT) || mimeType.equals(PPT_MIME) || mimeType.equals(PPT_MIME2) ||
	    fileName.endsWith(ODP_EXT) || mimeType.equals(ODT_MIME) || mimeType.equals(ODP_MIME) ||
	    mimeType.equals(ODS_MIME) || fileName.endsWith(CSV_EXT) || mimeType.equals(CSV_MIME) || mimeType.equals(CSV_MIME2) ||
	    fileName.endsWith(DOC_EXT) || mimeType.equals(DOC_MIME) || mimeType.equals(DOC_MIME2) ||
	    fileName.endsWith(WPD_EXT) || fileName.endsWith(ODT_EXT) || fileName.endsWith(CSV_EXT);
    }
}
