/*
 * The Broad Institute
 * SOFTWARE COPYRIGHT NOTICE AGREEMENT
 * This is copyright (2007-2008) by the Broad Institute/Massachusetts Institute
 * of Technology.  It is licensed to You under the Gnu Public 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.opensource.org/licenses/gpl-2.0.php
 *
 * This software is supplied without any warranty or guaranteed support
 * whatsoever. Neither the Broad Institute nor MIT can be responsible for its
 * use, misuse, or functionality.
*/

/*
 * IGVTestReadH5Lib.java
 *
 * Tests reading an IGV H5 file using the low level C wrapper library.
 * This file maps java functions as directly as possible to the underlying
 * C functions.  It is much faster than the object API.
 */
package org.broad.igv.h5;

import java.io.FileNotFoundException;
import java.lang.reflect.Array;
import ncsa.hdf.hdf5lib.*;
import ncsa.hdf.hdf5lib.exceptions.*;
import org.apache.log4j.*;

/**
 *
 * @author jrobinso
 */
public class HDF5LocalWriter implements HDFWriter {

    private static Logger log = Logger.getLogger(HDF5LocalWriter.class);

    public static int RDWR = ncsa.hdf.hdf5lib.HDF5Constants.H5F_ACC_RDWR;

    public int createFile(String name) {
        try {
            return H5.H5Fcreate(name, HDF5Constants.H5F_ACC_TRUNC, HDF5Constants.H5P_DEFAULT, HDF5Constants.H5P_DEFAULT);
        } catch (HDF5LibraryException ex) {
            String msg = "Error creating HDF5 file";
            log.error(msg, ex);
            throw new DataAccessException(msg, ex);
        }
    }

    /**
     * Open an hdf5 file
     */
    public int openFile(String path) throws FileNotFoundException {
        return openFile(path, RDWR);
    }

    /**
     * Open an hdf5 file
     */
    public int openFile(String path, int mode) throws FileNotFoundException {
        try {
            return ncsa.hdf.hdf5lib.H5.H5Fopen(path, mode, ncsa.hdf.hdf5lib.HDF5Constants.H5P_DEFAULT);
        } catch (HDF5LibraryException ex) {
            String msg = "Error opening HDF5 file: " + path;
            log.error(msg, ex);
            throw new DataAccessException(msg, ex);
        }
    }

    /**
     * Close an hdf5 file
     */
    public void closeFile(int fileId) {
        try {
            H5.H5Fclose(fileId);
        } catch (HDF5LibraryException ex) {
            String msg = "Error closing HDF5 file";
            log.error(msg, ex);
            throw new DataAccessException(msg, ex);
        }
    }

    public int openDataset(int fileId, String dsName) {

        try {
            return H5.H5Dopen(fileId, dsName);
        } catch (HDF5SymbolTableException ex) {
            throw new ObjectNotFoundException("Error opening dataset: " + dsName);
        } catch (HDF5LibraryException ex) {
            log.error("Error opening dataset", ex);
            throw new RuntimeException(ex);
        }

    }

    /**
     * Close an hdf5 dataset
     */
    public void closeDataset(int datasetId) {

        try {
            H5.H5Dclose(datasetId);
        } catch (HDF5LibraryException ex) {
            log.error("Error closing dataset", ex);
            throw new RuntimeException("Error closing dataset");
        }

    }

    /**
     * Create a group with the given name at the given location.
     *   nodeId - the handle to a file or another group.  If nodeId refers to a file
     *           name should be an absolute path.  If nodeId referst to a group
     *           name should be relative to that group.
     *   name - the name of the new group
     *   @returns the identifier for the newly created group.
     */
    public int createGroup(int locId, String name) {
        try {
            return H5.H5Gcreate(locId, name, 0);
        } catch (HDF5LibraryException ex) {
            String msg = "Error creating group: " + name;
            log.error(msg, ex);
            throw new DataAccessException(msg, ex);
        }

    }

    public int openGroup(int locId, String name) {
        try {
            return H5.H5Gopen(locId, name);
        } catch (ncsa.hdf.hdf5lib.exceptions.HDF5SymbolTableException ex) {
            String msg = "Error opening group: " + name;
            if (log.isDebugEnabled()) {
                log.debug(msg, ex);
            }
            throw new ObjectNotFoundException(msg);
        } catch (HDF5LibraryException ex) {
            String msg = "Error opening group: " + name;
            log.error(msg, ex);
            throw new DataAccessException(msg, ex);
        }

    }

    /**
     * Close an hdf5 group
     */
    public void closeGroup(int groupId) {
        try {
            H5.H5Gclose(groupId);
        } catch (HDF5LibraryException ex) {
            String msg = "Error closing HDF5 group";
            log.error(msg, ex);
            throw new DataAccessException(msg, ex);
        }

    }

    /**
     * Create a dataset in the group identifed by nodeId
     *  dims = new long[]{nRows, nCols});
     **/
    public int createDataset(int locId, String name, int typeId, long[] dims) {
        try {
            int dataspace = H5.H5Screate_simple(dims.length, dims, null);
            return H5.H5Dcreate(locId, name, typeId, dataspace, HDF5Constants.H5P_DEFAULT);
        } catch (HDF5Exception ex) {
            String msg = "Error creating dataset: " + name;
            log.error(msg, ex);
            throw new DataAccessException(msg, ex);
        }
    }

    public void createAndWriteDataset(int nodeId, String dsName, float[][] data) {

        int typeInfo = HDF5Constants.H5T_NATIVE_FLOAT;
        int nRows = Array.getLength(data);
        if (nRows == 0) {
            log.info("Attempt to create zero length dataset: " + dsName);
        } else {
            long nCols = data[0].length;
            long[] dims = new long[]{nRows, nCols};
            int dataset = -1;
            try {
                dataset = createDataset(nodeId, dsName, typeInfo, dims);
                writeAllData(dataset, typeInfo, data);
            } finally {
                if (dataset > 0) {
                    closeDataset(dataset);
                }
            }

        }
    }

    /**
     * Create a dataset, write the supplied data, and close the dataset.
     *
     *  nodeId - parent node of the dataset, usually a group
     *  dsName - name of the dataset
     *  data - the data, either int[], long[], float[], double[], or String[].
     *
     *             int dataspace = H5.H5Screate_simple(dims.nRows, dims, null);
    return H5.H5Dcreate(locId, name, typeId, dataspace, HDF5Constants.H5P_DEFAULT);
     */
    public void createAndWriteVectorDataset(int nodeId, String dsName, Object data) {
        if (data instanceof String[]) {
            createAndWriteStringDataset(nodeId, dsName, (String[]) data);
        }

        int typeInfo = getTypeForArray(data);
        int length = Array.getLength(data);
        if (length == 0) {
            log.info("Attempt to create zero length dataset: " + dsName);
        } else {
            long[] dims = new long[]{length};
            int dataset = -1;
            try {
                dataset = createDataset(nodeId, dsName, typeInfo, dims);
                writeAllData(dataset, typeInfo, data);
            } finally {
                if (dataset > 0) {
                    closeDataset(dataset);
                }
            }

        }
    }

    /**
     * Returns the HDF typeId and array nRows for an array.  Info is returned as
     * a 2 element int array.  Yes, a class could be used but seems excessive
     * for this private method.
     */
    private int getTypeForArray(Object array) {

        if (array instanceof Integer || array instanceof int[] || array instanceof int[][]) {
            return HDF5Constants.H5T_NATIVE_INT;
        }

        if (array instanceof Long || array instanceof long[] || array instanceof long[][]) {
            return HDF5Constants.H5T_NATIVE_LLONG;
        }

        if (array instanceof Float || array instanceof float[] || array instanceof float[][]) {
            return HDF5Constants.H5T_NATIVE_FLOAT;
        }

        if (array instanceof Number || array instanceof double[] || array instanceof double[][]) {
            return HDF5Constants.H5T_NATIVE_DOUBLE;
        }

        if (array instanceof char[]) {
            return createStringDatatype(1);
        }

        String msg = "Error: No HDF Type for: " + array.getClass().getName();
        log.error(msg);
        throw new DataAccessException("No HDF Type for: " + array.getClass().getName());

    }

    /**
     * Create a dataset of  strings.
     * // TODO -- look at H5DwriteString
     */
    public void createAndWriteStringDataset(int locId, String dsName, String[] stringArray) {
        int maxSize = 0;
        for (String string : stringArray) {
            maxSize = Math.max(maxSize, string.length());
        }

        createAndWriteStringDataset(locId, dsName, stringArray, maxSize);

    }

    /**
     * Create a dataset of fixed width strings
     * //TODO look at
     */
    public void createAndWriteStringDataset(int locId, String dsName, String[] stringArray, int stringSize) {
        StringBuffer sb = new StringBuffer(stringArray.length * stringSize);
        for (int i = 0; i <
                stringArray.length; i++) {
            char[] chars = new char[stringSize];

            char[] tmp = stringArray[i].toCharArray();
            System.arraycopy(tmp, 0, chars, 0, tmp.length);
            sb.append(new String(chars));
        }

        byte[] buf = sb.toString().getBytes();

        // Reading string datatypes is not supported in the java api
        int dataType = createStringDatatype(stringSize);
        long[] dims = new long[]{stringArray.length};

        //int dataType = HDF5Constants.H5T_NATIVE_CHAR;
        //long[] dims = new long[]{stringArray.nRows, stringSize};

        int nameDS = -1;
        try {
            nameDS = createDataset(locId, dsName, dataType, dims);
            writeAllData(nameDS, dataType, buf);
        } finally {
            if (nameDS > 0) {
                closeDataset(nameDS);
            }
        }

    }

    public int createStringDatatype(int strLength) {
        try {
            int tid = H5.H5Tcopy(HDF5Constants.H5T_C_S1);
            H5.H5Tset_size(tid, strLength);
            return tid;
        } catch (HDF5LibraryException ex) {
            String msg = "Error creating string datatype. Length = " + strLength;
            log.error(msg, ex);
            throw new DataAccessException(msg, ex);
        }

    }

    /**
     * Write to a dataset
     *   datasetId - dataset handle
     *   data -  an array of primitives matching the data typeId
     *
     *       *
     */
    public void writeAllData(int datasetId, int type, Object data) {
        try {
            int status = H5.H5Dwrite(datasetId, type, HDF5Constants.H5S_ALL,
                    HDF5Constants.H5S_ALL, HDF5Constants.H5S_ALL, data);
            if (status != 0) {
                throw new DataAccessException("Error writing data: " + status);
            }

        } catch (HDF5Exception ex) {
            String msg = "Error writing data";
            log.error(msg, ex);
            throw new DataAccessException(msg, ex);
        }

    }

    /**
     * Convenience function for writing a row of float data to an assumed 2-d
     * dataset.
     * // TODO -- verification, check that dataset shape is 2-D, rowId and data.nRows are in bounds
     * @param datasetId
     * @param rowId
     * @param data
     */
    public void writeDataRow(int datasetId, int rowId, float [] data, int length) {
        try {

            int dataspace = getDataspace(datasetId);

            long[] offset = new long[2];
            long[] count = new long[2];
            offset[0] = rowId;
            offset[1] = 0;
            count[0] = 1;
            count[1] = length;

            int status1 = H5.H5Sselect_hyperslab(dataspace, HDF5Constants.H5S_SELECT_SET, offset, null, count, null);
            if (status1 != 0) {
                log.error("Error selecting hyberslab  Error code:" + status1);
                throw new DataAccessException("Error selecting hyberslab.  Error code: " + status1);
            }

            int memspace = H5.H5Screate_simple(1, new long[]{length}, null); //HDF5Constants.H5S_ALL;

            int type = getTypeForArray(data);

            int status = H5.H5Dwrite(datasetId, type, memspace,
                    dataspace, HDF5Constants.H5S_ALL, data);

            H5.H5Sclose(dataspace);

            if (status != 0) {
                throw new DataAccessException("Error writing data: " + status);
            }

        } catch (HDF5Exception ex) {
            String msg = "Error writing data row.  rowId= " + rowId + " length= " + length;
            ex.printStackTrace();
            log.error(msg, ex);
            throw new DataAccessException(msg, ex);
        }

    }

    /**
     * Convenience function for writing a single float value to an assumed 1-d
     * dataset.
     * // TODO -- verification, check that dataset shape is 1-D, cellId in bounds
     * @param datasetId
     * @param rowId
     * @param data
     */
    public void writeDataValue(int datasetId, int cellId, float value) {
        try {

            int dataspace = getDataspace(datasetId);

            long[] offset = new long[1];
            long[] count = new long[1];
            offset[0] = cellId;
            count[0] = 1;
            int status1 = H5.H5Sselect_hyperslab(dataspace, HDF5Constants.H5S_SELECT_SET, offset, null, count, null);

            int memspace = H5.H5Screate_simple(1, new long[]{1}, null); //HDF5Constants.H5S_ALL;

            int type = HDF5Constants.H5T_NATIVE_FLOAT;

            int status = H5.H5Dwrite(datasetId, type, memspace, dataspace,
                    HDF5Constants.H5S_ALL, new float[]{value});

            H5.H5Sclose(dataspace);

            if (status != 0) {
                throw new DataAccessException("Error writing data: " + status);
            }

        } catch (HDF5Exception ex) {
            String msg = "Error writing data value.  cellId= " + cellId + " value= " + value;
            log.error(msg, ex);
            throw new DataAccessException(msg, ex);
        }

    }

    public int deleteAttribute(int locId, String name) {
        try
        {
            int herr = H5.H5Adelete(locId, name);
            return herr;
        } catch (HDF5LibraryException ex)
        {
            String msg = "Error deleting attribute.  locId= " + locId + " name= " + name;
            //log.error(msg, ex);
            throw new DataAccessException(msg, ex);
        }
    }

    /**
     * Write the attribute (string/value pair).  The method attempts to assign
     * the correct HDF5 typeId based on the runtime class of the value.  The rules
     * for typeId assignement are
     *   Integer (int)     =>  H5T_NATIVE_INT
     *   Long (long)       =>  H5T_NATIVE_LLONG
     *   Float (float)     =>  H5T_NATIVE_FLOAT
     *   All other numeric =>  H5T_NATIVE_DOUBLE
     *   String            =>  H5T_C_S1
     */
    public int writeAttribute(int locId, String name, Object value) {
        try {
            int dataType = 0;
            Object buf = null;
            if (value instanceof Integer) {
                dataType = HDF5Constants.H5T_NATIVE_INT;
                buf =
                        new int[]{((Number) value).intValue()};
            } else if (value instanceof Long) {
                dataType = HDF5Constants.H5T_NATIVE_LLONG;
                buf =
                        new long[]{((Number) value).longValue()};
            } else if (value instanceof Float) {
                dataType = HDF5Constants.H5T_NATIVE_FLOAT;
                buf =
                        new float[]{((Number) value).floatValue()};
            } else if (value instanceof Number) {
                dataType = HDF5Constants.H5T_NATIVE_DOUBLE;
                buf =
                        new double[]{((Number) value).doubleValue()};
            } else {
                dataType = H5.H5Tcopy(H5.J2C(HDF5Constants.H5T_C_S1));
                buf =
                        value.toString().getBytes();
                H5.H5Tset_size(dataType, ((byte[]) buf).length);
            }

            int spaceId = H5.H5Screate(HDF5Constants.H5S_SCALAR);
            int attrId = H5.H5Acreate(locId, name, dataType, spaceId, HDF5Constants.H5P_DEFAULT);
            int status = H5.H5Awrite(attrId, dataType, buf);
            status =
                    H5.H5Sclose(spaceId);  // TODO check status
            status =
                    H5.H5Aclose(attrId);  // TODO check status
            return status;
        } catch (HDF5Exception ex) {
            String msg = "Error writing to dataset ";
            log.error(msg, ex);
            throw new DataAccessException(msg, ex);
        }

    }

    /**
     * Return the dataspace for the dataset
     */
    private int getDataspace(int datasetId) {
        try {
            return H5.H5Dget_space(datasetId);
        } catch (HDF5LibraryException ex) {
            String msg = "Error getting dataspace";
            log.error(msg);
            throw new DataAccessException(msg, ex);
        }

    }
}
