/*
 * 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.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import org.opensourcephysics.controls.XML;

/**
 * A tree model to display files and jar/zip contents.
 *
 * @author Doug Brown
 * @version 1.0
 */
public class JarTreeModel implements TreeModel {
  // instance fields
  protected File root;                                                                            // root must be a directory
  protected Map<File, JarNode[]> topLevelNodeArrays = new HashMap<File, JarNode[]>();             // maps jar to JarNode[]
  protected Map<File, Map<String, JarNode>> pathMaps = new HashMap<File, Map<String, JarNode>>(); // maps jar to Map of relative path to JarNode

  /**
   * Constructor.
   *
   * @param root a directory file
   */
  public JarTreeModel(File root) {
    this.root = root;
  }

  /**
   * Gets the root of this tree model.
   *
   * @return the root file
   */
  public Object getRoot() {
    return root;
  }

  /**
   * Returns true if the specified node is a leaf.
   *
   * @param node the tree node
   * @return true if node is a leaf
   */
  public boolean isLeaf(Object node) {
    if(node instanceof File) {
      File file = (File) node;
      if(file.getName().endsWith(".jar")) { //$NON-NLS-1$
        return false;
      }
      return((File) node).isFile();
    } else if(node instanceof JarNode) {
      JarNode treeNode = (JarNode) node;
      return treeNode.isLeaf();
    }
    return true;
  }

  /**
   * Determines the number of child nodes for the specified node.
   *
   * @param parent the parent node
   * @return the number of child nodes
   */
  public int getChildCount(Object parent) {
    if(parent instanceof File) {
      File parentFile = (File) parent;
      if(parentFile.getName().endsWith(".jar")) { //$NON-NLS-1$
        JarNode[] nodes = getJarNodes(parentFile);
        return(nodes==null) ? 0 : nodes.length;
      }
      String[] children = ((File) parent).list();
      return(children==null) ? 0 : children.length;
    } else if(parent instanceof JarNode) {
      JarNode treeNode = (JarNode) parent;
      return treeNode.getChildCount();
    }
    return 0;
  }

  /**
   * Gets the child node at a specified index. Parent and child may be a
   * File or JarNode.
   *
   * @param parent the parent node
   * @param index the index
   * @return the child node
   */
  public Object getChild(Object parent, int index) {
    if(parent instanceof File) {
      File parentFile = (File) parent;
      // if parent is the launch jar, return a JarNode
      if(parentFile.getName().endsWith(".jar")) { //$NON-NLS-1$
        JarNode[] nodes = getJarNodes(parentFile);
        if((nodes!=null)&&(nodes.length>index)) {
          return nodes[index];
        }
        return "no child found";                  //$NON-NLS-1$
      }
      String[] children = parentFile.list();
      if((children==null)||(index>=children.length)) {
        return null;
      }
      return new File(parentFile, children[index]) {
        public String toString() {
          return getName();
        }

      };
    } else if(parent instanceof JarNode) {
      JarNode treeNode = (JarNode) parent;
      return treeNode.getChildAt(index);
    }
    return null;
  }

  /**
   * Gets the index of the specified child node.
   *
   * @param parent the parent node
   * @param child the child node
   * @return the index of the child
   */
  public int getIndexOfChild(Object parent, Object child) {
    if(parent instanceof File) {
      File parentFile = (File) parent;
      if(parentFile.getName().endsWith(".jar")) { //$NON-NLS-1$
        JarNode[] nodes = getJarNodes(parentFile);
        if(nodes==null) {
          return -1;
        }
        for(int i = 0; i<nodes.length; i++) {
          if(nodes[i].equals(child)) {
            return i;
          }
        }
      }
      String[] children = ((File) parent).list();
      if(children==null) {
        return -1;
      }
      String childname = ((File) child).getName();
      for(int i = 0; i<children.length; i++) {
        if(childname.equals(children[i])) {
          return i;
        }
      }
    } else if(parent instanceof JarNode) {
      JarNode treeNode = (JarNode) parent;
      return treeNode.getIndex((JarNode) child);
    }
    return -1;
  }

  // methods required by TreeModel
  // these methods are empty since this is not an editable model
  public void valueForPathChanged(TreePath path, Object newvalue) {
    /** empty method */
  }

  public void addTreeModelListener(TreeModelListener l) {
    /** empty method */
  }

  public void removeTreeModelListener(TreeModelListener l) {
    /** empty method */
  }

  /**
   * Gets a child node with a given name. Parent and child may be a
   * File or JarNode.
   *
   * @param parent the parent node
   * @param name the name
   * @return the child node
   */
  public Object getChild(Object parent, String name) {
    if(parent instanceof File) {
      File parentFile = (File) parent;
      // if parent is the launch jar, return a JarNode
      if(parentFile.getName().endsWith(".jar")) { //$NON-NLS-1$
        JarNode[] nodes = getJarNodes(parentFile);
        if(nodes!=null) {
          for(int i = 0; i<nodes.length; i++) {
            if(nodes[i].toString().equals(name)) {
              return nodes[i];
            }
          }
        }
      }
      String[] children = parentFile.list();
      if(children!=null) {
        for(int i = 0; i<children.length; i++) {
          if(children[i].toString().equals(name)) {
            return new File(parentFile, children[i]) {
              public String toString() {
                return getName();
              }

            };
          }
        }
      }
    } else if(parent instanceof JarNode) {
      JarNode treeNode = (JarNode) parent;
      Enumeration<?> e = treeNode.children();
      while(e.hasMoreElements()) {
        JarNode next = (JarNode) e.nextElement();
        if(next.toString().equals(name)) {
          return next;
        }
      }
    }
    return null;
  }

  /**
   * Returns all descendant paths for a parent path.
   * Descendants include the parent path itself.
   *
   * @param parentPath the parent Object[] path
   * @return a collection of descendant Object[] paths
   */
  protected Collection<Object[]> getDescendantPaths(Object[] parentPath) {
    Collection<Object[]> c = new ArrayList<Object[]>();
    c.add(parentPath);
    Object parent = parentPath[parentPath.length-1];
    int n = getChildCount(parent);
    for(int i = 0; i<n; i++) {
      Object child = getChild(parent, i);
      // construct new path by adding child
      Object[] childPath = new Object[parentPath.length+1];
      System.arraycopy(parentPath, 0, childPath, 0, parentPath.length);
      childPath[parentPath.length] = child;
      Collection<Object[]> childPaths = getDescendantPaths(childPath);
      c.addAll(childPaths);
    }
    return c;
  }

  // JarNode class represents a compressed file in a jar
  class JarNode extends DefaultMutableTreeNode {
    String name;

    /**
     * Constructor JarNode
     * @param path
     */
    public JarNode(String path) {
      name = XML.getName(path);
      if(name.equals("")) { //$NON-NLS-1$
        name = XML.getName(path.substring(0, path.length()-1));
      }
    }

    public String toString() {
      return name;
    }

  }

  // returns the JarNode associated with a path in a given jar file
  public JarNode getJarNode(File jarFile, String path) {
    Map<String, JarNode> pathMap = pathMaps.get(jarFile);
    if(pathMap==null) {
      readJar(jarFile);
      pathMap = pathMaps.get(jarFile);
    }
    return pathMap.get(path);
  }

  // returns the top-level JarNodes in the specified jar
  public JarNode[] getJarNodes(File jarFile) {
    JarNode[] array = topLevelNodeArrays.get(jarFile);
    if(array==null) {
      readJar(jarFile);
      array = topLevelNodeArrays.get(jarFile);
    }
    return array;
  }

  // reads the specified jar
  private void readJar(File jarFile) {
    // get all jar entries
    Collection<String> entries = getJarEntries(jarFile);
    ArrayList<JarNode> topLevelNodes = new ArrayList<JarNode>();
    Map<String, JarNode> nodes = new HashMap<String, JarNode>(); // maps String to JarNode
    for(Iterator<String> it = entries.iterator(); it.hasNext(); ) {
      String path = XML.forwardSlash(it.next().toString());
      // don't include meta-inf files
      if(path.startsWith("META-INF")) { //$NON-NLS-1$   
        continue;
      }
      // for each path, make and connect JarNodes in the path
      JarNode parent = null;
      String parentPath = "";           //$NON-NLS-1$
      while(path!=null) {
        int n = path.indexOf("/");      //$NON-NLS-1$
        if(n>-1) {                      // found directory
          parentPath += path.substring(0, n+1);
          JarNode node = nodes.get(parentPath);
          if(node==null) {
            node = new JarNode(parentPath);
            nodes.put(parentPath, node);
            if(parent!=null) {
              parent.add(node);
            } else {                    // this is a top level node
              topLevelNodes.add(node);
            }
          }
          path = path.substring(n+1);
          parent = node;
        } else {                        // found file
          path = parentPath+path;
          JarNode node = nodes.get(path);
          if(node==null) {
            node = new JarNode(path);
            nodes.put(path, node);
            if(parent!=null) {
              parent.add(node);
            } else {                    // this is a top level node
              topLevelNodes.add(node);
            }
          }
          path = null;
        }
      }
    }
    JarNode[] array = topLevelNodes.toArray(new JarNode[0]);
    topLevelNodeArrays.put(jarFile, array);
    pathMaps.put(jarFile, nodes);
  }

  // returns all JarEntries in the specified jar
  private Collection<String> getJarEntries(File jarFile) {
    // create a JarFile
    JarFile jar = null;
    try {
      jar = new JarFile(jarFile);
    } catch(Exception ex) {
      ex.printStackTrace();
    }
    if(jar!=null) {
      TreeSet<String> entries = new TreeSet<String>();
      for(Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
        JarEntry entry = e.nextElement();
        entries.add(entry.getName());
      }
      return entries;
    }
    return null;
  }

}

/*
 * 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
 */
