package prefuse.data.column;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.logging.Logger;

import prefuse.data.DataReadOnlyException;
import prefuse.data.DataTypeException;

/**
 * Column implementation for storing arbitrary Object values.
 * 
 * @author <a href="http://jheer.org">jeffrey heer</a>
 */
public class ObjectColumn extends AbstractColumn {

    private Object[] m_values;
    private int      m_size;
    
    /**
     * Create a new empty ObjectColumn. The type is assumed to be Object.
     */
    public ObjectColumn() {
        this(Object.class);
    }
    
    /**
     * Create a new ObjectColumn.
     * @param type the data type of Objects in this column 
     */
    public ObjectColumn(Class type) {
        this(type, 0, 10, null);
    }
    
    /**
     * Create a new ObjectColumn. The type is assumed to be Object. 
     * @param nrows the initial size of the column
     */
    public ObjectColumn(int nrows) {
        this(Object.class, nrows, nrows, null);
    }
    
    /**
     * Create a new ObjectColumn.
     * @param type the data type of Objects in this column 
     * @param nrows the initial size of the column
     */
    public ObjectColumn(Class type, int nrows) {
        this(type, nrows, nrows, null);
    }
    
    /**
     * Create a new ObjectColumn.
     * @param type the data type of Objects in this column 
     * @param nrows the initial size of the column
     * @param capacity the initial capacity of the column
     * @param defaultValue the default value for the column. If this value
     * is cloneable, it will be cloned when assigned as defaultValue, otherwise
     * the input reference will be used for every default value.
     */
    public ObjectColumn(Class type, int nrows, int capacity, Object defaultValue) {
        super(type, defaultValue);
        if ( capacity < nrows ) {
            throw new IllegalArgumentException(
                "Capacity value can not be less than the row count.");
        }
        m_values = new Object[capacity];
        try {
            // since Object's clone method is protected, we default to
            // using reflection to create clones.
            Cloneable def = (Cloneable)defaultValue;
            Method m = def.getClass().getMethod("clone", null);
            for ( int i=0; i<capacity; ++i ) {
                m_values[i] = m.invoke(m_defaultValue, null);
            }
        } catch ( Exception e ) {
            if ( defaultValue != null ) {
                Logger.getLogger(getClass().getName()).fine(
                    "Default value of type \"" + 
                    defaultValue.getClass().getName() + "\" is not " +
                    "cloneable. Using Object reference directly.");
            }
            Arrays.fill(m_values, defaultValue);
        }
        m_size = nrows;
    }
    
    // ------------------------------------------------------------------------
    // Column Metadata
    
    /**
     * @see prefuse.data.column.Column#getRowCount()
     */
    public int getRowCount() {
        return m_size;
    }
    
    /**
     * @see prefuse.data.column.Column#setMaximumRow(int)
     */
    public void setMaximumRow(int nrows) {
        if ( nrows > m_values.length ) {
            int capacity = Math.max((3*m_values.length)/2 + 1, nrows);
            Object[] values = new Object[capacity];
            System.arraycopy(m_values, 0, values, 0, m_size);
            try {
                // since Object's clone method is protected, we default to
                // using reflection to create clones.
                Cloneable def = (Cloneable)m_defaultValue;
                Method m = def.getClass().getMethod("clone", null);
                for ( int i=m_size; i<capacity; ++i ) {
                    values[i] = m.invoke(m_defaultValue, null);
                }
            } catch ( Exception e ) {
                Arrays.fill(values, m_size, capacity, m_defaultValue);
            }
            m_values = values;
        }
        m_size = nrows;
    }

    // ------------------------------------------------------------------------
    // Data Access Methods
    
    public void revertToDefault(int row) {
        try {
            // since Object's clone method is protected, we default to
            // using reflection to create clones.
            Cloneable def = (Cloneable)m_defaultValue;
            Method m = def.getClass().getMethod("clone", null);
            set(m.invoke(m_defaultValue, null), row);
        } catch ( Exception e ) {
            set(m_defaultValue, row);
        }
    }
    
    /**
     * Get the data value at the specified row
     * @param row the row from which to retrieve the value
     * @return the data value
     */
    public Object get(int row) {
        if ( row < 0 || row > m_size ) {
            throw new IllegalArgumentException(
                "Row index out of bounds: "+row);
        }
        return m_values[row];
    }
        
    /**
     * Set the data value at the specified row
     * @param val the value to set
     * @param row the row at which to set the value
     */
    public void set(Object val, int row) {
        if ( m_readOnly ) {
            throw new DataReadOnlyException();
        } else if ( row < 0 || row > m_size ) {
            throw new IllegalArgumentException(
                "Row index out of bounds: "+row);
        } else if ( val == null || canSet(val.getClass()) ) {
            // get the previous value
            Object prev = m_values[row];
            
            // exit early if no change
            // do we trust .equals() here? for now, no.
            if ( prev == val ) return;
            
            // set the new value
            m_values[row] = val;
            
            // fire a change event
            fireColumnEvent(row, prev);
        } else {
            throw new DataTypeException(val.getClass());
        }
    }
    
} // end of class ObjectColumn
