/*
 * 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.util;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.SQLXML;
import java.sql.Types;
import javax.jcr.Value;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.hamcrest.core.IsInstanceOf;
import org.modeshape.jdbc.JcrResultSet;
import org.modeshape.jdbc.JcrType;

/**
 * This object wraps/extends a SQL ResultSet object as Reader object. Once the ResultSet can read as reader then it can be
 * persisted, printed or compared easily without lot of code hassle of walking it every time.
 * <p>
 * PS: remember this is a Reader not InputStream, so all the fields read going to be converted to strings before they returned.
 */
public class ResultSetReader extends StringLineReader {

    ResultSet source = null;
    ResultSetMetaData metadata = null;

    // Number of columns in the result set
    int columnCount = 0;

    // delimiter between the fields while reading each row
    String delimiter = null;

    boolean firstTime = true;
    int[] columnTypes = null;

    private int rowCount = 0;

    private boolean compareColumns = true;

    public ResultSetReader( ResultSet in,
                            String delimiter,
                            boolean compareCols ) {
        this.source = in;
        this.delimiter = delimiter;
        this.compareColumns = compareCols;
    }

    /**
     * @see java.io.Reader#close()
     */
    @Override
    public void close() throws IOException {
        try {
            source.close();
        } catch (SQLException e) {

        }
        super.close();
    }

    /**
     * Get the next line of results from the ResultSet. The first line will be the metadata of the resultset and then followed by
     * the result rows. Each row will be returned as one line.
     * 
     * @return next result line from result set.
     * @throws IOException
     */
    @Override
    protected String nextLine() throws IOException, SQLException {
        if (firstTime) {
            firstTime = false;
            this.metadata = source.getMetaData();
            columnCount = metadata.getColumnCount();
            columnTypes = new int[columnCount];
            for (int i = 0; i < columnCount; i++) {
                columnTypes[i] = metadata.getColumnType(i + 1);
            }
            return resultSetMetaDataToString(metadata, delimiter);
        }

        // if you get here then we are ready to read the results.
        if (source.next()) {
            rowCount++;

            final StringBuilder sb = new StringBuilder();
            // Walk through column values in this row
            for (int col = 1; col <= columnCount; col++) {
                // this does not work when database metadata is being queried
                final Object anObj = source.getObject(col);

                if (compareColumns) compareColumn(col, anObj);

                if (columnTypes[col - 1] == Types.CLOB) {
                    sb.append(anObj != null ? anObj : "null"); //$NON-NLS-1$
                } else if (columnTypes[col - 1] == Types.BLOB) {
                    sb.append(anObj != null ? "BLOB" : "null"); //$NON-NLS-1$ //$NON-NLS-2$
                } else if (columnTypes[col - 1] == Types.SQLXML) {
                    final SQLXML xml = (SQLXML)anObj;
                    sb.append(anObj != null ? prettyPrint(xml) : "null"); //$NON-NLS-1$
                } else {
                    sb.append(anObj != null ? anObj : "null"); //$NON-NLS-1$
                }
                if (col != columnCount) {
                    sb.append(delimiter);
                }
            }
            sb.append("\n"); //$NON-NLS-1$
            return sb.toString();
        }

        return null;
    }

    private void compareColumn( int col,
                                Object objIdx ) throws SQLException {

        String colName = metadata.getColumnName(col);
        Object objName = source.getObject(colName);

        assertThat(objIdx, is(objName));

        if (objIdx == null) return;

        if (source instanceof JcrResultSet) {
            Value v = ((JcrResultSet)source).getValue(col);
            JcrType jcrType = JcrType.typeInfo(v.getType());
            assertThat(objIdx, IsInstanceOf.instanceOf(jcrType.getRepresentationClass()));
        }
    }

    public int getRowCount() {
        return rowCount;
    }

    /**
     * Get the first line from the result set. This is the resultset metadata line where we gather the column names and their
     * types.
     * 
     * @param metadata
     * @param delimiter
     * @return String
     * @throws SQLException
     */
    public static String resultSetMetaDataToString( ResultSetMetaData metadata,
                                                    String delimiter ) throws SQLException {

        StringBuilder sb = new StringBuilder();
        int columnCount = metadata.getColumnCount();

        for (int col = 1; col <= columnCount; col++) {
            String colName = metadata.getColumnName(col);
            String colTypeName = metadata.getColumnTypeName(col);

            /**
             * performing specific checks to make sure these are defined as expected.
             */
            if (colName.equalsIgnoreCase("jcr:score")) {
                JcrType jcrType = JcrType.typeInfo(JcrType.DefaultDataTypes.DOUBLE);
                assertThat(colTypeName, is(jcrType.getJcrName()));
            } else if (colName.equalsIgnoreCase("mode:depth")) {
                JcrType jcrType = JcrType.typeInfo(JcrType.DefaultDataTypes.LONG);
                assertThat(colTypeName, is(jcrType.getJcrName()));
            }

            sb.append(colName).append("[") //$NON-NLS-1$
            .append(colTypeName).append("]"); //$NON-NLS-1$
            if (col != columnCount) {
                sb.append(delimiter);
            }
        }
        sb.append("\n"); //$NON-NLS-1$
        return sb.toString();
    }

    public static String prettyPrint( SQLXML xml ) throws SQLException {
        try {
            TransformerFactory transFactory = TransformerFactory.newInstance();
            transFactory.setAttribute("indent-number", new Integer(2)); //$NON-NLS-1$

            Transformer tf = transFactory.newTransformer();
            tf.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); //$NON-NLS-1$
            tf.setOutputProperty(OutputKeys.ENCODING, "UTF-8");//$NON-NLS-1$
            tf.setOutputProperty(OutputKeys.INDENT, "yes");//$NON-NLS-1$
            tf.setOutputProperty(OutputKeys.METHOD, "xml");//$NON-NLS-1$
            tf.setOutputProperty(OutputKeys.STANDALONE, "yes");//$NON-NLS-1$
            tf.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); //$NON-NLS-1$ //$NON-NLS-2$

            ByteArrayOutputStream out = new ByteArrayOutputStream();
            StreamResult xmlOut = new StreamResult(new BufferedOutputStream(out));
            tf.transform(xml.getSource(StreamSource.class), xmlOut);

            return out.toString();
        } catch (Exception e) {
            return xml.getString();
        }
    }
}
