/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.storage.rdbms;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * Represents a database row.
 * 
 * @author Peter Breton
 * @version $Revision: 5844 $
 */
public class TableRow
{
    /** Marker object to indicate NULLs. */
    private static final Object NULL_OBJECT = new Object();

    /** The name of the database table containing this row */
    private String table;

    /**
     * A map of column names to column values. The key of the map is a String,
     * the column name; the value is an Object, either an Integer, Boolean,
     * Date, or String. If the value is NULL_OBJECT, then the column was NULL.
     */
    private Map<String, Object> data = new HashMap<String, Object>();

    private Map<String, Boolean> changed = new HashMap<String, Boolean>();

    /**
     * Constructor
     * 
     * @param table
     *            The name of the database table containing this row.
     * @param columns
     *            A list of column names. Each member of the List is a String.
     *            After construction, the list of columns is fixed; attempting
     *            to access a column not in the list will cause an
     *            IllegalArgumentException to be thrown.
     */
    public TableRow(String table, List<String> columns)
    {
        this.table = table;

        for (String column : columns)
        {
            String canonicalized = ColumnInfo.canonicalize(column);
            data.put(canonicalized, NULL_OBJECT);
            changed.put(canonicalized, Boolean.TRUE);
        }
    }

    /**
     * Return the name of the table containing this row, or null if this row is
     * not associated with a database table.
     * 
     * @return The name of the table containing this row
     */
    public String getTable()
    {
        return table;
    }

    /**
     * Return true if this row contains a column with this name.
     * 
     * @param column
     *            The column name (case-insensitive)
     * @return True if this row contains a column with this name.
     */
    public boolean hasColumn(String column)
    {
        try
        {
            return canonicalizeAndCheck(column) != null;
        }
        catch (IllegalArgumentException e)
        {
            return false;
        }
    }

    /**
     * Return true if this row contains this column and the value has been updated.
     *
     * @param column
     *            The column name (case-insensitive)
     * @return True if this row contains a column with this name.
     */
    public boolean hasColumnChanged(String column)
    {
        return hasColumnChangedCanonicalized(ColumnInfo.canonicalize(column));
    }

    boolean hasColumnChangedCanonicalized(String column)
    {
        return changed.get(column) == Boolean.TRUE;
    }

    /**
     * Return true if the column is an SQL NULL.
     * 
     * @param column
     *            The column name (case-insensitive)
     * @return True if the column is an SQL NULL
     */
    public boolean isColumnNull(String column)
    {
        return isColumnNullCanonicalized(canonicalizeAndCheck(column));
    }

    boolean isColumnNullCanonicalized(String column)
    {
        return data.get(column) == NULL_OBJECT;
    }

    /**
     * Return the integer value of column.
     * 
     * If the column's type is not an integer, or the column does not exist, an
     * IllegalArgumentException is thrown.
     * 
     * @param column
     *            The column name (case-insensitive)
     * @return The integer value of the column, or -1 if the column is an SQL
     *         null.
     */
    public int getIntColumn(String column)
    {
        String canonicalized = canonicalizeAndCheck(column);
        if (isColumnNullCanonicalized(canonicalized))
        {
            return -1;
        }

        Object value = data.get(canonicalized);

        if (value == null)
        {
            throw new IllegalArgumentException("Column " + column + " not present");
        }

        if (!(value instanceof Integer))
        {
            throw new IllegalArgumentException("Value for " + column + " is not an integer");
        }

        return ((Integer) value).intValue();
    }

    /**
     * Return the long value of column.
     * 
     * If the column's type is not an long, or the column does not exist, an
     * IllegalArgumentException is thrown.
     * 
     * @param column
     *            The column name (case-insensitive)
     * @return The long value of the column, or -1 if the column is an SQL null.
     */
    public long getLongColumn(String column)
    {
        String canonicalized = canonicalizeAndCheck(column);
        if (isColumnNullCanonicalized(canonicalized))
        {
            return -1;
        }

        Object value = data.get(canonicalized);

        if (value == null)
        {
            throw new IllegalArgumentException("Column " + column + " not present");
        }

        // If the value is an integer, it can be represented without error as a long
        // So, allow the return of a long. (This is needed for Oracle support).
        if ((value instanceof Integer))
        {
            return ((Integer) value).longValue();
        }
        
        if (!(value instanceof Long))
        {
            throw new IllegalArgumentException("Value for " + column + " is not a long");
        }

        return ((Long) value).longValue();
    }

    /**
     * Return the double value of column.
     * 
     * If the column's type is not an float, or the column does not exist, an
     * IllegalArgumentException is thrown.
     * 
     * @param column
     *            The column name (case-insensitive)
     * @return The double value of the column, or -1 if the column is an SQL null.
     */
    public double getDoubleColumn(String column)
    {
        String canonicalized = canonicalizeAndCheck(column);
        if (isColumnNullCanonicalized(canonicalized))
        {
            return -1;
        }

        Object value = data.get(canonicalized);

        if (value == null)
        {
            throw new IllegalArgumentException("Column " + column + " not present");
        }
        
        if (!(value instanceof Double))
        {
            throw new IllegalArgumentException("Value for " + column + " is not a double");
        }

        return ((Double) value).doubleValue();
    }

    /**
     * Return the String value of column.
     * 
     * If the column's type is not a String, or the column does not exist, an
     * IllegalArgumentException is thrown.
     * 
     * @param column
     *            The column name (case-insensitive)
     * @return The String value of the column, or null if the column is an SQL
     *         null.
     */
    public String getStringColumn(String column)
    {
        String canonicalized = canonicalizeAndCheck(column);
        if (isColumnNullCanonicalized(canonicalized))
        {
            return null;
        }

        Object value = data.get(canonicalized);

        if (value == null)
        {
            throw new IllegalArgumentException("Column " + column + " not present");
        }

        if (!(value instanceof String))
        {
            throw new IllegalArgumentException("Value is not an string");
        }

        return (String) value;
    }

    /**
     * Return the boolean value of column.
     * 
     * If the column's type is not a boolean, or the column does not exist, an
     * IllegalArgumentException is thrown.
     * 
     * @param column
     *            The column name (case-insensitive)
     * @return The boolean value of the column, or false if the column is an SQL
     *         null.
     */
    public boolean getBooleanColumn(String column)
    {
        String canonicalized = canonicalizeAndCheck(column);
        if (isColumnNullCanonicalized(canonicalized))
        {
            return false;
        }

        Object value = data.get(canonicalized);

        // make sure that we tolerate integers or booleans
        if (value == null)
        {
            throw new IllegalArgumentException("Column " + column + " not present");
        }

        if ((value instanceof Boolean))
        {
            return ((Boolean) value).booleanValue();
        }
        else if ((value instanceof Integer))
        {
            int i = ((Integer) value).intValue();

            if (i == 0)
            {
                return false; // 0 is false
            }

            return true; // nonzero is true
        }
        else
        {
            throw new IllegalArgumentException("Value is not a boolean or an integer");
        }
    }

    /**
     * Return the date value of column.
     * 
     * If the column's type is not a date, or the column does not exist, an
     * IllegalArgumentException is thrown.
     * 
     * @param column
     *            The column name (case-insensitive)
     * @return - The date value of the column, or null if the column is an SQL
     *         null.
     */
    public java.util.Date getDateColumn(String column)
    {
        String canonicalized = canonicalizeAndCheck(column);
        if (isColumnNullCanonicalized(canonicalized))
        {
            return null;
        }

        Object value = data.get(canonicalized);

        if (value == null)
        {
            throw new IllegalArgumentException("Column " + column + " not present");
        }

        if (!(value instanceof java.util.Date))
        {
            throw new IllegalArgumentException("Value is not a Date");
        }

        return (java.util.Date) value;
    }

    /**
     * Set column to an SQL NULL.
     * 
     * If the column does not exist, an IllegalArgumentException is thrown.
     * 
     * @param column
     *            The column name (case-insensitive)
     */
    public void setColumnNull(String column)
    {
        String canonicalized = canonicalizeAndCheck(column);
        if (data.get(canonicalized) != NULL_OBJECT)
        {
            data.put(canonicalized, NULL_OBJECT);
            changed.put(canonicalized, Boolean.TRUE);
        }
    }
    /**
     * Set column to the boolean b.
     * 
     * If the column does not exist, an IllegalArgumentException is thrown.
     * 
     * @param column
     *            The column name (case-insensitive)
     * @param b
     *            The boolean value
     */
    public void setColumn(String column, boolean b)
    {
        String canonicalized = canonicalizeAndCheck(column);
        if (DatabaseManager.isOracle())
        {
            // if oracle, use 1 or 0 for true/false
            Integer value = b ? Integer.valueOf(1) : Integer.valueOf(0);
            if (!value.equals(data.get(canonicalized)))
            {
                data.put(canonicalized, value);
                changed.put(canonicalized, Boolean.TRUE);
            }
        }
        else
        {
            // default to postgres true/false
            Boolean value = b ? Boolean.TRUE : Boolean.FALSE;
            if (!value.equals(data.get(canonicalized)))
            {
                data.put(canonicalized, value);
                changed.put(canonicalized, Boolean.TRUE);
            }
        }
    }

    /**
     * Set column to the String s. If s is null, the column is set to null.
     * 
     * If the column does not exist, an IllegalArgumentException is thrown.
     * 
     * @param column
     *            The column name (case-insensitive)
     * @param s
     *            The String value
     */
    public void setColumn(String column, String s)
    {
        String canonicalized = canonicalizeAndCheck(column);
        Object value = (s == null) ? NULL_OBJECT : s;
        if (!value.equals(data.get(canonicalized)))
        {
            data.put(canonicalized, value);
            changed.put(canonicalized, Boolean.TRUE);
        }
    }

    /**
     * Set column to the integer i.
     * 
     * If the column does not exist, an IllegalArgumentException is thrown.
     * 
     * @param column
     *            The column name (case-insensitive)
     * @param i
     *            The integer value
     */
    public void setColumn(String column, int i)
    {
        String canonicalized = canonicalizeAndCheck(column);
        Integer value = Integer.valueOf(i);
        if (!value.equals(data.get(canonicalized)))
        {
            data.put(canonicalized, value);
            changed.put(canonicalized, Boolean.TRUE);
        }
    }

    /**
     * Set column to the long l.
     * 
     * If the column does not exist, an IllegalArgumentException is thrown.
     * 
     * @param column
     *            The column name (case-insensitive)
     * @param l
     *            The long value
     */
    public void setColumn(String column, long l)
    {
        String canonicalized = canonicalizeAndCheck(column);
        Long value = Long.valueOf(l);
        if (!value.equals(data.get(canonicalized)))
        {
            data.put(canonicalized, value);
            changed.put(canonicalized, Boolean.TRUE);
        }
    }

    /**
     * Set column to the double d.
     * 
     * If the column does not exist, an IllegalArgumentException is thrown.
     * 
     * @param column
     *            The column name (case-insensitive)
     * @param d
     *            The double value
     */
    public void setColumn(String column, double d)
    {
        String canonicalized = canonicalizeAndCheck(column);
        Double value = new Double(d);
        if (!value.equals(data.get(canonicalized)))
        {
            data.put(canonicalized, value);
            changed.put(canonicalized, Boolean.TRUE);
        }
    }

    /**
     * Set column to the date d. If the date is null, the column is set to NULL
     * as well.
     * 
     * If the column does not exist, an IllegalArgumentException is thrown.
     * 
     * @param column
     *            The column name (case-insensitive)
     * @param d
     *            The date value
     */
    public void setColumn(String column, java.util.Date d)
    {
        String canonicalized = canonicalizeAndCheck(column);
        Object value = (d == null) ? NULL_OBJECT : d;
        if (!value.equals(data.get(canonicalized)))
        {
            data.put(canonicalized, value);
            changed.put(canonicalized, Boolean.TRUE);
        }
    }

    ////////////////////////////////////////
    // Utility methods
    ////////////////////////////////////////

    /**
     * Return a String representation of this object.
     * 
     * @return String representation
     */
    public String toString()
    {
        final String NEWLINE = System.getProperty("line.separator");
    	StringBuffer result;
    	
    	if (table==null)
    	{
    		result = new StringBuffer("no_table");
    	}
    	else
    	{
    		result = new StringBuffer(table);
    	}
    	
    	result.append(NEWLINE);

        for (Iterator iterator = data.keySet().iterator(); iterator.hasNext();)
        {
            String column = (String) iterator.next();
            result.append("\t").append(column).append(" = ").append(
                    isColumnNull(column) ? "NULL" : data.get(column)).append(
                    NEWLINE);
        }

        return result.toString();
    }

    /**
     * Return a hash code for this object.
     * 
     * @return int hash of object
     */
    public int hashCode()
    {
        return toString().hashCode();
    }

    /**
     * Return true if this object equals obj, false otherwise.
     * 
     * @param obj
     * @return true if TableRow objects are equal
     */
    public boolean equals(Object obj)
    {
        if (!(obj instanceof TableRow))
        {
            return false;
        }

        return data.equals(((TableRow) obj).data);
    }

    private String canonicalizeAndCheck(String column)
    {
        if (data.containsKey(column))
        {
            return column;
        }

        String canonicalized = ColumnInfo.canonicalize(column);
        if (data.containsKey(canonicalized))
        {
            return canonicalized;
        }

        throw new IllegalArgumentException("No such column " + canonicalized);
    }

    /**
     * package private method to reset the flags of which columns have been updated
     * This is used by the database manager after it has finished processing the contents
     * of a resultset, so that it can update only columns that have been updated.
     * Note that this method does not reset the values themselves, only the flags,
     * and should not be considered safe to call from anywhere other than the DatabaseManager.
     */
    void resetChanged()
    {
        for (String column : changed.keySet())
        {
            changed.put(column, Boolean.FALSE);
        }
    }
}
