/*
 * 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.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Iterator;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.exolab.castor.mapping.FieldDescriptor;
import org.exolab.castor.mapping.FieldHandler;
import org.exolab.castor.xml.util.XMLClassDescriptorImpl;

import uk.ac.vamsas.test.simpleclient.VamsasArchive;

/**
 * Base class for all Vamsas objects extracted from an IClientDocument. An
 * Vobject maybe registered or unregistered.
 * 
 * @author jimp
 * 
 */
public abstract class Vobject {
  static Log log = LogFactory.getLog(Vobject.class);

  /**
   * true if Vobject was stored in a vamsas Document or has been retrieved from
   * it
   */
  protected boolean __stored_in_document = false;

  /**
   * true if Vobject was updated since the vamsas library last read a Vobj with
   * the same VorbaId from a document.
   */
  protected boolean __updated_since_last_read = false;

  /**
   * true if Vobject appeared in the document after the last access by this
   * vamsas library instance
   */
  protected boolean __added_since_last_read = false;

  /**
   * memory of the last doHash() value computed for the Vobject
   * 
   * @see doHash()
   */
  protected int __last_hash = 0;

  /**
   * set by testInstanceForIdField() if Vobject should have a VorbaId
   */
  protected boolean registerable = false;

  protected boolean __visited = false;

  /**
   * reference to containing object for this Vobject.
   */
  protected Vobject V_parent = null;

  /**
   * unique id for all vamsas objects allows unambiguous referencing to any
   * Vobject in the vamsas document
   */
  protected VorbaId vorbaId = null;

  /**
   * the source of unique VorbaIds.
   */
  protected IVorbaIdFactory __vorba = null;

  /*
   * (non-Javadoc)
   * 
   * @see java.lang.Object#finalize()
   */
  protected void finalize() throws Throwable {
    V_parent = null;
    __vorba = null;
    vorbaId = null;
    super.finalize();
  }

  /**
   * 
   */
  protected Vobject() {
    super();
    testInstanceForIdField();
  }

  /**
   * Override Object.hashCode with base value for castor generated object
   * hashcodes.
   */
  public int hashCode() {
    return 17;
  }

  java.lang.reflect.Field ___id_field = null; // set to ease pain of reflection

  /**
   * set the isRegisterable flag based on the presence of a 'private String _id'
   * field in the reflected class instance.
   */
  private void testInstanceForIdField() {
    // TODO: decide if 'id' is an appropriate reserved attribute name for the
    // VorbaId
    // look for the id field in all castor classes (should be an NCName string)

    Class thisclass = this.getClass();
    setRegisterable(false);
    while (!thisclass.equals(Vobject.class)) {
      try {
        java.lang.reflect.Field fd = thisclass.getDeclaredField("_id");
        if (String.class.isAssignableFrom(fd.getType())) {
          ___id_field = fd;
          this.setRegisterable(true);
          break;
        }
      } catch (SecurityException e) {
        log
            .error(
                "Unexpected Security Exception whilst finding id fields to set!",
                e);
      } catch (NoSuchFieldException e) {
        thisclass = thisclass.getSuperclass();
      }
    }
  }

  // boolean __testedInstance=false;
  /**
   * update the Vobject instance's _id field, based on the contents of the
   * VorbaId. Only call this if you mean to do it!
   */
  protected void setInstanceIdField() {
    /*
     * if (!registerable && !__testedInstance) { testInstanceForIdField();
     * __testedInstance=true; }
     */
    if (registerable) {
      if (__vorba != null)
        try {
          Method fd = this.getClass().getMethod("setId",
              new Class[] { String.class });
          fd.invoke((Object) this, new Object[] { new String(
              this.getVorbaId().id) });
          log.debug(this.getClass().getName() + " called setInstanceIdField!");
        } catch (InvocationTargetException e) {
          log
              .error(
                  "SourceGeneration of "
                      + this.getClass().toString()
                      + "\n has resulted in an inaccessible 'setId' method!\nCannot set ID from the vorbaId Vobject.",
                  e);
        } catch (IllegalAccessException e) {
          log
              .error(
                  "SourceGeneration of "
                      + this.getClass().toString()
                      + "\n has resulted in an inaccessible 'setId' method!\nCannot set ID from the vorbaId Vobject.",
                  e);
        } catch (SecurityException e) {
          log.error("Security access violation for "
              + this.getClass().toString(), e);
        } catch (NoSuchMethodException e) {
          log
              .warn(this.getClass().toString()
                  + " was erroneously marked as a Vorba Vobject class (Implementation error?)");
          this.setRegisterable(false);
        }
    } else {
      System.err.println("Client error. Trying to setInstanceIdField on a "
          + this.getClass().toString() + " (which cannot be given a vorbaId)");
    }
  }

  protected String __getInstanceIdField() {
    /*
     * if (!registerable && !__testedInstance) { testInstanceForIdField();
     * __testedInstance=true; }
     */
    if (registerable) {
      if (__vorba != null)
        try {
          Method fd = this.getClass().getMethod("getId", (Class[]) null);
          Object idstring = fd.invoke((Object) this, (Object[]) null);
          log.debug(this.getClass().getName() + " called getInstanceIdField!");
          if (idstring != null && idstring instanceof String) {
            if (((String) idstring).length() > 0)
              return (String) idstring;
          }
        } catch (InvocationTargetException e) {
          log
              .error(
                  "SourceGeneration of "
                      + this.getClass().toString()
                      + "\n has resulted in an inaccessible 'getId' method!\nCannot set ID from the vorbaId Vobject.",
                  e);
        } catch (IllegalAccessException e) {
          log
              .error(
                  "SourceGeneration of "
                      + this.getClass().toString()
                      + "\n has resulted in an inaccessible 'getId' method!\nCannot set ID from the vorbaId Vobject.",
                  e);
        } catch (SecurityException e) {
          log.error("Security access violation for "
              + this.getClass().toString(), e);
        } catch (NoSuchMethodException e) {
          log
              .warn(this.getClass().toString()
                  + " was erroneously marked as a Vorba Vobject class (Implementation error?)");
          this.setRegisterable(false);
        }
    } else {
      System.err.println("Client error. Trying to getInstanceIdField on a "
          + this.getClass().toString() + " (which cannot be given a vorbaId)");
    }
    return null;
  }

  /**
   * calls the castor-generated hashCode() method
   * 
   * @return
   */
  protected int __callHash() {
    try {
      Method fd = this.getClass().getMethod("hashCode", (Class[]) null);
      Object hashvalue = fd.invoke((Object) this, (Object[]) null);
      if (log.isDebugEnabled())
        log.debug(this.getClass().getName() + " called hashCode()!");
      if (hashvalue != null && hashvalue instanceof Integer) {
        return ((Integer) hashvalue).intValue();
      }
    } catch (InvocationTargetException e) {
      log
          .error(
              "SourceGeneration of "
                  + this.getClass().toString()
                  + "\n has resulted in an inaccessible 'hashCode' method!\nHave you set org.exolab.castor.builder.equalsmethod=true in castorbuilder.properties ?.",
              e);
    } catch (IllegalAccessException e) {
      log
          .error(
              "SourceGeneration of "
                  + this.getClass().toString()
                  + "\n has resulted in an inaccessible 'hashCode' method!\nHave you set org.exolab.castor.builder.equalsmethod=true in castorbuilder.properties ?.",
              e);
    } catch (SecurityException e) {
      log.error("Security access violation for " + this.getClass().toString(),
          e);
    } catch (NoSuchMethodException e) {
      log
          .warn(
              this.getClass().toString()
                  + " was erroneously extending from a Vorba Vobject class (Implementation error? no hashCode() method)"
                  + "\nHave you set org.exolab.castor.builder.equalsmethod=true in castorbuilder.properties ?.",
              e);
    }
    return 0;
  }

  /**
   * calculate a hash for the Vobject with all housekeeping fields at standard
   * values. (isRegisterable is an immutable attribute property) TODO: LATER:
   * make this hash function compute a hash that truly reflects changes in
   * Vobject attributes for benefit of update mechanism
   * 
   * @return true if new hash different to last hash (or first time its been
   *         computed)
   */
  synchronized protected boolean doHash() {
    boolean stored = __stored_in_document;
    __stored_in_document = false;
    boolean updated = __updated_since_last_read;
    __updated_since_last_read = false;
    boolean added_since = __added_since_last_read;
    __added_since_last_read = false;
    long __old_hash = __last_hash;
    __last_hash = 0;
    // leave registerable - doesn't change
    boolean visited = __visited;
    __visited = false;
    Vobject _V_parent = V_parent;
    V_parent = null;
    VorbaId thisid = vorbaId;
    vorbaId = null;
    IVorbaIdFactory factory = __vorba;
    __vorba = null;
    java.lang.reflect.Field idfield = ___id_field;
    ___id_field = null;
    long l_hash = __l_hash;
    __l_hash = 0;
    // compute hash
    __last_hash = __callHash();
    // reset houseskeeping variables
    ___id_field = idfield;
    vorbaId = thisid;
    __vorba = factory;
    __stored_in_document = stored;
    __updated_since_last_read = updated;
    V_parent = _V_parent;
    __visited = visited;
    __added_since_last_read = added_since;
    __l_hash = l_hash;
    // return true if first time hash was computed or if hash has changed
    return (__old_hash == 0) || (__old_hash != __last_hash);
  }

  /**
   * TODO: combine two versions of the same collection Vobject to resolve
   * asynchronous updates to the same vamsas Vobject Merges two vamsas objects,
   * one of which is a later version of the earlier (ie they have the same
   * vorbaId but one is a later version recently read from the vamsasDocument
   * collection.
   * 
   * @return
   */
  protected boolean merge(Vobject laterCopy) {
    log.warn(this.getClass().getName() + ".merge() not implemented.");
    return true;
  }

  /**
   * 
   * @return true if Vobject is registered
   */
  public boolean isRegistered() {
    return (registerable) ? (vorbaId != null) : false;
  }

  /**
   * Method to get fixed reference for the Vobject in the vamsas document.
   * 
   * @returns null if Vobject is neither registered or not associated with a
   *          properly instantiated VorbaIdFactory.
   */
  public VorbaId getVorbaId() {
    if (registerable && vorbaId == null) {
      if (this.__stored_in_document) {
        if (__vorba != null)
          vorbaId = uk.ac.vamsas.client.VorbaId.newId(this
              .__getInstanceIdField());
      }
      // Try to use the associated factory.
      if (__vorba != null)
        if ((vorbaId = __vorba.makeVorbaId(this)) == null)
          return null; // Factory not valid.
        else {
          this.setInstanceIdField();
          return vorbaId;
        }
    }
    return vorbaId;
  }

  /**
   * used by the IClient implementation to generate unique Id based on client
   * applications current namespace.
   */
  protected void setVorbaId(VorbaId newid) {
    vorbaId = newid;
  }

  /**
   * @return true if Vobject is present in Vamsas Document.
   */
  public boolean is__stored_in_document() {
    return __stored_in_document;
  }

  /**
   * @return true if this object has been updated in the currently stored
   *         document since the last time a Vobject with the same ID was read
   *         from a Vamsas Document
   */
  public boolean isUpdated() {
    return __updated_since_last_read;
  }

  /**
   * 
   * @return true if this object was added to the document after the last time
   *         the vamsas library acessed the session document
   */
  public boolean isNewInDocument() {
    return __added_since_last_read;
  }

  /**
   * Set internal flag to indicate this object was updated since the last
   * document read
   * 
   * @param __updated_since_last_read
   *          the __updated_since_last_read to set
   */
  protected void set__updated_since_last_read(boolean __updated_since_last_read) {
    this.__updated_since_last_read = __updated_since_last_read;
    if (__updated_since_last_read && log.isDebugEnabled())
      log.debug("Registered update for " + this.getVorbaId());
  }

  /**
   * for use by Vorba agent to reflect state of vamsas Vobject to client
   * application. Setting stored_in_document on a registerable Vobject without a
   * vorbaId will mean is will *never* get a vorbaId and horrible things will
   * happen.
   * 
   * @param __stored_in_document
   *          true if Vobject has been marshalled into current document.
   */
  protected void set__stored_in_document(boolean __stored_in_document) {
    this.__stored_in_document = __stored_in_document;
    if (__stored_in_document && log.isDebugEnabled())
      log.debug("Retrieved document object: " + this.getVorbaId());
  }

  /**
   * @param __added_since_last_read
   *          the __added_since_last_read to set
   */
  protected void set__added_since_last_read(boolean __added_since_last_read) {
    this.__added_since_last_read = __added_since_last_read;

    if (__added_since_last_read && log.isDebugEnabled())
      log.debug("New object in document: " + this.getVorbaId());
  }

  /**
   * __last_hash is the hash value computed when the Vobject was last checked
   * against a IClientDocument generated by the Vobject's parent IClient
   * instance.
   * 
   * @return Returns the __last_hash.
   */
  public long get__last_hash() {
    return __last_hash;
  }

  /**
   * @return true if Vobject can have a vorbaId
   */
  public boolean isRegisterable() {
    return registerable;
  }

  /**
   * Called by __testInstanceForidField and the post-unmarshalling handler to
   * indicate if Vobject will have a vorbaId.
   * 
   * @param registerable
   */
  protected void setRegisterable(boolean registerable) {
    this.registerable = registerable;
  }

  /**
   * ensure's internal id field corresponds to vorbaId and cascade through all
   * fields referring to an instance of Vobject calling the same method on them.
   * TODO: LATER: properly apply castors own field mechanisms to get at
   * accessors TODO: FIX CYCLIC __ensure+instance_ids Implementation note for
   * the todo: this works like a depth-first search over all vamsas objects in
   * an vamsasDocument. __visited is the visited flag, any Vobj who's flag is of
   * a different parity to the visited argument will be recursed on. note - the
   * doHash() function used to be used as the 'visited' flag - this *is not* a
   * valid heuristic, although it will work "most of the time". TODO: LATER? Add
   * another method for setDefaultProvenanceField (in the spirit of
   * setInstanceIdField) using the info from the __vorba.getClient/User/Session
   * methods
   */
  protected void __ensure_instance_ids() {
    __ensure_instance_ids(!__visited);
  }

  protected void __ensure_instance_ids(boolean visited) {
    if (__vorba == null)
      throw new Error(
          "Improperly intialised uk.ac.vamsas.client.Vobject - no VorbaFactory given.");
    log.debug("doing " + this.getClass() + ".__ensure_instance_ids()");
    if (!__stored_in_document && registerable)
      setInstanceIdField();
    if (__visited == visited)
      return;
    __visited = visited;
    // __vorba.updateHashValue(this);

    Class descriptor = null;
    XMLClassDescriptorImpl descimpl = null;
    try {
      // castor descriptor resolver magic
      StringBuffer desname = new StringBuffer(this.getClass().getName());
      desname.insert(desname.lastIndexOf("."), ".descriptors");
      desname.append("Descriptor");
      descriptor = this.getClass().getClassLoader().loadClass(
          desname.toString());
      descimpl = (XMLClassDescriptorImpl) descriptor.getConstructor(
          (Class[]) null).newInstance((Object[]) null);
    } catch (Exception e) {
      log.fatal("Source Generation Error!: Couldn't resolve descriptor for "
          + this.getClass().getName()
          + " was 'generate descriptors' set for castorbuilder.properties?");
      return;
    }
    FieldDescriptor fields[] = descimpl.getFields();
    for (int i = 0, j = fields.length; i < j; i++) {
      Class type = fields[i].getFieldType();
      if (type.isArray()) {
        if (Vobject[].class.isAssignableFrom(type)) {
          try {
            Object val = fields[i].getHandler().getValue(this);
            if (val != null) {
              Vobject vals[] = (Vobject[]) val;
              for (int k = 0, l = vals.length; k < l; k++) {
                if (vals[k].__vorba == null)
                  vals[k].__vorba = __vorba; // propagate IVorbaIdFactory
                if (vals[k].V_parent == null)
                  vals[k].V_parent = this; // propagate parent reference to this
                                           // element.
                vals[k].__ensure_instance_ids(visited);
              }
            }
          } catch (Exception e) {
            log.error("Client error - could not access array " + type.getName()
                + " in " + this.getClass().getName(), e);
          }
        }
      } else if (Vobject.class.isAssignableFrom(type)) {
        try {
          FieldHandler fh = fields[i].getHandler();
          Vobject rf = null;
          if (fh != null) {
            Object fval = fh.getValue(this);
            if (fval != null) {
              if (fval.getClass().isArray()) {
                // if (Vobject[].class.isAssignableFrom(type)) {
                try {
                  Vobject vals[] = (Vobject[]) fval;
                  for (int k = 0, l = vals.length; k < l; k++) {
                    if (vals[k].__vorba == null)
                      vals[k].__vorba = __vorba; // propagate IVorbaIdFactory
                    if (vals[k].V_parent == null)
                      vals[k].V_parent = this; // propagate parent reference to
                                               // this field object
                    vals[k].__ensure_instance_ids(visited);
                  }
                } catch (Exception e) {
                  log.error("Client error - could not access (fhval)array "
                      + type.getName() + " in " + this.getClass().getName(), e);
                }
                // }
              } else {
                rf = (Vobject) fval;
                log.debug("Got value for " + fields[i].getFieldName());
              }
            }
          } else {
            // castor's mechanism doesn't work for this object... so...*/
            // fuck around, fuck around, jump up jump up and get down! */
            Object o = fields[i].getClassDescriptor();
            if (o != null) {
              // XMLClassDescriptorImpl fclasdes = (XMLClassDescriptorImpl) o;
              String methname = "get" + fields[i].getFieldName();
              Method fgetmeth = this.getClass().getMethod(methname,
                  (Class[]) null);
              if (fgetmeth != null) {
                Object fval = fgetmeth.invoke(this, (Object[]) null);
                if (fval != null)
                  rf = (Vobject) fval;
              } else {
                log.warn("Couldn't find " + this.getClass().getName() + "."
                    + methname);
              }
            }
          }
          if (rf != null) {
            if (rf.__vorba == null)
              rf.__vorba = __vorba; // propagate IVorbaIdFactory
            if (rf.V_parent == null)
              rf.V_parent = this; // propagate parent reference
            rf.__ensure_instance_ids(visited);
          }
        } catch (Exception e) {
          log.error("Client error - could not access " + type.getName()
              + " in " + this.getClass().getName(), e);
        }
      }
    }

  }

  /**
   * @return the __parent
   */
  public Vobject getV_parent() {
    return V_parent;
  }

  /**
   * @param __parent
   *          the __parent to set
   */
  protected void setV_parent(Vobject V_parent) {
    this.V_parent = V_parent;
  }

  /**
   * LhashValue - used for change detection between document updates.
   */
  private long __l_hash = 0;

  /**
   * set the base LhashValue for this object
   * 
   * @param checksum
   */
  protected void __setInitHash(long checksum) {
    __l_hash = checksum;
  }

  /**
   * compute the final LhashValue as a difference between checksum and the
   * current base
   * 
   * @param checksum
   */
  protected void __setFinalHash(long checksum) {
    __l_hash = checksum - __l_hash;
  }

  /**
   * get the LhashValue for this object
   * 
   * @return the difference in values passed to __setFinalHash less
   *         __setInitHash
   */
  protected long __getLHash() {
    return __l_hash;
  }
}
