/*
 * #%L
 * Bio-Formats Plugins for ImageJ: a collection of ImageJ plugins including the
 * Bio-Formats Importer, Bio-Formats Exporter, Bio-Formats Macro Extensions,
 * Data Browser and Stack Slicer.
 * %%
 * Copyright (C) 2006 - 2017 Open Microscopy Environment:
 *   - Board of Regents of the University of Wisconsin-Madison
 *   - Glencoe Software, Inc.
 *   - University of Dundee
 * %%
 * 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 2 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/gpl-2.0.html>.
 * #L%
 */

package loci.plugins.config;

import ij.Prefs;

import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Properties;

import javax.imageio.spi.IIORegistry;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.SpringLayout;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import loci.common.Constants;
import loci.common.DateTools;
import loci.formats.FormatTools;
import loci.plugins.util.LociPrefs;
import loci.plugins.util.WindowTools;

/**
 * A window for managing configuration of the Bio-Formats plugins.
 *
 * @author Curtis Rueden ctrueden at wisc.edu
 */
public class ConfigWindow extends JFrame
  implements ActionListener, ItemListener, ListSelectionListener, Runnable
{

  // -- Constants --

  private static final int PAD = 5;
  private static final String UPGRADE_CHECK_KEY = "bioformats.upgradeCheck";

  // -- Fields --

  private DefaultListModel formatsListModel;
  private JList formatsList;
  private JPanel formatInfo;
  private JTextField extensions, sliceLabel;
  private JCheckBox enabledBox, windowlessBox, upgradeBox;

  private DefaultListModel libsListModel;
  private JList libsList;
  private JTextField type, status, version, path, url, license;
  private JTextArea notes;
  private JButton installButton;
  private JButton setSliceLabelButton;
  private JButton resetSliceLabelButton;

  private PrintWriter log;

  // -- Constructor --

  public ConfigWindow() {
    setTitle("Bio-Formats Plugins Configuration");
    setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);

    // build UI

    final String loading = "Loading...";

    JTabbedPane tabs = new JTabbedPane();
    tabs.setBorder(new EmptyBorder(PAD, PAD, PAD, PAD));
    setContentPane(tabs);

    JPanel bfOptionsPanel = new JPanel(new SpringLayout());
    tabs.addTab("General", bfOptionsPanel);

    JPanel upgradePanel = new JPanel(new SpringLayout());
    JLabel upgradeLabel =
      new JLabel("Automatically check for new versions of the Bio-Formats plugins");
    upgradePanel.add(upgradeLabel);

    final boolean checkForUpgrades = Prefs.get(UPGRADE_CHECK_KEY, true);
    upgradeBox = new JCheckBox("", checkForUpgrades);
    upgradeBox.addItemListener(this);
    upgradePanel.add(upgradeBox);

    SpringUtilities.makeCompactGrid(upgradePanel,1, 2, PAD, PAD, PAD, PAD);

    JPanel sliceNamePanel = new JPanel(new SpringLayout());
    JLabel sliceNameLabel = new JLabel("Slice Label Pattern");
    sliceNamePanel.add(sliceNameLabel);

    final String sliceLabelPattern = LociPrefs.getSliceLabelPattern();
    sliceLabel = new JTextField(sliceLabelPattern);
    int prefHeight = sliceLabel.getPreferredSize().height;
    sliceLabel.setMaximumSize(new Dimension(Integer.MAX_VALUE, prefHeight));
    sliceNamePanel.add(sliceLabel);

    setSliceLabelButton = new JButton("Set");
    resetSliceLabelButton = new JButton("Reset");
    setSliceLabelButton.addActionListener(this);
    resetSliceLabelButton.addActionListener(this);
    sliceNamePanel.add(setSliceLabelButton);
    sliceNamePanel.add(resetSliceLabelButton);
    SpringUtilities.makeCompactGrid(sliceNamePanel,1, 4, PAD, PAD, PAD, PAD);

    bfOptionsPanel.add(upgradePanel);
    bfOptionsPanel.add(sliceNamePanel);
    JLabel sliceDescription = new JLabel("<html>Customize the slice label by specifying a pattern string:"
                                          + "<br>%s - series index"
                                          + "<br>%n - series name"
                                          + "<br>%c - channel index"
                                          + "<br>%w - channel name"
                                          + "<br>%z - Z index"
                                          + "<br>%t - T index"
                                          + "<br>%A - acquisition timestamp</html>");
    bfOptionsPanel.add(sliceDescription);
    SpringUtilities.makeCompactGrid(bfOptionsPanel,3, 1, PAD, PAD, PAD, PAD);

    JPanel installPanel = new JPanel();
    //tabs.addTab("Install", installPanel);

    installButton = new JButton(
      "<html><center><br><font size=\"+1\">Check my system,<br>" +
      "install missing libraries,<br>" +
      "and upgrade old files</font><br>" +
      "<font size=\"-1\">&nbsp;<br>" +
      "(We'll ask before we install or upgrade anything.)" +
      "</font><br>&nbsp;");
    installButton.addActionListener(this);
    installPanel.add(installButton);

    formatsListModel = new DefaultListModel();
    formatsListModel.addElement(loading);
    formatsList = makeList(formatsListModel);

    formatInfo = new JPanel();
    tabs.addTab("Formats", makeSplitPane(formatsList, formatInfo));

    extensions = makeTextField();
    enabledBox = new JCheckBox("", false);
    enabledBox.addItemListener(this);
    windowlessBox = new JCheckBox("", false);
    windowlessBox.addItemListener(this);

    doFormatLayout(null);

    libsListModel = new DefaultListModel();
    libsListModel.addElement(loading);
    libsList = makeList(libsListModel);
    JPanel libInfo = new JPanel();
    tabs.addTab("Libraries", makeSplitPane(libsList, libInfo));

    libInfo.setLayout(new SpringLayout());

    libInfo.add(makeLabel("Type", false));
    type = makeTextField();
    libInfo.add(type);

    libInfo.add(makeLabel("Status", false));
    status = makeTextField();
    libInfo.add(status);

    libInfo.add(makeLabel("Version", false));
    version = makeTextField();
    libInfo.add(version);

    // TODO: install/upgrade button, if applicable
    // - can upgrade any JAR from the repository
    //   + upgrade button for "ImageJ" just launches ImageJ upgrade plugin
    // - can install native libs by downloading installer from its web site
    //   + QuickTime for Java
    //   + ImageIO Tools

    libInfo.add(makeLabel("Path", false));
    path = makeTextField();
    libInfo.add(path);

    libInfo.add(makeLabel("URL", false));
    url = makeTextField();
    libInfo.add(url);

    libInfo.add(makeLabel("License", false));
    license = makeTextField();
    libInfo.add(license);

    libInfo.add(makeLabel("Notes", true));
    notes = new JTextArea();
    notes.setEditable(false);
    notes.setWrapStyleWord(true);
    notes.setLineWrap(true);
    libInfo.add(new JScrollPane(notes));

    // TODO: "How to install" for each library?

    JPanel logPanel = new JPanel();
    tabs.addTab("Log", logPanel);

    logPanel.setLayout(new java.awt.BorderLayout());

    JTextArea logArea = new JTextArea();
    logArea.setEditable(false);
    logArea.setRows(10);
    logPanel.add(new JScrollPane(logArea));

    SpringUtilities.makeCompactGrid(libInfo, 7, 2, PAD, PAD, PAD, PAD);

    pack();

    TextAreaWriter taw = new TextAreaWriter(logArea);
    log = new PrintWriter(taw);

    new Thread(this, "ConfigWindow-Loader").start();
  }

  // -- ActionListener API methods --

  @Override
  public void actionPerformed(ActionEvent e) {
    Object src = e.getSource();
    if (src == installButton) {
      new LociInstaller().run(null);
      dispose();
    }
    else if (src == setSliceLabelButton) {
      Prefs.set(LociPrefs.PREF_SLICE_LABEL_PATTERN, sliceLabel.getText());
      return;     
    }
    else if (src == resetSliceLabelButton) {
      Prefs.set(LociPrefs.PREF_SLICE_LABEL_PATTERN, null);
      sliceLabel.setText(LociPrefs.getSliceLabelPattern());
      return;     
    }
  }

  // -- ItemListener API methods --

  @Override
  public void itemStateChanged(ItemEvent e) {
    Object src = e.getSource();

    if (src == upgradeBox) {
      Prefs.set(UPGRADE_CHECK_KEY, upgradeBox.isSelected());
      return;
    }

    Object value = formatsList.getSelectedValue();
    if (!(value instanceof FormatEntry)) return;
    FormatEntry entry = (FormatEntry) value;

    if (src == enabledBox) {
      setReaderEnabled(entry, enabledBox.isSelected());
    }
    else if (src == windowlessBox) {
      setReaderWindowless(entry, windowlessBox.isSelected());
    }
  }

  // -- ListSelectionListener API methods --

  @Override
  public void valueChanged(ListSelectionEvent e) {
    Object src = e.getSource();
    if (src == formatsList) {
      Object value = formatsList.getSelectedValue();
      if (!(value instanceof FormatEntry)) return;
      FormatEntry entry = (FormatEntry) formatsList.getSelectedValue();
      doFormatLayout(entry);
    }
    else if (src == libsList) {
      Object value = libsList.getSelectedValue();
      if (!(value instanceof LibraryEntry)) return;
      LibraryEntry entry = (LibraryEntry) libsList.getSelectedValue();
      type.setText(entry.type);
      status.setText(entry.status);
      version.setText(entry.version);
      path.setText(entry.path);
      url.setText(entry.url);
      license.setText(entry.license);
      notes.setText(entry.notes);
    }
  }

  // -- Runnable API methods --

  /** Populate configuration information in a separate thread. */
  @Override
  public void run() {
    log.println("Bio-Formats Plugins configuration - " + DateTools.getTimestamp());

    // list system properties
    log.println();
    log.println("-- System properties --");
    Properties sysProps = System.getProperties();
    for (Object name : sysProps.keySet()) {
      log.println(name + " = " + sysProps.getProperty(name.toString()));
    }

    // generate list of formats
    log.println();
    log.println("-- Formats --");
    try {
      Class<?> irClass = Class.forName("loci.formats.ImageReader");
      Object ir = irClass.getDeclaredConstructor().newInstance();
      Method getClasses = irClass.getMethod("getReaders");
      Object[] readers = (Object[]) getClasses.invoke(ir);
      for (int i=0; i<readers.length; i++) {
        FormatEntry entry = new FormatEntry(log, readers[i]);
        addEntry(entry, formatsListModel);
      }
    }
    catch (Throwable t) {
      log.println("Could not generate list of supported formats:");
      t.printStackTrace(log);
    }

    log.println();
    log.println("-- Libraries --");

    // enumerate list of libraries

    String javaVersion = System.getProperty("java.version") +
      " (" + System.getProperty("java.vendor") + ")";

    String bfVersion = FormatTools.DATE;
    // Ant replaces date token with datestamp of the build
    if (bfVersion.equals("@" + "date" + "@")) bfVersion = "Internal build";

    String clibIIOVersion = null;
    try {
      Class<?> jpegSpi = Class.forName(
        "com.sun.media.imageioimpl.plugins.jpeg.CLibJPEGImageReaderSpi");
      IIORegistry registry = IIORegistry.getDefaultInstance();
      Object jpeg = registry.getServiceProviderByClass(jpegSpi);
      if (jpeg == null) clibIIOVersion = LibraryEntry.MISSING_VERSION_CODE;
    }
    catch (Throwable t) {
      if (t instanceof ClassNotFoundException) {
        // JAI Image I/O Tools library not available
      }
      else {
        log.println("Error determining native Image I/O Tools version:");
        t.printStackTrace(log);
      }
    }

    String matlabVersion = null;
    try {
      Class<?> matlabClass = Class.forName("com.mathworks.jmi.Matlab");
      Object matlab = matlabClass.getDeclaredConstructor().newInstance();
      Method eval = matlabClass.getMethod("eval", new Class[] {String.class});

      String ans = (String) eval.invoke(matlab, new Object[] {"version"});
      if (ans.startsWith("ans =")) ans = ans.substring(5);
      matlabVersion = ans.trim();
    }
    catch (Throwable t) {
      if (t instanceof ClassNotFoundException) {
        // MATLAB library not available
      }
      else {
        log.println("Error determining MATLAB version:");
        t.printStackTrace(log);
      }
    }

    HashMap<String, String> versions = new HashMap<String, String>();
    versions.put("javaVersion", javaVersion);
    versions.put("bfVersion", bfVersion);
    if (clibIIOVersion != null) versions.put("clibIIOVersion", clibIIOVersion);
    if (matlabVersion != null) versions.put("matlabVersion", matlabVersion);

    // parse libraries
    HashMap<String, String> props = null;
    String propKey = null;
    StringBuffer propValue = new StringBuffer();
    String resource = "libraries.txt";
    try (BufferedReader in = new BufferedReader(new InputStreamReader(
        ConfigWindow.class.getResourceAsStream(resource), Constants.ENCODING))) {
      while (true) {
        String line = null;
        try {
          line = in.readLine();
        }
        catch (IOException exc) {
          log.println("Error parsing " + resource + ":");
          exc.printStackTrace(log);
          break;
        }
        if (line == null) break;

        // ignore characters following # sign (comments)
        int ndx = line.indexOf('#');
        if (ndx >= 0) line = line.substring(0, ndx);
        boolean space = line.startsWith(" ");
        line = line.trim();
        if (line.equals("")) continue;

        // parse key/value pairs
        int equals = line.indexOf('=');
        if (line.startsWith("[")) {
          // new entry
          if (props == null) props = new HashMap<String, String>();
          else {
             addProp(props, propKey, propValue.toString(), versions);
             LibraryEntry entry = new LibraryEntry(log, props);
             addEntry(entry, libsListModel);
          }
          props.clear();
          props.put("name", line.substring(1, line.length() - 1));
          propKey = null;
        }
        else if (space) {
          // append to previous property value
          propValue.append(" ");
          propValue.append(line);
        }
        else if (equals >= 0) {
          addProp(props, propKey, propValue.toString(), versions);
          propKey = line.substring(0, equals - 1).trim();
          propValue.setLength(0);
          propValue.append(line.substring(equals + 1).trim());
        }
      }
    }
    catch (UnsupportedEncodingException e) {
      log.println("UTF-8 encoding is not supported.  Something is very wrong.");
      e.printStackTrace(log);
    } catch (IOException exc) {
      log.println("Error closing " + resource + ":");
      exc.printStackTrace(log);
    }
  }

  // -- Utility methods --

  public static JTextField makeTextField() {
    JTextField textField = new JTextField(48);
    int prefHeight = textField.getPreferredSize().height;
    textField.setMaximumSize(new Dimension(Integer.MAX_VALUE, prefHeight));
    textField.setEditable(false);
    return textField;
  }

  public static void addEntry(final Comparable<Object> c,
    final DefaultListModel listModel)
  {
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        // remove "Loading..." message
        if (listModel.size() == 1 && listModel.get(0) instanceof String) {
          listModel.remove(0);
        }
        // binary search for proper location
        int min = 0, max = listModel.size();
        while (min < max) {
          int mid = (min + max) / 2;
          Object o = listModel.get(mid);
          int result = c.compareTo(o);
          if (result > 0) min = mid + 1;
          else max = mid;
        }
        listModel.add(max, c);
      }
    });
  }

  // -- Helper methods --

  private void addProp(HashMap<String, String> props,
    String key, String value, HashMap<String, String> versions)
  {
    if (key == null) return;

    // replace \n sequence with newlines
    value = value.replaceAll("\\\\n *", "\n");

    if (key.equals("version")) {
      // get actual value from versions hashtable
      value = versions.get(value);
    }
    if (value != null) props.put(key, value);
  }

  private JLabel makeLabel(String text, boolean top) {
    JLabel label = new JLabel(text);
    label.setHorizontalAlignment(SwingConstants.RIGHT);
    if (top) label.setVerticalAlignment(SwingConstants.TOP);
    return label;
  }

  private JList makeList(DefaultListModel listModel) {
    JList list = new JList(listModel);
    list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
    list.setVisibleRowCount(25);
    list.addListSelectionListener(this);
    return list;
  }

  private JSplitPane makeSplitPane(JList list, JPanel info) {
    JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
      new JScrollPane(list), info);
    splitPane.setDividerLocation(260);
    return splitPane;
  }

  private void doFormatLayout(FormatEntry entry) {
    if (entry != null) {
      // build list of extensions
      StringBuffer sb = new StringBuffer();
      for (int i=0; i<entry.suffixes.length; i++) {
        if (i > 0) sb.append(", ");
        sb.append(entry.suffixes[i]);
      }
      extensions.setText(sb.toString());
    }

    enabledBox.setSelected(isReaderEnabled(entry));
    windowlessBox.setSelected(isReaderWindowless(entry));

    formatInfo.removeAll();
    formatInfo.setLayout(new SpringLayout());

    formatInfo.add(makeLabel("Extensions", false));
    formatInfo.add(extensions);

    formatInfo.add(makeLabel("Enabled", false));
    formatInfo.add(enabledBox);

    formatInfo.add(makeLabel("Windowless", false));
    formatInfo.add(windowlessBox);

    // format-specific widgets
    int rows = entry == null ? 0 : entry.widgets.length;
    for (int i=0; i<rows; i++) {
      formatInfo.add(makeLabel(entry.labels[i], false));
      formatInfo.add(entry.widgets[i]);
    }

    SpringUtilities.makeCompactGrid(formatInfo,
      3 + rows, 2, PAD, PAD, PAD, PAD);

    formatInfo.validate();
    formatInfo.repaint();
  }

  private boolean isReaderEnabled(FormatEntry entry) {
    return isReaderPref("ENABLED", entry, true);
  }

  private void setReaderEnabled(FormatEntry entry, boolean value) {
    setReaderPref("ENABLED", entry, value);
  }

  private boolean isReaderWindowless(FormatEntry entry) {
    return isReaderPref("WINDOWLESS", entry, false);
  }

  private void setReaderWindowless(FormatEntry entry, boolean value) {
    setReaderPref("WINDOWLESS", entry, value);
  }

  private boolean isReaderPref(String pref, FormatEntry entry,
    boolean defaultValue)
  {
    if (entry == null) return false;
    try {
      Boolean result = (Boolean)
        invokeMethod("PREF_READER_" + pref, "get", entry, defaultValue);
      return result.booleanValue();
    }
    catch (Throwable t) {
      t.printStackTrace();
      log.println("Error querying property '" + pref.toLowerCase() +
        "' for reader '" + entry.readerName + "':");
      t.printStackTrace(log);
      return false;
    }
  }

  private void setReaderPref(String pref, FormatEntry entry, boolean value) {
    if (entry == null) return;
    try {
      invokeMethod("PREF_READER_" + pref, "set", entry, value);
    }
    catch (Throwable t) {
      t.printStackTrace();
      log.println("Error setting property '" + pref.toLowerCase() +
        "' for reader '" + entry.readerName + "':");
      t.printStackTrace(log);
    }
  }

  private static final Class<?>[] PARAMS = {String.class, boolean.class};

  private Object invokeMethod(String fieldName, String methodName,
    FormatEntry entry, boolean value) throws ClassNotFoundException,
    NoSuchFieldException, IllegalAccessException, NoSuchMethodException,
    InvocationTargetException
  {
    Class<?> utilClass = Class.forName("loci.plugins.util.LociPrefs");
    Field field = utilClass.getField(fieldName);
    String key = field.get(null) + "." + entry.readerName;
    Class<?> prefsClass = Class.forName("ij.Prefs");
    Method method = prefsClass.getMethod(methodName, PARAMS);
    Object[] args = {key, value ? Boolean.TRUE : Boolean.FALSE};
    return method.invoke(null, args);
  }

}
