/*
 * $Source: /usr/cvsroot/melati/poem/src/main/java/org/melati/poem/Column.java,v $
 * $Revision: 1.90 $
 *
 * Copyright (C) 2000 William Chesters
 *
 * Part of Melati (http://melati.org), a framework for the rapid
 * development of clean, maintainable web applications.
 *
 * Melati is free software; Permission is granted to copy, distribute
 * and/or modify this software under the terms either:
 *
 * a) the GNU General Public License as published by the Free Software
 *    Foundation; either version 2 of the License, or (at your option)
 *    any later version,
 *
 *    or
 *
 * b) any version of the Melati Software License, as published
 *    at http://melati.org
 *
 * You should have received a copy of the GNU General Public License and
 * the Melati Software License along with this program;
 * if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA to obtain the
 * GNU General Public License and visit http://melati.org to obtain the
 * Melati Software License.
 *
 * Feel free to contact the Developers of Melati (http://melati.org),
 * if you would like to work out a different arrangement than the options
 * outlined here.  It is our intention to allow Melati to be used by as
 * wide an audience as possible.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * Contact details for copyright holder:
 *
 *     William Chesters <williamc At paneris.org>
 *     http://paneris.org/~williamc
 *     Obrechtstraat 114, 2517VX Den Haag, The Netherlands
 */

package org.melati.poem;

import java.io.PrintStream;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Enumeration;

import org.melati.poem.dbms.Dbms;
import org.melati.poem.util.EmptyEnumeration;
import org.melati.poem.util.StringUtils;

/**
 * Abstract {@link Table} column which is extended by the generated classes.
 *
 * @author WilliamC At paneris.org
 * 
 */
public abstract class Column<T> implements FieldAttributes<T> {
  private Table table = null;
  private String name;
  private String quotedName;
  private SQLPoemType<T> type;
  private DefinitionSource definitionSource;
  private ColumnInfo info = null;

  /**
   * Constructor.
   * @param table this column belongs to
   * @param name of this Column
   * @param type datatype
   * @param definitionSource where it is being defined from
   */
  public Column(
    Table table,
    String name,
    SQLPoemType<T> type,
    DefinitionSource definitionSource) {
    this.table = table;
    this.name = name;
    this.quotedName = table.getDatabase().quotedName(name);
    this.type = type;
    this.definitionSource = definitionSource;
  }

  // 
  // ================
  //  Initialisation
  // ================
  // 

  /**
   * @return the underlying Dbms
   */
  Dbms dbms() {
    return getDatabase().getDbms();
  }

  void unifyType(SQLPoemType<Object> storeType, DefinitionSource source) {
    PoemType<T> unified = dbms().canRepresent(storeType, type);
    if (unified == null || !(unified instanceof SQLPoemType))
      throw new TypeDefinitionMismatchException(this, storeType, source);

    type = (SQLPoemType<T>) unified;
  }

  void assertMatches(ResultSet colDesc)
      throws SQLException, TypeDefinitionMismatchException {
    PoemType<?> dbType = getDatabase().defaultPoemTypeOfColumnMetaData(colDesc);

    if (dbms().canRepresent(dbType, type) == null)
      throw new TypeDefinitionMismatchException(
        this,
        dbType,
        DefinitionSource.sqlMetaData);
  }

  void setColumnInfo(ColumnInfo columnInfo) {
    try {
      unifyType(columnInfo.getType(), DefinitionSource.infoTables);
      columnInfo.setColumn(this);
      if (columnInfo.getDisplaylevel() == DisplayLevel.primary)
        table.setDisplayColumn(this);
      if (columnInfo.getSearchability() == Searchability.primary)
        table.setSearchColumn(this);
      info = columnInfo;
      table.notifyColumnInfo(info);
    } catch (Exception e) {
      throw new UnexpectedExceptionPoemException(
        e,
        "Setting column info for " + name + " to " + columnInfo);
    }
  }

  protected DisplayLevel defaultDisplayLevel() {
    return DisplayLevel.summary;
  }

  protected Searchability defaultSearchability() {
    return Searchability.yes;
  }

  protected Integer defaultDisplayOrderPriority() {
    return null;
  }

  protected boolean defaultSortDescending() {
    return false;
  }

  protected String defaultDisplayName() {
    return StringUtils.capitalised(getName());
  }

  protected int defaultDisplayOrder() {
    return 100;
  }

  protected String defaultDescription() {
    return null;
  }

  protected boolean defaultUserEditable() {
    return true;
  }

  protected boolean defaultUserCreateable() {
    return true;
  }

  protected boolean defaultIndexed() {
    return isTroidColumn();
  }

  protected boolean defaultUnique() {
    return isTroidColumn();
  }

  /**
   * @return the StandardIntegrityFix prevent
   */
  protected StandardIntegrityFix defaultIntegrityFix() {
    return StandardIntegrityFix.prevent;
  }

  protected int defaultWidth() {
    return 20;
  }

  protected int defaultHeight() {
    return 1;
  }

  protected int defaultPrecision() {
    return 22;
  }

  protected int defaultScale() {
    return 2;
  }

  protected String defaultRenderinfo() {
    return null;
  }

  void createColumnInfo() throws PoemException {
    if (info == null) {
      info = (ColumnInfo)getDatabase().
          getColumnInfoTable().create(new Initialiser() {
        public void init(Persistent g) throws AccessPoemException {
          ColumnInfo i = (ColumnInfo)g;
          i.setName(getName());
          i.setDisplayname(defaultDisplayName());
          i.setDisplayorder(defaultDisplayOrder());
          i.setDescription(defaultDescription());
          i.setDisplaylevel(defaultDisplayLevel());
          i.setSearchability(defaultSearchability());
          i.setSortdescending(defaultSortDescending());
          i.setDisplayorderpriority(defaultDisplayOrderPriority());
          i.setTableinfoTroid(table.tableInfoID());
          i.setUsereditable(defaultUserEditable());
          i.setUsercreateable(defaultUserCreateable());
          i.setIndexed(defaultIndexed());
          i.setUnique(defaultUnique());
          i.setWidth(defaultWidth());
          i.setHeight(defaultHeight());
          i.setRenderinfo(defaultRenderinfo());
          i.setIntegrityfix(defaultIntegrityFix());
          i.setPrecision(defaultPrecision());
          i.setScale(defaultScale());
          getType().saveColumnInfo(i);
        }
      });

      // FIXME Repeating this in setColumnInfo(ColumnInfo) is a bad sign

      if (defaultDisplayLevel() == DisplayLevel.primary)
        table.setDisplayColumn(this);
      if (defaultSearchability() == Searchability.primary)
        table.setSearchColumn(this);
    }
  }

  void unifyWithIndex(String indexName, ResultSet index) throws SQLException, 
                              IndexUniquenessPoemException {
    boolean indexUnique = !index.getBoolean("NON_UNIQUE");
    if (indexUnique != getUnique()) 
        throw new IndexUniquenessPoemException(
          this,
          indexName,
          getUnique());
  }

  // 
  // ===========
  //  Accessors
  // ===========
  // 

  /**
   * @return the Databse our table is in
   */
  public final Database getDatabase() {
    return getTable().getDatabase();
  }

  /**
   * @return our Table
   */
  public final Table getTable() {
    return table;
  }

  final void setTable(Table table) {
    this.table = table;
  }

  /**
   * {@inheritDoc}
   * @see org.melati.poem.FieldAttributes#getName()
   */
  public final String getName() {
    return name;
  }

  /**
   * @return the name quoted appropriately for the DBMS
   */
  public final String quotedName() {
    return quotedName;
  }

  /**
   * @return the name in table.column notation
   */
  public final String fullQuotedName() {
    return table.quotedName() + "." + quotedName;
  }

  /**
   * Return a human readable name from the metadata.
   * 
   * {@inheritDoc}
   * @see org.melati.poem.FieldAttributes#getDisplayName()
   */
  public final String getDisplayName() {
    return info.getDisplayname();
  }

  /**
   * {@inheritDoc}
   * @see org.melati.poem.FieldAttributes#getDescription()
   */
  public final String getDescription() {
    return info.getDescription();
  }

  /**
   * The troid (<TT>id</TT>) of the column's entry in the <TT>columninfo</TT>
   * table.  It will always have one (except during initialisation, which the
   * application programmer will never see).
   * @return the troid of our record in the columnInfo table
   */
  final Integer columnInfoID() {
    return info == null ? null : info.troid();
  }

  /**
   * @return the metadata record for this Column
   */
  public final ColumnInfo getColumnInfo() {
    return info;
  }

  /**
   * @return the defined or default DsiplayLevel
   */
  public DisplayLevel getDisplayLevel() {
    return info == null ? defaultDisplayLevel() : info.getDisplaylevel();
  }

  /**
   * @param level the DisplayLevel to set
   */
  public void setDisplayLevel(DisplayLevel level) {
    if (info != null)
      info.setDisplaylevel(level);
  }

  /**
   * @return our defined or default Searchabillity
   */
  public Searchability getSearchability() {
    return info == null ? defaultSearchability() : info.getSearchability();
  }

  /**
   * @param searchability the Searchability to set
   */
  public void setSearchability(Searchability searchability) {
    if (info != null)
      info.setSearchability(searchability);
  }

  /**
   * {@inheritDoc}
   * @see org.melati.poem.FieldAttributes#getUserEditable()
   */
  public final boolean getUserEditable() {
    return !isTroidColumn()
      && (info == null || info.getUsereditable().booleanValue());
  }

  /**
   * {@inheritDoc}
   * @see org.melati.poem.FieldAttributes#getUserCreateable()
   */
  public final boolean getUserCreateable() {
    return !isTroidColumn()
      && (info == null || info.getUsercreateable().booleanValue());
  }

  /**
   * @return the SQLPoemType of this Column
   */
  public final SQLPoemType<T> getSQLType() {
    return type;
  }

  /**
   * {@inheritDoc}
   * @see org.melati.poem.FieldAttributes#getType()
   */
  public final PoemType<T> getType() {
    return type;
  }

  /**
   * @return whether this is a Troid Column
   */
  public final boolean isTroidColumn() {
    return getType() instanceof TroidPoemType;
  }

  /**
   * A Deleted Column is a Column which signal whether 
   * the record has been soft-deleted.
   * @return whether this is a Deleted Column
   */
  public final boolean isDeletedColumn() {
    return getType() instanceof DeletedPoemType;
  }

  /**
   * {@inheritDoc}
   * @see org.melati.poem.FieldAttributes#getIndexed()
   */
  public final boolean getIndexed() {
    return getUnique() || info.getIndexed().booleanValue();
  }

  /**
   * @return whether this Column's values are unique in this table
   */
  public final boolean getUnique() {
    return isTroidColumn() || info.getUnique().booleanValue();
  }

  /**
   * Get the IntegrityFix, if any.
   * @return the IntegrityFix or default
   */
  public IntegrityFix getIntegrityFix() {
    IntegrityFix it = info.getIntegrityfix();
    return it == null ? defaultIntegrityFix() : it;
  }

  /**
   * @param fix the IntegrityFix to set
   */
  public void setIntegrityFix(StandardIntegrityFix fix) {
    info.setIntegrityfix(fix);
  }

  /**
   * {@inheritDoc}
   * @see org.melati.poem.FieldAttributes#getRenderInfo()
   */
  public final String getRenderInfo() {
    return info.getRenderinfo();
  }

  /**
   * {@inheritDoc}
   * @see org.melati.poem.FieldAttributes#getWidth()
   */
  public final int getWidth() {
    return info.getWidth().intValue();
  }

  /**
   * {@inheritDoc}
   * @see org.melati.poem.FieldAttributes#getHeight()
   */
  public final int getHeight() {
    return info.getHeight().intValue();
  }

  /**
   * @return the set or default DisplayOrderPriority
   */
  public final Integer getDisplayOrderPriority() {
    return info == null ? null : info.getDisplayorderpriority();
  }

  /**
   * Defaults to false.
   * @return whether this Column should be sorted in descending order 
   */
  public final boolean getSortDescending() {
    return info.getSortdescending() == null
      ? false
      : info.getSortdescending().booleanValue();
  }

  // 
  // ===========
  //  Utilities
  // ===========
  // 

  /**
   * {@inheritDoc}
   * @see java.lang.Object#toString()
   */
  public String toString() {
    return table.getName()
      + "."
      + name
      + ": "
      + getType().toString()
      + " (from "
      + definitionSource
      + ")";
  }

  /**
   * Print information about the structure of the Column to stdout.
   */
  public void dump() {
    dump(System.out);
  }

  /**
   * Print information to PrintStream. 
   * 
   * @param ps PrintStream to dump to
   */
  public void dump(PrintStream ps) {
    ps.println(toString());
  }

  /**
   * 
   * @param raw An object with an equivalent SQL type
   * @return the SQL euals clause that would capture equality with the raw
   */
  public String eqClause(Object raw) {
    return fullQuotedName()
      + (raw == null ? " IS NULL" : " = " + type.quotedRaw(raw));
  }

  private PreparedStatementFactory selectionWhereEq = null;

  private PreparedStatementFactory statementWhereEq() {
    if (selectionWhereEq == null)
      selectionWhereEq =
        new PreparedStatementFactory(
          getDatabase(),
          getTable().selectionSQL(
            getTable().quotedName(),
            fullQuotedName()
              + " = "
              + dbms().preparedStatementPlaceholder(getType()),
            null,
            false,
            true));

    return selectionWhereEq;
  }

  ResultSet resultSetWhereEq(Object raw) {
    SessionToken token = PoemThread.sessionToken();
    PreparedStatement ps =
      statementWhereEq().preparedStatement(token.transaction);
    type.setRaw(ps, 1, raw);
    return statementWhereEq().resultSet(token, ps);
  }

  /**
   * Not used in Melati or PanEris tree.
   * 
   * @param raw value 
   * @return an Enumeration of Integers 
   */
  /*
  Enumeration troidSelectionWhereEq(Object raw) {
    return new ResultSetEnumeration(resultSetWhereEq(raw)) {
      public Object mapped(ResultSet rs) throws SQLException {
        return new Integer(rs.getInt(1));
      }
    };
  }
  */
  
  /**
   * Get rows where column equal to value.
   * 
   * @param raw a raw value such as a String
   * @return an enumeration of Persistents
   */
  public Enumeration<Persistent> selectionWhereEq(Object raw) {
    return new ResultSetEnumeration<Persistent>(resultSetWhereEq(raw)) {
      public Object mapped(ResultSet rs) throws SQLException {
        return getTable().getObject(rs.getInt(1));
      }
    };
  }

  /**
   * Return the first one found or null if not found. 
   * 
   * @param raw Object of correct type for this Column
   * @return the first one found based upon default ordering
   */
  public Persistent firstWhereEq(Object raw) {
    Enumeration<Persistent> them = selectionWhereEq(raw);
    return them.hasMoreElements() ? (Persistent)them.nextElement() : null;
  }

  /**
   * Create a new CachedSelection of objects equal to this raw parameter. 
   * 
   * @param raw Object of correct type for this Column
   * @return a new CachedSelection of objects equal to raw.
   */
  public CachedSelection cachedSelectionWhereEq(Object raw) {
    return new CachedSelection(getTable(), eqClause(raw), null);
  }

  // 
  // =======================================
  //  Reading/setting the column in records
  // =======================================
  // 

 /**
  * Retrieves the field value, with locking,
  * for this {@link Column}.
  * 
  * @param g  the {@link Persistent} to read
  * @return the Object itself
  * @throws AccessPoemException 
  *         if the current <code>AccessToken</code> 
  *         does not confer read access rights
  */
  public abstract Object getRaw(Persistent g) throws AccessPoemException;

 /**
  * Retrieves the field value, without locking,
  * for this <code>Column</code>.
  * 
  * @param g  the {@link Persistent} to read
  * @return the Object without checks
  */
  public abstract Object getRaw_unsafe(Persistent g);

 /**
  * Sets the field value, with locking,
  * for this <code>Column</code>.
  * 
  * @param g  the {@link Persistent} to modify
  * @param raw the value to set the field to 
  * @throws AccessPoemException 
  *         if the current <code>AccessToken</code> 
  *         does not confer write access rights
  * @throws ValidationPoemException 
  *         if the raw value is not valid
  */
  public abstract void setRaw(Persistent g, Object raw)
    throws AccessPoemException, ValidationPoemException;

 /**
  * Sets the field value, without locking,
  * for this <code>Column</code>.
  * 
  * @param g  the {@link Persistent} to modify
  * @param raw the value to set the field to 
  */
  public abstract void setRaw_unsafe(Persistent g, Object raw);

 /**
  * Retrieves the field value, with locking and  access control 
  * for this <code>Column</code>.
  * 
  * @param g  the {@link Persistent} to modify
  * @return either the value or what is represented by the value
  * @throws AccessPoemException 
  *         if the current <code>AccessToken</code> 
  *         does not confer read access rights
  * @throws PoemException 
  *         if any problem occurs
  */
  public abstract Object getCooked(Persistent g)
    throws AccessPoemException, PoemException;

 /**
  * Sets the field value, with locking, access control 
  * and validation for this <code>Column</code>.
  * 
  * @param g  the {@link Persistent} to modify
  * @param cooked  the value to set
  * @throws AccessPoemException 
  *         if the current <code>AccessToken</code> 
  *         does not confer read access rights
  * @throws ValidationPoemException 
  *         if the value is not valid
  */
  public abstract void setCooked(Persistent g, Object cooked)
    throws AccessPoemException, ValidationPoemException;

  /**
   * Thrown when any unforeseen problem arises loading a {@link Column}.
   */
  public static class LoadException extends UnexpectedExceptionPoemException {

    private Column<?> column;

    /**
     * Constructor.
     * @param column Column relevant to
     * @param problem the Exception
     */
    public LoadException(Column<?> column, Exception problem) {
      super(problem);
      this.column = column;
    }

    /** @return Returns the message */
    public String getMessage() {
      return "An unexpected problem arose loading "
        + column
        + " from the "
        + "database:\n"
        + subException;
    }

  
    /**
     * @return Returns the column.
     */
     @SuppressWarnings("rawtypes")
    protected Column getColumn() {
      return column;
    }
  }

  /**
   * Load a Persistent field from a column of a ResultSet.
   * 
   * @todo Double validation
   * @param rs A <code>ResultSet</code>containing the value(s) to load
   * @param rsCol The index in the <tt>ResultSet</tt> of this {link column}
   * @param g The {@link Persistent} to load db values into
   * @throws LoadException
   */
  void load_unsafe(ResultSet rs, int rsCol, Persistent g)
    throws LoadException {
    try {
      setRaw_unsafe(g, type.getRaw(rs, rsCol));
    } catch (Exception e) {
      throw new LoadException(this, e);
    }
  }

  /**
   * Set value in a PreparedStatement which is to be used to save to database.
   * 
   * @param g The {@link Persistent} containing unsaved values
   * @param ps <tt>PreparedStatement</tt> to save this column
   * @param psCol index of this Column in the PreparedStatement
   * @todo Double validation
   */
  void save_unsafe(Persistent g, PreparedStatement ps, int psCol) {
    try {
      type.setRaw(ps, psCol, getRaw_unsafe(g));
    } catch (Exception e) {
      throw new FieldContentsPoemException(this, e); 
    }
  }

  // 
  // ============
  //  Operations
  // ============
  // 

  /**
   * Return a Field of the same type as this Column from the Persistent.
   * @param g the Persistent
   * @return a Field
   */
  public abstract Field<T> asField(Persistent g);

  /**
   * Return a Field of the same type as this Column with default attributes.
   * @return the empty Field
   */
  public Field<T> asEmptyField() {
    return new Field<T>((T) null, this);
  }

  /**
   * Thrown when any unforseen problem arises setting the value 
   * of a {@link Column}.
   */
  public static class SettingException extends NormalPoemException {
    /** The Persistent to which this Column belongs. */
    public Persistent persistent;
    /** The Column setting which caused the problem. */
    public Column<?> column;
    /** The description of the Column. */
    public String columnDesc;

    /**
     * Constructor.
     * @param persistent the Persistent with the problem
     * @param column he Column with the problem
     * @param trouble the problem
     */
    public SettingException(Persistent persistent, Column<?> column, Exception trouble) {
      super(trouble);
      this.persistent = persistent;
      this.column = column;
      columnDesc =
        "field `"
          + column.getDisplayName()
          + "' in object `"
          + persistent.displayString()
          + "' of type `"
          + column.getTable().getDisplayName()
          + "'";
    }

    /** @return the message */
    public String getMessage() {
      return "Unable to set " + columnDesc + "\n" + subException;
    }
  }

  /**
   * Set the value from its String representation, if possible.
   * 
   * Throws SettingException if the String value cannot be 
   * converted to the appropriate type 
   * @param g the Persistent to alter
   * @param rawString the String representation of the value to set
   */
  public void setRawString(Persistent g, String rawString) {
    Object raw;
    try {
      raw = getType().rawOfString(rawString);
    } catch (Exception e) {
      throw new SettingException(g, this, e);
    }
    setRaw(g, raw);
  }

  /**
   * Return an Enumeration of {@link Persistent}s from the 
   * Table this column refers to, if this is a reference column, 
   * otherwise the Empty Enumeration.
   * @param object A persistent of the type referred to by this column
   * @return an Enumeration {@link Persistent}s referencing this Column of the Persistent
   */
  public Enumeration<Persistent> referencesTo(Persistent object) {
    return (Enumeration<Persistent>) (getType() instanceof ReferencePoemType
      && ((ReferencePoemType) getType()).targetTable() == object.getTable()
        ? selectionWhereEq(object.troid())
        : new EmptyEnumeration<Persistent>());
  }

  /**
   * Ensures a row exists for which this column matches when compared with
   * the given {@link Persistent}.
   * 
   * The given object is used to create a new row if
   * necessary, in which case it will be assigned the next troid and
   * cached.
   * @param orCreate the Persistent to use as criteria and ensure
   * @return the existing or newly created Persistent
   */
  public Persistent ensure(Persistent orCreate) {
    Persistent there = firstWhereEq(getRaw_unsafe(orCreate));
    if (there == null) {
      getTable().create(orCreate);
      return orCreate;
    } else
      return there;
  }

  
  /**
   * Find the next free value in an Integer column.
   * 
   * This is not used in Melati, but is used in Bibliomania. 
   * Throws AppBugPoemException if this Column is not an Integer column.
   * 
   * @param whereClause
   * @return the incremented value 
   * @since 04/05/2000
   */
  public int firstFree(String whereClause) {
    if (! (getType() instanceof IntegerPoemType)) 
      throw new AppBugPoemException("firstFree called on a non Integer column");
    getTable().readLock();
    String querySelection = 
        quotedName
      + " + 1 "
      + "FROM "
      + getTable().quotedName()
      + " AS t1 "
      + "WHERE "
      + (whereClause == null ? "" : "(t1." + whereClause + ") AND ")
      + "NOT EXISTS ("
      + "SELECT * FROM "
      + getTable().quotedName()
      + " AS t2 "
      + "WHERE "
      + (whereClause == null ? "" : "(t2." + whereClause + ") AND ")
      + "t2."
      + quotedName
      + " = t1."
      + quotedName
      + " + 1) ";

    String query = getDatabase().getDbms().selectLimit(querySelection, 1); 
    ResultSet results = getDatabase().sqlQuery(query);
    try {
      if (results.next())
        return results.getInt(1);
      else
        return 0;
    } catch (SQLException e) {
      throw new SQLSeriousPoemException(e);
    }
  }
}
