/*
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 * Created on March 25 2003
 */
package org.jboss.cache;


import org.jboss.cache.lock.IdentityLock;
import org.jboss.cache.lock.LockingException;
import org.jboss.cache.lock.TimeoutException;
import org.jboss.logging.Logger;

import java.io.*;
import java.util.*;

/**
 * Represents a node in the tree. Has a relative name and a Fqn. Maintains a hashmap.
 * If the node is created in a replicated cache, the relative and fully qualified name,
 * and the keys and values of the hashmap have to be serializable.
 * <p>Note that current version supports different levels of transaction locking such
 * as simple locking (<CODE>TreeCache.SERIALIZABLE</code>, or R/W lock with upgrade (
 * <code>REPEATABLE_READ</code>) --that is the read lock
 * will be automatically upgraded to write lock when the same owner intends to modify
 * the data after read.</p>
 * <br/>
 * @author Bela Ban March 25 2003
 * @author Ben Wang
 * @version $Revision: 1.24.2.6 $
 */
public class Node implements Externalizable {

   /**
    * Relative name of the node, e.g. "c"
    */
   Object name=null;

   /**
    * Fully qualified name (e.g. "/a/b/c")
    */
   Fqn fqn=null;

   /**
    * Parent node. For navigational purposes
    */
   Node parent=null;

   /**
    * Children. Keys: child name (Object), value: Node
    */
   Map children=null;

   /**
    * Data for the current node. Keys: Object, value: Object
    */
   Map data=null;


   /**
    * Lock manager that manages locks to be acquired when accessing
    * the node inside a transaction. Lazy loading just in case locking is not needed
    */
   transient IdentityLock lock_=null;

   /**
    * Declare locking type with the intention to read or write data.
    */
   public static final int LOCK_TYPE_READ=1;
   public static final int LOCK_TYPE_WRITE=2;

   static transient Logger log=Logger.getLogger(Node.class);

   protected static final int INDENT=4;

   public Node() {
      lock_ = new IdentityLock();
      if(data == null) data= new HashMap();
   }

   public Node(Object child_name, Fqn fqn, Node parent, Map data) {
      init(child_name, fqn, parent);
      if(data != null) {
         this.data.putAll(data);
      }
   }

   public Node(Object child_name, Fqn fqn, Node parent, Object key, Object value) {
      init(child_name, fqn, parent);
      data.put(key, value);
   }

   protected void init(Object child_name, Fqn fqn, Node parent) {
      lock_ = new IdentityLock();
      name=child_name;
      this.fqn=fqn;
      this.parent=parent;
      if(data == null) data= new HashMap();
   }

   public Object getName() {
      return name;
   }

   public Fqn getFqn() {
      return fqn;
   }

   public Object get(Object key) {
      return data != null ? data.get(key) : null;
   }

   public boolean containsKey(Object key) {
      return data != null? data.containsKey(key) : false;
   }

   public Node getChild(Object child_name) {
      return child_name == null ? null : children == null ? null : (Node)children.get(child_name);
   }

   public Node getParent() {
      return parent;
   }

   public Set getDataKeys() {
      return data != null ? data.keySet() : null;
   }


   public boolean childExists(Object child_name) {
      if(child_name == null) return false;
      return children != null ? children.containsKey(child_name) : false;
   }

   boolean isReadLocked() {
      return lock_ != null ? lock_.isReadLocked() : false;
   }

   boolean isWriteLocked() {
      return lock_ != null ? lock_.isWriteLocked() : false;
   }

   boolean isLocked() {
      return isWriteLocked() || isReadLocked();
   }

   public IdentityLock getImmutableLock() {
      return lock_;
   }

   /** @return Map<String,Node> */
   public Map getChildren() {
      return children;
   }

   public Map getData() {
      if(data == null) return null;
      Map tmp=new HashMap(data);
      return tmp;
   }

   public boolean hasChild() {
      if( children == null || children.size() == 0 ) return false;
      return true;
   }

   public void put(Map data, boolean erase) {
      if(erase && this.data != null)
         this.data.clear();
      if(data == null) return;
      if(this.data == null)
         this.data= new HashMap();
      this.data.putAll(data);
   }

   public void put(Map data) {
      put(data, false);
   }

//   protected void putInternal(HashMap data) {
//      this.data=data;
//   }

   public Object put(Object key, Object value) {
      if(this.data == null)
         this.data=new HashMap();
      return this.data.put(key, value);
   }

   public Node createChild(Object child_name, Fqn fqn, Node parent) {
      Node child=null;

      if(child_name == null) return null;
      if(children == null) children= new HashMap();
      child=(Node)children.get(child_name);
      if(child == null) {
         child=new Node(child_name, fqn, parent, null);
         children.put(child_name, child);
      }
      if(log.isDebugEnabled())
         log.debug("createChild: fqn=" + fqn);
      return child;
   }

   public Node createChild(Object child_name, Fqn fqn, Node parent, Map data) {
      Node child=null;

      if(child_name == null) return null;
      if(children == null) children= new HashMap();
      child=(Node)children.get(child_name);
      if(child != null)
         child.put(data);
      else {
         child=new Node(child_name, fqn, parent, data);
         children.put(child_name, child);
      }
      if(log.isDebugEnabled())
         log.debug("createChild: fqn=" + fqn);
      return child;
   }

   public Node createChild(Object child_name, Fqn fqn, Node parent, Object key, Object value) {
      Node child=null;

      if(child_name == null) return null;
      if(children == null) children= new HashMap();
      child=(Node)children.get(child_name);
      if(child != null)
         child.put(key, value);
      else {
         child=new Node(child_name, fqn, parent, key, value);
         children.put(child_name, child);
      }
      if(log.isDebugEnabled())
         log.debug("createChild: fqn=" + fqn);
      return child;
   }

   /**
    * Adds the (already created) child node. Replaces existing node if present.
    *
    * @param child_name
    * @param n
    */
   void addChild(Object child_name, Node n) {
      if(child_name == null)
         return;
      children.put(child_name, n);
   }

   public Object remove(Object key) {
      return data != null ? data.remove(key) : null;
   }

   public void clear() {
      if(data != null)
         data.clear();
      data=null;
   }

   public void removeChild(Object child_name) {
      if(children != null)
         children.remove(child_name);
   }

   public void removeAllChildren() {
      if(children != null) {
         children.clear();
         children=null;
      }
   }

   public void print(StringBuffer sb, int indent) {
      printIndent(sb, indent);
      sb.append(TreeCache.SEPARATOR).append(name);
      if(children != null && children.size() > 0) {
         Collection values=children.values();
         for(Iterator it=values.iterator(); it.hasNext();) {
            sb.append("\n");
            ((Node)it.next()).print(sb, indent + INDENT);
         }
      }
   }

   public void printDetails(StringBuffer sb, int indent) {
      Map.Entry entry;
      printIndent(sb, indent);
      sb.append(TreeCache.SEPARATOR).append(name);
      if(data != null) {
         sb.append("\n");
         for(Iterator it=data.entrySet().iterator(); it.hasNext();) {
            entry=(Map.Entry)it.next();
            sb.append(entry.getKey()).append(": ").append(entry.getValue()).append("\n");
         }
      }
      if(children != null && children.size() > 0) {
         Collection values=children.values();
         for(Iterator it=values.iterator(); it.hasNext();) {
            sb.append("\n");
            ((Node)it.next()).printDetails(sb, indent);
         }
      }
   }

   public void printLockInfo(StringBuffer sb, int indent) {
      boolean locked=lock_ != null ? lock_.isLocked() : false;

      printIndent(sb, indent);
      sb.append(TreeCache.SEPARATOR).append(name);
      if(locked) {
         sb.append("\t(");
         lock_.toString(sb);
         sb.append(")");
      }

      if(children != null && children.size() > 0) {
         Collection values=children.values();
         for(Iterator it=values.iterator(); it.hasNext();) {
            sb.append("\n");
            ((Node)it.next()).printLockInfo(sb, indent + INDENT);
         }
      }
   }

   public void printIndent(StringBuffer sb, int indent) {
      if(sb != null) {
         for(int i=0; i < indent; i++)
            sb.append(" ");
      }
   }

   public String toString() {
      StringBuffer sb=new StringBuffer();
      if(name != null) sb.append("\nname=" + name);
      if(fqn != null) sb.append("\nfqn=" + fqn);
      if(data != null) {
         sb.append("\ndata=" + data);
      }
      if(lock_ != null) {
         sb.append("\n read locked=").append(isReadLocked());
         sb.append("\n write locked=").append(isWriteLocked());
//            sb.append(" (num permits=").append(getNumPermits()).append(")");
      }
      return sb.toString();
   }


   public Object clone() throws CloneNotSupportedException {
      Node n=new Node(name, fqn != null? (Fqn)fqn.clone() : null, parent != null ? (Node)parent.clone() : null, data);
      if(children != null) n.children=(HashMap)((HashMap)children).clone();
      return n;
   }

   /**
    * Acquire a node object by locking it first. The lock type is specified via
    * <code>lock_type</code>.
    *
    * @param owner     Tansaction context owner.
    * @param timeout   Timeout in milliseconds
    * @param lock_type @see Node
    * @throws LockingException
    * @throws TimeoutException
    */
   public void acquire(Object owner, long timeout, int lock_type) throws LockingException, TimeoutException {
      // Note that we rely on IdentityLock for synchronization
      try {
         if(lock_type == LOCK_TYPE_READ)
            acquireReadLock(owner, timeout);
         else
            acquireWriteLock(owner, timeout);
      }
      catch(LockingException e) {
         if(log.isDebugEnabled())
            log.debug("failure acquiring lock: fqn=" + fqn + ", owner=" + owner + ", lock=" + lock_.toString());
         throw e;
      }
      catch(TimeoutException e) {
         if(log.isDebugEnabled())
            log.debug("failure acquiring lock: fqn=" + fqn + ", owner=" + owner + ", lock=" + lock_.toString());
         throw e;
      }
   }

   protected void acquireReadLock(Object owner, long timeout) throws LockingException, TimeoutException {
      lock_.acquireReadLock(owner, timeout);
      if(log.isDebugEnabled())
         log.debug("acquired RL: fqn=" + fqn + ", owner=" + owner + ", lock=" + lock_.toString());
   }

   protected void acquireWriteLock(Object owner, long timeout)
           throws LockingException, TimeoutException {
      lock_.acquireWriteLock(owner, timeout);
      if(log.isDebugEnabled())
         log.debug("acquired WL: fqn=" + fqn + ", owner=" + owner + ", lock=" + lock_.toString());
   }

   /**
    * Acquires locks for the entire subtree
    */
   protected void acquireAll(Object owner, long timeout, int lock_type) throws LockingException, TimeoutException {
      Node tmp;
      acquire(owner, timeout, lock_type);
      if(children != null) {
         for(Iterator it=children.values().iterator(); it.hasNext();) {
            tmp=(Node)it.next();
            tmp.acquireAll(owner, timeout, lock_type);
         }
      }
   }

   protected void release(Object owner) {
      lock_.release(owner);
   }

   protected void releaseForce() {
      lock_.releaseForce();
   }

   /**
    * Releases locks for the entire subtree
    */
   protected void releaseAll(Object owner) {
      Node tmp;
      if(children != null) {
         for(Iterator it=children.values().iterator(); it.hasNext();) {
            tmp=(Node)it.next();
            tmp.releaseAll(owner);
         }
      }
      release(owner);
   }

   /**
    * Releases locks for the entire subtree. Forces release regardless of owner
    */
   protected void releaseAllForce() {
      Node tmp;
      if(children != null) {
         for(Iterator it=children.values().iterator(); it.hasNext();) {
            tmp=(Node)it.next();
            tmp.releaseAllForce();
         }
      }
      releaseForce();
   }


   public void writeExternal(ObjectOutput out) throws IOException {
      out.writeObject(name);
      out.writeObject(fqn);
      out.writeObject(parent);
      out.writeObject(children);
      out.writeObject(data);
   }

   public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
      name=in.readObject();
      fqn=(Fqn)in.readObject();
      parent=(Node)in.readObject();
      children=(Map)in.readObject();
      data=(Map)in.readObject();
   }


}

