package org.jcon.df;

import org.jcon.util.ArraySorter;
import org.jcon.util.DataLib;
import org.jcon.util.GenLib;
import org.jcon.df.column.ColumnDef;
import org.jcon.df.column.ColumnDefFactory;
import org.jcon.df.column.MIDColumn;
import org.jcon.df.column.TimeStampColumn;
import org.jcon.df.work.InvalidRow;
import org.jcon.df.work.Row;
import org.jcon.param.Param;
import org.jcon.param.ParamDriven;
import org.jcon.param.ParamDrivenInfo;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

/**
 * Conceptually an Entity is a set of zero or more rows,
 * each of the same class. An Entity is persistent. A
 * collection of entities forms the database for an
 * application.
 * <p>
 * An Entity merely describes its columns and provides
 * descriptive and integrity related behavior. It does NOT
 * carry rows.
 * <p>
 * Entities are fully object oriented, complete with fields
 * and methods. Note this differs from a relational database
 * entity that has a very limited selection of data types
 * and just a few events. A specialized Entity is created
 * by subclassing Entity or one of it's subclasses. This
 * allows inheritance in the Entity model. (not yet needed)
 * <p>
 * The main responsibility of an Entity is to
 * define it's collection of ColumnDefs and keep its rows
 * in a consistent state.
 *
 * @author Jack Harich
 */

public class Entity implements ParamDriven, java.io.Serializable {

//---------- Private Fields ------------------------------
private Param  param;
private Vector logicalOrderColumnDefs = new Vector(); // ColumnDef
private Vector entityChildren = new Vector(); // EntityChild
private Vector uniqueIndexes = new Vector();  // UniqueIndex
private Vector links = new Vector(); // MIDColumn
private EntityListener entityListener; // null if none    

//---------- Protected  Fields ---------------------------
protected String    name = "#Error#";
protected String    displayName;
protected String    descriptionColumnNames;

// Key is String name, Object is ColumnDef
protected Hashtable columnDefs = new Hashtable();

//---------- ParamDriven Implementation ------------------
public void setParam(Param param) {
    this.param = param;
    name = param.getString("Name");
    descriptionColumnNames = param.getString("DescriptionColumnNames");

    // Empty myself
    logicalOrderColumnDefs.removeAllElements();
    entityChildren.removeAllElements();
    uniqueIndexes.removeAllElements();

    // Add standard columns - Replaced by ParamBuilder 9/2/98
    /** addColumnDef(new MIDColumn());
    addColumnDef(new TimeStampColumn());

    MIDColumn def3 = new MIDColumn("LastUserSysMID");
    def3.setLinkEntityName("UserSys");
    addColumnDef((ColumnDef)def3); */

    // If no DisplayName provided then DisplayName = Name
    if (param.hasProperty("DisplayName")) {
        displayName = param.getString("DisplayName");
    } else {
        displayName = param.getString("Name");
    }
    // Unique indexes
    if (param.hasProperty("UniqueIndexes")) {
        Vector lines = param.getVector("UniqueIndexes");
        for (int i = 0; i < lines.size(); i++) {
            String line = (String)lines.elementAt(i);
            uniqueIndexes.addElement(new UniqueIndex(line));
        }
    }
    // Parent/child relationships
    if (param.hasProperty("EntityChildren")) {
        Vector children = param.getVector("EntityChildren");
        for (int i = 0; i < children.size(); i++) {
            // Example: Award has 2 children:
            // Args example: "AwardStage, AwardMID"
            // Args example: "AwardRecipient, AwardMID"
            String args = (String)children.elementAt(i);
            String arg1 = DataLib.getDelimitedField(args, 1, ", ");
            String arg2 = DataLib.getDelimitedField(args, 2, ", ");
            entityChildren.addElement(new EntityChild(arg1, arg2));
        }
    }
    // EntityListener
    if (param.hasProperty("EntityListener")) {
        entityListener = (EntityListener)GenLib
            .createInstance(param.getString("EntityListener"));
    }
    // Columns
    Param columns = param.getParam("Columns");
    String[] names = columns.getDataKeys();
    
    for (int i = 0; i < names.length; i++) {
        //print(".setParam() - Adding columnID " + name + "." + names[i]);
        Param columnParam = columns.getParam(names[i]);
        ColumnDef def = ColumnDefFactory.create(names[i], columnParam);
        this.addColumnDef(def);
        // Foreign Keys, excluding MID and LastUserSysMID
        if (def.getType().equals("MID")) {
            links.addElement(def);
        }
    }
}
public Param getParam() {
    return param;
}
public boolean applyNewParam(Param newParam) {
    setParam(param);
    return true; // Successful
}
public ParamDrivenInfo getParamDrivenInfo() {
    return null;
}
//---------- Properties ----------------------------------
/**
 * Returns the entity's name, which is read only.
 * <p>
 * Entity names follow the same standard as for Java
 * class names: alphanumeric characters and underscores
 * with no spaces. First letter is capatilized. They
 * should also be plural to imply a collection.
 * Examples are Awards, AwardCodes.
 */
public String getName() {
    return name;
}
/**
 * Returns a comma and space delimited String containing
 * the column names describing this entity uniquely in
 * alphabetical order. This is useful in providing the user
 * with a list to choose from. Returns null if none.
 * Examples: "LastName, FirstName" or "Name".
 */
public String getDescriptionColumnNames() {
    return descriptionColumnNames;
}
/**
 * The display name is used in the user interface, such as
 * on screens and reports. It may contain spaces since
 * this enhances readability. The first letter in each
 * word should be capatilized. Example: "First Name".
 */
public void setDisplayName(String displayName) {
    this.displayName = displayName;
}
public String getDisplayName() {
    return displayName;
}
//---------- Public Methods ------------------------------
/**
 * Returns an Enumeration of all column names.
 */
public Enumeration getColumnNames() {
    return columnDefs.keys();
}
/**
 * Returns a sorted array of all column names.
 */
public String[] getSortedColumnNames() {
    String[] names = DataLib.convertEnumerationToStringArray(
        columnDefs.keys(), columnDefs.size());
    ArraySorter.sortASCII(names);
    return names;
}
/**
 * Returns a Vector of all column names in "logical"
 * order, which is the order they were originally added.
 */
public Vector getColumnNamesInLogicalOrder() {
    Vector names = new Vector();
    for (int i = 0; i < logicalOrderColumnDefs.size(); i++) {
        ColumnDef def = (ColumnDef)logicalOrderColumnDefs.elementAt(i);
        names.addElement(def.getName());
    }
    return names;
}
/**
 * Returns a Hashtable clone of the ColumnDefs for this
 * entity. This is metadata, and is extremely useful in
 * parameter driven code.
 */
public Hashtable getColumnDefs() {
    return (Hashtable)columnDefs.clone();
}
/**
 * Returns an Enumeration of the ColumnDefs for this entity,
 * in no particular order.
 */
public Enumeration getColumnDefsEnum() {
    return columnDefs.elements();
}
public ColumnDef getColumnDef(String columnName) {
    return (ColumnDef)columnDefs.get(columnName);
}
/**
 * Returns a new Row with all columns in this
 * entity set to their default values. Columns not in
 * this entity are set to null. For simplicity applications
 * should be designed to have only ONE entity per Row when
 * editing, which is when this method is called for adds.
 *
 * columnIDs[] elements must be "EntityName.ColumnName".
 *
 * Note the row returned is a unique instance, and each of
 * its fields is (usually) a new instance.
 */
/** public Row createNewRow(String[] columnIDs) {
    Row newRow = new Row();
    for (int i = 0; i < columnIDs.length; i ++) {
        String columnID = columnIDs[i];
        String entityName = extractFirstDelimited(columnID, '.');
        String columnName = extractLastDelimited(columnID, '.');
        if (columnName.equals("TStamp")) continue;
        // Set value to default if my entity
        Object value = null;
        if (entityName.equals(name)) {
            ColumnDef def = (ColumnDef)columnDefs.get(columnName);
            value = def.getDefaultValue();
        }
        // Add the column to the row
        newRow.setValue(columnName, value);
    }
    return newRow;
} */ // Don't use in verion 2 ???
/**
 * Returns a new Row with all columns in this entity set
 * to their default values. *** Version 2
 * The columnIDs are "MyEntityName.ColumnName" format.
 */
public Row createNewRow() {
    Row newRow = new Row();
    Enumeration enum = getColumnNames();
    while (enum.hasMoreElements()) {
        String columnName = (String)enum.nextElement();
        ColumnDef def = (ColumnDef)columnDefs.get(columnName);
        Object value = def.getDefaultValue();
        // Add the column to the row
        newRow.setValue(name + "." + columnName, value);
    }
    //print(".createNewRow() - Row = \n" + newRow);
    return newRow;
}
/**
 * Validates the row columns in this entity. If any required
 * columns are missing declares the row invalid. Returns
 * null if okay or an InvalidRow containing the reason.
 * Skips MID and TStamp.
 */
public InvalidRow validateAddRow(Row row) {
    /** Enumeration enum = columnDefs.elements();
    while (enum.hasMoreElements()) {
        ColumnDef def = (ColumnDef)enum.nextElement(); */
    // Deliberately validate in logical order
    for (int i = 0; i < logicalOrderColumnDefs.size(); i++) {
        ColumnDef def = (ColumnDef)logicalOrderColumnDefs.elementAt(i);
        if (def.getName().equals("MID")) continue;
        if (def.getName().equals("TStamp")) continue;

        String invalidMessage = null;
        String columnID = name + "." + def.getName();
        if (row.hasColumnID(columnID)) {
            Object value = row.getValue(columnID);
            invalidMessage = def.validate(value);
        } else if (def.isRequired()) {
            invalidMessage = columnID + " is missing from row.\n" +
                "This is a programming error, since all new rows must have all required columns.";
            print(".validateAddRow() - Row is:\n" + row);
        }
        if (invalidMessage != null) {
            InvalidRow invalidRow = new InvalidRow();
            invalidRow.setColumnID(columnID);
            invalidRow.setText(invalidMessage);
            return invalidRow;
        }
    }
    return checkEntityValidator(EntityEvent.VALIDATE_ADD, row);
}
/**
 * Validates the row columns for this entity, skipping MID
 * and TStamp. Returns null if okay or an InvalidRow
 * containing the reason. Note the row need not contain all
 * required columns.
 */
public InvalidRow validateChangeRow(Row row) {
    // Row columnIDs are "EntityName.ColumnName"
    // Ideally we would validate is same order as above ***
    Enumeration enum = row.getColumnIDs();
    while (enum.hasMoreElements()) {
        //ColumnDef def = (ColumnDef)enum.nextElement();
        String columnID = (String)enum.nextElement();
        if (! columnID.startsWith(name + ".")) continue;

        String columnName = DataLib.getLastDelimited(columnID, '.');
        // Skip those never needing validation
        if (columnName.equals("MID")) continue;
        if (columnName.equals("TStamp")) continue;

      //print(".validateRow() - Row:\n" + row);
        Object columnValue = row.getValue(columnID);
        ColumnDef def = (ColumnDef)columnDefs.get(columnName);
        String invalidMessage = def.validate(columnValue);
        if (invalidMessage != null) {
            InvalidRow invalidRow = new InvalidRow();
            invalidRow.setColumnID(columnID);
            invalidRow.setText(invalidMessage);
            return invalidRow;
        }
    }
    return checkEntityValidator(EntityEvent.VALIDATE_UPDATE, row);
}
public Enumeration getUniqueIndexes() {
    return uniqueIndexes.elements();
}
/**
 * Returns an Enumeration of all the MIDColumns that are
 * links (foreign keys) in this entity, excluding the MID
 * and LastUserSysMID since they are framework maintained.
 */
public Enumeration getLinkMIDColumns() {
    return links.elements();
}
//----- Parent/child methods
/**
 * Returns an Enumeration of EntityChild, which is empty
 * if none.
 */
public Enumeration getChildren() {
    return entityChildren.elements();
}
public boolean hasChildren() {
    return (entityChildren.isEmpty() ? false : true);
}
//---------- Private Methods -----------------------------
// Returns null if okay or no entityListener
private InvalidRow checkEntityValidator(int eventType, Row row) {
    if (entityListener == null) return null;
    
    // Create event
    EntityEvent evt = new EntityEvent(eventType);
    evt.setRow(row);
    evt.setEntityName(name);
    // Notify listener
    EntityEventReply reply = entityListener.processEntityEvent(evt);
    if (reply != null) {
        InvalidRow invalidRow = new InvalidRow();
        invalidRow.setColumnID(name + "." + reply.getInvalidColumnName());
        invalidRow.setText(reply.getInvalidMessage());
        return invalidRow;
    } else {
        return null;   
    }
}
private void addColumnDef(ColumnDef def) {
    columnDefs.put(def.getName(), def);
    logicalOrderColumnDefs.addElement(def);
}
//--- Std
private void print(String text) {
    System.out.println("Entity" + text);
}


} // End class
