/**
 *
 * Copyright INRA-URGI 2009-2010
 * 
 * This software is governed by the CeCILL license under French law and
 * abiding by the rules of distribution of free software. You can use,
 * modify and/ or redistribute the software under the terms of the CeCILL
 * license as circulated by CEA, CNRS and INRIA at the following URL
 * "http://www.cecill.info".
 * 
 * As a counterpart to the access to the source code and rights to copy,
 * modify and redistribute granted by the license, users are provided only
 * with a limited warranty and the software's author, the holder of the
 * economic rights, and the successive licensors have only limited
 * liability.
 * 
 * In this respect, the user's attention is drawn to the risks associated
 * with loading, using, modifying and/or developing or reproducing the
 * software by the user in light of its specific status of free software,
 * that may mean that it is complicated to manipulate, and that also
 * therefore means that it is reserved for developers and experienced
 * professionals having in-depth computer knowledge. Users are therefore
 * encouraged to load and test the software's suitability as regards their
 * requirements in conditions enabling the security of their systems and/or
 * data to be ensured and, more generally, to use and operate it in the
 * same conditions as regards security.
 * 
 * The fact that you are presently reading this means that you have had
 * knowledge of the CeCILL license and that you accept its terms.
 *
 */
import java.util.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowAdapter;
import java.io.*;
import javax.swing.*;
import javax.swing.filechooser.*;
import javax.swing.border.*;
import javax.swing.SwingUtilities;
import java.util.prefs.BackingStoreException;


public class Smart extends JPanel implements ActionListener {

  String version = "1.1.0";

  JFrame mainFrame;
  JButton openButton;
  JButton resetFileButton;

  JComboBox formatTypes;
  JComboBox fileFormats;
  String[]  emptyFormats = {"Choose a type first..."};

  JFrame  askFrame;
  JButton pythonButton;
  JButton mySqlButton;
  JButton rButton;

  HashMap <JButton, Program> callingProgram;

  static JList        fileList;
  static JTextArea    logArea;

  // progress bar
  static JLabel       messageField;
  static JProgressBar progressBar;
  static JLabel       etaField;

  // process
  Program           currentProgram;
  Process           process;
  javax.swing.Timer processTimer;


  int previousStatus;

  public Smart() {
    super(new BorderLayout());

    callingProgram = new HashMap <JButton, Program> ();

    previousStatus = -1;

    processTimer = new javax.swing.Timer(1000, this);
    processTimer.stop();

    // Ask frame buttons
    pythonButton = new JButton("find...");
    mySqlButton  = new JButton("find...");
    rButton      = new JButton("find...");

    // Get available formats
    FormatsReader formatReader = new FormatsReader(Global.smartFormatsFileName);
    if (! formatReader.read()) {
      System.out.println("Something was wrong while reading file format...");
    }

    // Get screen size
    Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();

    // Log
    logArea = new JTextArea(512, Global.logAreaSize);
    logArea.setPreferredSize(new Dimension(screenSize.width, (int) (screenSize.height * 0.22)));
    logArea.setFont(new Font("Monospaced", logArea.getFont().getStyle(), logArea.getFont().getSize()));
    JScrollPane logScroll  = new JScrollPane(logArea, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
    TitledBorder logBorder = BorderFactory.createTitledBorder("Log");
    logScroll.setBorder(logBorder);
    logArea.append("Using S-MART " + version + "\n");

    GridLayout horizontalLayout = new GridLayout(1, 0);

    // check configuration
    this.readConfigurationFile();
    if (System.getProperty("os.name").matches("(?i).*Windows.*")) {
      if (! this.checkDefaultDir()) {
        this.checkRegistries();
      }
    }
    this.checkConfiguration();

    // Tabs
    JTabbedPane tabbedPane = new JTabbedPane();
    tabbedPane.setPreferredSize(new Dimension(screenSize.width, (int) (screenSize.height * 0.75)));

    // File panel
    JPanel filePanel = new JPanel(false);
    filePanel.setLayout(new FlowLayout());
    tabbedPane.add("Files", filePanel);

    // Format sub-panel
    JPanel         formatComboPanel  = new JPanel(false);
    JPanel         formatPanel       = new JPanel(false);
    Vector<String> formatTypesString = Global.formats.getFormatTypes();
    formatPanel.setLayout(horizontalLayout);
    formatTypesString.insertElementAt("Choose the format type", 0);
    JLabel formatLabel = new JLabel("Format");
    formatTypes        = new JComboBox(formatTypesString);
    fileFormats        = new JComboBox(emptyFormats);
    formatLabel.setLabelFor(fileFormats);
    formatTypes.addActionListener(this);
    formatComboPanel.add(formatTypes);
    formatComboPanel.add(fileFormats);

    formatPanel.add(formatLabel);
    formatPanel.add(formatComboPanel);

    // File chooser sub-panel
    JPanel fileChooserPanel = new JPanel(false);
    fileChooserPanel.setLayout(horizontalLayout);
    JLabel fileLabel = new JLabel("File");
    openButton = new JButton("Open a File...");
    openButton.addActionListener(this);

    fileChooserPanel.add(fileLabel);
    fileChooserPanel.add(openButton);

    // File list sub-panel
    JPanel existingFilesPanel = new JPanel(false);
    existingFilesPanel.setLayout(horizontalLayout);
    existingFilesPanel.setMinimumSize(new Dimension(10000, 10000));
    JLabel existingFilesLabel = new JLabel("Existing files");
    Box fileListBox = Box.createHorizontalBox();
    fileListBox.add(Box.createRigidArea(new Dimension(0, 100)));
    fileList = new JList(new DefaultListModel());
    fileList.setLayoutOrientation(JList.HORIZONTAL_WRAP);
    fileList.setVisibleRowCount(4);
    JScrollPane listScroller = new JScrollPane(fileList);
    fileListBox.add(listScroller);

    existingFilesPanel.add(existingFilesLabel);
    existingFilesPanel.add(fileListBox);

    // Reset sub-panel
    JPanel resetFilePanel = new JPanel(false);
    resetFileButton = new JButton("Reset");
    resetFileButton.addActionListener(this);

    // File panel layout
    Box box = Box.createVerticalBox();
    box.add(formatPanel);
    box.add(fileChooserPanel);
    box.add(existingFilesPanel);
    box.add(resetFileButton);
    filePanel.add(box);

    // Program panels
    TreeMap < String, JTabbedPane > panels = new TreeMap < String, JTabbedPane >();
    PythonProgramFinder programFinder = new PythonProgramFinder("Python");
    String comments = programFinder.findPrograms();
    if (comments != null) {
      logArea.append(comments);
    }
    for (int i = 0; i < programFinder.getPrograms().size(); i++) {
      Program program         = programFinder.getPrograms().get(i);
      JPanel programPanel     = program.getPanel();
      String section          = program.getSection();
      JTabbedPane sectionPane = null;
      if (panels.containsKey(section)) {
        sectionPane = panels.get(section);
      }
      else {
        sectionPane = new JTabbedPane();
        tabbedPane.addTab(section, sectionPane);
        panels.put(section, sectionPane);
      }

      JScrollPane programScroll  = new JScrollPane(programPanel, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
      sectionPane.addTab(program.getName(), programScroll);

      JButton button  = program.getButton();
      button.addActionListener(this);
      callingProgram.put(button, program);
    }

    // Progress bar
    JPanel progressPanel = new JPanel(new GridLayout(1, 0), false);
    progressPanel.setPreferredSize(new Dimension(screenSize.width, (int) (screenSize.height * 0.02)));
    messageField = new JLabel();
    progressBar  = new JProgressBar(0, 100);
    etaField     = new JLabel();
    messageField.setHorizontalAlignment(JLabel.LEFT);
    progressBar.setValue(0);
    etaField.setHorizontalAlignment(JLabel.RIGHT);
    progressBar.setStringPainted(true);
    progressPanel.add(messageField);
    progressPanel.add(progressBar);
    progressPanel.add(etaField);
      
    add(tabbedPane, BorderLayout.PAGE_START);
    add(logScroll, BorderLayout.CENTER);
    add(progressPanel, BorderLayout.PAGE_END);
  }


  public String checkSubKey(int hkey, String key, String subKey, String trace) {
    try {
      for (String currentSubKey: WindowsRegistry.readStringSubKeys(hkey, key)) {
        trace += "Looking at sub-key " + currentSubKey;
        if (currentSubKey.matches(subKey)) {
          trace += "  OK";
          return subKey;
        }
        trace += "\n";
      }
    }
    catch (Exception e) {
      final Writer writer = new StringWriter();
      final PrintWriter printWriter = new PrintWriter(writer);
      e.printStackTrace(printWriter);
      trace += writer.toString();          
    }
    return null;
  }

  public void debugRegistry(int hkey, String keys[], String valueName, String trace) {
    String concatenatedKeys = "";
    String selectedKey      = null;
    for (String key: keys) {
      selectedKey = checkSubKey(hkey, concatenatedKeys, key, trace);
      if (selectedKey != null) {
        concatenatedKeys += "\\" + selectedKey;
      }
      else {
        return;
      }
    }
  }


  public String checkRegistry(int hkey, String key, String valueName, String trace) {
    String result = null;
    try {
      result = WindowsRegistry.readString(hkey, key, valueName);
    }
    catch (Exception e) {
      final Writer writer = new StringWriter();
      final PrintWriter printWriter = new PrintWriter(writer);
      e.printStackTrace(printWriter);
      trace += result.toString();          
    }
    return result;
  }


  public boolean checkDefaultDir() {
    String       defaultPythonPath = System.getProperty("user.dir") + "\\Apps\\Python\\python.exe";
    java.io.File defaultPythonFile = new java.io.File(defaultPythonPath);
    String       defaultRPath      = System.getProperty("user.dir") + "\\Apps\\R\\bin\\R.exe";
    java.io.File defaultRFile      = new java.io.File(defaultRPath);
    if (defaultPythonFile.exists()) {
        Global.pythonCommand = defaultPythonPath;
        logArea.append("Python found in default directory: " + defaultPythonPath + "\n");
    }
    else {
        logArea.append("Python not found in default directory: " + defaultPythonPath + "\n");
        return false;
    }
    if (defaultRFile.exists()) {
        logArea.append("R found in default directory: " + defaultRPath + "\n");
        Global.rCommand = defaultRPath;
        return true;
    }
    logArea.append("Python not found in default directory: " + defaultPythonPath + "\n");
    return false;
  }


  public boolean checkRegistries() {
    String          pythonDir  = null;
    String          validValue = null;
    String          rDir;
    String[]        pythonFlavors     = {"2.5", "2.6", "2.7"};
    String[]        pythonDirectories = {"Python25", "Python26", "Python27"};
    String[]        rDirectories      = {"R-2.11.0", "R-2.11.0-x64"};
    String          trace             = "";
    for (String pythonFlavor: pythonFlavors) {
      pythonDir = checkRegistry(WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\Python\\PythonCore\\" + pythonFlavor + "\\InstallPath", "", trace);
      if (pythonDir != null) {
        break;
      }
    }
    if (pythonDir == null) {
      try {
        logArea.append("Using OS: " + WindowsRegistry.readString(WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", "ProductName") + "\n");
      }
      catch (Exception e) {
        logArea.append("Cannot do simple registry test. Strange...\n");
      }
      String keys[] = {"SOFTWARE", "Python", "PythonCore", "2.[567]", "InstallPath"};
      debugRegistry(WindowsRegistry.HKEY_LOCAL_MACHINE, keys, "", trace);
      logArea.append("S-MART cannot find Python installation directory using registry. Trying desperate move...\n");
      for (String currentDirectory: pythonDirectories) {
        String fileName = "C:\\" + currentDirectory;
        java.io.File file = new java.io.File(fileName);
        if (file.exists()) {
          pythonDir = fileName;
          break;
        }
      }
      if (pythonDir == null) {
        logArea.append("S-MART cannot find Python installation directory despite all my efforts...\n" + trace);
        return false;
      }
    }
    rDir = checkRegistry(WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\R-core\\R", "InstallPath", trace);
    if (rDir == null) {
      logArea.append("S-MART cannot find R installation directory using registry. Trying desperate move...\n");
      for (String currentDirectory: rDirectories) {
        String fileName = "C:\\Program Files\\R\\" + currentDirectory;
        java.io.File file = new java.io.File(fileName);
        if (file.exists()) {
          rDir = fileName;
          break;
        }
      }
      if (rDir == null) {
        logArea.append("S-MART cannot find R installation directory despite all my efforts...\n" + trace);
        return false;
      }
    }
    Global.pythonCommand = pythonDir + "\\" + "python.exe";
    Global.rCommand      = rDir + "\\" + "bin\\R.exe";
    return true;
  }


  public boolean checkConfiguration() {
    int status = this.testConfiguration();
  
    if (status == previousStatus) {
      logArea.append("S-MART does not seem to work properly... Tried to manage it by myself, unsuccessfully... Check documentation for further explanation...\n");
      return false;
    }

    switch (status) {
      case 0:
        return true;
      case 1:
        logArea.append("S-MART does not seem to work properly... Check documentation for further explanation...\n");
        break;
      case 3:
        this.askWhereIsProgram("python");
        break;
      case 4:
        break;
      case 5:
        this.askWhereIsProgram("mySQL");
        break;
      case 6:
        this.askWhereIsProgram("R");
        break;
      case 7:
        logArea.append("Please install 'ColorBrewer' R package...\n");
        break;
      default:
        logArea.append("Weird configuration test status: " + status + "...\n");
    }
    previousStatus = status;
    return true;
  }


  public int testConfiguration() {
    String[]        command  = {Global.pythonCommand, "Python" + java.io.File.separator + "testInstall.py"};
    ProgramLauncher launcher = new ProgramLauncher(command, logArea, messageField, progressBar, etaField);
    String          line;
    launcher.execute();
    return launcher.getExitValue();
  }


  public void readConfigurationFile() {
    java.io.File   file      = new java.io.File(Global.smartConfFileName);
    String         line      = null;

    if (! file.exists()) {
      return;
    }

    try {
      BufferedReader reader = new BufferedReader(new FileReader(file));

      while ((line = reader.readLine()) != null) {
        line = line.trim();
        if      (line.startsWith("python:")) Global.pythonCommand = line.substring("python:".length()).trim();
        else if (line.startsWith("mysql:"))  Global.mysqlCommand  = line.substring("mysql:".length()).trim();
        else if (line.startsWith("r:"))      Global.rCommand      = line.substring("r:".length()).trim();
      }
      reader.close();
    }
    catch (FileNotFoundException e) {
      logArea.append("Configuration file is empty: " + e.getMessage() + "!\n");
      return;
    }
    catch (IOException e) {
      logArea.append("Weird with configuration file: " + e.getMessage() + "!\n");
      return;
    }
  }


  public void askWhereIsProgram(String program) {
    askFrame = new JFrame("Where is " + program + "?");
    askFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    JLabel label = new JLabel("Where is your " + program + " (or " + program + ".exe) file?");
    JButton button = null;
    if ("python".equals(program)) {
      button = pythonButton;
    }
    else if ("mySQL".equals(program)) {
      button = mySqlButton;
    }
    else if ("R".equals(program)) {
      button = rButton;
    }
    else {
      logArea.append("Problem with the button!\n");
    }
    askFrame.getContentPane().add(label, BorderLayout.WEST);
    askFrame.getContentPane().add(button, BorderLayout.EAST);
    button.addActionListener(this);
    askFrame.pack();
    askFrame.setVisible(true);
    askFrame.setAlwaysOnTop(true);
  }


  public void actionPerformed(ActionEvent e) {

    // Python command chooser
    if (e.getSource() == pythonButton) {
      JFileChooser chooser = new JFileChooser();
      if (chooser.showOpenDialog(mainFrame) == JFileChooser.APPROVE_OPTION) {
        Global.pythonCommand = chooser.getSelectedFile().getPath();
        askFrame.setVisible(false);
        askFrame.dispose();
        try {
          BufferedWriter out = new BufferedWriter(new FileWriter(Global.smartConfFileName, true));
          out.write("python: " + Global.pythonCommand + "\n");
          out.close();
        }
        catch (IOException exception) {
          logArea.append("Cannot write configuration file!\n");
        }
      }
      this.checkConfiguration();
    }
    // MySQL command chooser
    else if (e.getSource() == mySqlButton) {
      JFileChooser chooser = new JFileChooser();
      if (chooser.showOpenDialog(mainFrame) == JFileChooser.APPROVE_OPTION) {
        Global.mysqlCommand = chooser.getSelectedFile().getPath();
        askFrame.setVisible(false);
        askFrame.dispose();
        try {
          BufferedWriter out = new BufferedWriter(new FileWriter(Global.smartConfFileName, true));
          out.write("mysql: " + Global.mysqlCommand + "\n");
          out.close();
        }
        catch (IOException exception) {
          logArea.append("Cannot write configuration file!\n");
        }
      }
      this.checkConfiguration();
    }
    // R command chooser
    else if (e.getSource() == rButton) {
      JFileChooser chooser = new JFileChooser();
      if (chooser.showOpenDialog(mainFrame) == JFileChooser.APPROVE_OPTION) {
        Global.rCommand = chooser.getSelectedFile().getPath();
        askFrame.setVisible(false);
        askFrame.dispose();
        try {
          BufferedWriter out = new BufferedWriter(new FileWriter(Global.smartConfFileName, true));
          out.write("r: " + Global.rCommand + "\n");
          out.close();
        }
        catch (IOException exception) {
          logArea.append("Cannot write configuration file!\n");
        }
      }
      this.checkConfiguration();
    }
    // Format type
    else if (e.getSource() == formatTypes) {
      if (((String) formatTypes.getSelectedItem()).startsWith("Choose")) {
        return;
      }
      fileFormats.removeAllItems();
      Vector < String > selectedFormats = Global.formats.getFormats((String) formatTypes.getSelectedItem()).getFormats();
      for (int i = 0; i < selectedFormats.size(); i++) {
        fileFormats.addItem(selectedFormats.get(i));
      }
    }
    // Main file chooser
    else if (e.getSource() == openButton) {
      if (((String) formatTypes.getSelectedItem()).startsWith("Choose")) {
        logArea.append("Please choose a type and format before selecting a file!\n");
        return;
      }
      JFileChooser chooser = new JFileChooser();
      if (chooser.showOpenDialog(mainFrame) == JFileChooser.APPROVE_OPTION) {
        String fileName = chooser.getSelectedFile().getPath();
        Global.fileNames.addElement(fileName);
        Global.files.addFile(fileName, (String) formatTypes.getSelectedItem(), (String) fileFormats.getSelectedItem());
        DefaultListModel defaultListModel = (DefaultListModel) fileList.getModel();
        defaultListModel.addElement(fileName);
      }
    }
    // Reset file chooser
    else if (e.getSource() == resetFileButton) {
      Global.files.clear();
      Global.fileNames.clear();
      DefaultListModel defaultListModel = (DefaultListModel) fileList.getModel();
      defaultListModel.clear();
    }
    // Other file choosers
    else if (Global.otherFilesChooser.containsKey(e.getSource())) {
      JTextField textField = Global.otherFilesChooser.get(e.getSource());
      JFileChooser chooser = new JFileChooser();
      if (chooser.showOpenDialog(mainFrame) == JFileChooser.APPROVE_OPTION) {
        textField.setText(chooser.getSelectedFile().getPath());
      }
    }
    // Other directories choosers
    else if (Global.otherDirectoriesChooser.containsKey(e.getSource())) {
      JTextField textField = Global.otherDirectoriesChooser.get(e.getSource());
      JFileChooser chooser = new JFileChooser();
      chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
      if (chooser.showOpenDialog(mainFrame) == JFileChooser.APPROVE_OPTION) {
        textField.setText(chooser.getSelectedFile().getPath());
      }
    }
    else if (Global.otherFileConcatenationChooser.containsKey(e.getSource())) {
      JTextField textField = Global.otherDirectoriesChooser.get(e.getSource());
      JFileChooser chooser = new JFileChooser();
      chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
      if (chooser.showOpenDialog(mainFrame) == JFileChooser.APPROVE_OPTION) {
        String text = textField.getText();
        if ((text == null) || ("".equals(text))) {
          textField.setText(chooser.getSelectedFile().getPath());
        }
        else {
          textField.setText(text + "," + chooser.getSelectedFile().getPath());
        }
      }
    }
    // Programs
    else {
      currentProgram = callingProgram.get(e.getSource());
      String comment = currentProgram.checkValues();
      if (comment != null) {
        logArea.append(comment);
        return;
      }
      LinkedList <String> command = currentProgram.getCommand();
      ProgramLauncher launcher = new ProgramLauncher(command, logArea, messageField, progressBar, etaField);
      launcher.execute();
      Vector < File > outputFiles = currentProgram.getOutputFiles();
      for (int i = 0; i < outputFiles.size(); i++) {
        File file = outputFiles.get(i);
        if (file.getFormatType().compareToIgnoreCase("other") != 0) {
          Global.fileNames.addElement(file.getName());
          Global.files.addFile(file);
        }
      }
      currentProgram = null;
    }
  }


  private static void removeTmpFiles() {
    logArea.append("You want to quit already?\nRemoving temporary files...");
    String[]        command  = {Global.pythonCommand, "Python" + java.io.File.separator + "removeAllTmpTables.py"};
    ProgramLauncher launcher = new ProgramLauncher(command, logArea, messageField, progressBar, etaField);
    launcher.execute();
    logArea.append(" done.\nNow quitting.\nBye!");
  }


  private static void printJavaVersions() {
    String[] pro = {"java.version", "java.vm.version", "java.runtime.version"};

    Properties properties = System.getProperties();
    for (int i = 0; i < pro.length; i++) {
      logArea.append(pro[i] + ": " + properties.getProperty(pro[i]) + "\n");
    }
  }
  
  private static void createAndShowGUI() {
    // Create and set up the window.
    JFrame mainFrame = new JFrame("S-Mart");
    mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    //Create and set up the content pane.
    JComponent newContentPane = new Smart();
    newContentPane.setOpaque(true);
    mainFrame.setContentPane(newContentPane);

    // Display the window.
    mainFrame.pack();
    mainFrame.setVisible(true);
    Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
    mainFrame.setBounds(0, 0, screenSize.width, screenSize.height);
    printJavaVersions();

    // Remove tmp file while quitting.
    mainFrame.addWindowListener(new WindowAdapter() {
      @Override
      public void windowClosing(WindowEvent e) {
        removeTmpFiles();
      }
    });
  }


  public static void main(String[] args) {
    javax.swing.SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        createAndShowGUI();
      }
    });
  }
}
