/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 flex.messaging.io;

import flex.messaging.util.UUIDUtils;

import javax.sql.RowSet;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * A wrapper for a RowSet to make it pageable. This technique is recommended
 * if the RowSet is 'too big' to download all at once, AND the developer's
 * client-side code is capable of dealing with non-fully-populated recordsets,
 * that is, ActionScript RecordSets which are missing some of their data.
 *
 * @version 1.0
 */
public class PagedRowSet implements PageableRowSet {
    private RowSet rowSet;
    private String[] colNames;
    private int pageSize = 50; //Default to 50 records a page
    private int colCount = 0;
    private int rowCount = 0;

    private String id = null;
    private String serviceName = null;

    /**
     * Pageable Rowset Service Name.
     */
    public static final String DEFAULT_PAGING_SERVICE_NAME = "PageableRowSetCache";


    /**
     * Constructor
     * <p>
     * Creates a UUID for this object. Format: `XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'
     * </p>
     *
     * @param r The RowSet to be paged.
     * @param p The initial page size.
     */
    public PagedRowSet(RowSet r, int p) {
        serviceName = DEFAULT_PAGING_SERVICE_NAME;
        rowSet = r;
        pageSize = p;
        id = UUIDUtils.createUUID();
        init();
    }

    /**
     * Allows the unique id generation of the RowSet to be toggled.
     *
     * @param r        the row set
     * @param p        the page size
     * @param createID should we create an id?
     * @see #PagedRowSet(RowSet, int)
     */
    public PagedRowSet(RowSet r, int p, boolean createID) {
        serviceName = DEFAULT_PAGING_SERVICE_NAME;
        rowSet = r;
        pageSize = p;
        if (createID) {
            id = UUIDUtils.createUUID();
        }
        init();
    }

    private void init() {
        if (rowSet != null) {
            //Initialize columns
            initColumns();

            //Initialize records
            initRecords();
        } else {
            colNames = new String[0];
        }
    }


    private synchronized void initColumns() {
        try {
            ResultSetMetaData rsmd = rowSet.getMetaData();
            if (rsmd != null) {
                colCount = rsmd.getColumnCount();
            }
        } catch (SQLException ex) {
            colCount = 0;
        }
    }


    private synchronized void initRecords() {
        //Determine rs size
        if (rowSet != null) {
            try {
                int currentIndex = rowSet.getRow();

                //Go to the end and get that row number
                if (rowSet.last()) {
                    rowCount = rowSet.getRow();
                }

                //Put the cursor back
                if (currentIndex > 0) {
                    rowSet.absolute(currentIndex);
                } else {
                    rowSet.beforeFirst();
                }
            } catch (SQLException ex) {
                //TODO: Decide whether if absolute() not be supported, try first() as a last resort??
                try {
                    rowSet.first();
                } catch (SQLException se) {
                    //we won't try anymore.
                }
            }
        }
    }


    /**
     * List the column names of the result set.
     *
     * @return String[] An array of the column names as strings, as ordered
     * by the result set provider's column number assignment.
     */
    public synchronized String[] getColumnNames() {
        //  Cache the column names lookup
        if (colNames == null) {
            try {
                //Ensure column count is initialized
                if (colCount == 0) {
                    initColumns();
                }

                colNames = new String[colCount];

                for (int i = 0; i < colCount; i++) {
                    //Note: column numbers start at 1
                    colNames[i] = rowSet.getMetaData().getColumnName(i + 1);
                }
            } catch (SQLException ex) {
                colNames = new String[0];
            }
        }

        // Create a copy
        String[] ret = new String[colNames.length];
        System.arraycopy(colNames, 0, ret, 0, colNames.length);
        return ret;
    }


    /**
     * Use this method to get a map of the index used to start the data page,
     * and an array of arrays of the actual data page itself.
     *
     * @param startIndex starting index
     * @param count      how many records to return
     * @return Map A map with two fields, the index of the row to start the page, and an array of
     * arrays for the actual data page.
     * @throws SQLException if unable to get data from the rowset
     */
    public synchronized Map getRecords(int startIndex, int count) throws SQLException {
        List aRecords = new ArrayList(); //Don't initialize with count as it could be Integer.MAX_VALUE

        //Ensure column count is initialized
        if (colCount == 0) {
            initColumns();
        }

        //Starting index cannot be less than 1
        if (startIndex < 1)
            startIndex = 1;

        //Populate the page, moving cursor to index
        if (rowSet.absolute(startIndex)) {
            //Loop over the result set for the count specified
            for (int i = 0; i < count; i++) {
                boolean hasNext;

                List row;

                if (colCount > 0) {
                    row = new ArrayList(rowCount + 1);
                    //Loop over columns to create an array for the row
                    for (int j = 1; j <= colCount; j++) {
                        Object data = rowSet.getObject(j);
                        if (data instanceof Clob) {
                            Clob clob = (Clob) data;
                            row.add(clob.getSubString(0, (int) clob.length()));
                        } else if (data instanceof Blob) {
                            Blob blob = (Blob) data;
                            byte[] bytes = blob.getBytes(1, (int) blob.length());
                            row.add(bytes);
                        } else
                            row.add(data);
                    }
                } else //HACK: Handle any ColdFusion Query Objects that have no column metadata!
                {
                    row = new ArrayList();

                    try {
                        //Get as many columns as possible to build the row
                        //Stop on error or the first null column returned.
                        for (int j = 1; j <= 50; j++) {
                            Object o = rowSet.getObject(j);
                            if (o != null) {
                                row.add(o);
                            } else {
                                break;
                            }
                        }
                    } catch (SQLException ex) {
                        //Stop looking and just add the row.
                    }
                }

                aRecords.add(row.toArray());

                hasNext = rowSet.next();

                //Cursor beyond last row, stop!
                if (!hasNext) {
                    break;
                }
            }
        }

        Map result = new HashMap(2);
        result.put(PAGE, aRecords.toArray());
        result.put(CURSOR, Integer.valueOf(startIndex));

        return result;
    }

    /**
     * Get the row count.
     *
     * @return int The total number of rows in the result set.
     */
    public int getRowCount() {
        return rowCount;
    }

    /**
     * If this function returns a number &gt;= the total number of rows in the result set,
     * then the result set should be simply returned to the client in full. However,
     * if it is &lt; the total size, then this object itself is saved in Session data,
     * and tagged with a unique ID.
     *
     * @return the page size
     */
    public int getInitialDownloadCount() {
        return pageSize;
    }

    /**
     * Return the id of this row set.
     *
     * @return the id
     */
    public String getID() {
        return id;
    }

    /**
     * Get the service name.
     *
     * @return String The name of the service that will manage this paged result.
     */
    public String getServiceName() {
        return serviceName;
    }

    /**
     * Set the service name.
     *
     * @param serviceName Update the name of the service that manages the pages for this query.
     */
    public void setServicename(String serviceName) {
        this.serviceName = serviceName;
    }
}
