/**
 * Decorates a <code>ResultSet</code> with checks for a SQL NULL value on each <code>getXXX</code>
 * method. If a column value obtained by a <code>getXXX</code> method is not SQL NULL, the column
 * value is returned. If the column value is SQL null, an alternate value is returned. The alternate
 * value defaults to the Java <code>null</code> value, which can be overridden for instances of the
 * class.
 *
 * <p>Usage example:
 *
 * <blockquote>
 *
 * <pre>
 * Connection conn = // somehow get a connection
 * Statement stmt = conn.createStatement();
 * ResultSet rs = stmt.executeQuery("SELECT col1, col2 FROM table1");
 *
 * // Wrap the result set for SQL NULL checking
 * SqlNullCheckedResultSet wrapper = new SqlNullCheckedResultSet(rs);
 * wrapper.setNullString("---N/A---"); // Set null string
 * wrapper.setNullInt(-999); // Set null integer
 * rs = ProxyFactory.instance().createResultSet(wrapper);
 *
 * while (rs.next()) {
 *     // If col1 is SQL NULL, value returned will be "---N/A---"
 *     String col1 = rs.getString("col1");
 *     // If col2 is SQL NULL, value returned will be -999
 *     int col2 = rs.getInt("col2");
 * }
 * rs.close();
 * </pre>
 *
 * </blockquote>
 *
 * <p>Unlike some other classes in DbUtils, this class is NOT thread-safe.
 */
public class SqlNullCheckedResultSet implements InvocationHandler {

  /**
   * Maps normal method names (ie. "getBigDecimal") to the corresponding null Method object (ie.
   * getNullBigDecimal).
   */
  private static final Map<String, Method> nullMethods = new HashMap<String, Method>();

  /**
   * The {@code getNull} string prefix.
   *
   * @since 1.4
   */
  private static final String GET_NULL_PREFIX = "getNull";

  static {
    Method[] methods = SqlNullCheckedResultSet.class.getMethods();
    for (int i = 0; i < methods.length; i++) {
      String methodName = methods[i].getName();

      if (methodName.startsWith(GET_NULL_PREFIX)) {
        String normalName = "get" + methodName.substring(GET_NULL_PREFIX.length());
        nullMethods.put(normalName, methods[i]);
      }
    }
  }

  /** The factory to create proxies with. */
  private static final ProxyFactory factory = ProxyFactory.instance();

  /**
   * Wraps the <code>ResultSet</code> in an instance of this class. This is equivalent to:
   *
   * <pre>
   * ProxyFactory.instance().createResultSet(new SqlNullCheckedResultSet(rs));
   * </pre>
   *
   * @param rs The <code>ResultSet</code> to wrap.
   * @return wrapped ResultSet
   */
  public static ResultSet wrap(ResultSet rs) {
    return factory.createResultSet(new SqlNullCheckedResultSet(rs));
  }

  private InputStream nullAsciiStream = null;
  private BigDecimal nullBigDecimal = null;
  private InputStream nullBinaryStream = null;
  private Blob nullBlob = null;
  private boolean nullBoolean = false;
  private byte nullByte = 0;
  private byte[] nullBytes = null;
  private Reader nullCharacterStream = null;
  private Clob nullClob = null;
  private Date nullDate = null;
  private double nullDouble = 0.0;
  private float nullFloat = 0.0f;
  private int nullInt = 0;
  private long nullLong = 0;
  private Object nullObject = null;
  private Ref nullRef = null;
  private short nullShort = 0;
  private String nullString = null;
  private Time nullTime = null;
  private Timestamp nullTimestamp = null;
  private URL nullURL = null;

  /** The wrapped result. */
  private final ResultSet rs;

  /**
   * Constructs a new instance of <code>SqlNullCheckedResultSet</code> to wrap the specified <code>
   * ResultSet</code>.
   *
   * @param rs ResultSet to wrap
   */
  public SqlNullCheckedResultSet(ResultSet rs) {
    super();
    this.rs = rs;
  }

  /**
   * Returns the value when a SQL null is encountered as the result of invoking a <code>
   * getAsciiStream</code> method.
   *
   * @return the value
   */
  public InputStream getNullAsciiStream() {
    return this.nullAsciiStream;
  }

  /**
   * Returns the value when a SQL null is encountered as the result of invoking a <code>
   * getBigDecimal</code> method.
   *
   * @return the value
   */
  public BigDecimal getNullBigDecimal() {
    return this.nullBigDecimal;
  }

  /**
   * Returns the value when a SQL null is encountered as the result of invoking a <code>
   * getBinaryStream</code> method.
   *
   * @return the value
   */
  public InputStream getNullBinaryStream() {
    return this.nullBinaryStream;
  }

  /**
   * Returns the value when a SQL null is encountered as the result of invoking a <code>getBlob
   * </code> method.
   *
   * @return the value
   */
  public Blob getNullBlob() {
    return this.nullBlob;
  }

  /**
   * Returns the value when a SQL null is encountered as the result of invoking a <code>getBoolean
   * </code> method.
   *
   * @return the value
   */
  public boolean getNullBoolean() {
    return this.nullBoolean;
  }

  /**
   * Returns the value when a SQL null is encountered as the result of invoking a <code>getByte
   * </code> method.
   *
   * @return the value
   */
  public byte getNullByte() {
    return this.nullByte;
  }

  /**
   * Returns the value when a SQL null is encountered as the result of invoking a <code>getBytes
   * </code> method.
   *
   * @return the value
   */
  public byte[] getNullBytes() {
    if (this.nullBytes == null) {
      return null;
    }
    byte[] copy = new byte[this.nullBytes.length];
    System.arraycopy(this.nullBytes, 0, copy, 0, this.nullBytes.length);
    return copy;
  }

  /**
   * Returns the value when a SQL null is encountered as the result of invoking a <code>
   * getCharacterStream</code> method.
   *
   * @return the value
   */
  public Reader getNullCharacterStream() {
    return this.nullCharacterStream;
  }

  /**
   * Returns the value when a SQL null is encountered as the result of invoking a <code>getClob
   * </code> method.
   *
   * @return the value
   */
  public Clob getNullClob() {
    return this.nullClob;
  }

  /**
   * Returns the value when a SQL null is encountered as the result of invoking a <code>getDate
   * </code> method.
   *
   * @return the value
   */
  public Date getNullDate() {
    return this.nullDate != null ? new Date(this.nullDate.getTime()) : null;
  }

  /**
   * Returns the value when a SQL null is encountered as the result of invoking a <code>getDouble
   * </code> method.
   *
   * @return the value
   */
  public double getNullDouble() {
    return this.nullDouble;
  }

  /**
   * Returns the value when a SQL null is encountered as the result of invoking a <code>getFloat
   * </code> method.
   *
   * @return the value
   */
  public float getNullFloat() {
    return this.nullFloat;
  }

  /**
   * Returns the value when a SQL null is encountered as the result of invoking a <code>getInt
   * </code> method.
   *
   * @return the value
   */
  public int getNullInt() {
    return this.nullInt;
  }

  /**
   * Returns the value when a SQL null is encountered as the result of invoking a <code>getLong
   * </code> method.
   *
   * @return the value
   */
  public long getNullLong() {
    return this.nullLong;
  }

  /**
   * Returns the value when a SQL null is encountered as the result of invoking a <code>getObject
   * </code> method.
   *
   * @return the value
   */
  public Object getNullObject() {
    return this.nullObject;
  }

  /**
   * Returns the value when a SQL null is encountered as the result of invoking a <code>getRef
   * </code> method.
   *
   * @return the value
   */
  public Ref getNullRef() {
    return this.nullRef;
  }

  /**
   * Returns the value when a SQL null is encountered as the result of invoking a <code>getShort
   * </code> method.
   *
   * @return the value
   */
  public short getNullShort() {
    return this.nullShort;
  }

  /**
   * Returns the value when a SQL null is encountered as the result of invoking a <code>getString
   * </code> method.
   *
   * @return the value
   */
  public String getNullString() {
    return this.nullString;
  }

  /**
   * Returns the value when a SQL null is encountered as the result of invoking a <code>getTime
   * </code> method.
   *
   * @return the value
   */
  public Time getNullTime() {
    return this.nullTime;
  }

  /**
   * Returns the value when a SQL null is encountered as the result of invoking a <code>getTimestamp
   * </code> method.
   *
   * @return the value
   */
  public Timestamp getNullTimestamp() {
    return this.nullTimestamp != null ? new Timestamp(this.nullTimestamp.getTime()) : null;
  }

  /**
   * Returns the value when a SQL null is encountered as the result of invoking a <code>getURL
   * </code> method.
   *
   * @return the value
   */
  public URL getNullURL() {
    return this.nullURL;
  }

  /**
   * Intercepts calls to <code>get*</code> methods and calls the appropriate <code>getNull*</code>
   * method if the <code>ResultSet</code> returned <code>null</code>.
   *
   * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method,
   *     java.lang.Object[])
   * @param proxy Not used; all method calls go to the internal result set
   * @param method The method to invoke on the result set
   * @param args The arguments to pass to the result set
   * @return null checked result
   * @throws Throwable error
   */
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

    Object result = method.invoke(this.rs, args);

    Method nullMethod = nullMethods.get(method.getName());

    // Check nullMethod != null first so that we don't call wasNull()
    // before a true getter method was invoked on the ResultSet.
    return (nullMethod != null && this.rs.wasNull())
        ? nullMethod.invoke(this, (Object[]) null)
        : result;
  }

  /**
   * Sets the value to return when a SQL null is encountered as the result of invoking a <code>
   * getAsciiStream</code> method.
   *
   * @param nullAsciiStream the value
   */
  public void setNullAsciiStream(InputStream nullAsciiStream) {
    this.nullAsciiStream = nullAsciiStream;
  }

  /**
   * Sets the value to return when a SQL null is encountered as the result of invoking a <code>
   * getBigDecimal</code> method.
   *
   * @param nullBigDecimal the value
   */
  public void setNullBigDecimal(BigDecimal nullBigDecimal) {
    this.nullBigDecimal = nullBigDecimal;
  }

  /**
   * Sets the value to return when a SQL null is encountered as the result of invoking a <code>
   * getBinaryStream</code> method.
   *
   * @param nullBinaryStream the value
   */
  public void setNullBinaryStream(InputStream nullBinaryStream) {
    this.nullBinaryStream = nullBinaryStream;
  }

  /**
   * Sets the value to return when a SQL null is encountered as the result of invoking a <code>
   * getBlob</code> method.
   *
   * @param nullBlob the value
   */
  public void setNullBlob(Blob nullBlob) {
    this.nullBlob = nullBlob;
  }

  /**
   * Sets the value to return when a SQL null is encountered as the result of invoking a <code>
   * getBoolean</code> method.
   *
   * @param nullBoolean the value
   */
  public void setNullBoolean(boolean nullBoolean) {
    this.nullBoolean = nullBoolean;
  }

  /**
   * Sets the value to return when a SQL null is encountered as the result of invoking a <code>
   * getByte</code> method.
   *
   * @param nullByte the value
   */
  public void setNullByte(byte nullByte) {
    this.nullByte = nullByte;
  }

  /**
   * Sets the value to return when a SQL null is encountered as the result of invoking a <code>
   * getBytes</code> method.
   *
   * @param nullBytes the value
   */
  public void setNullBytes(byte[] nullBytes) {
    byte[] copy = new byte[nullBytes.length];
    System.arraycopy(nullBytes, 0, copy, 0, nullBytes.length);
    this.nullBytes = copy;
  }

  /**
   * Sets the value to return when a SQL null is encountered as the result of invoking a <code>
   * getCharacterStream</code> method.
   *
   * @param nullCharacterStream the value
   */
  public void setNullCharacterStream(Reader nullCharacterStream) {
    this.nullCharacterStream = nullCharacterStream;
  }

  /**
   * Sets the value to return when a SQL null is encountered as the result of invoking a <code>
   * getClob</code> method.
   *
   * @param nullClob the value
   */
  public void setNullClob(Clob nullClob) {
    this.nullClob = nullClob;
  }

  /**
   * Sets the value to return when a SQL null is encountered as the result of invoking a <code>
   * getDate</code> method.
   *
   * @param nullDate the value
   */
  public void setNullDate(Date nullDate) {
    this.nullDate = nullDate != null ? new Date(nullDate.getTime()) : null;
  }

  /**
   * Sets the value to return when a SQL null is encountered as the result of invoking a <code>
   * getDouble</code> method.
   *
   * @param nullDouble the value
   */
  public void setNullDouble(double nullDouble) {
    this.nullDouble = nullDouble;
  }

  /**
   * Sets the value to return when a SQL null is encountered as the result of invoking a <code>
   * getFloat</code> method.
   *
   * @param nullFloat the value
   */
  public void setNullFloat(float nullFloat) {
    this.nullFloat = nullFloat;
  }

  /**
   * Sets the value to return when a SQL null is encountered as the result of invoking a <code>
   * getInt</code> method.
   *
   * @param nullInt the value
   */
  public void setNullInt(int nullInt) {
    this.nullInt = nullInt;
  }

  /**
   * Sets the value to return when a SQL null is encountered as the result of invoking a <code>
   * getLong</code> method.
   *
   * @param nullLong the value
   */
  public void setNullLong(long nullLong) {
    this.nullLong = nullLong;
  }

  /**
   * Sets the value to return when a SQL null is encountered as the result of invoking a <code>
   * getObject</code> method.
   *
   * @param nullObject the value
   */
  public void setNullObject(Object nullObject) {
    this.nullObject = nullObject;
  }

  /**
   * Sets the value to return when a SQL null is encountered as the result of invoking a <code>
   * getRef</code> method.
   *
   * @param nullRef the value
   */
  public void setNullRef(Ref nullRef) {
    this.nullRef = nullRef;
  }

  /**
   * Sets the value to return when a SQL null is encountered as the result of invoking a <code>
   * getShort</code> method.
   *
   * @param nullShort the value
   */
  public void setNullShort(short nullShort) {
    this.nullShort = nullShort;
  }

  /**
   * Sets the value to return when a SQL null is encountered as the result of invoking a <code>
   * getString</code> method.
   *
   * @param nullString the value
   */
  public void setNullString(String nullString) {
    this.nullString = nullString;
  }

  /**
   * Sets the value to return when a SQL null is encountered as the result of invoking a <code>
   * getTime</code> method.
   *
   * @param nullTime the value
   */
  public void setNullTime(Time nullTime) {
    this.nullTime = nullTime;
  }

  /**
   * Sets the value to return when a SQL null is encountered as the result of invoking a <code>
   * getTimestamp</code> method.
   *
   * @param nullTimestamp the value
   */
  public void setNullTimestamp(Timestamp nullTimestamp) {
    this.nullTimestamp = nullTimestamp != null ? new Timestamp(nullTimestamp.getTime()) : null;
  }

  /**
   * Sets the value to return when a SQL null is encountered as the result of invoking a <code>
   * getURL</code> method.
   *
   * @param nullURL the value
   */
  public void setNullURL(URL nullURL) {
    this.nullURL = nullURL;
  }
}