/* ======================================================================
The Bodington System Software License, Version 1.0
  
Copyright (c) 2001 The University of Leeds.  All rights reserved.
  
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

1.  Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.

2.  Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

3.  The end-user documentation included with the redistribution, if any,
must include the following acknowledgement:  "This product includes
software developed by the University of Leeds
(http://www.bodington.org/)."  Alternately, this acknowledgement may
appear in the software itself, if and wherever such third-party
acknowledgements normally appear.

4.  The names "Bodington", "Nathan Bodington", "Bodington System",
"Bodington Open Source Project", and "The University of Leeds" must not be
used to endorse or promote products derived from this software without
prior written permission. For written permission, please contact
d.gardner@leeds.ac.uk.

5.  The name "Bodington" may not appear in the name of products derived
from this software without prior written permission of the University of
Leeds.

THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO,  TITLE,  THE IMPLIED WARRANTIES 
OF QUALITY  AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO 
EVENT SHALL THE UNIVERSITY OF LEEDS OR ITS CONTRIBUTORS BE LIABLE FOR 
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
POSSIBILITY OF SUCH DAMAGE.
=========================================================

This software was originally created by the University of Leeds and may contain voluntary 
contributions from others.  For more information on the Bodington Open Source Project, please 
see http://bodington.org/

====================================================================== */

package org.bodington.server.resources;

import java.util.logging.*;



import org.bodington.database.*;
import org.bodington.pool.*;
import org.bodington.server.*;
import org.bodington.server.realm.*;
import java.sql.*;
import java.util.*;

/**
 * Maintains a tree structure for a set of resources.  May be subject
 * to a rethink at some stage.
 * 
 * @author Jon Maber
 * @version 1.0
 */
public class ResourceTree extends Object
    {
    private static ResourceTree the_instance;
    
    
    PrimaryKey root_id;   //, key;
    //Hashtable resources;
    
    int visit;


	public static synchronized ResourceTree getInstance()
        throws BuildingServerException
		{
		if ( the_instance==null )
			{
			the_instance=new ResourceTree();
			the_instance.loadResources();
			}
		return the_instance;
		}

    public Resource findResource( String delimited_name )
	throws BuildingServerException
	{
	if ( root_id == null || delimited_name == null ) return null;

	StringTokenizer tok  = new StringTokenizer( delimited_name, "/" );
	String[] names = new String[tok.countTokens()];
	for ( int i=0; tok.hasMoreTokens(); i++ )
	    names[i] = tok.nextToken();
	
	return findResource( names );
	}
	
    public Resource findResource( String[] names )
	throws BuildingServerException
	{
	if ( root_id == null || names == null ) return null;

	int i;
	boolean found;
	Resource child;
	Resource current = Resource.findResource( root_id );
	String name;
	Enumeration children;
	for ( i=0; i<names.length; i++ )
	    {
	    name = (String)names[i];
	    children = current.findChildren();
	    found=false;
	    child=null;
	    while ( children.hasMoreElements() )
		{
		child = (Resource)children.nextElement();
		if ( name.equals( child.getName() ) )
		    {
		    found=true;
		    break;
		    }
		}
	    if ( !found )
		return null;
	    current = child;
	    }
	
	return current;
	}

    /**
     * Used to put the correct left/right indexes into
     * a resource, also calls recursively children of 
     * resource.
     * 
     * @param parent The starting resource.
     * @exception org.bodington.server.BuildingServerException If data base error.
     */
    private synchronized void updateChildren( Resource parent, PreparedStatement l, PreparedStatement r )
        throws SQLException, BuildingServerException
        {
        l.setInt( 1, visit++ );
        l.setInt( 2, parent.getResourceId().intValue() );
        l.executeUpdate();
        Enumeration enum=parent.findChildren();
        Resource child;
        while ( enum.hasMoreElements() )
            {
            child=(Resource)enum.nextElement();
            updateChildren( child, l, r );
            }
        r.setInt( 1, visit++ );
        r.setInt( 2, parent.getResourceId().intValue() );
        r.executeUpdate();
        }
        
    /**
     * Updates left/right index of all resources.
     * 
     * @exception org.bodington.server.BuildingServerException If data base error.
     */
    private synchronized void updateIndices()
        throws BuildingServerException
        {
        if ( root_id==null ) return;
        visit=1;
        Resource root = Resource.findResource( root_id );
        if ( root==null ) return;

        try
        	{
        	Connection connection = BuildingContext.getContext().getConnection();
        	Statement statement = connection.createStatement();
        	ResultSet results = statement.executeQuery( "SELECT min(left_index), max(left_index) FROM resources" );
        	results.next();
        	int lmin = results.getInt( 1 );
        	int lmax = results.getInt( 2 );
        	results.close();
        	results = statement.executeQuery( "SELECT min(right_index), max(right_index) FROM resources" );
        	results.next();
        	int rmin = results.getInt( 1 );
        	int rmax = results.getInt( 2 );
        	results.close();

			//get the badly indexed entries out of the way of the others
			/*
			if ( lmin<0 )
				{
	        	statement.executeUpdate( "UPDATE resources SET left_index = left_index - " + rmax + " WHERE left_index<0" );
        		}
        	
			if ( rmin<0 )
				{
	        	statement.executeUpdate( "UPDATE resources SET right_index = right_index - " + rmax + " WHERE right_index<0" );
        		}
        	*/
        	
        	//get all the indices out the range of the final values.
        	statement.executeUpdate( "UPDATE resources SET left_index = left_index  - " + (rmax+1) );
        	statement.executeUpdate( "UPDATE resources SET right_index = right_index - " + (rmax+1) );
        	statement.close();
        	
        	// prepare statements for updating individual entries...
        	PreparedStatement l = connection.prepareStatement( "UPDATE resources SET left_index = ? WHERE resource_id = ?" );
        	PreparedStatement r = connection.prepareStatement( "UPDATE resources SET right_index = ? WHERE resource_id = ?" );
        	
        	// enter recursive method
        	updateChildren( root, l, r );
        	l.close();
        	r.close();
        	}
		catch ( ObjectPoolException opex )
			{
			throw new BuildingServerException( opex );
			}
        catch ( SQLException sqlex )
        	{
        	throw new BuildingServerException( "Database error reindexing resource: " + sqlex );
        	}
        }
    
    
    /**
     * Loads all the resources and indexes them.
     * 
     * @exception org.bodington.server.BuildingServerException If data base error.
     */
    public synchronized void loadResources()
        throws BuildingServerException
        {
        int i, count, min, max, lsum, rsum;
        
        //load the root
        Resource root = Resource.findResource( "parent_resource_id IS NULL" );
        if ( root == null )
        	root_id = null;
        else
        	root_id = root.getResourceId();
        
		//are there signs that others need reindexing?
		try
			{
			Connection connection = BuildingContext.getContext().getConnection();
			Statement statement = connection.createStatement();
			ResultSet results= statement.executeQuery( 
					"SELECT Count(*), Min(left_index), Max(right_index), Sum(left_index), Sum(right_index) FROM resources" );
			if ( !results.next() )
				throw new BuildingServerException( "Problem with resource indexing." );
				
			count = results.getInt( 1 );
			min   = results.getInt( 2 );
			max   = results.getInt( 3 );
			lsum  = results.getInt( 4 );
			rsum  = results.getInt( 5 );
			results.close();
			statement.close();
			}
		catch ( ObjectPoolException opex )
			{
			throw new BuildingServerException( opex );
			}
		catch ( SQLException sqlex )
			{
			throw new BuildingServerException( "Database problem assessing resource indices." );
			}
			
		if ( count>0 && (min != 1 || max != count*2 || (lsum+rsum) != (count*((2*count)+1)) )  )
			{
	    	Logger.getLogger( "org.bodington" ).warning( "Resources need reindexing." );
	        updateIndices();
	        }
	    else
	    	Logger.getLogger( "org.bodington" ).info( "No need to reindex resources." );
        }
            
        
    /**
     * Takes a resource (or whole branch of tree) and moves
     * it to a new place.
     * 
     * @param newparent The new location.
     * @param resource The resource to move.
     * @exception org.bodington.server.BuildingServerException If data base error.
     */
    public synchronized void moveResource( Resource newparent, Resource resource )
        throws BuildingServerException
        {
        Connection connection=null;
        int end, d_left, d_right, new_left, new_right, left, right, s_left, s_right, width, shift;
        String op;
	        
        if ( resource==null || resource.getResourceId() == null )
            throw new BuildingServerException( "Error moving resource in table, resource doesn't exist." );
        if ( newparent==null || newparent.getResourceId() == null )
            throw new BuildingServerException( "Error moving resource in table, destination doesn't exist." );

        Resource parent = resource.getParent();
        if ( parent == null )
            throw new BuildingServerException( "Error moving resource in table, can't move root resource." );

        try
        	{
	       	connection = BuildingContext.getContext().getConnection();
        	Statement statement = connection.createStatement();
        	ResultSet results;

			results = statement.executeQuery( 
				"SELECT left_index, right_index FROM resources WHERE resource_id = " + 
				resource.getResourceId() );
			if ( !results.next() )
				throw new BuildingServerException( "Unable to create resource - unable to find current index." );
			left = results.getInt( 1 );
			right = results.getInt( 2 );
			results.close();

			results = statement.executeQuery( 
				"SELECT left_index, right_index FROM resources WHERE resource_id = " + 
				newparent.getResourceId() );
			if ( !results.next() )
				throw new BuildingServerException( "Unable to create resource - unable to index destination." );
			d_left = results.getInt( 1 );
			d_right = results.getInt( 2 );
			new_left = d_right;
			new_right = new_left+(right-left);
			results.close();


        	if ( d_left >= left && d_right <= right )
            	throw new BuildingServerException( "Error moving resource in table, the specified destination is inside the resource to be moved." );

			width = right - left + 1;
			if ( d_right>right )
				{
				shift = (d_right-1) - right;
				s_left = right+1;
				s_right = d_right-1;
				op = "-";
				}
			else
				{
				shift = d_right - left;
				s_left = d_right;
				s_right = left-1;
				op = "+";
				}

			connection.setAutoCommit( false );
			
			//move the "moving" resources outside of main range of indices (by negating)
			statement.executeUpdate( 
				"UPDATE resources SET left_index = -left_index WHERE left_index BETWEEN " + left + " AND " + right );
			statement.executeUpdate( 
				"UPDATE resources SET right_index = -right_index WHERE right_index BETWEEN " + left + " AND " + right );

				
			//shift indices to close gap made by moving resource
			statement.executeUpdate( 
				"UPDATE resources SET left_index = left_index " + op + " (" + width + 
				") WHERE left_index BETWEEN " + s_left + " AND " + s_right );
			statement.executeUpdate( 
				"UPDATE resources SET right_index = right_index " + op + " (" + width + 
				") WHERE right_index BETWEEN " + s_left + " AND " + s_right );
			
			//move the "moving" resources into new gap
			statement.executeUpdate( 
				"UPDATE resources SET left_index = -left_index + (" + shift + 
				") WHERE left_index < 0" );
			statement.executeUpdate( 
				"UPDATE resources SET right_index = -right_index + (" + shift + 
				") WHERE right_index < 0" );

			statement.close();
			
			// if all that worked update reference to parent in child
        	resource.setParentResourceId( newparent.getResourceId() );
        	resource.save();

			connection.commit();
			connection.setAutoCommit( false );

			}
		catch ( ObjectPoolException opex )
			{
			throw new BuildingServerException( opex );
			}
		catch ( SQLException sqlex )
			{
			try
				{
				connection.rollback();
				}
			catch ( SQLException sqlex2 )
				{
				}
				
			throw new BuildingServerException( "Unable to move resource - database error: " + sqlex );
			}

		//update linked lists to children
        parent.removeChildResourceId( resource.getResourceId() );
        newparent.addChildResourceId( resource.getResourceId() );

        return;
        }

    /**
     * Sorts sibling resources.
     * 
     * @param newparent The new location.
     * @param resource The resource to move.
     * @exception org.bodington.server.BuildingServerException If data base error.
     */
    public synchronized void sortResources( Resource parent, Vector siblings )
        throws BuildingServerException
        {
        Resource current;
        PrimaryKey key;
        Object thing;
       	Connection connection = null;
        int end, new_left[], new_right[], left[], right[], distance, width, i, j, target_left;
        int internal_left, internal_right;
        
        String op;
	        
        if ( parent==null || parent.getResourceId() == null )
            throw new BuildingServerException( "Error sorting resources, containing resource doesn't exist." );
            
        if ( siblings == null || siblings.size()==0 )
            throw new BuildingServerException( "Error sorting resources, no resources to sort." );

        if ( siblings.size() != parent.getChildIds().size() )
            throw new BuildingServerException( "Error sorting resources, not all resources listed." );
        	
            
        try
        	{
	       	connection = BuildingContext.getContext().getConnection();
        	Statement statement = connection.createStatement();
        	ResultSet results;

        	Enumeration enum = parent.findChildren();
        	while ( enum.hasMoreElements() )
        		{
        		current = (Resource)enum.nextElement();
        		if ( !siblings.contains( current.getResourceId() ) )
	            	throw new BuildingServerException( "Error sorting resources, sort list omitted a resource." );
        		}

			results = statement.executeQuery( 
				"SELECT left_index, right_index FROM resources WHERE resource_id = " + 
				parent.getResourceId() );
			if ( !results.next() )
				throw new BuildingServerException( "Unable to create resource - unable to find parent index." );
			internal_left  = results.getInt( 1 )+1;
			internal_right = results.getInt( 2 )-1;
			results.close();
	            
        	left = new int[siblings.size()];
        	right = new int[siblings.size()];
        	new_left = new int[siblings.size()];
        	new_right = new int[siblings.size()];
        	for ( i=0; i< siblings.size(); i++ )
        		{
        		thing = siblings.elementAt( i );
        		if ( !(thing instanceof PrimaryKey) )
        			throw new BuildingServerException( "Invalid paramater to sortResources function" );
        		key = (PrimaryKey)thing;
        		if ( key == null )
	            	throw new BuildingServerException( "Error sorting resources, null resource in list." );
        		if ( !parent.getChildIds().contains( key ) )
        			throw new BuildingServerException( "Attempt to sort resource that is not contained in this resource." );
		            
		        
				results = statement.executeQuery( 
					"SELECT left_index, right_index FROM resources WHERE resource_id = " + 
					siblings.elementAt(i) );
				if ( !results.next() )
					throw new BuildingServerException( "Unable to create resource - unable to find current index." );
				left[i] = results.getInt( 1 );
				right[i] = results.getInt( 2 );
				new_left[i]=left[i];
				new_right[i]=right[i];
				results.close();
        		}


			for ( i=0; i<siblings.size(); i++ )
				{
				// adjust the left and right values in the i'th slot and shift others to
				// accomadate the move.
				
				// i'th entry left index must be (i-1)'th entry right index plus one.
				if ( i==0 )
					target_left = internal_left;
				else
					target_left = new_right[i-1]+1;
					
				//how far must it move?					
				distance = new_left[i] - target_left;
				//how much to intervening entries have to move?
				width = new_right[i]-new_left[i]+1;
				
				// shift any values that lie between i'th entry old and new indices
				for ( j=i+1; j<siblings.size(); j++ )
					{
					if ( new_right[j]<new_left[i] )
						{
						new_left[j]+=width;
						new_right[j]+=width;
						}
					}
				// shift i'th entry down
				new_left[i]  -= distance;
				new_right[i] -= distance;
				}
			
			for ( i=0; i<siblings.size(); i++ )
				{
				Logger.getLogger( "org.bodington" ).fine( siblings.elementAt( i ).toString() + 
									"\tleft=" + left[i] + 
									"\tright=" + right[i] +
									"\tchange to left=" + new_left[i] + 
									"\tright=" + new_right[i]				);
				}
				
			//now save changes

			connection.setAutoCommit( false );
			
			//move the "moving" resources outside of main range of indices (by negating)
			statement.executeUpdate( 
				"UPDATE resources SET left_index = -left_index WHERE left_index BETWEEN " + internal_left + " AND " + internal_right );
			statement.executeUpdate( 
				"UPDATE resources SET right_index = -right_index WHERE right_index BETWEEN " + internal_left + " AND " + internal_right );

			statement.close();


			PreparedStatement pstatementl = connection.prepareStatement(
					"UPDATE resources SET left_index = -left_index + (?) WHERE left_index BETWEEN ? AND ?" );
			PreparedStatement pstatementr = connection.prepareStatement(
					"UPDATE resources SET right_index = -right_index + (?) WHERE right_index BETWEEN ? AND ?" );

			for ( i=0; i<siblings.size(); i++ )
				{
				distance = new_left[i] - left[i];
				//move the "moving" resources into new gap
				pstatementl.setInt( 1, distance );
				pstatementl.setInt( 2, -right[i] );
				pstatementl.setInt( 3, -left[i] );
				pstatementl.executeUpdate();

				pstatementr.setInt( 1, distance );
				pstatementr.setInt( 2, -right[i] );
				pstatementr.setInt( 3, -left[i] );
				pstatementr.executeUpdate();
				}

			pstatementl.close();
			pstatementr.close();

       		//now change reference to Vector
        	parent.setChildIds( siblings );

			//commit all the database changes
			connection.commit();
			connection.setAutoCommit( false );

			}
		catch ( ObjectPoolException opex )
			{
			throw new BuildingServerException( opex );
			}
		catch ( SQLException sqlex )
			{
			try
				{
				connection.rollback();
				}
			catch ( SQLException sqlex2 )
				{
				}
				
			throw new BuildingServerException( "Unable to move resource - database error: " + sqlex );
			}

        return;
        }

    private synchronized void addAclToResource( Resource newresource )
        {
        Acl newacl;
        AclEntry newaclentry;
        try
            {
            User user = (User)BuildingContext.getContext().getUser();
            
            //this constructor does a lot of setting up and saves
            //the resource, the acl and owners group.
            newacl = new Acl( user, newresource );
		    }
		catch ( Exception ex )
		    {
		    Logger.getLogger( "org.bodington" ).logp( 
			Level.SEVERE, 
			"ResourceTree", 
			"addAclToResource", 
			ex.getMessage(), 
			ex );
    		newresource.setAclId( null );
		    }
        }

        	
        	
    /**
     * Adds a newly instantiated resource to the tree.
     * The methods will also set up an ACL for the resource
     * too.
     * 
     * @param parent The location of the new resource.
     * @param newresource The (unsaved) resource to add in.
     * @exception org.bodington.server.BuildingServerException If data base error.
     */
    public synchronized void addResource( Resource parent, Resource newresource )
        throws BuildingServerException
        {
        try
        	{
        	Connection connection = BuildingContext.getContext().getConnection();
        	Statement statement = connection.createStatement();
        	ResultSet results;
        	int left, right;
	        
        	int end;

        	if ( newresource == null )
            	throw new BuildingServerException( "Error adding resource to table, null resource can't be added." );
        	if ( newresource.getResourceId() != null )
            	throw new BuildingServerException( "Error adding resource to table, not a new resource - can only add new resources." );
        	if ( newresource.getParentResourceId() != null )
            	throw new BuildingServerException( "Error adding resource to table, resource already placed elsewhere." );

	        
        	if ( parent!=null && parent.getResourceId() == null )
                throw new BuildingServerException( "Error adding resource to table, unknown/unsaved destination." );

        	addAclToResource( newresource );

        	if ( parent==null )
            	{
            	root_id=newresource.getResourceId();
            	newresource.setParentResourceId( null );
        		left = 0;
        		right = 1;
            	}
	        else
            	{
				results = statement.executeQuery( 
					"SELECT right_index FROM resources WHERE resource_id = " + 
					parent.getResourceId() );
				if ( !results.next() )
					throw new BuildingServerException( "Unable to create resource - unable to index containing resource." );
				left = results.getInt( 1 );
				right = left+1;
				results.close();
            	}

	        	
			statement.executeUpdate( 
				"UPDATE resources SET left_index = left_index+2 WHERE left_index >= " + left );
			statement.executeUpdate( 
				"UPDATE resources SET right_index = right_index+2 WHERE right_index >= " + left );
	            
			statement.executeUpdate( 
				"UPDATE resources SET left_index = " + left +
				", right_index = " + right +
				" WHERE resource_id = " + newresource.getResourceId() );

			statement.close();

        	if ( parent!=null )
            	newresource.setParentResourceId( parent.getResourceId() );
        	newresource.save();
		    
			
			//if reached here then database work is complete
			//so now put reference to new resource in tables and
			//reference from parent

        	if ( parent!=null )
            	{
            	parent.addChildResourceId( newresource.getResourceId() );
            	}
			}
		catch ( ObjectPoolException opex )
			{
			throw new BuildingServerException( opex );
			}
		catch ( SQLException sqlex )
			{
			Logger.getLogger( "org.bodington" ).logp( 
			    Level.SEVERE, 
			    "ResourceTree", 
			    "addResource", 
			    sqlex.getMessage(), 
			    sqlex );
			throw new BuildingServerException( "Unable to add resource - database error: " + sqlex );
			}
       	return;
        }

    /**
     * Removes resource and all its children from the tree.
     * It must only called when rolling back the addition of
     * a resource because it doesn't attempt to close up the
     * gap in the left and right indices of the resources
     * 
     * @param r The resource to remove.
     * @exception org.bodington.server.BuildingServerException
     */
     
    
    public synchronized void removeResource( Resource r )
        throws BuildingServerException
        {
        if ( r.getResourceId() == null )
      	return;
        Resource parent = r.getParent();
        if ( parent !=null )
      	{
      	parent.removeChildResourceId( r.getResourceId() );
      	}
        }
    
    
    public Resource findRootResource()
        throws BuildingServerException
        {
        if ( root_id==null ) return null;
        return Resource.findResource( root_id );
        }

	public synchronized boolean isInside( Resource a, Resource b )
        throws BuildingServerException
		{
		try
			{
    		Connection connection = BuildingContext.getContext().getConnection();
    		Statement statement = connection.createStatement();
    		ResultSet results;
			int a_left, a_right, b_left, b_right;

			results = statement.executeQuery( 
				"SELECT left_index, right_index FROM resources WHERE resource_id = " + 
				a.getResourceId() );
			if ( !results.next() )
				throw new BuildingServerException( "Unable to create resource - unable to find current index." );
			a_left = results.getInt( 1 );
			a_right = results.getInt( 2 );
			results.close();
				
			results = statement.executeQuery( 
				"SELECT left_index, right_index FROM resources WHERE resource_id = " + 
				b.getResourceId() );
			if ( !results.next() )
				throw new BuildingServerException( "Unable to create resource - unable to find current index." );
			b_left = results.getInt( 1 );
			b_right = results.getInt( 2 );
			results.close();
			statement.close();
				
			return a_left > b_left && a_right < b_right;
			}
		catch ( ObjectPoolException opex )
			{
			throw new BuildingServerException( opex );
			}
		catch ( SQLException sqlex )
			{
			throw new BuildingServerException( "Database error: " + sqlex );
			}
		}

    }
