/*
 * This file is part of the Vamsas Client version 0.2. 
 * Copyright 2010 by Jim Procter, Iain Milne, Pierre Marguerite, 
 *  Andrew Waterhouse and Dominik Lindner.
 * 
 * Earlier versions have also been incorporated into Jalview version 2.4 
 * since 2008, and TOPALi version 2 since 2007.
 * 
 * The Vamsas Client is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *  
 * The Vamsas Client 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.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with the Vamsas Client.  If not, see <http://www.gnu.org/licenses/>.
 */
package uk.ac.vamsas.client;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Field;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Vector;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.exolab.castor.mapping.FieldHandler;
import org.exolab.castor.mapping.GeneralizedFieldHandler;
import org.exolab.castor.mapping.ValidityException;
import org.exolab.castor.xml.IDResolver;
import org.exolab.castor.xml.MarshalException;
import org.exolab.castor.xml.MarshalListener;
import org.exolab.castor.xml.Marshaller;
import org.exolab.castor.xml.UnmarshalListener;
import org.exolab.castor.xml.Unmarshaller;
import org.exolab.castor.xml.ValidationException;

import uk.ac.vamsas.objects.core.VamsasDocument;

/**
 * Implements the Vamsas Vobject ID machinery for translating between
 * non-volatile XML IDs and Vobject references. Use the marshalling and
 * unmarshalling methods in this class in order to add automatically computed
 * values for required fields in objects, so as to avoid validation exceptions
 * when marshalling new objects into the vamsas document.
 */
public class VorbaXmlBinder implements UnmarshalListener {
  private static Log log = LogFactory.getLog(VorbaXmlBinder.class);

  private final IVorbaIdFactory vorbafactory;

  private final Vector obj;

  private final Hashtable oldobjhashes;

  private final Hashtable objrefs;

  private final Vector updatedobjs; // not yet used elswhere ?

  public VorbaXmlBinder(IVorbaIdFactory vorbafactory2, Vector unrefedObj,
      Hashtable objrefs2, Hashtable oldobjhashes, Vector updatedObj) {
    this.vorbafactory = vorbafactory2;
    this.obj = unrefedObj;
    this.objrefs = objrefs2;
    this.oldobjhashes = oldobjhashes;
    this.updatedobjs = updatedObj;
  }

  /*
   * (non-Javadoc)
   * 
   * @see
   * org.exolab.castor.xml.UnmarshalListener#attributesProcessed(java.lang.Object
   * )
   */
  public void attributesProcessed(Object object) {

  }

  /*
   * (non-Javadoc)
   * 
   * @see org.exolab.castor.xml.UnmarshalListener#fieldAdded(java.lang.String,
   * java.lang.Object, java.lang.Object)
   */
  public void fieldAdded(String fieldName, Object parent, Object child) {
    if (parent instanceof Vobject && child instanceof Vobject) {
      if (((Vobject) child).V_parent == null) {
        // System.err.println("Setting parent of "+fieldName);
        ((Vobject) child).setV_parent((Vobject) parent);
      }
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.exolab.castor.xml.UnmarshalListener#initialized(java.lang.Object)
   */
  public void initialized(Object object) {
    if (object instanceof Vobject) {
      Vobject nobj = (Vobject) object;
    }
  }

  /*
   * Check if the object has an 'id' field - if it does, copy the value into the
   * VorbaId field of Vobject, and add the Vobject to the VorbaId hash.
   * 
   * @see org.exolab.castor.xml.UnmarshalListener#unmarshalled(java.lang.Object)
   */
  public void unmarshalled(Object newobj) {
    if (newobj instanceof Vobject) {
      Vobject nobj = (Vobject) newobj;
      nobj.set__stored_in_document(true);
      try {
        if (nobj.isRegisterable() && nobj.___id_field != null) {
          VorbaId nobj_id = null;
          // look for the id field (should be an NCName string)
          nobj.__vorba = vorbafactory;
          // use the Vobject accessor method to avoid unpleasant security
          // exceptions.
          String idstring = nobj.__getInstanceIdField();
          if (idstring != null) {
            if (idstring.length() > 0) {
              nobj.setVorbaId(VorbaId.newId(idstring));
              nobj_id = nobj.getVorbaId();
              if (objrefs.containsKey(nobj_id)
                  && !objrefs.get(nobj_id).equals(nobj)) {
                System.err.println("Serious problem : duplicate id '"
                    + idstring + "' found! expect badness.");
                // TODO: HANDLE duplicate XML ids correctly
              }
              objrefs.put(nobj_id, nobj);
            } else {
              // add to list of objects without a valid vorbaId
              obj.add(nobj);
            }
          } else {
            // TODO: add to list of objects without a valid vorbaId
            obj.add(nobj);
          }
          nobj.doHash(); // updates detected by comparing with last hash when
                         // marshalling
          // check to see if new object was present in old object hash
          if (nobj_id != null) {
            if (oldobjhashes.containsKey(nobj_id)) {
              Vobjhash oldhash = (Vobjhash) oldobjhashes.get(nobj_id);
              if (oldhash.isUpdated(nobj)) {
                // mark the object as updated in this document read.
                nobj.set__updated_since_last_read(true);
                updatedobjs.addElement(nobj);
              }
            } else {
              // object has no entry in the oldhashvalue list but
              // there is a valid vorbaId so
              nobj.set__added_since_last_read(true);
            }
            // and record the just-unmarshalled hash value
            oldobjhashes.put(nobj_id, new Vobjhash(nobj));
          } else {
            // for objects yet to get a valid vorba_id
            nobj.set__added_since_last_read(true);
          }
        }

      } catch (Exception e) {
        return;
      }
      ;

    }
  }

  /**
   * writes the VamsasDocument to the given stream. TODO: ensure that (at least)
   * default provenance entries are written for objects.
   * 
   * @param outstream
   * @param vorba
   *          valid VorbaIdFactory to construct any missing IDs
   * @param doc
   * @throws IOException
   * @throws MarshalException
   * @throws ValidationException
   */
  public static void putVamsasDocument(PrintWriter outstream,
      VorbaIdFactory vorba, VamsasDocument doc) throws IOException,
      MarshalException, ValidationException {
    // Ensure references
    if (vorba == null)
      throw new Error("Null VorbaIdFactory Parameter");
    if (doc.__vorba == null)
      doc.__vorba = vorba;
    doc.__ensure_instance_ids(); // this may take a while. Do we allow for
                                 // cyclic references ?
    Marshaller mshl = new Marshaller(outstream);
    mshl.marshal(doc);

  }

  /**
   * creates new VorbaId references where necessary for newly unmarshalled
   * objects
   * 
   * @param unrefed
   * @param objrefs
   * @return false if any new object references were made
   */
  private static boolean ensure_references(Vector unrefed, Hashtable objrefs) {
    boolean sync = true;
    if (unrefed.size() > 0) {
      sync = false; // document is out of sync - ids have been created.
      java.util.Iterator newobj = unrefed.listIterator();
      while (newobj.hasNext()) {
        Vobject o = (Vobject) newobj.next();
        // forces registration and id field update.
        VorbaId id = o.getVorbaId();
        if (!objrefs.containsKey(id)) {
          objrefs.put(id, o);
        } else {
          if (!objrefs.get(id).equals(o))
            throw new Error(
                "Serious! Duplicate reference made by vorbaIdFactory!");
        }
      }
    }
    return sync;
  }

  /**
   * Unmarshals a vamsasDocument Vobject from a stream, registers unregistered
   * objects, records existing VorbaIds, and completes the
   * uk.ac.vamsas.client.Vobject housekeeping fields. For a valid unmarshalling,
   * the array of returned objects also includes a <return>sync</return>
   * parameter which is true if new VorbaIds were created. If sync is false,
   * then the caller should ensure that the vamsasDocument is written back to
   * disk to propagate the new VorbaIds. TODO: ensure that provenance is correct
   * for newly registered objects as getVamsasObjects but will detect updated
   * objects based on differing hash values obtained from the VorbaIdFactory's
   * VorbaId, Vobject.get__last_Hash() pairs (if any)
   * 
   * @param instream
   *          - the XML input stream
   * @param factory
   *          - the SimpleClient's properly configured VorbaId factory to make
   *          new references.
   * @param root
   *          the root element's uk.ac.vamsas.objects.core Vobject.
   * @return null or {(Object) VamsasDocument Vobject, (Object) Hashtable of
   *         Vobject references, (Object) Boolean(sync), (Object) Vector of
   *         updated objects in document }
   */
  public static Object[] getVamsasObjects(Reader instream,
      VorbaIdFactory factory, Vobject root) {
    Unmarshaller unmarshaller = new Unmarshaller(root);
    final VorbaIdFactory ourfactory = factory;
    unmarshaller.setIDResolver(new IDResolver() {
      public Object resolve(String id) {
        if (ourfactory.warnUnresolved) {
          // TODO: allow for external ID resolution
          VorbaXmlBinder.log
          .warn("Warning - id "
              + id
              + " is not found in the Vamsas XML! (TODO: Ignore if this is a forward reference!)");
        }
        return null;
      }
    });
    final Hashtable objrefs = new Hashtable();
    if (factory.extanthashv == null)
      factory.extanthashv = new Hashtable();
    final Hashtable oobjhashes = factory.extanthashv;
    final VorbaIdFactory vorbafactory = factory;
    final Vector unrefedObj = new Vector();
    final Vector updatedObj = new Vector();
    unmarshaller.setUnmarshalListener(new VorbaXmlBinder(vorbafactory,
        unrefedObj, objrefs, oobjhashes, updatedObj));
    // Call the unmarshaller.
    try {
      while (instream.ready()) {
        // TODO: mark objects in oobjhash prior to unmarshalling, to detect when
        // objects have been lost through an update.
        // tohere
        factory.warnUnresolved = false;
        Object obj = unmarshaller.unmarshal(instream);
        factory.warnUnresolved = true;
        boolean sync = ensure_references(unrefedObj, objrefs);
        if (!(obj instanceof Vobject))
          return null;
        vorbafactory.setNewIdHash(objrefs); // update the Document IO Handler's
                                            // set of vorbaId<>Object bindings.
        return new Object[] { obj, objrefs, new Boolean(sync), updatedObj };
      }
    } catch (MarshalException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } catch (ValidationException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } catch (IOException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
    return null;
  }
}
