/*

   Derby - Class org.apache.derby.iapi.sql.dictionary.ConstraintDescriptor

   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to you under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

 */

package org.apache.derby.iapi.sql.dictionary;

import java.util.Arrays;
import org.apache.derby.shared.common.error.StandardException;

import org.apache.derby.iapi.sql.depend.Provider;
import org.apache.derby.iapi.sql.depend.Dependent;

import org.apache.derby.catalog.UUID;

import org.apache.derby.shared.common.reference.SQLState;
import org.apache.derby.shared.common.util.ArrayUtil;
import org.apache.derby.shared.common.sanity.SanityManager;
import org.apache.derby.catalog.DependableFinder;
import org.apache.derby.catalog.Dependable;
import org.apache.derby.iapi.services.io.StoredFormatIds;
import org.apache.derby.iapi.sql.depend.DependencyManager;
import	org.apache.derby.iapi.sql.conn.LanguageConnectionContext;
import org.apache.derby.iapi.store.access.TransactionController;

/**
 * This class is used to get information from a ConstraintDescriptor.
 * A ConstraintDescriptor can represent a constraint on a table or on a
 * column.
 *
 * @version 0.1
 */

public abstract class ConstraintDescriptor 
	extends UniqueTupleDescriptor
	implements Provider, Dependent
{
	// used to indicate what type of constraints we 
	// are interested in
	public static final int ENABLED		= 1;
	public static final int DISABLED	= 2;
	public static final int ALL			= 3;

	// field that we want users to be able to know about
	public static final int SYSCONSTRAINTS_STATE_FIELD = 6;

    TableDescriptor       table;
    final String          constraintName;
    private boolean deferrable;
    private boolean initiallyDeferred;
    private boolean enforced;
	private final int[]				referencedColumns;
	final UUID					constraintId;
	private final SchemaDescriptor	schemaDesc;
	private ColumnDescriptorList	colDL;

	/**
	 * Constructor for a ConstraintDescriptor
	 *
	 * @param dataDictionary		The data dictionary that this descriptor lives in
	 * @param table		The descriptor of the table the constraint is on
	 * @param constraintName	The name of the constraint.
	 * @param deferrable		If the constraint can be deferred.
	 * @param initiallyDeferred If the constraint starts life deferred.
	 * @param referencedColumns columns that the constraint references
	 * @param constraintId		UUID of constraint
	 * @param schemaDesc		SchemaDescriptor
     * @param enforced          Is the constraint enforced?
	 */

	ConstraintDescriptor(
		    DataDictionary dataDictionary,
			TableDescriptor table,
			String constraintName,
			boolean deferrable,
			boolean initiallyDeferred,
			int[] referencedColumns,
			UUID constraintId,
			SchemaDescriptor schemaDesc,
            boolean enforced
			)
	{
		super( dataDictionary );

		this.table = table;
		this.constraintName = constraintName;
		this.deferrable = deferrable;
        this.initiallyDeferred = initiallyDeferred;
		this.referencedColumns = referencedColumns;
		this.constraintId = constraintId;
		this.schemaDesc = schemaDesc;
        this.enforced = enforced;
	}


	/**
	 * Gets the UUID of the table the constraint is on.
	 *
	 * @return	The UUID of the table the constraint is on.
	 */
	public UUID	getTableId()
	{
		return table.getUUID();
	}

	/**
	 * Gets the UUID of the constraint.
	 *
	 * @return	The UUID of the constraint.
	 */
	public UUID	getUUID()
	{
		return constraintId;
	}

	/**
	 * Gets the name of the constraint.
	 *
	 * @return	A String containing the name of the constraint.
	 */
	public String	getConstraintName()
	{
		return constraintName;
	}

	/**
	 * Gets an identifier telling what type of descriptor it is
	 * (UNIQUE, PRIMARY KEY, FOREIGN KEY, CHECK).
	 *
	 * @return	An identifier telling what type of descriptor it is
	 *		(UNIQUE, PRIMARY KEY, FOREIGN KEY, CHECK).
	 */
	public abstract int	getConstraintType();

	public abstract UUID getConglomerateId();

	/**
	 * Get the text of the constraint. (Only non-null/meaningful for check
	 * constraints.)
	 * @return	The constraint text.
	 */
	public String getConstraintText()
	{
		return null;
	}

	/**
	 * Returns TRUE if the constraint is deferrable
	 *
     * @return  TRUE if the constraint is DEFERRABLE, FALSE if it is
     *          NOT DEFERRABLE.
	 */
    public boolean deferrable()
	{
		return deferrable;
	}

    public void setDeferrable(boolean b) {
        deferrable = b;
    }

	/**
	 * Returns TRUE if the constraint is initially deferred
	 *
     * @return  TRUE if the constraint is initially DEFERRED,
     *      FALSE if the constraint is initially IMMEDIATE
	 */
	public boolean	initiallyDeferred()
	{
        return initiallyDeferred;
	}

    public void setInitiallyDeferred(boolean b) {
        initiallyDeferred = b;
    }

	/**
	 * Returns an array of column ids (i.e. ordinal positions) for
	 * the columns referenced in this table for a primary key, unique
	 * key, referential, or check constraint.
	 *
	 * @return	An array of column ids for those constraints that can
	 *		be on columns (primary, unique key, referential
	 *		constraints, and check constraints).  For check and
	 *		unique constraints, it returns an array of columns ids
	 *		that are referenced in the constraint.  For primary key
	 *		and referential constraints, it returns an array of
	 *		column ids for the columns in this table (i.e. the
	 *		primary key columns for a primary key constraint,
	 *		and the foreign key columns for a foreign key
	 *		constraint.
	 */
	public int[]	getReferencedColumns()
	{
		return ArrayUtil.copy( referencedColumns );
	}

	/**
	 * Does this constraint have a backing index?
	 *
	 * @return boolean	Whether or not there is a backing index for this constraint.
	 */
	public abstract boolean hasBackingIndex();

	/**
	 * Get the SchemaDescriptor for the schema that this constraint
	 * belongs to.
	 *
	 * @return SchemaDescriptor The SchemaDescriptor for this constraint.
	 */
	public SchemaDescriptor getSchemaDescriptor()
	{
		return schemaDesc;
	}

	/**
	  RESOLVE: For now the ConstraintDescriptor code stores the array of key
	  columns in the field 'otherColumns'. Jerry plans to re-organize things.
	  For now to minimize his rototill I've implemented this function on the
	  old structures. All new code should use getKeyColumns to get a constraint's
	  key columns.
	  
	  @see org.apache.derby.iapi.sql.dictionary.KeyConstraintDescriptor#getKeyColumns
	  */
	public int[]	getKeyColumns()
	{
		return getReferencedColumns();
	}

	/**
     * Is this constraint enforced?
	 *
	 * @return true/false
	 */
    public boolean enforced()
	{
        return enforced;
	}

    public void setEnforced(boolean b) {
        enforced = b;
    }

	/**
	 * Is this constraint referenced?  Return
	 * false.  Overridden by ReferencedKeyConstraints.
	 *
	 * @return false
	 */
	public boolean isReferenced()
	{
		return false;
	}

	/**
     * Get the number of enforced fks that
	 * reference this key.  Overriden by
	 * ReferencedKeyConstraints.
	 *
	 * @return the number of fks
	 */
	public int getReferenceCount()
	{
		return 0;
	}

	/**
	 * Does this constraint need to fire on this type of
	 * DML?
	 *
	 * @param stmtType	the type of DML 
	 * (StatementType.INSERT|StatementType.UPDATE|StatementType.DELETE)
	 * @param modifiedCols	the columns modified, or null for all
	 *
	 * @return true/false
	 */
	public abstract boolean needsToFire(int stmtType, int[] modifiedCols);

	/**
	 * Get the table descriptor upon which this constraint
	 * is declared.
	 *
	 * @return the table descriptor
	 */
	public TableDescriptor getTableDescriptor()
	{
		return table;
	}

	/**
	 * Get the column descriptors for all the columns
	 * referenced by this constraint.
	 *
	 * @return the column descriptor list
	 *
	 * @exception StandardException on error
	 */
	public ColumnDescriptorList getColumnDescriptors()
		throws StandardException
	{
		if (colDL == null)
		{
			colDL = new ColumnDescriptorList();
	
			int[]	refCols = getReferencedColumns();
			for (int i = 0; i < refCols.length; i++)
			{
				colDL.add(table.getColumnDescriptor(refCols[i]));
			}
		}
		return colDL;
	}

	/**
	 * Indicates whether the column descriptor list is
	 * type comparable with the constraints columns.  The
	 * types have to be identical AND in the same order 
	 * to succeed.
	 *
	 * @param otherColumns the columns to compare
	 *
	 * @return true/false
	 *
	 * @exception StandardException on error
	 */
	public boolean areColumnsComparable(ColumnDescriptorList otherColumns)
		throws StandardException
	{
		ColumnDescriptor		myColumn;
		ColumnDescriptor		otherColumn;

		ColumnDescriptorList	myColDl = getColumnDescriptors();

		/*
		** Check the lenghts of the lists
		*/
		if (otherColumns.size() != myColDl.size())
		{
			return false;
		}

		int mySize = myColDl.size();
		int otherSize = otherColumns.size();
		int index;
		for (index = 0; index < mySize && index < otherSize; index++)
		{
            myColumn = myColDl.elementAt(index);
            otherColumn = otherColumns.elementAt(index);

			/*
			** Just compare the types.  Note that this will
	 		** say a decimal(x,y) != numeric(x,y) even though
			** it does.
			*/
			if (!(myColumn.getType()).isExactTypeAndLengthMatch(
					(otherColumn.getType())))
			{
				break;
			}
		}

		return (index == mySize && index == otherSize);
	}

	/**
	 * Does a column intersect with our referenced columns
	 * @param columnArray columns to check
	 * 
	 * Note-- this is not a static method.
	 */
	public boolean columnIntersects(int columnArray[])
	{
		// call static method.
		return doColumnsIntersect(getReferencedColumns(), columnArray);
	}

	/**
	 * Does a column in the input set intersect with
	 * our referenced columns?
	 *
	 * @param otherColumns the columns to compare. If
	 *	null, asssumed to mean all columns
	 *
	 * @param referencedColumns the columns referenced by the caller
	 *
	 * @return true/false
	 */
	static boolean doColumnsIntersect(int[] otherColumns, int[] referencedColumns)
	{
		/*
		** It is assumed that if otherColumns is null, then
		** all other columns are modified.  In this case,
		** it is assumed that it intersects with some column
	 	** of ours, so just return true.
		*/
		if ((otherColumns == null) || (referencedColumns == null))
		{
			return true;
		}

		for (int outer = 0; outer < referencedColumns.length; outer++)
		{	
			for (int inner = 0; inner < otherColumns.length; inner++)
			{
				if (referencedColumns[outer] == otherColumns[inner])
				{
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Convert the ColumnDescriptor to a String.
	 *
	 * @return	A String representation of this ColumnDescriptor
	 */

    @Override
    public String toString()
	{
		if (SanityManager.DEBUG)
		{
			String tableDesc =
				"table: " +
				table.getQualifiedName() + "(" +
				table.getUUID()+","+
				table.getTableType()+")";

			return tableDesc + "\n"+
				"constraintName: " + constraintName + "\n" +
				"constraintId: " + constraintId + "\n" +
				"deferrable: " + deferrable + "\n" +
                "initiallyDeferred: " + initiallyDeferred + "\n" +
               "referencedColumns: " +
                    Arrays.toString(referencedColumns) + "\n" +
				"schemaDesc: " + schemaDesc + "\n"
				;
		}
		else
		{
			return "";
		}
	}

	////////////////////////////////////////////////////////////////////
	//
	// PROVIDER INTERFACE
	//
	////////////////////////////////////////////////////////////////////

	/**		
		@return the stored form of this provider

			@see Dependable#getDependableFinder
	 */
	public DependableFinder getDependableFinder() 
	{
	    return	getDependableFinder(StoredFormatIds.CONSTRAINT_DESCRIPTOR_FINDER_V01_ID);
	}

	/**
	 * Return the name of this Provider.  (Useful for errors.)
	 *
	 * @return String	The name of this provider.
	 */
	public String getObjectName()
	{
		return constraintName;
	}

	/**
	 * Get the provider's UUID
	 *
	 * @return 	The provider's UUID
	 */
	public UUID getObjectID()
	{
		return constraintId;
	}

	/**
	 * Get the provider's type.
	 *
	 * @return char		The provider's type.
	 */
	public String getClassType()
	{
		return Dependable.CONSTRAINT;
	}

	//////////////////////////////////////////////////////
	//
	// DEPENDENT INTERFACE
	//
	//////////////////////////////////////////////////////
	/**
	 * Check that all of the dependent's dependencies are valid.
	 *
	 * @return true if the dependent is currently valid
	 */
	public synchronized boolean isValid()
	{
		return true;
	}

	/**
	 * Prepare to mark the dependent as invalid (due to at least one of
	 * its dependencies being invalid).
	 *
	 * @param action	The action causing the invalidation
	 * @param p		the provider
	 *
	 * @exception StandardException thrown if unable to make it invalid
	 */
	public void prepareToInvalidate(Provider p, int action,
					LanguageConnectionContext lcc) 
		throws StandardException
	{
		DependencyManager dm = getDataDictionary().getDependencyManager();

		switch (action)
		{
			/*
			** A SET CONSTRAINT stmt will throw an SET_CONSTRAINTS action
			** when enabling/disabling constraints.  We'll ignore it.
			** Same for SET TRIGGERS
			*/
		    case DependencyManager.SET_CONSTRAINTS_ENABLE:
		    case DependencyManager.SET_CONSTRAINTS_DISABLE:
		    case DependencyManager.SET_TRIGGERS_ENABLE:
		    case DependencyManager.SET_TRIGGERS_DISABLE:
		    case DependencyManager.RENAME:
			//When REVOKE_PRIVILEGE gets sent (this happens for privilege 
			//types SELECT, UPDATE, DELETE, INSERT, REFERENCES, TRIGGER), we  
			//don't do anything here. Later in makeInvalid method, we make  
			//the ConstraintDescriptor drop itself. 
			//Ditto for role grant conferring a privilege.
		    case DependencyManager.REVOKE_PRIVILEGE:
		    case DependencyManager.REVOKE_ROLE:
		    case DependencyManager.INTERNAL_RECOMPILE_REQUEST:
				// Only used by Activations
		    case DependencyManager.RECHECK_PRIVILEGES:
				break;

			/*
			** Currently, the only thing we are depenedent
			** on is another constraint or an alias..
			*/
			//Notice that REVOKE_PRIVILEGE_RESTRICT is not caught earlier.
		    //It gets handled in this default: action where an exception
		    //will be thrown. This is because, if such an invalidation 
		    //action type is ever received by a dependent, the dependent 
		    //should throw an exception.
			//In Derby, at this point, REVOKE_PRIVILEGE_RESTRICT gets sent
		    //when execute privilege on a routine is getting revoked.
		    //Currently, in Derby, a constraint can't depend on a routine 
		    //and hence a REVOKE_PRIVILEGE_RESTRICT invalidation action
		    //should never be received by a ConstraintDescriptor. But this
		    //may change in future and when it does, the code to do the right
		    //thing is already here.
		    default:
				throw StandardException.newException(SQLState.LANG_PROVIDER_HAS_DEPENDENT_OBJECT, 
									dm.getActionString(action), 
									p.getObjectName(), "CONSTRAINT", constraintName);
		}
	}

	/**
	 * Mark the dependent as invalid (due to at least one of
	 * its dependencies being invalid).  Always an error
	 * for a constraint -- should never have gotten here.
	 *
	 * @param	action	The action causing the invalidation
	 *
	 * @exception StandardException thrown if called in sanity mode
	 */
	public void makeInvalid(int action, LanguageConnectionContext lcc) 
		throws StandardException
	{
		/*
		** For ConstraintDescriptor, SET_CONSTRAINTS/TRIGGERS and 
		*  REVOKE_PRIVILEGE are the only valid actions
		*/

		//Let's handle REVOKE_PRIVILEGE and REVOKE_ROLE first
		if (action == DependencyManager.REVOKE_PRIVILEGE ||
			action == DependencyManager.REVOKE_ROLE)
		{
			//At this point (Derby 10.2), only a FOREIGN KEY key constraint can
			//depend on a privilege. None of the other constraint types 
			//can be dependent on a privilege becuse those constraint types
			//can not reference a table/routine.
			ConglomerateDescriptor newBackingConglomCD = drop(lcc, true);

            //
            // Invalidate every statement which depends on the table.
            // This causes us to follow the same code path which we pursue
            // when the CHECK constraint is dropped explicitly.
            //
            getDataDictionary().getDependencyManager().invalidateFor( table, DependencyManager.ALTER_TABLE, lcc );

			lcc.getLastActivation().addWarning(
				StandardException.newWarning(
					SQLState.LANG_CONSTRAINT_DROPPED,
					getConstraintName(),
					getTableDescriptor().getName()));

			if (newBackingConglomCD != null)
			{
				/* Since foreign keys can never be unique, and since
				 * we only (currently) share conglomerates if two
				 * constraints/indexes have identical columns, dropping
				 * a foreign key should not necessitate the creation of
				 * another physical conglomerate.  That will change if
				 * DERBY-2204 is implemented, but for now we don't expect
				 * it to happen...
				 */
				if (SanityManager.DEBUG)
				{
					SanityManager.THROWASSERT(
						"Dropped shared conglomerate due to a REVOKE " +
						"and found that a new conglomerate was needed " +
						"to replace it...but that shouldn't happen!");
				}
			}
			return;
		}

		/*
		** Now, handle SET_CONSTRAINTS/TRIGGERS
		*/
		if ((action != DependencyManager.SET_CONSTRAINTS_DISABLE) &&
			(action != DependencyManager.SET_CONSTRAINTS_ENABLE) &&
			(action != DependencyManager.SET_TRIGGERS_ENABLE) &&
			(action != DependencyManager.SET_TRIGGERS_DISABLE) &&
			(action != DependencyManager.INTERNAL_RECOMPILE_REQUEST) &&
			(action != DependencyManager.RECHECK_PRIVILEGES) &&
			(action != DependencyManager.RENAME)
		   )
		{
			/* 
			** We should never get here, we should have barfed on 
			** prepareToInvalidate().
			*/
			if (SanityManager.DEBUG)
			{
				DependencyManager dm;
		
				dm = getDataDictionary().getDependencyManager();
	
				SanityManager.THROWASSERT("makeInvalid("+
					dm.getActionString(action)+
					") not expected to get called");
			}
		}
	}
    
    /**
     * Drop the constraint.  Clears dependencies, drops 
     * the backing index and removes the constraint
     * from the list on the table descriptor.  Does NOT
     * do an dm.invalidateFor()
     *
     * @return If the backing conglomerate for this constraint
     *  was a) dropped and b) shared by other constraints/indexes,
     *  then this method will return a ConglomerateDescriptor that
     *  describes what a new backing conglomerate must look like
     *  to stay "sharable" across the remaining constraints/indexes.
     *  It is then up to the caller to create a corresponding 
     *  conglomerate.  We don't create the conglomerate here
     *  because depending on who called us, it might not make
     *  sense to create it--ex. if we get here because of a DROP
     *  TABLE, the DropTable action doesn't need to create a
     *  new backing conglomerate since the table (and all of
     *  its constraints/indexes) are going to disappear anyway.
     */
    public ConglomerateDescriptor drop(LanguageConnectionContext lcc,
        boolean clearDependencies) throws StandardException
    {       
        DataDictionary dd = getDataDictionary();
        TransactionController tc = lcc.getTransactionExecute();

        if (clearDependencies)
        {
            DependencyManager dm = dd.getDependencyManager();
            dm.clearDependencies(lcc, this);
        }

        /* Drop the constraint.
         * NOTE: This must occur before dropping any backing index, since
         * a user is not allowed to drop a backing index without dropping
         * the constraint.
         */
        dd.dropConstraintDescriptor(this, tc);

        /* Drop the index, if there's one for this constraint.
         * NOTE: There will always be an indexAction. We don't
         * force the constraint to exist at bind time, so we always
         * generate one.
         */
        ConglomerateDescriptor newBackingConglomCD = null;
        if (hasBackingIndex())
        {

            // it may have duplicates, and we drop a backing index
            // Bug 4307
            // We need to get the conglomerate descriptors from the 
            // dd in case we dropped other constraints in a cascade operation. 
            ConglomerateDescriptor[]conglomDescs =
                    dd.getConglomerateDescriptors(getConglomerateId());

            // Typically there is only one ConglomerateDescriptor
            // for a given UUID, but due to an old bug
            // there may be more than one. If there is more
            // than one then which one is remvoed does not
            // matter since they will all have the same critical
            // information since they point to the same physical index.
            for (ConglomerateDescriptor cd : conglomDescs) {
                if (cd.isConstraint()) {
                    newBackingConglomCD = cd.drop(lcc, table);
                    break;
                }
            }
        }

        table.removeConstraintDescriptor(this);
        return newBackingConglomCD;
    }
	
	/** @see TupleDescriptor#getDescriptorName */
    @Override
	public String getDescriptorName() { return constraintName; }
	
    @Override
	public String getDescriptorType() { return "Constraint"; }
}
