package org.jcon.df;

import org.jcon.util.DataLib;
import org.jcon.util.GenLib;
import org.jcon.df.column.MIDColumn;
import org.jcon.df.request.*;
import org.jcon.df.work.DatabaseUtil;
import java.util.Enumeration;
import java.util.Vector;

/**
 * Enforces database referential integrity. The types of
 * integrity enforced are: <pre>
 * - Prevent overwrites
 * - Assert foreign keys exist
 * - Assert unique indexes are unique
 * - Cascade delete for parent/child </pre>
 * <p>
 * Cascade delete is handled by RemoveRows and CascadeDelete
 * <p>
 * Note we have not optimized yet, preferring to go for a
 * simple and correct first iteration.
 *
 * @author Jack Harich
 */
public class Integrity {

//---------- Private Fields ------------------------------
private Schema          schema;
private RequestServices services;
private DataRequest     request;
private DatabaseUtil    databaseUtil = new DatabaseUtil();

//---------- Properties ----------------------------------
public void setSchema(Schema schema) {
    this.schema = schema;
}
//---------- Public Methods ------------------------------
public String validateRequest(Request request,
        RequestServices services) {
    this.services = services;
    databaseUtil.setRequestServices(services);
        String okay = null;
        if (request instanceof DataRequest) {
            this.request = (DataRequest)request;
            okay = validateDataRequest();
        } else {
            // Okay until more types
        }
    databaseUtil.setRequestServices(null);
    services = null;
    request = null;
    return okay; // null if okay

}
//----- Other
/**
 * Returns an array of the entity names that should be
 * locked during the transaction, which includes integrity
 * validation and database calls. The array may be empty
 * for no locks, such as a read. Duplicate names are okay.
 */
public String[] getEntityLockNames(Request request) {

    if (! request.isMutator()) {
        return new String[0]; // Reads need no locks

    } else if (request instanceof RemoveRows) {
        return CascadeDelete.getEntityLockNames(
            (RemoveRows)request, schema);

    } else if (request instanceof DataRequest) {

        DataRequest dataRequest = (DataRequest)request;
        String primeEntityName = dataRequest.getPrimeEntity();
        Vector names = new Vector();

        // We always need to lock the prime entity
        names.addElement(primeEntityName);

        // Foreign keys, aka links
        loadLinkEntityNames(primeEntityName, names);

        return DataLib.convertVectorToStringArray(names);

    } else {
        return new String[0]; // No locks
    }
}
//---------- Private Methods -----------------------------
/**
 * Load all link entityNames in primeEntity. This will
 * include duplicates, but LockPool will remove them.
 * Null names are excluded.
 */
private void loadLinkEntityNames(String primeEntityName,
        Vector names) {

    MIDColumn link;
    Entity entity = schema.getEntity(primeEntityName);
    Enumeration enum = entity.getLinkMIDColumns();
    while (enum.hasMoreElements()) {
        link = (MIDColumn)enum.nextElement();
        if (link.getLinkEntityName() == null) continue;

        names.addElement(link.getLinkEntityName());
    }
}
private String validateDataRequest() {
    if (! request.isMutator()) return null;
    try {
        if (request instanceof AddOneRow) {
            String okay = validateUniqueIndexes(true);
            if (okay != null) return okay;

            okay = validateLinks(true);
            if (okay != null) return okay;
        }
        if (request instanceof UpdateRow) {
            String okay = validateUniqueIndexes(false);
            if (okay != null) return okay;

            okay = validateLinks(false);
            if (okay != null) return okay;

            okay = preventOverWrite();
            if (okay != null) return okay;
        }
        // *** more here

        return null;
    } catch(Exception ex) {
        GenLib.exception("Integrity.validateDataRequest()",
            "Unanticipated exception.", ex);
        return "Invalid due to exception.";
    }
}
private String preventOverWrite() {
    String entityName = request.getPrimeEntity();
    String requestTimeStamp = request.getUpdateColumn(
        entityName + ".TStamp").getValue();

    // Get oldTimeStamp from row in database
    String midColumnID = entityName + ".MID";
    String mid = request.getUpdateColumn(midColumnID).getValue();

    Filter filter = new Filter();
    filter.addExpression(midColumnID, "=", mid);

    String oldTimeStamp = databaseUtil.readColumn(
        "TStamp", entityName, filter);

    //print(" request: " + requestTimeStamp + ", old = " + oldTimeStamp);
    // Compare
    if (requestTimeStamp == null) {
        GenLib.error("Integrity.preventOverWrite()",
            "Request TStamp is null, letting proceed anyhow.");

    } else if (oldTimeStamp == null) {
        GenLib.error("Integrity.preventOverWrite()",
            "Old TStamp in database is null, letting proceed anyhow.");

    } else if(! requestTimeStamp.equals(oldTimeStamp)) {
        return "Request time stamp of " + requestTimeStamp +
        " is not equal to time stamp in database of " +
        oldTimeStamp + ". Someone else has updated this " +
        "record before you. Please cancel your update.";
    }
    return null;
}
//----- Links
private String validateLinks(boolean isAdding) {
    String entityName = request.getPrimeEntity();
    if (entityName == null) return "No entity name.";

    // Set columnExps
    Vector columnExps;
    if (isAdding) {
        columnExps = request.getInsertColumns();
    } else {
        columnExps = request.getUpdateColumns();
    }
    // Check each link
    MIDColumn link;
    Entity entity = schema.getEntity(entityName);
    Enumeration enum = entity.getLinkMIDColumns();
    while (enum.hasMoreElements()) {
        link = (MIDColumn)enum.nextElement();
        String linkEntityName = link.getLinkEntityName();
        if (linkEntityName == null) continue;
        String okay = validateLink(link, entityName, columnExps);
        if (okay != null) return okay;
    }
    return null; // Okay
}
// Filter example: "ForeignEntityName.LinkColumnName = value"
// Valid if value is null or "", ie "isEmpty"
// Valid if value is not empty and count > 0
private String validateLink(MIDColumn link,
        String entityName, Vector columnExps) {

    String foreignEntityName = link.getLinkEntityName();
    String linkColumnName = link.getName();

    // Prepare filter
    Filter filter = new Filter();
    String columnID = entityName + "." + linkColumnName;
    //print(".validateLinks() - Checking " + columnID + ", columnExps count = " + columnExps.size());
    ColumnExp columnExp = DataRequest.findColumnID(
        columnExps, columnID);
    //print(".validateLinks() - Before null check");
    if (columnExp == null) return null; // Okay
    //print(".validateLinks() - Before empty check");
    if (columnExp.isValueEmpty()) return null; // Okay

    filter.addExpression(foreignEntityName + ".MID",
        "=", columnExp.getValue());

    // Count occurances
    int count = databaseUtil.countRows(foreignEntityName, filter);
    //print(" - columnID = " + columnID + ", count = " + count);

    // Use result
    if (count < 0) {
        return "Failure in counting rows for '" + foreignEntityName + "'.";

    } else if (count == 0) {
        return "Adding or updating this row is not allowed " +
        "since foreign key '" + linkColumnName +
        "'is missing in '" + foreignEntityName + "'.";

    } else {
        // count > 0
        return null; // Okay
    }
}
//----- Unique indexes
// These originate in entity parex "UniqueIndexs hasLines:"
private String validateUniqueIndexes(boolean isAdding) {
    String entityName = request.getPrimeEntity();
    if (entityName == null) return "No entity name.";

    Entity entity = schema.getEntity(entityName);
    Enumeration enum = entity.getUniqueIndexes();
    while (enum.hasMoreElements()) {
        UniqueIndex uniqueIndex = (UniqueIndex)enum.nextElement();
        String okay = validateUniqueIndex(uniqueIndex,
            entity, isAdding);
        if (okay != null) return okay;
    }
    return null; // Okay
}
// If adding: WHERE EntityName.Col1 = 'A' AND ...
// If update: WHERE EntityName.MID <> 123 AND (above)
private String validateUniqueIndex(UniqueIndex uniqueIndex,
        Entity entity, boolean isAdding) {

    //print(".validateUniqueIndex() - Checking " + uniqueIndex.getDelimColumnNames());
    String entityName = entity.getName();
    // Prepare filter
    Filter filter = new Filter();
    Vector exps;
    if (isAdding) {
        // Include all rows since adding
        exps = request.getInsertColumns();
    } else {
        // Exclude update row from query
        String columnID = entityName + ".MID";
        exps = request.getUpdateColumns();
        ColumnExp columnExp = DataRequest.findColumnID(
            exps, columnID);
        filter.addExpression(columnID, "<>", columnExp.getValue());
    }
    // Add one AND & expression per column
    String[] names = uniqueIndex.getColumnNames();
    for (int i = 0; i < names.length; i++) {
        filter.addAnd(); // No effect if first

        String columnID = entityName + "." + names[i];
        ColumnExp columnExp = DataRequest.findColumnID(
            exps, columnID);
        if (columnExp == null) {
            GenLib.error("Integrity.validateUniqueIndex()",
                "columnExp is null for columnID = " + columnID);
            // Let NullPointerException occur
        }
        String value = columnExp.getValue();
        filter.addExpression(columnID, "=", value);
    }
    // Count occurances
    int count = databaseUtil.countRows(entityName, filter);
    //print(" count = " + count);

    // Use result
    if (count < 0) {
        return "Failure in counting rows for '" + entityName + "'.";

    } else if (count == 0) {
        return null;

    } else {
        // count > 0
        return "Adding or updating this row would cause " +
        "a duplicate index for columns '" + uniqueIndex.getDelimColumnNames() + "'.";
    }
}
//--- Std
private static void print(String text) {
    System.out.println("Integrity" + text);
}

} // End class
