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

/*
 * PropertyPath.java
 * Copyright (C) 2006-2012 University of Waikato, Hamilton, New Zealand
 */

package weka.core;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.StringTokenizer;
import java.util.Vector;

/**
 * A helper class for accessing properties in nested objects, e.g., accessing
 * the "getRidge" method of a LinearRegression classifier part of 
 * MultipleClassifierCombiner, e.g., Vote. For doing so, one needs to 
 * supply the object to work on and a property path. The property path is a
 * dot delimited path of property names ("getFoo()" and "setFoo(int)" have 
 * "foo" as property name), indices of arrays are 0-based. E.g.: <p/>
 * 
 * <code>getPropertyDescriptor(vote, "classifiers[1].ridge")</code> will return
 * the second classifier (which should be our LinearRegression) of the given
 * Vote meta-classifier and there the property descriptor of the "ridge" 
 * property. <code>getValue(...)</code> will return the actual value of the
 * ridge parameter and <code>setValue(...)</code> will set it.
 * 
 * @author  fracpete (fracpete at waikato dot ac dot nz)
 * @version $Revision: 8034 $
 */
public class PropertyPath
  implements RevisionHandler {

  /**
   * Represents a single element of a property path
   * 
   * @author  fracpete (fracpete at waikato dot ac dot nz)
   * @version $Revision: 8034 $
   */
  public static class PathElement
    implements Cloneable, RevisionHandler {
    
    /** the property */
    protected String m_Name;
    
    /** the index of the array (-1 for none) */
    protected int m_Index;
    
    /**
     * initializes the path element with the given property
     * 
     * @param property	the property to initialize with
     */
    public PathElement(String property) {
      super();
      
      if (property.indexOf("[") > -1) {
	m_Name  = property.replaceAll("\\[.*$", "");
	m_Index = Integer.parseInt(
    		     property.replaceAll(".*\\[", "").replaceAll("\\].*", ""));
      }
      else {
	m_Name   = property;
	m_Index = -1;
      }
    }

    /**
     * returns a clone of the current object
     * 
     * @return		the clone of the current state
     */
    public Object clone() {
      return new PathElement(this.toString());
    }
    
    /**
     * returns the name of the property
     * 
     * @return		the name of the property
     */
    public String getName() {
      return m_Name;
    }
    
    /**
     * returns whether the property is an index-based one
     * 
     * @return		true if the property has an index
     */
    public boolean hasIndex() {
      return (getIndex() > -1);
    }
    
    /**
     * returns the index of the property, -1 if the property is not an
     * index-based one
     * 
     * @return		the index of the property
     */
    public int getIndex() {
      return m_Index;
    }
    
    /**
     * returns the element once again as string
     * 
     * @return		the property as string
     */
    public String toString() {
      String	result;
      
      result = getName();
      if (hasIndex())
	result += "[" + getIndex() + "]";
      
      return result;
    }
    
    /**
     * Returns the revision string.
     * 
     * @return		the revision
     */
    public String getRevision() {
      return RevisionUtils.extract("$Revision: 8034 $");
    }
  }
  
  /**
   * Contains a (property) path structure
   * 
   * @author  fracpete (fracpete at waikato dot ac dot nz)
   * @version $Revision: 8034 $
   */
  public static class Path
    implements RevisionHandler {
    
    /** the structure */
    protected Vector<PathElement> m_Elements;
    
    /**
     * default constructor, only used internally
     */
    protected Path() {
      super();
      
      m_Elements = new Vector<PathElement>();
    }
    
    /**
     * uses the given dot-path
     * 
     * @param path	path in dot-notation
     */
    public Path(String path) {
      this();
      
      m_Elements = breakUp(path);
    }
    
    /**
     * uses the vector with PathElement objects to initialize with
     * 
     * @param elements	the PathElements to use
     */
    public Path(Vector<PathElement> elements) {
      this();
      
      for (int i = 0; i < elements.size(); i++)
	m_Elements.add((PathElement) elements.get(i).clone());
    }
    
    /**
     * uses the given array as elements for the path
     * 
     * @param elements	the path elements to use
     */
    public Path(String[] elements) {
      this();
      
      for (int i = 0; i < elements.length; i++)
	m_Elements.add(new PathElement(elements[i]));
    }
    
    /**
     * breaks up the given path and returns it as vector
     * 
     * @param path	the path to break up
     * @return		the single elements of the path
     */
    protected Vector<PathElement> breakUp(String path) {
      Vector<PathElement>		result;
      StringTokenizer	tok;
      
      result = new Vector<PathElement>();
      
      tok = new StringTokenizer(path, ".");
      while (tok.hasMoreTokens())
        result.add(new PathElement(tok.nextToken()));
      
      return result;
    }

    /**
     * returns the element at the given index
     * 
     * @param index	the index of the element to return
     * @return		the specified element
     */
    public PathElement get(int index) {
      return (PathElement) m_Elements.get(index);
    }

    /**
     * returns the number of path elements of this structure
     * 
     * @return		the number of path elements
     */
    public int size() {
      return m_Elements.size();
    }
    
    /**
     * returns a path object based on the given path string
     * 
     * @param path	path to work on
     * @return		the path structure
     */
    public static Path parsePath(String path) {
      return new Path(path);
    }

    /**
     * returns a subpath of the current structure, starting with the specified
     * element index up to the end
     * 
     * @param startIndex	the first element of the subpath
     * @return			the new subpath
     */
    public Path subpath(int startIndex) {
      return subpath(startIndex, size());
    }

    /**
     * returns a subpath of the current structure, starting with the specified
     * element index up. The endIndex specifies the element that is not part
     * of the new subpath. In other words, the new path contains the elements
     * from "startIndex" up to "(endIndex-1)".
     * 
     * @param startIndex	the first element of the subpath
     * @param endIndex		the element that is after the last added element
     * @return			the new subpath
     */
    public Path subpath(int startIndex, int endIndex) {
      Vector<PathElement>	list;
      int	i;
      
      list = new Vector<PathElement>();
      for (i = startIndex; i < endIndex; i++)
	list.add(get(i));
      
      return new Path(list);
    }
    
    /**
     * returns the structure again as a dot-path
     * 
     * @return		the path structure as dot-path
     */
    public String toString() {
      String	result;
      int	i;
      
      result = "";
      
      for (i = 0; i < m_Elements.size(); i++) {
	if (i > 0)
	  result += ".";
	result += m_Elements.get(i);
      }
      
      return result;
    }
    
    /**
     * Returns the revision string.
     * 
     * @return		the revision
     */
    public String getRevision() {
      return RevisionUtils.extract("$Revision: 8034 $");
    }
  }

  /**
   * A helper class that stores Object and PropertyDescriptor together.
   * 
   * @author  fracpete (fracpete at waikato dot ac dot nz)
   * @version $Revision: 8034 $
   */
  protected static class PropertyContainer
    implements RevisionHandler {
    
    /** the descriptor */
    protected PropertyDescriptor m_Descriptor;
    
    /** the associated object */
    protected Object m_Object;
    
    /**
     * initializes the container
     * 
     * @param desc	the property descriptor
     * @param obj	the associated object
     */
    public PropertyContainer(PropertyDescriptor desc, Object obj) {
      super();
      
      m_Descriptor = desc;
      m_Object     = obj;
    }
    
    /**
     * returns the stored descriptor
     * 
     * @return		the stored descriptor
     */
    public PropertyDescriptor getDescriptor() {
      return m_Descriptor;
    }
    
    /**
     * returns the stored object
     * 
     * @return		the stored object
     */
    public Object getObject() {
      return m_Object;
    }
    
    /**
     * Returns the revision string.
     * 
     * @return		the revision
     */
    public String getRevision() {
      return RevisionUtils.extract("$Revision: 8034 $");
    }
  }
  
  /**
   * returns the property and object associated with the given path, null if 
   * a problem occurred.
   * 
   * @param src		the object to start from
   * @param path	the path to follow
   * @return		not null, if the property could be found
   */
  public static PropertyContainer find(Object src, Path path) {
    PropertyContainer	result;
    PropertyDescriptor	desc;
    Object		newSrc;
    PathElement		part;
    Method		method;
    Object		methodResult;

    // get descriptor
    part = path.get(0);
    try {
      desc = new PropertyDescriptor(part.getName(), src.getClass());
    }
    catch (Exception e) {
      desc = null;
      e.printStackTrace();
    }

    // problem occurred? -> stop
    if (desc == null)
      return null;
    
    // end of path reached?
    if (path.size() == 1) {
      result = new PropertyContainer(desc, src);
    }
    // recurse further
    else {
      try {
	method       = desc.getReadMethod();
	methodResult = method.invoke(src, (Object[]) null);
	if (part.hasIndex())
	  newSrc = Array.get(methodResult, part.getIndex());
	else
	  newSrc = methodResult;
	result = find(newSrc, path.subpath(1));
      }
      catch (Exception e) {
	result = null;
	e.printStackTrace();
      }
    }
    
    return result;
  }
  
  /**
   * returns the property associated with the given path, null if a problem
   * occurred.
   * 
   * @param src		the object to start from
   * @param path	the path to follow
   * @return		not null, if the property could be found
   */
  public static PropertyDescriptor getPropertyDescriptor(Object src, Path path) {
    PropertyContainer	cont;
    
    cont = find(src, path);
    
    if (cont == null)
      return null;
    else
      return cont.getDescriptor();
  }
  
  /**
   * returns the property associated with the given path
   * 
   * @param src		the object to start from
   * @param path	the path to follow
   * @return		not null, if the property could be found
   */
  public static PropertyDescriptor getPropertyDescriptor(Object src, String path) {
    return getPropertyDescriptor(src, new Path(path));
  }
  
  /**
   * returns the value specified by the given path from the object
   * 
   * @param src		the object to work on
   * @param path	the retrieval path
   * @return		the value, null if an error occurred
   */
  public static Object getValue(Object src, Path path) {
    Object		result;
    PropertyContainer	cont;
    Method		method;
    Object		methodResult;
    PathElement		part;
    
    result = null;
    
    cont = find(src, path);
    // problem?
    if (cont == null)
      return null;
    
    // retrieve the value
    try {
      part         = path.get(path.size() - 1);
      method       = cont.getDescriptor().getReadMethod();
      methodResult = method.invoke(cont.getObject(), (Object[]) null);
      if (part.hasIndex())
	result = Array.get(methodResult, part.getIndex());
      else
	result = methodResult;
    }
    catch (Exception e) {
      result = null;
      e.printStackTrace();
    }
    
    return result;
  }
  
  /**
   * returns the value specified by the given path from the object
   * 
   * @param src		the object to work on
   * @param path	the retrieval path
   * @return		the value, null if an error occurred
   */
  public static Object getValue(Object src, String path) {
    return getValue(src, new Path(path));
  }
  
  /**
   * set the given value specified by the given path in the object
   * 
   * @param src		the object to work on
   * @param path	the retrieval path
   * @param value	the value to set
   * @return		true if the value could be set
   */
  public static boolean setValue(Object src, Path path, Object value) {
    boolean		result;
    PropertyContainer	cont;
    Method		methodRead;
    Method		methodWrite;
    Object		methodResult;
    PathElement		part;
    
    result = false;
    
    cont = find(src, path);
    // problem?
    if (cont == null)
      return result;
    
    // set the value
    try {
      part         = path.get(path.size() - 1);
      methodRead   = cont.getDescriptor().getReadMethod();
      methodWrite  = cont.getDescriptor().getWriteMethod();
      if (part.hasIndex()) {
	methodResult = methodRead.invoke(cont.getObject(), (Object[]) null);
	Array.set(methodResult, part.getIndex(), value);
	methodWrite.invoke(cont.getObject(), new Object[]{methodResult});
      }
      else {
	methodWrite.invoke(cont.getObject(), new Object[]{value});
      }
      result = true;
    }
    catch (Exception e) {
      result = false;
      e.printStackTrace();
    }
    
    return result;
  }
  
  /**
   * set the given value specified by the given path in the object
   * 
   * @param src		the object to work on
   * @param path	the retrieval path
   * @param value	the value to set
   */
  public static void setValue(Object src, String path, Object value) {
    setValue(src, new Path(path), value);
  }
  
  /**
   * Returns the revision string.
   * 
   * @return		the revision
   */
  public String getRevision() {
    return RevisionUtils.extract("$Revision: 8034 $");
  }
  
  /**
   * for testing only
   * 
   * @param args	the commandline options - ignored
   * @throws Exception	if something goes wrong
   */
  public static void main(String[] args) throws Exception {
    // Path
    Path path = new Path("hello.world[2].nothing");
    System.out.println("Path: " + path);
    System.out.println(" -size: " + path.size());
    System.out.println(" -elements:");
    for (int i = 0; i < path.size(); i++)
      System.out.println(
	  "  " + i + ". " + path.get(i).getName() 
	  + " -> " + path.get(i).getIndex());
    
    /*
    // retrieving ridge with path
    weka.classifiers.meta.Vote vote = new weka.classifiers.meta.Vote();
    vote.setClassifiers(
	new weka.classifiers.Classifier[]{
	    new weka.classifiers.trees.J48(),
	    new weka.classifiers.functions.LinearRegression()});
    path = new Path("classifiers[1].ridge");
    System.out.println("path: " + path + " -> " + getValue(vote, path));
    
    // setting ridge with path and retrieving it again
    setValue(vote, path.toString(), new Double(0.1));
    System.out.println("path: " + path + " -> " + getValue(vote, path));
    */
  }
}
