/*
 */
package com.ergotech.util;

import java.awt.Font;
import java.beans.BeanInfo;
import java.beans.PropertyDescriptor;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.ResourceBundle;

import org.apache.log4j.Logger;

import com.ergotech.vib.servers.DataSource;
import com.thoughtworks.xstream.alias.ClassMapper;
import com.thoughtworks.xstream.converters.ConversionException;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;

/**
 * This converter will archive any component that has a beaninfo.  It will
 * use the property descriptors from the beaninfo to archive the bean.
 */
public class BeanWithBeanInfoConverter implements Converter {
  
  /** The name of the resource that contains the error messages. */
  public static final String beanErrorFileResource = "com.ergotech.util.BeanErrors";
  
  /** The static properties object that contains the class name mapping.
   * This is normally read from a properties file called "ClassNameMapping.properties" that
   * will provide a listing of fully qualified old class names and new class names.  This allows 
   * class names and packages to be change and still be able to load existing applications.   
   * This will always be non-null, but may be empty.
   * This is similar to the XStream class mapper, but it only maps the names.  This allows lazy creation 
   * of the class.
   */
  // I suspect that this could be implemented as the XStream class mapper, but it's not immediately obvious how.
  protected static Properties classNameMap;
  
  /** Special handling of class not loadable errors.  If an application is built with beans, and the jar is
   * removed, then the application won't load.  This resource allows intelligent(?) messages to be returned
   * for known classes. For example if the application is organized such that optional packages can be added,
   * it's possible to report the name of the missing option.
   * The format of the resource that backs this up should be fullqualifiedclassname=Package
   */
  protected static ResourceBundle classErrors;
  
  /** The static initializer reads a properties file called "ClassNameMapping.properties" that
   * will provide a listing of fully qualified old class names and new class names.  This allows 
   * class names and packages to be change and still be able to load existing applications.
   */
  static {
    classNameMap = new Properties();
    try {
      InputStream is = BeanWithBeanInfoConverter.class.getClassLoader().getResourceAsStream("ClassNameMapping.properties");
      classNameMap.load(is);
    } catch (Exception e) {
      // ignore
    }
    try {
      classErrors = ResourceBundle.getBundle(beanErrorFileResource, Locale.getDefault(), BeanWithBeanInfoConverter.class.getClassLoader());
    } catch ( Exception e ) {
      // ignore
    }
  }

  /** The class mapper that this converter is using. */
  protected ClassMapper classMapper;
  
  /** True (the default) if hidden properties should be saved. */
  protected boolean saveHiddenProperties = true;
  
  /**
   * 
   */
  public BeanWithBeanInfoConverter(ClassMapper classMapper) {
    super();
    this.classMapper = classMapper;
  }

  /** Returns true if the component has a beaninfo.
   * @see com.thoughtworks.xstream.converters.Converter#canConvert(java.lang.Class)
   */
  public boolean canConvert(Class beanClass) {
    try {
    BeanInfo bi = findBeanInfo(beanClass);
    //  the propertyDescriptors will be null if the bean info is hopelessly broken.
    return (bi != null && bi.getPropertyDescriptors() !=null);
    } catch ( Exception any ) {
      DataSource.logException(any);
    }
    return false;
  }

  /**
   * @param beanClass
   * @return
   */
  protected BeanInfo findBeanInfo(Class beanClass) {
    BeanInfo beanInfo = null;
    try {
      // this is not quite correct, there is a rule about searching for
      // beaninfos in places other than the current directory, etc. etc.
      // but we'll just assume that there in the current directory
      // see Introspector.getBeanInfoSearchPath() if you want to do it right.
      String beanInfoClassName = beanClass.getName() + "BeanInfo";
      Class beanInfoClass = Class.forName(beanInfoClassName, true, beanClass.getClassLoader());
      //System.out.println ("findBeanInfo " + beanInfoClass.getClassLoader() + " " + beanClass.getClassLoader());
      beanInfo = (BeanInfo)beanInfoClass.newInstance();
    } catch ( ClassNotFoundException noBeanInfo ) {
      // failure - this is OK a beaninfo need not exist
      beanInfo = null; // just to make sure.
    } catch ( Exception noBeanInfo ) {
      DataSource.logException(noBeanInfo);
      noBeanInfo.printStackTrace();
      // failure 
      beanInfo = null; // just to make sure.
    }
    return beanInfo;
  }

  /**
   * @see com.thoughtworks.xstream.converters.Converter#marshal(java.lang.Object, com.thoughtworks.xstream.io.HierarchicalStreamWriter, com.thoughtworks.xstream.converters.MarshallingContext)
   */
  public void marshal(Object bean, HierarchicalStreamWriter writer, MarshallingContext context) {
//    if ( bean.getClass().getName().equals("Status" )) {
//      System.out.println ("Marshal:Status stop here.");
//    }
    BeanInfo beanInfo = findBeanInfo (bean.getClass());
    if ( beanInfo == null ) {
      throw new ConversionException ("Not a bean");
    }
    // create an instance of the class.  We'll use this for comparison
    // to see if the property has changed and so needs to be written.
    //final Class type = classMapper.realClass(bean.getClass().getName());
    Class type = bean.getClass();
    
    Object testBean;
    try {
      testBean = type.newInstance();
    } catch (Exception e) {
      e.printStackTrace();
      throw new ConversionException("Cannot instantiate Bean \"" + bean.getClass().getName() + "\"");
    }
    //writer.startNode(classMapper.serializedClass(bean.getClass()));
    //BeanDescriptor bd =  beanInfo.getBeanDescriptor();
    //String beanDescription = bd.getDisplayName();
    PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
    if (propertyDescriptors == null ) { // the propertyDescriptors will be null if the bean info is hopelessly broken.
      throw new ConversionException("BeanInfo is hopelessly broken");
    }
    for ( int counter = 0 ; counter < propertyDescriptors.length ; counter++ ) {
      PropertyDescriptor propertyDescriptor = propertyDescriptors[counter];
      // I don't understand how a property descriptor can be missing a read or a write method, but is seems that
      // some are missing at least the write method.  These will be ignored.
      boolean dontSave = propertyDescriptor.isExpert() && propertyDescriptor.isHidden();
//      if ( dontSave ) {
//        System.out.println ("BeanWithBeanInfoConverter:marshal");
//      }
      if ( !dontSave && propertyDescriptor.getWriteMethod() != null && propertyDescriptor.getReadMethod() != null) {
        Method getMethod = propertyDescriptor.getReadMethod();
        Method setMethod = propertyDescriptor.getWriteMethod();
        // another bold assumption.  The "set" method always starts with "set"
        // it need not but usually does (Fix this in getPropertyMap if you fix it here).
        String propertyName = setMethod.getName().substring(3);
        Object value = null; // invoke the "get" method and get the current value
        Object testValue = null; // invoke the "get" method and get the default value
        try {
          value = getMethod.invoke(bean, null);
//          if ( testBean.getClass().getClassLoader() != getMethod.getDeclaringClass().getClassLoader()) {
//            System.out.println ("Marshal " + testBean.getClass().getClassLoader() + " Method " + getMethod.getDeclaringClass().getClassLoader());
//          }
          testValue = getMethod.invoke(testBean, null);
          if ( (value == null && testValue != null ) || (value != null && testValue == null ) || (value != null && testValue != null && !value.equals(testValue)) ) {
            writer.startNode(propertyName);
            //writer.setValue(String.valueOf(value));
            writer.addAttribute("Class", value.getClass().getName());
            context.convertAnother(value);
            writer.endNode();
          }
        } catch (Exception e) {
          Logger l = Logger.getLogger("console." + getClass());
          l.error(e.getMessage(), e);
          l = Logger.getLogger("popup." + getClass());
          l.error("Error writing property \"" + propertyDescriptor.getDisplayName() + "\" Property will not be saved.");
        }
      }
    }
    //writer.endNode();
  }
   
  /** Returns a hashmap.  The key is the <code>propertyDescriptor.getName()</code>
   * the target is the propertyDescriptor.
   */
  protected Map getPropertyMap (PropertyDescriptor[] propertyDescriptors ) {
    HashMap ret = new HashMap();
    for ( int counter = 0 ; counter < propertyDescriptors.length ; counter++ ) {
      PropertyDescriptor propertyDescriptor = propertyDescriptors[counter];
      if ( propertyDescriptor != null ) {
      Method setMethod = propertyDescriptor.getWriteMethod();
      // another bold assumption.  The "set" method always starts with "set"
      // it need not but usually does (Fix this in marshal if you fix it here).
      String propertyName = setMethod.getName().substring(3);
      ret.put(propertyName, propertyDescriptor);
      }
      else {
        System.out.println("getPropertyMap null element");
      }
    }
    return ret;
  }

  /** Restores a JavaBean from the xml stream.
   * @see com.thoughtworks.xstream.converters.Converter#unmarshal(com.thoughtworks.xstream.io.HierarchicalStreamReader, com.thoughtworks.xstream.converters.UnmarshallingContext)
   */
  public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
    String className = reader.getAttribute("Class");
    if ( className == null ) {
      className = reader.getNodeName();
    }
    className = classNameMap.getProperty(className, className);
    Class type = classMapper.realClass(className);
    Object bean;
    try {
      bean = type.newInstance();
    } catch (Exception e) {
      e.printStackTrace();
      throw new ConversionException("Cannot instantiate Bean \"" + reader.getNodeName() + "\"");
    }
    BeanInfo beanInfo = findBeanInfo (type);
    if ( beanInfo == null ) {
      throw new ConversionException ("Not a bean");
    }
    if ( beanInfo.getPropertyDescriptors() == null ) {
      throw new ConversionException("BeanInfo is hopelessly broken");
    }
    Map propertyMap = getPropertyMap(beanInfo.getPropertyDescriptors());
    while (reader.hasMoreChildren()) {
      reader.moveDown();
      try {
        String nodeName = reader.getNodeName();
        //System.out.println ("unmarshal:NodeName " + nodeName);
        String propertyName = classMapper.realMember(bean.getClass(), reader.getNodeName());
        PropertyDescriptor propertyDescriptor = (PropertyDescriptor)propertyMap.get(nodeName);
        if ( propertyDescriptor == null ) {
          propertyDescriptor = (PropertyDescriptor)propertyMap.get(propertyName);
        }
        if (propertyDescriptor != null ) {
          String neededClass = classNameMap.getProperty(reader.getAttribute("Class"), reader.getAttribute("Class"));
          Class valueType = classMapper.realClass(neededClass);
          Class tmp = context.getRequiredType();
          Object value = context.convertAnother(bean, valueType);
          Method set = propertyDescriptor.getWriteMethod();
          try {
            set.invoke(bean, new Object[]{value});
          } catch (Exception e1) {
            // ignore this exception and continue
            // the worst case is that this property will be lost
            // perhaps the method signature has change, etc. etc. etc.
            // for now we'll print it
            e1.printStackTrace();
          }
          
        }
        else {
          // I assume that I have to read this, but maybe not
          try {
            Object value = context.convertAnother(bean, context.getRequiredType());
          } catch (Exception any ) {
            // this will happen when a property is removed from the bean info
            // and so can be ignored if there is an error.
          }
          
        }
      } catch ( ThreadDeath td ) {
        throw td;
      } catch ( Throwable t ) {  // catch Error (eg classnotfoundERROR
        String failedClass = t.getMessage();
        int index = failedClass.indexOf(':');
        if ( index > 0 ) {
          failedClass = failedClass.substring(0, index);
        }
        failedClass = failedClass.trim();
        if ( classErrors != null ) {
          // because of the options for wild cards, we have to do an ugly linear search here
          // since this should happen infrequently and the error list shouldn't be _that_ large
          // this should not be a problem.  
          // Check for an exact match first
          String errorMessage = null;
          try {
            classErrors.getString(failedClass);
          } catch ( Exception any ) {
            // ignore, this is probably a "MissingResourceException"
            //any.printStackTrace();
          }
          if ( errorMessage == null ) {
            Enumeration e = classErrors.getKeys();
            while ( e.hasMoreElements() ) {
              String key  = ((String)e.nextElement()).trim();
              if ( key.endsWith("*") && failedClass.startsWith(key.substring(0, key.length()-1) )){
                errorMessage = classErrors.getString(key);
                break;
              }
            }
          }
          if ( errorMessage != null ) {
            StringBuffer fullError = new StringBuffer();
            if ( classErrors.getString("FullMessageStart") != null ) {
              fullError.append(classErrors.getString("FullMessageStart"));
            }
            fullError.append(errorMessage);
            if ( classErrors.getString("FullMessageEnd") != null ) {
              fullError.append('\n');
              fullError.append(classErrors.getString("FullMessageEnd"));
            }
            Logger.getLogger("console." + getClass().getName()).error(fullError.toString());
            Logger.getLogger("popup." + getClass().getName()).error(fullError.toString());
            System.exit(99);
          }
        }
        // ignore this bean and move on to the next - the file should still load
        Logger.getLogger("console." + getClass().getName()).error(t.getMessage(), t);
        Logger.getLogger("popup." + getClass().getName()).error("Cannot restore component: \n" +  t.getMessage() + ".\nIf this was a custom component, please add the correct jar to the MIStudio \"jars\" directory.\nContinue loading by pressing \"OK\". \nThis component will be removed from your application.", t );
      }
      reader.moveUp();
    }
    
    return bean;
  }

}
