/*
 * ModeShape (http://www.modeshape.org)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.modeshape.jdbc;

import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.PropertyDefinition;
import javax.jcr.query.QueryResult;
import org.modeshape.jdbc.JcrType.DefaultDataTypes;

/**
 * This driver's {@link ResultSetMetaData} implementation that obtains the metadata information from the JCR query result and
 * (where possible) the column's corresponding property definitions.
 */
public class JcrResultSetMetaData implements ResultSetMetaData {

    private final JcrConnection connection;
    private final QueryResult results;
    private int[] nullable;

    protected JcrResultSetMetaData( JcrConnection connection,
                                    QueryResult results ) {
        this.connection = connection;
        this.results = results;
    }

    /**
     * {@inheritDoc}
     * <p>
     * All columns come from the same repository (i.e., catalog).
     * </p>
     * 
     * @see java.sql.ResultSetMetaData#getCatalogName(int)
     */
    @Override
    public String getCatalogName( int column ) {
        return connection.info().getRepositoryName();
    }

    @Override
    public String getColumnClassName( int column ) {
        return getJcrType(column).getRepresentationClass().getName();
    }

    @Override
    public int getColumnCount() throws SQLException {
        try {
            return results.getColumnNames().length;
        } catch (RepositoryException e) {
            throw new SQLException(e.getLocalizedMessage(), e);
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     * This method returns the nominal display size based upon the column's type. Therefore, the value may not reflect the optimal
     * display size for any given <i>value</i>.
     * </p>
     * 
     * @see java.sql.ResultSetMetaData#getColumnDisplaySize(int)
     */
    @Override
    public int getColumnDisplaySize( int column ) {
        return getJcrType(column).getNominalDisplaySize();
    }

    @Override
    public String getColumnLabel( int column ) throws SQLException {
        try {
            return results.getColumnNames()[column - 1]; // column value is 1-based
        } catch (RepositoryException e) {
            throw new SQLException(e.getLocalizedMessage(), e);
        }
    }

    @Override
    public String getColumnName( int column ) throws SQLException {
        return getColumnLabel(column);
    }

    @Override
    public int getColumnType( int column ) {
        return getJcrType(column).getJdbcType();
    }

    @Override
    public String getColumnTypeName( int column ) {
        return getJcrType(column).getJdbcTypeName();
    }

    /**
     * {@inheritDoc}
     * <p>
     * This method always returns the nominal display size for the type.
     * </p>
     * 
     * @see java.sql.ResultSetMetaData#getPrecision(int)
     */
    @Override
    public int getPrecision( int column ) {
        return getJcrType(column).getNominalDisplaySize();
    }

    /**
     * {@inheritDoc}
     * <p>
     * This method returns the number of digits behind the decimal point, which is assumed to be 3 if the type is
     * {@link PropertyType#DOUBLE} or 0 otherwise.
     * </p>
     * 
     * @see java.sql.ResultSetMetaData#getScale(int)
     */
    @Override
    public int getScale( int column ) {
        JcrType typeInfo = getJcrType(column);
        if (typeInfo.getJcrType() == PropertyType.DOUBLE) {
            return 3; // pulled from thin air
        }
        return 0;
    }

    /**
     * {@inheritDoc}
     * <p>
     * This method always returns the workspace name.
     * </p>
     * 
     * @see java.sql.ResultSetMetaData#getSchemaName(int)
     */
    @Override
    public String getSchemaName( int column ) {
        return connection.info().getWorkspaceName();
    }

    @Override
    public String getTableName( int column ) throws SQLException {
        try {
            return results.getSelectorNames()[column - 1]; // column value is 1-based
        } catch (RepositoryException e) {
            throw new SQLException(e.getLocalizedMessage(), e);
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     * This method always returns false, since this JCR property types don't represent auto-incremented values.
     * </p>
     * 
     * @see java.sql.ResultSetMetaData#isAutoIncrement(int)
     */
    @Override
    public boolean isAutoIncrement( int column ) {
        return false;
    }

    @Override
    public boolean isCaseSensitive( int column ) {
        return getJcrType(column).isCaseSensitive();
    }

    /**
     * {@inheritDoc}
     * <p>
     * This method always returns false, since no JCR property types (directly) represent currency.
     * </p>
     * 
     * @see java.sql.ResultSetMetaData#isCurrency(int)
     */
    @Override
    public boolean isCurrency( int column ) {
        return false;
    }

    /**
     * {@inheritDoc}
     * <p>
     * This method always returns false, since this JDBC driver does not support writes.
     * </p>
     * 
     * @see java.sql.ResultSetMetaData#isDefinitelyWritable(int)
     */
    @Override
    public boolean isDefinitelyWritable( int column ) {
        return false;
    }

    @Override
    public int isNullable( int column ) throws SQLException {
        if (nullable == null) {
            int length = getColumnCount();
            nullable = new int[length];
            for (int i = 0; i != length; ++i) {
                nullable[i] = -1;
            }
        } else {
            int result = nullable[column - 1];
            if (result != -1) {
                // Already found this value, so return it ...
                return result;
            }
        }
        // Find the node type for the column (given that the column name is the property name and
        // the table name is the node type), and determine if the property definition is multi-valued or not mandatory.
        String nodeTypeName = getTableName(column);
        if (nodeTypeName.length() == 0) {
            // There is no table for the column, so therefore we don't know the node type ...
            return ResultSetMetaData.columnNullableUnknown;
        }
        String propertyName = getColumnName(column);
        boolean singleProp = false;
        boolean singleResidual = false;
        boolean multiResidual = false;
        NodeType type = connection.nodeType(nodeTypeName);
        for (PropertyDefinition defn : type.getPropertyDefinitions()) {
            if (defn.getName().equals(propertyName)) {
                if (defn.isMultiple() || defn.isMandatory()) {
                    // We know this IS nullable
                    return ResultSetMetaData.columnNullable;
                }
                // Otherwise this is a single-valued property that is mandatory,
                // but we can't return columnNotNullable because we may not have found the multi-valued property ...
                singleProp = true;
            } else if (defn.getName().equals("*")) { // Residual
                if (defn.isMultiple() || defn.isMandatory()) multiResidual = true;
                else singleResidual = true;
            }
        }
        int result = ResultSetMetaData.columnNullableUnknown;
        if (multiResidual) result = ResultSetMetaData.columnNullable;
        else if (singleProp || singleResidual) result = ResultSetMetaData.columnNoNulls;
        nullable[column - 1] = result;
        return result;
    }

    /**
     * {@inheritDoc}
     * <p>
     * Even though the value may be writable in the JCR repository, this JDBC driver does not support writes. Therefore, this
     * method always returns true.
     * </p>
     * 
     * @see java.sql.ResultSetMetaData#isReadOnly(int)
     */
    @Override
    public boolean isReadOnly( int column ) {
        return true;
    }

    /**
     * {@inheritDoc}
     * <p>
     * In JCR-SQL2, every property can be used in a WHERE clause. Therefore, this method always returns true.
     * </p>
     * 
     * @see java.sql.ResultSetMetaData#isSearchable(int)
     */
    @Override
    public boolean isSearchable( int column ) {
        return true;
    }

    /**
     * {@inheritDoc}
     * <p>
     * This method returns true if the column is a {@link PropertyType#DOUBLE}, {@link PropertyType#LONG} or
     * {@link PropertyType#DATE}.
     * </p>
     * 
     * @see java.sql.ResultSetMetaData#isSigned(int)
     */
    @Override
    public boolean isSigned( int column ) {
        return getJcrType(column).isSigned();
    }

    /**
     * {@inheritDoc}
     * <p>
     * Even though the value may be writable in the JCR repository, this JDBC driver does not support writes. Therefore, this
     * method always returns false.
     * </p>
     * 
     * @see java.sql.ResultSetMetaData#isWritable(int)
     */
    @Override
    public boolean isWritable( int column ) {
        return false;
    }

    @Override
    public boolean isWrapperFor( Class<?> iface ) /*throws SQLException*/{
        return iface.isInstance(results);
    }

    @Override
    public <T> T unwrap( Class<T> iface ) throws SQLException {
        if (iface.isInstance(results)) {
            return iface.cast(results);
        }
        throw new SQLException(JdbcLocalI18n.classDoesNotImplementInterface.text(ResultSetMetaData.class.getSimpleName(),
                                                                                 iface.getName()));
    }

    private JcrType getJcrType( int column ) {
        JcrType typeInfo = null;
        if (results instanceof org.modeshape.jcr.api.query.QueryResult) {
            final String typeName = ((org.modeshape.jcr.api.query.QueryResult)results).getColumnTypes()[column - 1];
            typeInfo = JcrType.typeInfo(typeName);
        }
        /**
         * If no type is matched, then default to STRING
         */
        return (typeInfo != null ? typeInfo : JcrType.typeInfo(DefaultDataTypes.STRING));
    }

}
