/*
 * This program is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
 * Foundation.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
 * or from the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * 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 Lesser General Public License for more details.
 *
 * Copyright (c) 2006 - 2009 Object Refinery Limited, Pentaho Corporation and Contributors.  All rights reserved.
 */

package org.pentaho.reporting.libraries.serializer;

import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Iterator;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pentaho.reporting.libraries.base.config.Configuration;
import org.pentaho.reporting.libraries.base.util.ObjectUtilities;
import org.pentaho.reporting.libraries.base.util.DebugLog;


/**
 * The SerializeHelper is used to make implementing custom serialization
 * handlers easier. Handlers for certain object types need to be added to this
 * helper before this implementation is usable.
 *
 * @author Thomas Morgner
 */
public class SerializerHelper
{
  private static final Log logger = LogFactory.getLog(SerializerHelper.class);
  /**
   * The singleton instance of the serialize helper.
   */
  private static SerializerHelper singleton;

  /**
   * Returns or creates a new SerializerHelper. When a new instance is created
   * by this method, all known SerializeMethods are registered.
   *
   * @return the SerializerHelper singleton instance.
   */
  public static synchronized SerializerHelper getInstance()
  {
    if (singleton == null)
    {
      singleton = new SerializerHelper();
      singleton.registerMethods();
    }
    return singleton;
  }


  /**
   * This method can be used to replace the singleton instance of this helper.
   *
   * @param helper the new instance of the serialize helper.
   */
  protected static void setInstance(final SerializerHelper helper)
  {
    singleton = helper;
  }

  /**
   * A collection of the serializer methods.
   */
  private final HashMap methods;

  /**
   * A class comparator for searching the super class of an certain class.
   */
  private final ClassComparator comparator;

  /**
   * Creates a new SerializerHelper.
   */
  protected SerializerHelper()
  {
    this.comparator = new ClassComparator();
    this.methods = new HashMap();
  }

  /**
   * Registers a new SerializeMethod with this SerializerHelper.
   *
   * @param method the method that should be registered.
   */
  public synchronized void registerMethod(final SerializeMethod method)
  {
    this.methods.put(method.getObjectClass(), method);
  }

  /**
   * Traverses the configuration and registers all serialization handlers in this factory.
   */
  protected void registerMethods()
  {
    final Configuration config = LibSerializerBoot.getInstance().getGlobalConfig();
    final Iterator sit = config.findPropertyKeys("org.pentaho.reporting.libraries.serializer.handler.");

    while (sit.hasNext())
    {
      final String configkey = (String) sit.next();
      final String c = config.getConfigProperty(configkey);
      final Object maybeModule = ObjectUtilities.loadAndInstantiate
          (c, SerializerHelper.class, SerializeMethod.class);
      if (maybeModule != null)
      {
        final SerializeMethod module = (SerializeMethod) maybeModule;
        registerMethod(module);
      }
      else
      {
        logger.warn("Invalid SerializeMethod implementation: " + c);
      }
    }
  }

  /**
   * Deregisters a new SerializeMethod with this SerializerHelper.
   *
   * @param method the method that should be deregistered.
   */
  public synchronized void unregisterMethod(final SerializeMethod method)
  {
    this.methods.remove(method.getObjectClass());
  }

  /**
   * Returns the collection of all registered serialize methods.
   *
   * @return a collection of the registered serialize methods.
   */
  protected HashMap getMethods()
  {
    return methods;
  }

  /**
   * Returns the class comparator instance used to find correct super classes.
   *
   * @return the class comparator.
   */
  protected ClassComparator getComparator()
  {
    return comparator;
  }

  /**
   * Looks up the SerializeMethod for the given class or null if there is no
   * SerializeMethod for the given class.
   *
   * @param c the class for which we want to lookup a serialize method.
   * @return the method or null, if there is no registered method for the
   *         class.
   */
  protected SerializeMethod getSerializer(final Class c)
  {
    final SerializeMethod sm = (SerializeMethod) methods.get(c);
    if (sm != null)
    {
      return sm;
    }
    return getSuperClassObjectDescription(c);
  }

  /**
   * Looks up the SerializeMethod for the given class or null if there is no
   * SerializeMethod for the given class. This method searches all
   * superclasses.
   *
   * @param d the class for which we want to lookup a serialize
   *          method.
   * @return the method or null, if there is no registered method for the
   *         class.
   */
  protected SerializeMethod getSuperClassObjectDescription
      (final Class d)
  {
    SerializeMethod knownSuperClass = null;
    final Iterator keys = methods.keySet().iterator();
    while (keys.hasNext())
    {
      final Class keyClass = (Class) keys.next();
      if (keyClass.isAssignableFrom(d))
      {
        final SerializeMethod od = (SerializeMethod) methods.get(keyClass);
        if (knownSuperClass == null)
        {
          knownSuperClass = od;
        }
        else
        {
          if (comparator.isComparable
              (knownSuperClass.getObjectClass(), od.getObjectClass()))
          {
            if (comparator.compare
                (knownSuperClass.getObjectClass(), od.getObjectClass()) < 0)
            {
              knownSuperClass = od;
            }
          }
        }
      }
    }
    return knownSuperClass;
  }


  /**
   * Writes a serializable object description to the given object output stream.
   * This method selects the best serialize helper method for the given object.
   *
   * @param o   the to be serialized object.
   * @param out the outputstream that should receive the object.
   * @throws IOException if an I/O error occured.
   */
  public synchronized void writeObject(final Object o,
                                       final ObjectOutputStream out)
      throws IOException
  {
    try
    {
      if (o == null)
      {
        out.writeByte(0);
        return;
      }
      if (o instanceof Serializable)
      {
        out.writeByte(1);
        out.writeObject(o);
        return;
      }

      final SerializeMethod m = getSerializer(o.getClass());
      if (m == null)
      {
        throw new NotSerializableException(o.getClass().getName());
      }
      out.writeByte(2);
      out.writeObject(m.getObjectClass());
      m.writeObject(o, out);
    }
    catch (NotSerializableException nse)
    {
      DebugLog.log("Unable to serialize object: " + o);
      throw nse;
    }
  }

  public synchronized boolean isSerializable(final Object o)
  {
    if (o == null)
    {
      return true;
    }
    if (o instanceof Serializable)
    {
      return true;
    }

    final SerializeMethod m = getSerializer(o.getClass());
    return m != null;
  }

  /**
   * Reads the object from the object input stream. This object selects the best
   * serializer to read the object.
   * <p/>
   * Make sure, that you use the same configuration (library and class versions,
   * registered methods in the SerializerHelper) for reading as you used for
   * writing.
   *
   * @param in the object input stream from where to read the serialized data.
   * @return the generated object.
   * @throws IOException            if reading the stream failed.
   * @throws ClassNotFoundException if serialized object class cannot be found.
   */
  public synchronized Object readObject(final ObjectInputStream in)
      throws IOException, ClassNotFoundException
  {
    final int type = in.readByte();
    if (type == 0)
    {
      return null;
    }
    if (type == 1)
    {
      return in.readObject();
    }
    final Class c = (Class) in.readObject();
    final SerializeMethod m = getSerializer(c);
    if (m == null)
    {
      throw new NotSerializableException(c.getName());
    }
    return m.readObject(in);
  }
}
