/*
 *  Copyright (c) 1995-2014, The University of Sheffield. See the file
 *  COPYRIGHT.txt in the software or at http://gate.ac.uk/gate/COPYRIGHT.txt
 *
 *  This file is part of GATE (see http://gate.ac.uk/), and is free
 *  software, licenced under the GNU Library General Public License,
 *  Version 2, June 1991 (in the distribution as file licence.html,
 *  and also available at http://gate.ac.uk/gate/licence.html).
 *
 *  Mark A. Greenwood 11/07/2014
 *
 */

package gate.gui;

import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.table.TableCellEditor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import gate.Corpus;
import gate.CorpusExporter;
import gate.Document;
import gate.DocumentExporter;
import gate.Factory;
import gate.FeatureMap;
import gate.Gate;
import gate.Resource;
import gate.Utils;
import gate.corpora.export.GateXMLExporter;
import gate.creole.Parameter;
import gate.creole.ParameterException;
import gate.creole.ResourceData;
import gate.event.CreoleEvent;
import gate.event.CreoleListener;
import gate.swing.XJFileChooser;
import gate.swing.XJMenu;
import gate.util.Err;
import gate.util.Files;
import java.text.NumberFormat;

/**
 * A menu which updates as plugins are (un)loaded to allow the export of
 * documents and corpora to any of the supported output formats.
 */
@SuppressWarnings("serial")
public class DocumentExportMenu extends XJMenu implements CreoleListener {

  private static final Logger log = LoggerFactory.getLogger(DocumentExportMenu.class);

  static DocumentExportDialog dialog = new DocumentExportDialog();

  protected IdentityHashMap<Resource, JMenuItem> itemByResource =
          new IdentityHashMap<Resource, JMenuItem>();

  private Handle handle;

  public DocumentExportMenu(NameBearerHandle handle) {
    super("Save as...", "", handle.sListenerProxy);
    if(!(handle.getTarget() instanceof Document)
            && !(handle.getTarget() instanceof Corpus))
      throw new IllegalArgumentException(
              "We only deal with documents and corpora");
    this.handle = handle;
    init();
  }

  private void init() {

    DocumentExporter gateXMLExporter =
            (DocumentExporter)Gate.getCreoleRegister()
                    .get(GateXMLExporter.class.getCanonicalName())
                    .getInstantiations().iterator().next();
    addExporter(gateXMLExporter);

    Set<String> toolTypes = Gate.getCreoleRegister().getToolTypes();
    for(String type : toolTypes) {
      List<Resource> instances =
              Gate.getCreoleRegister().get(type).getInstantiations();
      for(Resource res : instances) {
        if(res instanceof DocumentExporter) {
          addExporter((DocumentExporter)res);
        }
      }
    }
    Gate.getCreoleRegister().addCreoleListener(this);
  }

  private File getSelectedFile(List<List<Parameter>> params,
          DocumentExporter de, FeatureMap options) {
    File selectedFile = null;

    Document document =
            (handle.getTarget() instanceof Document ? (Document)handle
                    .getTarget() : null);
    // are we looking for a file or a directory?
    boolean singleFile = (document != null) || (de instanceof CorpusExporter);

    if(document != null && document.getSourceUrl() != null) {
      // there is a document and it has a URL so let's try and figure out a
      // sensible filename for it

      try {
        // firstly is the source URL actually a file? if so just use it
        selectedFile = Files.fileFromURL(document.getSourceUrl());
      } catch(IllegalArgumentException e) {
        // if the URL isn't a file then...

        String fileName = null;
        if(document.getNamedAnnotationSets().containsKey("Original markups")
            && !document.getAnnotations("Original markups").get("title")
                .isEmpty()) {
          // use the title annotation from the document
          fileName = Utils.stringFor(document,
              document.getAnnotations("Original markups").get("title"));
        }

        if (fileName == null) {
          // if there was no title then see if we can get a file name from the URL path
          String path = document.getSourceUrl().getPath();
          if (!path.isEmpty()) {
            // this may return the empty string but it should always be a valid
            // substring call
            fileName = path.substring(1+path.lastIndexOf("/"));
          }
        }

        if (fileName == null) {
          // if we still fail then use the document name
          fileName = document.getName();
        }

        // just to be on the safe side replace any odd characters with
        // underscores before trying to create a file
        fileName = fileName.replaceAll("[^/a-zA-Z0-9._-]+", "_");
        fileName = fileName.replaceAll("__+", "_");

        selectedFile = new File(fileName);
      }

      // get just the filename so we can mess about with its extension
      String fileName = selectedFile.getName();

      // if the file has an extension already replace it with the default for
      // this exporter
      fileName = fileName.replaceAll("\\.[a-zA-Z]{1,4}$",
          "." + de.getDefaultExtension());

      // if the file still doesn't end with the right extension then append it
      // to the end and be done with it
      if(!fileName.endsWith("." + de.getDefaultExtension())) {
        fileName += "." + de.getDefaultExtension();
      }

      //finally create the File object
      selectedFile = new File(selectedFile, fileName);

    }

    if(params == null || params.isEmpty()) {
      JFileChooser fileChooser = new JFileChooser();
      fileChooser.resetChoosableFileFilters();
      fileChooser.setFileFilter(de.getFileFilter());
      fileChooser.setMultiSelectionEnabled(false);
      fileChooser.setDialogTitle("Save as " + de.getFileType());
      fileChooser.setFileSelectionMode(singleFile
              ? JFileChooser.FILES_ONLY
              : JFileChooser.DIRECTORIES_ONLY);

      if(selectedFile != null) {
        fileChooser.ensureFileIsVisible(selectedFile);
        fileChooser.setSelectedFile(selectedFile);
      }      

      if(fileChooser.showSaveDialog(MainFrame.getInstance()) != JFileChooser.APPROVE_OPTION)
        return null;
      selectedFile = fileChooser.getSelectedFile();
    } else {
      if(!dialog.show(de, params, singleFile, selectedFile != null
              ? selectedFile.getAbsolutePath()
              : "")) return null;

      options.putAll(dialog.getSelectedParameters());
      selectedFile = new File(dialog.getSelectedFileName());
    }

    return selectedFile;
  }

  private void addExporter(final DocumentExporter de) {

    if(itemByResource.containsKey(de)) return;

    final ResourceData rd =
            Gate.getCreoleRegister().get(de.getClass().getCanonicalName());

    if(DocumentExportMenu.this.getItemCount() == 1) {
      DocumentExportMenu.this.addSeparator();
    }

    JMenuItem item =
            DocumentExportMenu.this.add(new AbstractAction(de.getFileType()
                    + " (." + de.getDefaultExtension() + ")", MainFrame
                    .getIcon(rd.getIcon(),rd.getResourceClassLoader())) {

              @Override
              public void actionPerformed(ActionEvent ae) {

                List<List<Parameter>> params =
                        rd.getParameterList().getRuntimeParameters();

                final FeatureMap options = Factory.newFeatureMap();

                final File selectedFile = getSelectedFile(params, de, options);

                if(selectedFile == null) return;

                Runnable runnable = new Runnable() {
                  public void run() {

                    if(handle.getTarget() instanceof Document) {

                      long start = System.currentTimeMillis();
                      listener.statusChanged("Saving as " + de.getFileType()
                              + " to " + selectedFile.toString() + "...");
                      try {
                        de.export((Document)handle.getTarget(), selectedFile,
                                options);
                      } catch(IOException e) {
                        e.printStackTrace();
                      }

                      long time = System.currentTimeMillis() - start;
                      listener.statusChanged("Finished saving as "
                              + de.getFileType() + " into " + " the file: "
                              + selectedFile.toString() + " in "
                              + ((double)time) / 1000 + "s");
                    } else { // corpus
                      if(de instanceof CorpusExporter) {

                        long start = System.currentTimeMillis();
                        listener.statusChanged("Saving as " + de.getFileType()
                                + " to " + selectedFile.toString() + "...");
                        try {
                          ((CorpusExporter)de).export((Corpus)handle.getTarget(), selectedFile,
                                  options);
                        } catch(IOException e) {
                          e.printStackTrace();
                        }

                        long time = System.currentTimeMillis() - start;
                        listener.statusChanged("Finished saving as "
                                + de.getFileType() + " into " + " the file: "
                                + selectedFile.toString() + " in "
                                + ((double)time) / 1000 + "s");
                      } else { // not a CorpusExporter
                        try {
                          File dir = selectedFile;
                          // create the top directory if needed
                          if(!dir.exists()) {
                            if(!dir.mkdirs()) {
                              JOptionPane.showMessageDialog(
                                      MainFrame.getInstance(),
                                      "Could not create top directory!", "GATE",
                                      JOptionPane.ERROR_MESSAGE);
                              return;
                            }
                          }
  
                          MainFrame.lockGUI("Saving...");
                          Corpus corpus = (Corpus)handle.getTarget();
  
                          long startTime = System.currentTimeMillis();
                          // iterate through all the docs and save
                          // each of
                          // them
                          Iterator<Document> docIter = corpus.iterator();
                          boolean overwriteAll = false;
                          int docCnt = corpus.size();
                          int currentDocIndex = 0;
                          Set<String> usedFileNames = new HashSet<String>();
                          while(docIter.hasNext()) {
                            boolean docWasLoaded =
                                    corpus.isDocumentLoaded(currentDocIndex);
                            Document currentDoc = docIter.next();
  
                            URL sourceURL = currentDoc.getSourceUrl();
                            String fileName = null;
                            if(sourceURL != null) {
                              fileName = sourceURL.getPath();
                              fileName = Files.getLastPathComponent(fileName);
                            }
                            if(fileName == null || fileName.length() == 0) {
                              fileName = currentDoc.getName();
                            }
                            // makes sure that the filename does not
                            // contain
                            // any
                            // forbidden character
                            fileName =
                                    fileName.replaceAll("[\\/:\\*\\?\"<>|]", "_");
                            if(fileName.toLowerCase().endsWith(
                                    "." + de.getDefaultExtension())) {
                              fileName =
                                      fileName.substring(0,
                                              fileName.length()
                                                      - de.getDefaultExtension()
                                                              .length()
                                                      - 1);
                            }
                            if(usedFileNames.contains(fileName)) {
                              // name clash -> add unique ID
                              String fileNameBase = fileName;
                              int uniqId = 0;
                              fileName = fileNameBase + "-" + uniqId++;
                              while(usedFileNames.contains(fileName)) {
                                fileName = fileNameBase + "-" + uniqId++;
                              }
                            }
                            usedFileNames.add(fileName);
                            if(!fileName.toLowerCase().endsWith(
                                    "." + de.getDefaultExtension()))
                              fileName += "." + de.getDefaultExtension();
                            File docFile = null;
                            boolean nameOK = false;
                            do {
                              docFile = new File(dir, fileName);
                              if(docFile.exists() && !overwriteAll) {
                                // ask the user if we can overwrite
                                // the file
                                Object[] opts =
                                        new Object[] {"Yes", "All", "No",
                                            "Cancel"};
                                MainFrame.unlockGUI();
                                int answer =
                                        JOptionPane.showOptionDialog(
                                                MainFrame.getInstance(), "File "
                                                        + docFile.getName()
                                                        + " already exists!\n"
                                                        + "Overwrite?", "GATE",
                                                JOptionPane.DEFAULT_OPTION,
                                                JOptionPane.WARNING_MESSAGE,
                                                null, opts, opts[2]);
                                MainFrame.lockGUI("Saving...");
                                switch(answer) {
                                  case 0: {
                                    nameOK = true;
                                    break;
                                  }
                                  case 1: {
                                    nameOK = true;
                                    overwriteAll = true;
                                    break;
                                  }
                                  case 2: {
                                    // user said NO, allow them to
                                    // provide
                                    // an
                                    // alternative name;
                                    MainFrame.unlockGUI();
                                    fileName =
                                            (String)JOptionPane.showInputDialog(
                                                    MainFrame.getInstance(),
                                                    "Please provide an alternative file name",
                                                    "GATE",
                                                    JOptionPane.QUESTION_MESSAGE,
                                                    null, null, fileName);
                                    if(fileName == null) {
                                      handle.processFinished();
                                      return;
                                    }
                                    MainFrame.lockGUI("Saving");
                                    break;
                                  }
                                  case 3: {
                                    // user gave up; return
                                    handle.processFinished();
                                    return;
                                  }
                                }
  
                              } else {
                                nameOK = true;
                              }
                            } while(!nameOK);
                            // save the file
                            try {
                              // do the actual exporting
                              de.export(currentDoc, docFile, options);
                            } catch(Exception ioe) {
                              MainFrame.unlockGUI();
                              JOptionPane.showMessageDialog(
                                      MainFrame.getInstance(),
                                      "Could not create write file:"
                                              + ioe.toString(), "GATE",
                                      JOptionPane.ERROR_MESSAGE);
                              ioe.printStackTrace(Err.getPrintWriter());
                              return;
                            }
  
                            listener.statusChanged(currentDoc.getName()
                                    + " saved");
                            // close the doc if it wasn't already
                            // loaded
                            if(!docWasLoaded) {
                              corpus.unloadDocument(currentDoc);
                              Factory.deleteResource(currentDoc);
                            }
  
                            handle.progressChanged(100 * currentDocIndex++
                                    / docCnt);
                          }// while(docIter.hasNext())
                          long endTime = System.currentTimeMillis();
                          listener.statusChanged("Corpus saved in "+ 
                                  NumberFormat.getInstance().format(
                                    (double)(endTime - startTime) / 1000)+
                                  " seconds!");
                          handle.processFinished();
                        } finally {
                          MainFrame.unlockGUI();
                        }
                      }
                    }
                  }
                };

                Thread thread =
                        new Thread(Thread.currentThread().getThreadGroup(),
                                runnable, "Document Exporter Thread");
                thread.setPriority(Thread.MIN_PRIORITY);
                thread.start();
              }

            });

    itemByResource.put(de, item);

  }

  /**
   * If the resource just loaded is a tool (according to the creole
   * register) then see if it publishes any actions and if so, add them
   * to the menu in the appropriate places.
   */
  @Override
  public void resourceLoaded(CreoleEvent e) {
    final Resource res = e.getResource();

    if(res instanceof DocumentExporter) {
      Runnable runnable = new Runnable() {
        @Override
        public void run() {
          addExporter((DocumentExporter)res);
        }
      };
      
      if(SwingUtilities.isEventDispatchThread()) {
        runnable.run();
      } else {
        try {
          SwingUtilities.invokeAndWait(runnable);
        } catch(Exception ex) {
          log.warn("Exception registering document exporter", ex);
        }
      }
    }
  }

  @Override
  public void resourceUnloaded(CreoleEvent e) {
    final Resource res = e.getResource();

    if(res instanceof DocumentExporter) {
      SwingUtilities.invokeLater(new Runnable() {
        
        @Override
        public void run() {
          // TODO Auto-generated method stub
          JMenuItem item = itemByResource.get(res);

          if(item != null) {
            remove(item);
            itemByResource.remove(res);
          }
          
          if(getItemCount() == 2) {
            remove(1);
          }
        }
      });     
    }    
  }

  // remaining CreoleListener methods not used
  @Override
  public void datastoreClosed(CreoleEvent e) {
  }

  @Override
  public void datastoreCreated(CreoleEvent e) {
  }

  @Override
  public void datastoreOpened(CreoleEvent e) {
  }

  @Override
  public void resourceRenamed(Resource resource, String oldName, String newName) {
  }

  private static class DocumentExportDialog extends JDialog {

    private DocumentExporter de;

    private JButton okBtn, fileBtn, cancelBtn;

    private JTextField txtFileName;

    private ResourceParametersEditor parametersEditor;

    private boolean singleFile, userCanceled;
    
    private FeatureMap parameters;

    public DocumentExportDialog() {
      super(MainFrame.getInstance(), "Save As...", true);
      MainFrame.getGuiRoots().add(this);
      initGuiComponents();
      initListeners();
    }

    protected void initGuiComponents() {
      this.getContentPane().setLayout(
              new BoxLayout(this.getContentPane(), BoxLayout.Y_AXIS));

      // name field
      Box nameBox = Box.createHorizontalBox();
      nameBox.add(Box.createHorizontalStrut(5));
      nameBox.add(new JLabel("Save To:"));
      nameBox.add(Box.createHorizontalStrut(5));
      txtFileName = new JTextField(30);
      txtFileName.setMaximumSize(new Dimension(Integer.MAX_VALUE, txtFileName
              .getPreferredSize().height));
      txtFileName.setRequestFocusEnabled(true);
      txtFileName.setVerifyInputWhenFocusTarget(false);
      nameBox.add(txtFileName);
      // nameField.setToolTipText("Enter a name for the resource");

      nameBox.add(Box.createHorizontalStrut(5));
      nameBox.add(fileBtn = new JButton(MainFrame.getIcon("OpenFile")));
      nameBox.add(Box.createHorizontalGlue());
      this.getContentPane().add(nameBox);
      this.getContentPane().add(Box.createVerticalStrut(5));

      // parameters table
      parametersEditor = new ResourceParametersEditor();
      this.getContentPane().add(new JScrollPane(parametersEditor));
      this.getContentPane().add(Box.createVerticalStrut(5));
      this.getContentPane().add(Box.createVerticalGlue());
      // buttons box
      JPanel buttonsBox = new JPanel();
      buttonsBox.setLayout(new BoxLayout(buttonsBox, BoxLayout.X_AXIS));
      buttonsBox.add(Box.createHorizontalStrut(10));
      buttonsBox.add(okBtn = new JButton("OK"));
      buttonsBox.add(Box.createHorizontalStrut(10));
      buttonsBox.add(cancelBtn = new JButton("Cancel"));
      buttonsBox.add(Box.createHorizontalStrut(10));
      this.getContentPane().add(buttonsBox);
      this.getContentPane().add(Box.createVerticalStrut(5));
      setSize(400, 300);

      getRootPane().setDefaultButton(okBtn);
    }

    protected void initListeners() {
      Action fileAction = new AbstractAction() {

        @Override
        public void actionPerformed(ActionEvent ae) {
          XJFileChooser fileChooser = MainFrame.getFileChooser();
          fileChooser.resetChoosableFileFilters();
          fileChooser.setFileFilter(de.getFileFilter());
          fileChooser.setMultiSelectionEnabled(false);
          fileChooser.setDialogTitle("Save as " + de.getFileType());
          fileChooser.setFileSelectionMode(singleFile
                  ? JFileChooser.FILES_ONLY
                  : JFileChooser.DIRECTORIES_ONLY);

          try {
            File f = new File(txtFileName.getText());
            fileChooser.ensureFileIsVisible(f);
            fileChooser.setSelectedFile(f);
          } catch(Exception e) {
            // swallow and ignore
          }

          if(fileChooser.showSaveDialog(DocumentExportDialog.this) != JFileChooser.APPROVE_OPTION)
            return;

          File selectedFile = fileChooser.getSelectedFile();
          if(selectedFile == null) return;

          txtFileName.setText(selectedFile.getAbsolutePath());
        }
      };

      Action applyAction = new AbstractAction() {
        @Override
        public void actionPerformed(ActionEvent e) {
          userCanceled = false;
          TableCellEditor cellEditor = parametersEditor.getCellEditor();
          if(cellEditor != null) {
            cellEditor.stopCellEditing();
          }
          setVisible(false);
        }
      };
      Action cancelAction = new AbstractAction() {
        @Override
        public void actionPerformed(ActionEvent e) {
          userCanceled = true;
          setVisible(false);
        }
      };

      fileBtn.addActionListener(fileAction);
      okBtn.addActionListener(applyAction);
      cancelBtn.addActionListener(cancelAction);

      // disable Enter key in the table so this key will confirm the
      // dialog
      InputMap im =
              parametersEditor
                      .getInputMap(JTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
      KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
      im.put(enter, "none");

      // define keystrokes action bindings at the level of the main
      // window
      InputMap inputMap =
              ((JComponent)this.getContentPane())
                      .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
      ActionMap actionMap = ((JComponent)this.getContentPane()).getActionMap();
      inputMap.put(KeyStroke.getKeyStroke("ENTER"), "Apply");
      actionMap.put("Apply", applyAction);
      inputMap.put(KeyStroke.getKeyStroke("ESCAPE"), "Cancel");
      actionMap.put("Cancel", cancelAction);
    }

    public synchronized boolean show(DocumentExporter de,
            List<List<Parameter>> params, boolean singleFile, String filePath) {

      
      
      this.singleFile = singleFile;
      this.de = de;
      this.parameters = null;

      setTitle("Save as " + de.getFileType());

      txtFileName.setText(filePath);
      parametersEditor.init(null, params);
      pack();
      txtFileName.requestFocusInWindow();
      userCanceled = true;
      setModal(true);
      setLocationRelativeTo(getOwner());
      super.setVisible(true);
      dispose();
      if(userCanceled)
        return false;
      
      //update the feature map to convert values to objects of the correct type.
      
      parameters = parametersEditor.getParameterValues();
      
      for (List<Parameter> disjunction : params) {
        for (Parameter param : disjunction) {
          if (!param.getTypeName().equals("java.lang.String") && parameters.containsKey(param.getName())) {
            Object value = parameters.get(param.getName());
            if (value instanceof String) {
              try {
                parameters.put(param.getName(), param.calculateValueFromString((String)value));
              }
              catch (ParameterException pe) {
                pe.printStackTrace();
                parameters = null;
                return false;
              }
            }
          }
        }
      }
      
      return true;
    }

    @Override
    public void dispose() {
      de = null;
    }

    /**
     * Returns the selected params for the resource or null if none was
     * selected or the user pressed cancel
     */
    public FeatureMap getSelectedParameters() {
      return parameters;
    }

    /**
     * Return the String entered into the resource name field of the
     * dialog.
     * 
     * @param rData
     */
    public String getSelectedFileName() {
      return txtFileName.getText();
    }
  }
}