/*
 * Open Source Physics software is free software as described near the bottom of this code file.
 *
 * For additional information and documentation on Open Source Physics please see:
 * <http://www.opensourcephysics.org/>
 */

package org.opensourcephysics.tools;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Frame;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Locale;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.border.Border;
import javax.swing.border.TitledBorder;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreePath;
import javax.swing.undo.AbstractUndoableEdit;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;
import javax.swing.undo.UndoableEditSupport;
import org.opensourcephysics.controls.ListChooser;
import org.opensourcephysics.controls.XML;
import org.opensourcephysics.display.OSPRuntime;

/**
 * This is a JDialog that displays and controls a checkbox jar tree.
 *
 * @author Douglas Brown
 * @version 1.0
 */
public class JarTreeDialog extends JDialog {
  protected File rootFile;
  protected JarTreeModel jarModel;
  protected JTree jarTree;
  protected CheckTreeManager checkManager;
  protected TreePath[] selectionPaths;
  protected UndoableEditSupport undoSupport;
  protected UndoManager undoManager;
  protected JButton okButton, undoButton, redoButton, languagesButton;
  protected boolean ignoreEvents;
  protected int prevRow;
  protected Icon jarIcon, jarFileIcon, jarFolderIcon, fileIcon;

  /**
   * Constructor.
   *
   * @param owner the owner frame
   * @param root the root directory
   */
  public JarTreeDialog(Frame owner, File root) {
    super(owner, true);
    rootFile = root;
    createGUI();
    // set up the undo system
    undoManager = new UndoManager();
    undoSupport = new UndoableEditSupport();
    undoSupport.addUndoableEditListener(undoManager);
    CheckTreeSelectionModel checkModel = checkManager.getSelectionModel();
    checkModel.addPropertyChangeListener(new PropertyChangeListener() {
      public void propertyChange(PropertyChangeEvent e) {
        if(ignoreEvents) {
          return;
        }
        TreePath[] prev = (TreePath[]) e.getOldValue();
        TreePath[] curr = (TreePath[]) e.getNewValue();
        TreePath path = jarTree.getSelectionPath();
        int row = jarTree.getRowForPath(path);
        SelectionEdit edit = new SelectionEdit(prev, prevRow, curr, row);
        undoSupport.postEdit(edit);
        prevRow = row;
        refresh();
      }

    });
    refresh();
    // center on screen
    Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
    int x = (dim.width-getBounds().width)/2;
    int y = (dim.height-getBounds().height)/2;
    setLocation(x, y);
  }

  /**
   * Gets the selected paths as relative path strings.
   *
   * @return an array of string paths relative to the root directory
   */
  public String[] getSelectionRelativePaths() {
    if(selectionPaths==null) {
      return null;
    }
    ArrayList<String> temp = new ArrayList<String>();
    for(int i = 0; i<selectionPaths.length; i++) {
      temp.add(getRelativePath(selectionPaths[i]));
    }
    return temp.toArray(new String[0]);
  }

  /**
   * Sets the selected relative path strings.
   *
   * @param paths an array of string paths relative to the root directory
   */
  public void setSelectionRelativePaths(String[] paths) {
    ArrayList<TreePath> temp = new ArrayList<TreePath>();
    for(int i = 0; i<paths.length; i++) {
      temp.add(getTreePath(paths[i]));
    }
    TreePath[] treePaths = temp.toArray(new TreePath[0]);
    setSelectionPaths(treePaths);
  }

  /**
   * Gets the selected paths as TreePaths that start with the root.
   *
   * @return an array of selected TreePaths
   */
  public TreePath[] getSelectionPaths() {
    return selectionPaths;
  }

  /**
   * Sets the selected TreePaths.
   *
   * @param treePaths an array of TreePaths that start with the root
   */
  public void setSelectionPaths(TreePath[] treePaths) {
    ignoreEvents = true;
    CheckTreeSelectionModel checkModel = checkManager.getSelectionModel();
    if(treePaths==null) {
      checkModel.setSelectionPaths(new TreePath[0]);
    } else {
      checkModel.setSelectionPaths(treePaths);
    }
    ignoreEvents = false;
    refresh();
  }

  /**
   * Converts a TreePath to a relative path string.
   * The TreePath is assumed to start with the root directory.
   *
   * @param path the TreePath
   * @return the relative path
   */
  private String getRelativePath(TreePath path) {
    Object[] nodes = path.getPath();
    StringBuffer buffer = new StringBuffer();
    Object jarNode = rootFile;
    // skip node[0] since it is the root
    for(int j = 1; j<nodes.length; j++) {
      if(buffer.toString().endsWith(".jar")) { //$NON-NLS-1$
        buffer.append("!");                    //$NON-NLS-1$
      }
      if(j>1) {
        buffer.append("/");                    //$NON-NLS-1$
      }
      buffer.append(nodes[j].toString());
      jarNode = jarModel.getChild(jarNode, nodes[j].toString());
    }
    // add an end slash if jarNode is directory
    if(((jarNode instanceof File)&&((File) jarNode).isDirectory())||((jarNode instanceof JarTreeModel.JarNode)&&!((JarTreeModel.JarNode) jarNode).isLeaf())) {
      buffer.append("/"); //$NON-NLS-1$
    }
    return buffer.toString();
  }

  /**
   * Gets the TreePath associated with a path relative to the rootFile
   *
   * @param relativePath the path
   * @return the TreePath
   */
  private TreePath getTreePath(String relativePath) {
    String path = XML.forwardSlash(relativePath);
    TreePath treePath = new TreePath(rootFile);
    Object parent = rootFile;
    while(parent!=null) {
      Object child = null;
      int n = path.indexOf("/"); //$NON-NLS-1$
      if(n>-1) {
        String name = path.substring(0, n);
        path = path.substring(n+1);
        if(name.endsWith("!")) { //$NON-NLS-1$
          name = name.substring(0, name.length()-1);
        }
        child = jarModel.getChild(parent, name);
      } else {
        child = jarModel.getChild(parent, path);
      }
      if(child!=null) {
        treePath = treePath.pathByAddingChild(child);
      }
      parent = child;
    }
    return treePath;
  }

  /**
   * Refreshes the buttonbar.
   */
  protected void refresh() {
    CheckTreeSelectionModel checkModel = checkManager.getSelectionModel();
    selectionPaths = checkModel.getSelectionPaths();
    okButton.setEnabled(!checkModel.isSelectionEmpty());
    undoButton.setEnabled(undoManager.canUndo());
    redoButton.setEnabled(undoManager.canRedo());
  }

  /**
   * Creates the GUI.
   */
  protected void createGUI() {
    setTitle(ToolsRes.getString("JarTreeDialog.Title")); //$NON-NLS-1$
    JPanel contentPane = new JPanel(new BorderLayout());
    contentPane.setPreferredSize(new Dimension(480, 320));
    setContentPane(contentPane);
    // create icons
    String imageFile = "/org/opensourcephysics/resources/tools/images/jarfile.gif"; //$NON-NLS-1$
    jarIcon = ResourceLoader.getIcon(imageFile);
    imageFile = "/org/opensourcephysics/resources/tools/images/jarcontent.gif"; //$NON-NLS-1$
    jarFileIcon = ResourceLoader.getIcon(imageFile);
    imageFile = "/org/opensourcephysics/resources/tools/images/jarfolder.gif";  //$NON-NLS-1$
    jarFolderIcon = ResourceLoader.getIcon(imageFile);
    imageFile = "/org/opensourcephysics/resources/tools/images/whitefile.gif";  //$NON-NLS-1$
    fileIcon = ResourceLoader.getIcon(imageFile);
    // create the tree model and tree
    jarModel = new JarTreeModel(rootFile);
    jarTree = new JTree(jarModel);
    jarTree.setSelectionRow(0);
    jarTree.setCellRenderer(new JarRenderer());
    // add checkboxes to the tree
    checkManager = new CheckTreeManager(jarTree);
    // put the tree in a scroller
    JScrollPane scroller = new JScrollPane(jarTree);
    // put titled border around tree
    Border etched = BorderFactory.createEtchedBorder();
    TitledBorder title = BorderFactory.createTitledBorder(etched, ToolsRes.getString("JarTreeDialog.Border.Title")); //$NON-NLS-1$
    scroller.setBorder(title);
    contentPane.add(scroller, BorderLayout.CENTER);
    // create ok button
    okButton = new JButton(ToolsRes.getString("JarTreeDialog.Button.OK")); //$NON-NLS-1$
    okButton.setForeground(new Color(0, 0, 102));
    okButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        setVisible(false);
      }

    });
    // create cancel button
    JButton cancelButton = new JButton(ToolsRes.getString("JarTreeDialog.Button.Cancel")); //$NON-NLS-1$
    cancelButton.setForeground(new Color(0, 0, 102));
    cancelButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        selectionPaths = null;
        setVisible(false);
      }

    });
    // create undo button
    undoButton = new JButton(ToolsRes.getString("JarTreeDialog.Button.Undo")); //$NON-NLS-1$
    undoButton.setForeground(new Color(0, 0, 102));
    undoButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        undoManager.undo();
      }

    });
    // create redo button
    redoButton = new JButton(ToolsRes.getString("JarTreeDialog.Button.Redo")); //$NON-NLS-1$
    redoButton.setForeground(new Color(0, 0, 102));
    redoButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        undoManager.redo();
      }

    });
    // create languages button
    languagesButton = new JButton(ToolsRes.getString("JarTreeDialog.Button.Languages")); //$NON-NLS-1$
    languagesButton.setForeground(new Color(0, 0, 102));
    languagesButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        // create a list chooser
        ListChooser dialog = new ListChooser(ToolsRes.getString("JarTreeDialog.Chooser.Languages.Title"), //$NON-NLS-1$
          ToolsRes.getString("JarTreeDialog.Chooser.Languages.Message"),                                  //$NON-NLS-1$
            JarTreeDialog.this);
        // create the lists and fill with Locales
        ArrayList<Locale> choices = new ArrayList<Locale>();
        ArrayList<String> names = new ArrayList<String>();
        ArrayList<Locale> originals = new ArrayList<Locale>();
        Locale[] locales = OSPRuntime.getInstalledLocales();
        boolean[] selected = new boolean[locales.length];
        boolean[] disabled = new boolean[locales.length];
        Object[] rootPath = new Object[] {jarModel.getRoot()};
        Collection<Object[]> jarPaths = jarModel.getDescendantPaths(rootPath);
        CheckTreeSelectionModel checkModel = checkManager.getSelectionModel();
        for(int i = 0; i<locales.length; i++) {
          choices.add(locales[i]);
          originals.add(locales[i]);
          names.add(locales[i].getDisplayLanguage(ToolsRes.resourceLocale));
          String lang = locales[i].getLanguage();
          if(locales[i]==Locale.ENGLISH) {
            selected[i] = true;
            disabled[i] = true;
          }
          // look thru jar paths to see if locale is selected
          else {
            for(Iterator<Object[]> it = jarPaths.iterator(); it.hasNext(); ) {
              // find paths containing ".properties" and "display_res_xx"
              Object[] array = it.next();
              String s = array[array.length-1].toString();                                                // file namae
              if((s.indexOf(".properties")>-1                                                             //$NON-NLS-1$
                )&&(s.indexOf("display_res_"+lang)>-1)) {                                                 //$NON-NLS-1$
                // see if path or ancestor is selected
                TreePath thePath = new TreePath(array);
                if(thePath.toString().indexOf(OSPRuntime.getLaunchJarName())>-1) {
                  selected[i] = checkModel.isPathOrAncestorSelected(thePath);
                  break;
                }
              }
            }
          }
        }
        // show the list chooser for user input
        if(dialog.choose(choices, names, null, selected, disabled)) {                                     // false if canceled
          Collection<TreePath> removePaths = new ArrayList<TreePath>();
          Collection<TreePath> addPaths = new ArrayList<TreePath>();
          // compare choices with originals
          for(Iterator<Locale> it = originals.iterator(); it.hasNext(); ) {
            Locale next = it.next();
            String lang = next.getLanguage();
            for(Iterator<Object[]> pathIt = jarPaths.iterator(); pathIt.hasNext(); ) {
              // find paths containing ".properties" and "_xx"
              Object[] array = pathIt.next();                                                             // get next path
              String s = array[array.length-1].toString();                                                // get last path object
              if((s.indexOf(".properties")>-1                                                             //$NON-NLS-1$
                )&&(s.indexOf("_"+lang)>-1)) {                                                            //$NON-NLS-1$
                TreePath propPath = new TreePath(array);
                // don't change paths in jars other than the launch jar
                if(propPath.toString().indexOf(OSPRuntime.getLaunchJarName())>-1) {
                  // eliminate unselected languages
                  if(!choices.contains(next)) {
                    removePaths.add(propPath);
                  }
                  // add selected languages
                  else {
                    addPaths.add(propPath);
                  }
                }
              }
            }
          }
          TreePath[] paths = removePaths.toArray(new TreePath[0]);
          checkManager.getSelectionModel().removeSelectionPaths(paths);
          paths = addPaths.toArray(new TreePath[0]);
          checkManager.getSelectionModel().addSelectionPaths(paths);
          refresh();
        }
      }

    });
    // create and add buttonbar at bottom
    JPanel buttonbar = new JPanel(new FlowLayout());
    buttonbar.setBorder(BorderFactory.createEmptyBorder(1, 0, 3, 0));
    contentPane.add(buttonbar, BorderLayout.SOUTH);
    // add buttons to buttonbar
    buttonbar.add(languagesButton);
    buttonbar.add(undoButton);
    buttonbar.add(redoButton);
    buttonbar.add(okButton);
    buttonbar.add(cancelButton);
    pack();
  }

  /**
   * A class to undo/redo tree node selections.
   */
  protected class SelectionEdit extends AbstractUndoableEdit {
    TreePath[] undo;
    TreePath[] redo;
    int undoRow;
    int redoRow;

    /**
     * Constructor SelectionEdit
     * @param undoPaths
     * @param redoPaths
     */
    public SelectionEdit(TreePath[] undoPaths, TreePath[] redoPaths) {
      undo = undoPaths;
      redo = redoPaths;
    }

    /**
     * Constructor SelectionEdit
     * @param undoPaths
     * @param undoRow
     * @param redoPaths
     * @param redoRow
     */
    public SelectionEdit(TreePath[] undoPaths, int undoRow, TreePath[] redoPaths, int redoRow) {
      undo = undoPaths;
      redo = redoPaths;
      this.undoRow = undoRow;
      this.redoRow = redoRow;
    }

    public void undo() throws CannotUndoException {
      super.undo();
      ignoreEvents = true;
      CheckTreeSelectionModel checkModel = checkManager.getSelectionModel();
      checkModel.setSelectionPaths(undo);
      jarTree.setSelectionRow(undoRow);
      ignoreEvents = false;
      refresh();
      prevRow = undoRow;
    }

    public void redo() throws CannotUndoException {
      super.redo();
      ignoreEvents = true;
      CheckTreeSelectionModel checkModel = checkManager.getSelectionModel();
      checkModel.setSelectionPaths(redo);
      jarTree.setSelectionRow(redoRow);
      ignoreEvents = false;
      refresh();
      prevRow = redoRow;
    }

    public String getPresentationName() {
      return "Change Selection"; //$NON-NLS-1$
    }

  }

  /**
   * A cell renderer to display jar and file nodes.
   */
  protected class JarRenderer extends DefaultTreeCellRenderer {
    public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) {
      super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
      if(value instanceof File) {
        File file = (File) value;
        if(file.getName().endsWith(".jar")) { //$NON-NLS-1$
          setIcon(jarIcon);
        } else if(leaf) {
          setIcon(fileIcon);
        }
      } else if(value instanceof JarTreeModel.JarNode) {
        setIcon(leaf ? jarFileIcon : jarFolderIcon);
      }
      return this;
    }

  }
  //  // main method for testing
  //  public static void main(String[] args) {
  //    JFrame frame = new JFrame();
  //    frame.setSize(400,600);
  //    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  //    frame.setVisible(true);
  //    File root = new File(System.getProperty("user.home")); //$NON-NLS-1$
  //    JarTreeDialog tree = new JarTreeDialog(frame, root);
  //    tree.setVisible(true);
  //    String[] paths = tree.getSelectionRelativePaths();
  //    if (paths == null) System.out.println("no paths"); //$NON-NLS-1$
  //    else for (int i = 0; i < paths.length; i++) {
  //      System.out.println(paths[i]);
  //    }
  //  }

}

/*
 * Open Source Physics software is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License (GPL) as
 * published by the Free Software Foundation; either version 2 of the License,
 * or(at your option) any later version.
 *
 * Code that uses any portion of the code in the org.opensourcephysics package
 * or any subpackage (subdirectory) of this package must must also be be
 * released under the GNU GPL license.
 *
 * This software 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; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
 * Suite 330, Boston MA 02111-1307 USA or view the license online at
 * http://www.gnu.org/copyleft/gpl.html
 *
 * Copyright (c) 2007 The Open Source Physics project
 * http://www.opensourcephysics.org
 */
