package nom.tam.util;

/*
 * This code is part of the Java FITS library developed 1996-2012 by T.A. McGlynn (NASA/GSFC)
 * The code is available in the public domain and may be copied, modified and used
 * by anyone in any fashion for any purpose without restriction. 
 * 
 * No warranty regarding correctness or performance of this code is given or implied.
 * Users may contact the author if they have questions or concerns.
 * 
 * The author would like to thank many who have contributed suggestions, 
 * enhancements and bug fixes including:
 * David Glowacki, R.J. Mathar, Laurent Michel, Guillaume Belanger,
 * Laurent Bourges, Rose Early, Fred Romelfanger, Jorgo Baker, A. Kovacs, V. Forchi, J.C. Segovia,
 * Booth Hartley and Jason Weiss.  
 * I apologize to any contributors whose names may have been inadvertently omitted.
 * 
 *      Tom McGlynn
 */
import java.io.*;
import java.lang.reflect.Array;

/** A data table is conventionally considered to consist of rows and
 * columns, where the structure within each column is constant, but
 * different columns may have different structures.  I.e., structurally
 * columns may differ but rows are identical.
 * Typically tabular data is usually stored in row order which can
 * make it extremely difficult to access efficiently using Java.
 * This class provides efficient
 * access to data which is stored in row order and allows users to
 * get and set the elements of the table.
 * The table can consist only of arrays of primitive types.
 * Data stored in column order can
 * be efficiently read and written using the
 * BufferedDataXputStream classes.
 *
 * The table is represented entirely as a set of one-dimensional primitive
 * arrays.  For a given column, a row consists of some number of
 * contiguous elements of the array.  Each column is required to have
 * the same number of rows.
 */
public class ColumnTable implements DataTable {

    /** The columns to be read/written */
    private Object[] arrays;
    /** The number of elements in a row for each column */
    private int[] sizes;
    /** The number of rows */
    private int nrow;
    /** The number or rows to read/write in one I/O. */
    private int chunk;
    /** The size of a row in bytes */
    private int rowSize;
    /** The base type of each row (using the second character
     * of the [x class names of the arrays.
     */
    private char[] types;
    private Class[] bases;
    // The following arrays are used to avoid having to check
    // casts during the I/O loops.
    // They point to elements of arrays.
    private byte[][] bytePointers;
    private short[][] shortPointers;
    private int[][] intPointers;
    private long[][] longPointers;
    private float[][] floatPointers;
    private double[][] doublePointers;
    private char[][] charPointers;
    private boolean[][] booleanPointers;

    /** Create the object after checking consistency.
     * @param arrays  An array of one-d primitive arrays.
     * @param sizes   The number of elements in each row
     *                for the corresponding column
     */
    public ColumnTable(Object[] arrays, int[] sizes) throws TableException {
        setup(arrays, sizes);
    }

    /** Actually perform the initialization.
     */
    protected void setup(Object[] arrays, int[] sizes) throws TableException {

        checkArrayConsistency(arrays, sizes);
        getNumberOfRows();
        initializePointers();

    }

    /** Get the number of rows in the table.
     */
    public int getNRows() {
        return nrow;
    }

    /** Get the number of columns in the table.
     */
    public int getNCols() {
        return arrays.length;
    }

    /** Get a particular column.
     * @param col The column desired.
     * @return an object containing the column data desired.
     *         This will be an instance of a 1-d primitive array.
     */
    public Object getColumn(int col) {
        return arrays[col];
    }

    /**
     * Set the values in a particular column. The new values must match the old in
     * length but not necessarily in type.
     *
     * @param col
     *          The column to modify.
     * @param newColumn
     *          The new column data. This should be a primitive array.
     * @exception TableException
     *              Thrown when the new data is not commenserable with information
     *              in the table.
     */
    public void setColumn(int col, Object newColumn) throws TableException {

        boolean reset = newColumn.getClass() != arrays[col].getClass()
                || Array.getLength(newColumn) != Array.getLength(arrays[col]);
        arrays[col] = newColumn;
        if (reset) {
            setup(arrays, sizes);
        } else {
            // This is required, because otherwise the typed pointer may point to the old
            // array, which has been replaced by newColumn. Added by Jeroen de Jong, 1 Aug 2006
            initializePointers();
        }
    }

    /** Add a column */
    public void addColumn(Object newColumn, int size) throws TableException {

        String classname = newColumn.getClass().getName();
        nrow = checkColumnConsistency(newColumn, classname, nrow, size);

        rowSize += nrow * ArrayFuncs.getBaseLength(newColumn);

        getNumberOfRows();

        int ncol = arrays.length;

        Object[] newArrays = new Object[ncol + 1];
        int[] newSizes = new int[ncol + 1];
        Class[] newBases = new Class[ncol + 1];
        char[] newTypes = new char[ncol + 1];

        System.arraycopy(arrays, 0, newArrays, 0, ncol);
        System.arraycopy(sizes, 0, newSizes, 0, ncol);
        System.arraycopy(bases, 0, newBases, 0, ncol);
        System.arraycopy(types, 0, newTypes, 0, ncol);

        arrays = newArrays;
        sizes = newSizes;
        bases = newBases;
        types = newTypes;

        arrays[ncol] = newColumn;
        sizes[ncol] = size;
        bases[ncol] = ArrayFuncs.getBaseClass(newColumn);
        types[ncol] = classname.charAt(1);
        addPointer(newColumn);
    }

    /** Add a row to the table.  This method is very inefficient
     *  for adding multiple rows and should be avoided if possible.
     */
    public void addRow(Object[] row) throws TableException {

        if (arrays.length == 0) {

            for (int i = 0; i < row.length; i += 1) {
                addColumn(row[i], Array.getLength(row[i]));
            }

        } else {

            if (row.length != arrays.length) {
                throw new TableException("Row length mismatch");
            }

            for (int i = 0; i < row.length; i += 1) {
                if (row[i].getClass() != arrays[i].getClass()
                        || Array.getLength(row[i]) != sizes[i]) {
                    throw new TableException("Row column mismatch at column:" + i);
                }
                Object xarray = ArrayFuncs.newInstance(bases[i], (nrow + 1) * sizes[i]);
                System.arraycopy(arrays[i], 0, xarray, 0, nrow * sizes[i]);
                System.arraycopy(row[i], 0, xarray, nrow * sizes[i], sizes[i]);
                arrays[i] = xarray;
            }
            initializePointers();
            nrow += 1;
        }
    }

    /** Get a element of the table.
     * @param row The row desired.
     * @param col The column desired.
     * @return A primitive array containing the information.  Note
     *         that an array will be returned even if the element
     *         is a scalar.
     */
    public Object getElement(int row, int col) {

        Object x = ArrayFuncs.newInstance(bases[col], sizes[col]);
        System.arraycopy(arrays[col], sizes[col] * row, x, 0, sizes[col]);
        return x;
    }

    /** Modify an element of the table.
     * @param row The row containing the element.
     * @param col The column containing the element.
     * @param x   The new datum.  This should be 1-d primitive
     *            array.
     * @exception TableException Thrown when the new data
     *                           is not of the same type as
     *                           the data it replaces.
     */
    public void setElement(int row, int col, Object x)
            throws TableException {

        String classname = x.getClass().getName();

        if (!classname.equals("[" + types[col])) {
            throw new TableException("setElement: Incompatible element type");
        }

        if (Array.getLength(x) != sizes[col]) {
            throw new TableException("setElement: Incompatible element size");
        }

        System.arraycopy(x, 0, arrays[col], sizes[col] * row, sizes[col]);
    }

    /** Get a row of data.
     * @param  row The row desired.
     * @return An array of objects each containing a primitive array.
     */
    public Object getRow(int row) {

        Object[] x = new Object[arrays.length];
        for (int col = 0; col < arrays.length; col += 1) {
            x[col] = getElement(row, col);
        }
        return x;
    }

    /** Modify a row of data.
     * @param row The row to be modified.
     * @param x   The data to be modified.  This should be an
     *            array of objects.  It is described as an Object
     *            here since other table implementations may
     *            use other methods to store the data (e.g., @see nom.tam.util.ColumnTable)
     */
    public void setRow(int row, Object x) throws TableException {

        if (!(x instanceof Object[])) {
            throw new TableException("setRow: Incompatible row");
        }

        for (int col = 0; col < arrays.length; col += 1) {
            setElement(row, col, ((Object[]) x)[col]);
        }
    }

    /** Check that the columns and sizes are consistent.
     * Inconsistencies include:
     * <ul>
     * <li> arrays and sizes have different lengths.
     * <li> an element of arrays is not a primitive array.
     * <li> the size of an array is not divisible by the sizes entry.
     * <li> the number of rows differs for the columns.
     * </ul>
     * @param arrays The arrays defining the columns.
     * @param sizes  The number of elements in each row for the column.
     */
    protected void checkArrayConsistency(Object[] arrays, int[] sizes)
            throws TableException {

        // This routine throws an error if it detects an inconsistency
        // between the arrays being read in.

        // First check that the lengths of the two arrays are the same.
        if (arrays.length != sizes.length) {
            throw new TableException("readArraysAsColumns: Incompatible arrays and sizes.");
        }

        // Now check that that we fill up all of the arrays exactly.
        int ratio = 0;
        int rowSize = 0;

        this.types = new char[arrays.length];
        this.bases = new Class[arrays.length];

        // Check for a null table.
        boolean nullTable = true;

        for (int i = 0; i < arrays.length; i += 1) {

            String classname = arrays[i].getClass().getName();

            ratio = checkColumnConsistency(arrays[i], classname, ratio, sizes[i]);

            rowSize += sizes[i] * ArrayFuncs.getBaseLength(arrays[i]);
            types[i] = classname.charAt(1);
            bases[i] = ArrayFuncs.getBaseClass(arrays[i]);
        }

        this.nrow = ratio;
        this.rowSize = rowSize;
        this.arrays = arrays;
        this.sizes = sizes;
    }

    private int checkColumnConsistency(Object data, String classname, int ratio, int size)
            throws TableException {


        if (classname.charAt(0) != '[' || classname.length() != 2) {
            throw new TableException("Non-primitive array for column");
        }

        int thisSize = Array.getLength(data);
        if ((thisSize == 0 && size != 0 && ratio != 0)
                || (thisSize != 0 && size == 0)) {
            throw new TableException("Size mismatch in column: " + thisSize + " != " + size);
        }

        // The row size must evenly divide the size of the array.
        if (size != 0 && thisSize % size != 0) {
            throw new TableException("Row size does not divide array for column");
        }

        // Finally the ratio of sizes must be the same for all columns -- this
        // is the number of rows in the table.
        int thisRatio = 0;
        if (size > 0) {
            thisRatio = thisSize / size;

            if (ratio != 0 && (thisRatio != ratio)) {
                throw new TableException("Different number of rows in different columns");
            }
        }
        if (thisRatio > 0) {
            return thisRatio;
        } else {
            return ratio;
        }

    }

    /** Calculate the number of rows to read/write at a time.
     */
    protected void getNumberOfRows() {

        int bufSize = 65536;

        // If a row is larger than bufSize, then read one row at a time.
        if (rowSize == 0) {
            this.chunk = 0;

        } else if (rowSize > bufSize) {
            this.chunk = 1;

            // If the entire set is not too big, just read it all.
        } else if (bufSize / rowSize >= nrow) {
            this.chunk = nrow;
        } else {
            this.chunk = bufSize / rowSize + 1;
        }

    }

    /** Set the pointer arrays for the eight primitive types
     * to point to the appropriate elements of arrays.
     */
    protected void initializePointers() {

        int nbyte, nshort, nint, nlong, nfloat, ndouble, nchar, nboolean;

        // Count how many of each type we have.
        nbyte = 0;
        nshort = 0;
        nint = 0;
        nlong = 0;
        nfloat = 0;
        ndouble = 0;
        nchar = 0;
        nboolean = 0;

        for (int col = 0; col < arrays.length; col += 1) {
            switch (types[col]) {

                case 'B':
                    nbyte += 1;
                    break;
                case 'S':
                    nshort += 1;
                    break;
                case 'I':
                    nint += 1;
                    break;
                case 'J':
                    nlong += 1;
                    break;
                case 'F':
                    nfloat += 1;
                    break;
                case 'D':
                    ndouble += 1;
                    break;
                case 'C':
                    nchar += 1;
                    break;
                case 'Z':
                    nboolean += 1;
                    break;
            }
        }

        // Allocate the pointer arrays.  Note that many will be
        // zero-length.

        bytePointers = new byte[nbyte][];
        shortPointers = new short[nshort][];
        intPointers = new int[nint][];
        longPointers = new long[nlong][];
        floatPointers = new float[nfloat][];
        doublePointers = new double[ndouble][];
        charPointers = new char[nchar][];
        booleanPointers = new boolean[nboolean][];

        // Now set the pointers.
        nbyte = 0;
        nshort = 0;
        nint = 0;
        nlong = 0;
        nfloat = 0;
        ndouble = 0;
        nchar = 0;
        nboolean = 0;

        for (int col = 0; col < arrays.length; col += 1) {
            switch (types[col]) {

                case 'B':
                    bytePointers[nbyte] = (byte[]) arrays[col];
                    nbyte += 1;
                    break;
                case 'S':
                    shortPointers[nshort] = (short[]) arrays[col];
                    nshort += 1;
                    break;
                case 'I':
                    intPointers[nint] = (int[]) arrays[col];
                    nint += 1;
                    break;
                case 'J':
                    longPointers[nlong] = (long[]) arrays[col];
                    nlong += 1;
                    break;
                case 'F':
                    floatPointers[nfloat] = (float[]) arrays[col];
                    nfloat += 1;
                    break;
                case 'D':
                    doublePointers[ndouble] = (double[]) arrays[col];
                    ndouble += 1;
                    break;
                case 'C':
                    charPointers[nchar] = (char[]) arrays[col];
                    nchar += 1;
                    break;
                case 'Z':
                    booleanPointers[nboolean] = (boolean[]) arrays[col];
                    nboolean += 1;
                    break;
            }
        }
    }

    // Add a pointer in the pointer lists.
    protected void addPointer(Object data) throws TableException {
        String classname = data.getClass().getName();
        char type = classname.charAt(1);

        switch (type) {
            case 'B': {
                byte[][] xb = new byte[bytePointers.length + 1][];
                System.arraycopy(bytePointers, 0, xb, 0, bytePointers.length);
                xb[bytePointers.length] = (byte[]) data;
                bytePointers = xb;
                break;
            }
            case 'Z': {
                boolean[][] xb = new boolean[booleanPointers.length + 1][];
                System.arraycopy(booleanPointers, 0, xb, 0, booleanPointers.length);
                xb[booleanPointers.length] = (boolean[]) data;
                booleanPointers = xb;
                break;
            }
            case 'S': {
                short[][] xb = new short[shortPointers.length + 1][];
                System.arraycopy(shortPointers, 0, xb, 0, shortPointers.length);
                xb[shortPointers.length] = (short[]) data;
                shortPointers = xb;
                break;
            }
            case 'C': {
                char[][] xb = new char[charPointers.length + 1][];
                System.arraycopy(charPointers, 0, xb, 0, charPointers.length);
                xb[charPointers.length] = (char[]) data;
                charPointers = xb;
                break;
            }
            case 'I': {
                int[][] xb = new int[intPointers.length + 1][];
                System.arraycopy(intPointers, 0, xb, 0, intPointers.length);
                xb[intPointers.length] = (int[]) data;
                intPointers = xb;
                break;
            }
            case 'J': {
                long[][] xb = new long[longPointers.length + 1][];
                System.arraycopy(longPointers, 0, xb, 0, longPointers.length);
                xb[longPointers.length] = (long[]) data;
                longPointers = xb;
                break;
            }
            case 'F': {
                float[][] xb = new float[floatPointers.length + 1][];
                System.arraycopy(floatPointers, 0, xb, 0, floatPointers.length);
                xb[floatPointers.length] = (float[]) data;
                floatPointers = xb;
                break;
            }
            case 'D': {
                double[][] xb = new double[doublePointers.length + 1][];
                System.arraycopy(doublePointers, 0, xb, 0, doublePointers.length);
                xb[doublePointers.length] = (double[]) data;
                doublePointers = xb;
                break;
            }
            default:
                throw new TableException("Invalid type for added column:" + classname);
        }
    }

    /** Read a table.
     * @param is The input stream to read from.
     */
    public int read(ArrayDataInput is) throws IOException {

        int currRow = 0;

        // While we have not finished reading the table..
        for (int row = 0; row < nrow; row += 1) {

            int ibyte = 0;
            int ishort = 0;
            int iint = 0;
            int ilong = 0;
            int ichar = 0;
            int ifloat = 0;
            int idouble = 0;
            int iboolean = 0;

            // Loop over the columns within the row.
            for (int col = 0; col < arrays.length; col += 1) {

                int arrOffset = sizes[col] * row;
                int size = sizes[col];

                switch (types[col]) {
                    // In anticpated order of use.
                    case 'I':
                        int[] ia = intPointers[iint];
                        iint += 1;
                        is.read(ia, arrOffset, size);
                        break;

                    case 'S':
                        short[] s = shortPointers[ishort];
                        ishort += 1;
                        is.read(s, arrOffset, size);
                        break;

                    case 'B':
                        byte[] b = bytePointers[ibyte];
                        ibyte += 1;
                        is.read(b, arrOffset, size);
                        break;

                    case 'F':
                        float[] f = floatPointers[ifloat];
                        ifloat += 1;
                        is.read(f, arrOffset, size);
                        break;

                    case 'D':
                        double[] d = doublePointers[idouble];
                        idouble += 1;
                        is.read(d, arrOffset, size);
                        break;

                    case 'C':
                        char[] c = charPointers[ichar];
                        ichar += 1;
                        is.read(c, arrOffset, size);
                        break;

                    case 'J':
                        long[] l = longPointers[ilong];
                        ilong += 1;
                        is.read(l, arrOffset, size);
                        break;

                    case 'Z':

                        boolean[] bool = booleanPointers[iboolean];
                        iboolean += 1;
                        is.read(bool, arrOffset, size);
                        break;
                }
            }
        }

        // All done if we get here...
        return rowSize * nrow;
    }




    /** Write a table.
     * @param os the output stream to write to.
     */
    public int write(ArrayDataOutput os) throws IOException {

        if (rowSize == 0) {
            return 0;
        }

        for (int row = 0; row < nrow; row += 1) {

            int ibyte = 0;
            int ishort = 0;
            int iint = 0;
            int ilong = 0;
            int ichar = 0;
            int ifloat = 0;
            int idouble = 0;
            int iboolean = 0;

            // Loop over the columns within the row.
            for (int col = 0; col < arrays.length; col += 1) {

                int arrOffset = sizes[col] * row;
                int size = sizes[col];

                switch (types[col]) {
                    // In anticpated order of use.
                    case 'I':
                        int[] ia = intPointers[iint];
                        iint += 1;
                        os.write(ia, arrOffset, size);
                        break;

                    case 'S':
                        short[] s = shortPointers[ishort];
                        ishort += 1;
                        os.write(s, arrOffset, size);
                        break;

                    case 'B':
                        byte[] b = bytePointers[ibyte];
                        ibyte += 1;
                        os.write(b, arrOffset, size);
                        break;

                    case 'F':
                        float[] f = floatPointers[ifloat];
                        ifloat += 1;
                        os.write(f, arrOffset, size);
                        break;

                    case 'D':
                        double[] d = doublePointers[idouble];
                        idouble += 1;
                        os.write(d, arrOffset, size);
                        break;

                    case 'C':
                        char[] c = charPointers[ichar];
                        ichar += 1;
                        os.write(c, arrOffset, size);
                        break;

                    case 'J':
                        long[] l = longPointers[ilong];
                        ilong += 1;
                        os.write(l, arrOffset, size);
                        break;

                    case 'Z':
                        boolean[] bool = booleanPointers[iboolean];
                        iboolean += 1;
                        os.write(bool, arrOffset, size);
                        break;
                }

            }

        }

        // All done if we get here...
        return rowSize * nrow;
    }

    /** Get the base classes of the columns.
     * @return An array of Class objects, one for each column.
     */
    public Class[] getBases() {
        return bases;
    }

    /** Get the characters describing the base classes of the columns.
     * @return An array of char's, one for each column.
     */
    public char[] getTypes() {
        return types;
    }

    /** Get the actual data arrays */
    public Object[] getColumns() {
        return arrays;
    }

    public int[] getSizes() {
        return sizes;
    }

    /** Delete a row from the table.
     *  @param row  The row (0-indexed) to be deleted.
     */
    public void deleteRow(int row) throws TableException {
        deleteRows(row, 1);
    }

    /** Delete a contiguous set of rows from the table.
     *  @param row    The row (0-indexed) to be deleted.
     *  @param length The number of rows to be deleted.
     *  @throws TableException if the request goes outside
     *          the boundaries of the table or if the length is negative.
     */
    public void deleteRows(int row, int length) throws TableException {

        if (row < 0 || length < 0 || row + length > nrow) {
            throw new TableException("Invalid request to delete rows start: " + row + " length:" + length
                    + " for table with " + nrow + " rows.");
        }

        if (length == 0) {
            return;
        }

        for (int col = 0; col < arrays.length; col += 1) {

            int sz = sizes[col];
            int newSize = sz * (nrow - length);
            Object newArr = ArrayFuncs.newInstance(bases[col], newSize);

            // Copy whatever comes before the deletion
            System.arraycopy(arrays[col], 0, newArr, 0, row * sz);

            // Copy whatever comes after the deletion
            System.arraycopy(arrays[col], (row + length) * sz, newArr, row * sz, (nrow - row - length) * sz);
            arrays[col] = newArr;
        }
        nrow -= length;
        initializePointers();
    }

    /** Delete a contiguous set of columns from the table.
     *  @param start  The first column (0-indexed) to be deleted.
     *  @param len    The number of columns to be deleted.
     *  @throws TableException if the request goes outside
     *          the boundaries of the table or if the length is negative.
     */
    public int deleteColumns(int start, int len) throws TableException {

        int ncol = arrays.length;

        if (start < 0 || len < 0 || start + len > ncol) {
            throw new TableException("Invalid request to delete columns start: " + start + " length:" + len
                    + " for table with " + ncol + " columns.");
        }

        if (len == 0) {
            return rowSize;
        }

        for (int i = start; i < start + len; i += 1) {
            rowSize -= sizes[i] * ArrayFuncs.getBaseLength(arrays[i]);
        }

        int ocol = ncol;
        ncol -= len;

        Object[] newArrays = new Object[ncol];
        int[] newSizes = new int[ncol];
        Class[] newBases = new Class[ncol];
        char[] newTypes = new char[ncol];

        System.arraycopy(arrays, 0, newArrays, 0, start);
        System.arraycopy(sizes, 0, newSizes, 0, start);
        System.arraycopy(bases, 0, newBases, 0, start);
        System.arraycopy(types, 0, newTypes, 0, start);

        int rem = ocol - (start + len);

        System.arraycopy(arrays, start + len, newArrays, start, rem);
        System.arraycopy(sizes, start + len, newSizes, start, rem);
        System.arraycopy(bases, start + len, newBases, start, rem);
        System.arraycopy(types, start + len, newTypes, start, rem);


        arrays = newArrays;
        sizes = newSizes;
        bases = newBases;
        types = newTypes;

        initializePointers();
        return rowSize;
    }
}
