/*
 * 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.controls;
import java.awt.Color;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * This defines the ObjectLoader interface and static methods for managing and
 * accessing ObjectLoader implementations.
 *
 * @author Douglas Brown
 * @version 1.0
 */
public class XML {
  // static constants
  public static String NEW_LINE = System.getProperty("line.separator", "\n"); //$NON-NLS-1$ //$NON-NLS-2$
  public static final String CDATA_PRE = "<![CDATA["; //$NON-NLS-1$
  public static final String CDATA_POST = "]]>";      //$NON-NLS-1$
  public static final int INDENT = 4;
  // static fields
  private static Map<Class<?>, ObjectLoader> loaders = new HashMap<Class<?>, ObjectLoader>();
  private static ObjectLoader defaultLoader;
  private static String dtdName;
  private static String dtd;                          // the dtd as a string
  private static String defaultName = "osp10.dtd";    //$NON-NLS-1$
  private static ClassLoader classLoader;

  // added by W. Christian
  static {
    try {                                                    // system properties may not be readable in some environments
      NEW_LINE = System.getProperty("line.separator", "/n"); //$NON-NLS-1$ //$NON-NLS-2$
    } catch(SecurityException ex) {

    /** empty block */
    }
  }

  /**
   * Private constructor to prevent instantiation.
   */
  private XML() {

  /** empty block */
  }

  /**
   * Sets the ObjectLoader for a specified class.
   *
   * @param classtype the class
   * @param loader the ObjectLoader
   */
  public static void setLoader(Class<?> classtype, XML.ObjectLoader loader) {
    loaders.put(classtype, loader);
  }

  /**
   * Gets the ObjectLoader for the specified class.
   *
   * @param classtype the class
   * @return the ObjectLoader
   */
  public static XML.ObjectLoader getLoader(Class<?> classtype) {
    // look for registered loader first
    ObjectLoader loader = loaders.get(classtype);
    // if no registered loader, look for static getLoader() method in class
    if(loader==null) {
      try {
        Method method = classtype.getMethod("getLoader", (Class<?>[]) null); //$NON-NLS-1$
        if((method!=null)&&Modifier.isStatic(method.getModifiers())) {
          loader = (ObjectLoader) method.invoke(null, (Object[]) null);
          if(loader!=null) {
            // register loader for future calls
            setLoader(classtype, loader);
          }
        }
      } catch(Exception ex) {

      /** empty block */
      }
    }
    // if still no loader found, use the default loader
    if(loader==null) {
      if(defaultLoader==null) {
        defaultLoader = new XMLLoader();
      }
      loader = defaultLoader;
    }
    return loader;
  }

  /**
   * Sets the default ObjectLoader. May be set to null.
   *
   * @param loader the ObjectLoader
   */
  public static void setDefaultLoader(XML.ObjectLoader loader) {
    defaultLoader = loader;
  }

  /**
   * Gets the datatype of the object.
   *
   * @param obj the object
   * @return the type
   */
  public static String getDataType(Object obj) {
    if(obj==null) {
      return null;
    }
    if(obj instanceof String) {
      return "string";                                                      //$NON-NLS-1$
    } else if(obj instanceof Collection<?>) {
      return "collection";                                                  //$NON-NLS-1$
    } else if(obj.getClass().isArray()) {
      // make sure ultimate component class is acceptable
      Class<?> componentType = obj.getClass().getComponentType();
      while(componentType.isArray()) {
        componentType = componentType.getComponentType();
      }
      String type = componentType.getName();
      if((type.indexOf(".")==-1)&&("intdoubleboolean".indexOf(type)==-1)) { //$NON-NLS-1$ //$NON-NLS-2$
        return null;
      }
      return "array";                                                       //$NON-NLS-1$
    } else if(obj instanceof Double) {
      return "double";                                                      //$NON-NLS-1$
    } else if(obj instanceof Integer) {
      return "int";                                                         //$NON-NLS-1$
    } else {
      return "object";                                                      //$NON-NLS-1$
    }
  }

  /**
   * Gets an array containing all supported data types.
   *
   * @return an array of types
   */
  public static String[] getDataTypes() {
    return new String[] {
      "object", "array", "collection", "string", "int", "double", "boolean" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$
    };
  }

  /**
   * Determines whether the specified string requires CDATA tags.
   *
   * @param text the string
   * @return <code>true</code> if CDATA tags are required
   */
  public static boolean requiresCDATA(String text) {
    if((text.indexOf("\"")!=-1)||(text.indexOf("<")!=-1)||(text.indexOf(">")!=-1)||(text.indexOf("&")!=-1)||(text.indexOf("'")!=-1)) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
      return true;
    }
    return false;
  }

  /**
   * Gets the DTD for the specified doctype file name.
   *
   * @param doctype the doctype file name (e.g., "osp10.dtd")
   * @return the DTD as a string
   */
  public static String getDTD(String doctype) {
    if(dtdName!=doctype) {
      // set to defaults in case doctype is not found
      dtdName = defaultName;
      try {
        String dtdPath = "/org/opensourcephysics/resources/controls/doctypes/"; //$NON-NLS-1$
        java.net.URL url = XML.class.getResource(dtdPath+doctype);
        if(url==null) {
          return dtd;
        }
        Object content = url.getContent();
        if(content instanceof InputStream) {
          BufferedReader reader = new BufferedReader(new InputStreamReader((InputStream) content));
          StringBuffer buffer = new StringBuffer(0);
          String line;
          while((line = reader.readLine())!=null) {
            buffer.append(line+NEW_LINE);
          }
          dtd = buffer.toString();
          dtdName = doctype;
        }
      } catch(IOException ex) {
        ex.printStackTrace();
      }
    }
    return dtd;
  }

  /**
   * Sets the ClassLoader.
   *
   * @param loader the classLoader
   */
  public static void setClassLoader(ClassLoader loader) {
    classLoader = loader;
  }

  /**
   * Gets the ClassLoader. May be null.
   *
   * @return the classLoader
   */
  public static ClassLoader getClassLoader() {
    return classLoader;
  }

  /**
   * Replaces backslashes with slashes.
   *
   * @param path the path
   * @return the path with forward slashes
   */
  public static String forwardSlash(String path) {
    if(path==null) {
      return ""; //$NON-NLS-1$
    }
    int i = path.indexOf("\\"); //$NON-NLS-1$
    while(i!=-1) {
      path = path.substring(0, i)+"/"+path.substring(i+1); //$NON-NLS-1$
      i = path.indexOf("\\");                              //$NON-NLS-1$
    }
    return path;
  }

  /**
   * Gets the name from the specified path.
   *
   * @param path the full path
   * @return the name alone
   */
  public static String getName(String path) {
    if(path==null) {
      return ""; //$NON-NLS-1$
    }
    // remove path
    int i = path.lastIndexOf("/"); //$NON-NLS-1$
    if(i==-1) {
      i = path.lastIndexOf("\\"); //$NON-NLS-1$
    }
    if(i!=-1) {
      return path.substring(i+1);
    }
    return path;
  }

  /**
   * Gets the extension of the specified file name.
   *
   * @param fileName the file name with or without path
   * @return the extension, or null if none
   */
  public static String getExtension(String fileName) {
    if(fileName==null) {
      return null;
    }
    int i = fileName.lastIndexOf('.');
    if((i>0)&&(i<fileName.length()-1)) {
      return fileName.substring(i+1);
    }
    return null;
  }

  /**
   * Gets a simple class name for the specified class type.
   *
   * @param type the class
   * @return the simple class name
   */
  public static String getSimpleClassName(Class<?> type) {
    String name = type.getName();
    // trim trailing semicolon, if any
    int i = name.indexOf(";"); //$NON-NLS-1$
    if(i>-1) {
      name = name.substring(0, i);
    }
    // add brackets for arrays
    while(name.startsWith("[")) { //$NON-NLS-1$
      name = name.substring(1);
      name = name+"[]";           //$NON-NLS-1$
    }
    // eliminate leading package name, if any
    String ext = XML.getExtension(name);
    if(ext!=null) {
      name = ext;
    }
    // substitute int for I and double for D arrays
    i = name.indexOf("["); //$NON-NLS-1$
    if(i>-1) {
      String s = name.substring(0, i);
      if(s.equals("I")) {        //$NON-NLS-1$
        s = "int";               //$NON-NLS-1$
      } else if(s.equals("D")) { //$NON-NLS-1$
        s = "double";            //$NON-NLS-1$
      } else if(s.equals("Z")) { //$NON-NLS-1$
        s = "boolean";           //$NON-NLS-1$
      }
      name = s+name.substring(i);
    }
    return name;
  }

  /**
   * Strips the extension from the specified file name.
   *
   * @param fileName the file name with or without path
   * @return the file name without extension
   */
  public static String stripExtension(String fileName) {
    if(fileName==null) {
      return null;
    }
    int i = fileName.lastIndexOf('.');
    if((i>0)&&(i<fileName.length()-1)) {
      fileName = fileName.substring(0, i);
    }
    // strip off any extra dots at end
    while (fileName.lastIndexOf('.')==fileName.length()-1 && fileName.length()>0)
    	fileName = fileName.substring(0, fileName.length()-1);
    return fileName;
  }

  /**
   * Gets the path relative to the specified base directory.
   *
   * @param absolutePath the absolute path
   * @param base the absolute base directory path
   * @return (with forward slashes) the path relative to the base,
   *   or the absolutePath if no relative path is found (eg, different drive)
   *   or the absolutePath if it is really a relative path
   */
  public static String getPathRelativeTo(String absolutePath, String base) {
    // if no base specified then base is current user directory
    if((base==null)||base.equals("")) { //$NON-NLS-1$
      base = getUserDirectory();
    }
    // make sure both paths use forward slashes
    absolutePath = forwardSlash(absolutePath);
    base = forwardSlash(base);
    // return absolutePath if either path not absolute
    if(!absolutePath.startsWith("/")&&(absolutePath.indexOf(":")==-1)) { //$NON-NLS-1$ //$NON-NLS-2$
      return absolutePath;
    }
    if(!base.startsWith("/")&&(base.indexOf(":")==-1)) { //$NON-NLS-1$ //$NON-NLS-2$
      return absolutePath;
    }
    // look for paths in jar files
    int jar = absolutePath.indexOf("jar!"); //$NON-NLS-1$
    if(jar>-1) {
      absolutePath = absolutePath.substring(jar+5);
      return absolutePath;
    }
    // construct relative path
    String relativePath = ""; //$NON-NLS-1$
    // search for base up containing hierarchy
    if(base.endsWith("/")) { //$NON-NLS-1$
      base = base.substring(0, base.length()-1);
    }
    for(int j = 0; j<6; j++) {
      // move base up one level each iteration after the first
      if(j>0) {
        int k = base.lastIndexOf("/");                      //$NON-NLS-1$
        if(k!=-1) {
          base = base.substring(0, k);                      // doesn't include the slash
          relativePath += "../";                            //$NON-NLS-1$
        } else if(!base.equals("")) {                       //$NON-NLS-1$
          base = "";                                        //$NON-NLS-1$
          relativePath += "../";                            //$NON-NLS-1$
        } else {
          break;                                            // no more levels
        }
      }
      // construct and return relative path once base is in the path
      if(!base.equals("")&&absolutePath.startsWith(base)) { //$NON-NLS-1$
        String path = absolutePath.substring(base.length());
        // eliminate leading slash, if any
        int k = path.indexOf("/");                          //$NON-NLS-1$
        if(k==0) {
          path = path.substring(1);
        }
        relativePath += path;
        return relativePath;
      }
    }
    // relative path not found
    return absolutePath;
  }

  /**
   * Gets a path relative to the default user directory.
   *
   * @param absolutePath the absolute path
   * @return the relative path, with forward slashes
   */
  public static String getRelativePath(String absolutePath) {
    return getPathRelativeTo(absolutePath, getUserDirectory());
  }

  /**
   * Gets the default user directory.
   *
   * @return the user directory
   */
  public static String getUserDirectory() {
    String userDir = System.getProperty("user.dir", "."); //$NON-NLS-1$ //$NON-NLS-2$
    return userDir;
  }

  /**
   * Gets the path of the directory containing the specified file.
   *
   * @param fileName the full file name, including path
   * @return the directory path, with forward slashes
   */
  public static String getDirectoryPath(String fileName) {
    if(fileName==null) {
      return ""; //$NON-NLS-1$
    }
    fileName = forwardSlash(fileName);
    int slash = fileName.lastIndexOf("/"); //$NON-NLS-1$
    if(slash!=-1) {
      return fileName.substring(0, slash);
    }
    return ""; //$NON-NLS-1$
  }

  /**
   * Gets the absolute path of the specified file.
   *
   * @param file the file
   * @return the absolute path, with forward slashes
   */
  public static String getAbsolutePath(File file) {
    if(file==null) {
      return null;
    }
    String path = forwardSlash(file.getAbsolutePath());
    int n = path.indexOf("/../"); //$NON-NLS-1$
    while (n>-1) {
    	String pre = path.substring(0, n);
    	int m = pre.lastIndexOf("/"); //$NON-NLS-1$
    	if (m>-1) {
      	String post = path.substring(n+3);
    		path = pre.substring(0, m)+post;
    		n = path.indexOf("/../"); //$NON-NLS-1$
    	}
    }
    n = path.indexOf("/./"); //$NON-NLS-1$
    while (n>-1) {
    	path = path.substring(0, n)+path.substring(n+2);
    	n = path.indexOf("/./"); //$NON-NLS-1$
    }
    return path;
  }

  /**
   * Resolves the name of a file specified relative to a base path.
   *
   * @param relativePath the relative file name
   * @param base the absolute base path
   * @return the resolved file name with forward slashes
   */
  public static String getResolvedPath(String relativePath, String base) {
  	if (base!=null && base.endsWith("/")) //$NON-NLS-1$
  		base = base.substring(0, base.length()-1);
    relativePath = forwardSlash(relativePath);
    // return relativePath if it is really absolute
    if(relativePath.startsWith("/")||(relativePath.indexOf(":/")!=-1)) { //$NON-NLS-1$ //$NON-NLS-2$
      return relativePath;
    }
    base = forwardSlash(base);
    while(relativePath.startsWith("../")&&!base.equals("")) { //$NON-NLS-1$ //$NON-NLS-2$
      if(base.indexOf("/")==-1) {                             //$NON-NLS-1$
        base = "/"+base;                                      //$NON-NLS-1$
      }
      relativePath = relativePath.substring(3);
      base = base.substring(0, base.lastIndexOf("/"));        //$NON-NLS-1$
    }
    if (relativePath.startsWith("./")) //$NON-NLS-1$
    	relativePath = relativePath.substring(2);
    if (relativePath.equals(".")) //$NON-NLS-1$
    	relativePath = ""; //$NON-NLS-1$
    if(base.equals("")) { //$NON-NLS-1$
      return relativePath;
    }
    if(base.endsWith("/")) { //$NON-NLS-1$
      return base+relativePath;
    }
    return base+"/"+relativePath; //$NON-NLS-1$
  }

  /**
   * Creates any missing folders in the specified path.
   *
   * @param path the path to construct
   */
  public static void createFolders(String path) {
    // work way up path to find existing folder
    File dir = new File(path);
    ArrayList<File> dirs = new ArrayList<File>(); // list of needed directories
    while(!dir.exists()) {
      dirs.add(0, dir);
      int j = path.lastIndexOf("/"); //$NON-NLS-1$
      if(j==-1) {
        break;
      }
      path = path.substring(0, j);
      dir = new File(path);
    }
    // work back down path and create needed directories
    Iterator<File> it = dirs.iterator();
    while(it.hasNext()) {
      dir = it.next();
      dir.mkdir();
    }
  }

  // _________________________ ObjectLoader interface _____________________________

  /**
   * This defines methods for moving xml data between an XMLControl and
   * a corresponding Java object.
   *
   * @author Douglas Brown
   * @version 1.0
   */
  public interface ObjectLoader {
    /**
     * Saves data from an object to an XMLControl. The object must
     * be castable to the class control.getObjectClass().
     *
     * @param control the xml control
     * @param obj the object
     */
    public void saveObject(XMLControl control, Object obj);

    /**
     * Creates an object from data in an XMLControl. The returned object must
     * be castable to the class control.getObjectClass().
     *
     * @param control the xml control
     * @return a new object
     */
    public Object createObject(XMLControl control);

    /**
     * Loads an object with data from an XMLControl. The object must
     * be castable to the class control.getObjectClass().
     *
     * @param control the xml control
     * @param obj the object
     * @return the loaded object
     */
    public Object loadObject(XMLControl control, Object obj);

  }

  // static initializer defines loaders for commonly used classes
  static {
    setLoader(Color.class, new XML.ObjectLoader() {
      public void saveObject(XMLControl control, Object obj) {
        java.awt.Color color = (java.awt.Color) obj;
        control.setValue("red", color.getRed());     //$NON-NLS-1$
        control.setValue("green", color.getGreen()); //$NON-NLS-1$
        control.setValue("blue", color.getBlue());   //$NON-NLS-1$
        control.setValue("alpha", color.getAlpha()); //$NON-NLS-1$
      }
      public Object createObject(XMLControl control) {
        int r = control.getInt("red");               //$NON-NLS-1$
        int g = control.getInt("green");             //$NON-NLS-1$
        int b = control.getInt("blue");              //$NON-NLS-1$
        int a = control.getInt("alpha");             //$NON-NLS-1$
        return new java.awt.Color(r, g, b, a);
      }
      public Object loadObject(XMLControl control, Object obj) {
        int r = control.getInt("red");   //$NON-NLS-1$
        int g = control.getInt("green"); //$NON-NLS-1$
        int b = control.getInt("blue");  //$NON-NLS-1$
        int a = control.getInt("alpha"); //$NON-NLS-1$
        return new java.awt.Color(r, g, b, a);
      }

    });
    setLoader(Double.class, new XML.ObjectLoader() {
      public void saveObject(XMLControl control, Object obj) {
        Double dbl = (Double) obj;
        control.setValue("value", dbl.doubleValue()); //$NON-NLS-1$
      }
      public Object createObject(XMLControl control) {
        double val = control.getDouble("value"); //$NON-NLS-1$
        return new Double(val);
      }
      public Object loadObject(XMLControl control, Object obj) {
        Double dbl = (Double) obj;
        double val = control.getDouble("value"); //$NON-NLS-1$
        if(dbl.doubleValue()==val) {
          return dbl;
        }
        return new Double(val);
      }

    });
    setLoader(Integer.class, new XML.ObjectLoader() {
      public void saveObject(XMLControl control, Object obj) {
        Integer i = (Integer) obj;
        control.setValue("value", i.intValue()); //$NON-NLS-1$
      }
      public Object createObject(XMLControl control) {
        int val = control.getInt("value"); //$NON-NLS-1$
        return new Integer(val);
      }
      public Object loadObject(XMLControl control, Object obj) {
        Integer i = (Integer) obj;
        int val = control.getInt("value"); //$NON-NLS-1$
        if(i.intValue()==val) {
          return i;
        }
        return new Integer(val);
      }

    });
    setLoader(Boolean.class, new XML.ObjectLoader() {
      public void saveObject(XMLControl control, Object obj) {
        Boolean bool = (Boolean) obj;
        control.setValue("value", bool.booleanValue()); //$NON-NLS-1$
      }
      public Object createObject(XMLControl control) {
        boolean val = control.getBoolean("value"); //$NON-NLS-1$
        return new Boolean(val);
      }
      public Object loadObject(XMLControl control, Object obj) {
        Boolean bool = (Boolean) obj;
        boolean val = control.getBoolean("value"); //$NON-NLS-1$
        if(bool.booleanValue()==val) {
          return bool;
        }
        return new Boolean(val);
      }

    });
  }

}

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