/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.broad.igv.h5;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UTFDataFormatException;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.log4j.Logger;

/**
 * Assumptions -  dataset and group names follow the convention
 *       /features/chr/zoom
 *   or  /data/chr/zoom
 *
 * @author jrobinso
 */
public class BinWriter extends BinBase implements HDFWriter {

    static private Logger log = Logger.getLogger(BinWriter.class);
    static private byte version = 1;
    int fileId;
    File file;
    //DataOutputStream dos = null;
    BufferedFileOutputStream fos = null;

    public BinWriter(File file) {
        this.file = file;
    }

    /**
     * The filename argument is required by the current interface.
     *
     * TODO eliminate passing of filename here.
     *
     * @param fullName
     * @return
     */
    public int createFile(String filename) {

        String fullpath = new File(filename).getAbsolutePath();
        if (!this.file.getAbsolutePath().equals(fullpath)) {
            throw new IllegalArgumentException("Filename mismatch.  Expected " + this.file.getAbsolutePath() +
                    " argument: " + filename);
        }

        try {
            fileId = file.getAbsolutePath().hashCode();
            addEntity(fileId, "/");

            fos = new BufferedFileOutputStream(new FileOutputStream(file));

            fos.write(version);

            return fileId;
        } catch (IOException ex) {
            log.error("Error creating file: " + filename, ex);
            throw new RuntimeException("Error creating file: " + filename, ex);
        }

    }

    /**
     * Write out the group and dataset index and close the underlying file.
     *
     * @param fileId
     */
    public void closeFile(int fileId) {
        if (this.fileId != fileId) {
            throw new IllegalArgumentException("FileId mismatch.  Expected " + this.fileId +
                    " argument: " + fileId);
        }

        try {
            this.writeGroups();
            fos.close();
        } catch (IOException ex) {
            log.error("Error closing file");
        }

    }

    private void writeDataset(Map<Dataset, Long> datasetPositions, Dataset dataset) throws
            IOException {

        datasetPositions.put(dataset, fos.position());

        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(buffer);
        dos.writeUTF(dataset.fullName);
        dos.writeUTF(dataset.type.toString());
        dos.writeInt(dataset.nRows);
        dos.writeInt(dataset.nColumns);
        dos.writeInt(dataset.attributes.size());
        for (Map.Entry<String, String> kvPair : dataset.attributes.entrySet()) {
            dos.writeUTF(kvPair.getKey());
            dos.writeUTF(kvPair.getValue());
        }
        for (int i = 0; i < dataset.nRows; i++) {
            dos.writeLong(dataset.rowPositions[i]);
        }

        byte[] bytes = buffer.toByteArray();
        writeInt(bytes.length);
        write(bytes);
    }

    void writeShort(short v) throws IOException {
        fos.write((v >>> 8) & 0xFF);
        fos.write((v >>> 0) & 0xFF);
    }

    void writeInt(int v) throws IOException {
        fos.write((v >>> 24) & 0xFF);
        fos.write((v >>> 16) & 0xFF);
        fos.write((v >>> 8) & 0xFF);
        fos.write((v >>> 0) & 0xFF);
    }

    void write(byte [] bytes) throws IOException {
        fos.write(bytes);
    }

    private void writeGroups() throws IOException {


        // Write out datasets.  Remember position for index
        Map<Dataset, Long> datasetPositions = new LinkedHashMap();

        for (Dataset dataset : datasetCache.values()) {
            writeDataset(datasetPositions, dataset);
        }

        long indexStartPosition = fos.position();

        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(buffer);
        // Now write out dataset index
        dos.writeInt(datasetPositions.size());
        for (Map.Entry<Dataset, Long> entry : datasetPositions.entrySet()) {
            dos.writeUTF(entry.getKey().fullName);
            dos.writeLong(entry.getValue());
        }

// Write out groups
        int n = 0;
        for (Group group : groupCache.values()) {
            if (!group.isEmpty()) {
                n++;
            }

        }
        dos.writeInt(n);
        for (Group group : groupCache.values()) {
            if (!group.isEmpty()) {
                dos.writeUTF(group.fullName);
                dos.writeInt(group.attributes.size());
                for (Map.Entry<String, String> kvPair : group.attributes.entrySet()) {
                    dos.writeUTF(kvPair.getKey());
                    dos.writeUTF(kvPair.getValue());
                }

            }
        }

        dos.writeLong(indexStartPosition);

        write(buffer.toByteArray());
    }

    public void closeDataset(int datasetId) {
        // No op
    }

    public void closeGroup(int groupId) {
        // No op
    }

    private DataType convertType(int hdfType) {
        if (hdfType == H5Constants.NATIVE_FLOAT) {
            return DataType.FLOAT;
        } else if (hdfType == H5Constants.NATIVE_DOUBLE) {
            return DataType.DOUBLE;
        } else if (hdfType == H5Constants.NATIVE_INT) {
            return DataType.INT;
        } else if (hdfType == H5Constants.NATIVE_SHORT) {
            return DataType.SHORT;
        }

        return DataType.STRING;
    }

    /**
     * Required for the HDF5 implementation
     * @param locId
     * @param dsName
     * @param stringArray
     * @param ignored
     */
    public void createAndWriteStringDataset(int locId, String dsName, String[] stringArray,
            int stringSize) {
        createAndWriteStringDataset(locId, dsName, stringArray);
    }

    /**
     *
     * @param locId
     * @param name
     * @param typeId
     * @param dims
     * @return
     */
    public int createDataset(int locId, String name, int typeId, long[] dims) {

        DataType dataType = this.convertType(typeId);
        String fullName = this.getFullName(locId, name);

        int nRows = (int) (dims.length < 2 ? 1 : dims[0]);
        int nCols = (int) (dims.length >= 2 ? dims[1] : dims[0]);

        Dataset dataset = new Dataset(fullName, nRows, nCols, dataType);
        int id = fullName.hashCode();
        datasetCache.put(id, dataset);
        entityIdMap.put(fullName, id);
        return id;
    }

    public int createGroup(int locId, String name) {
        if (!groupCache.containsKey(locId)) {
            throw new IllegalArgumentException(
                    "Illegal entity identifier (trying to create entity " + name + ")");
        }

        String fullName = getFullName(locId, name);
        int id = fullName.hashCode();
        addEntity(id, fullName);
        return id;
    }

    public int openDataset(int fileId, String dsName) {
        return openEntity(fileId, dsName);
    }

    public int openGroup(int locId, String name) {
        return openEntity(locId, name);
    }

    private int openEntity(int locId, String name) {
        // If fullName starts with "/" it is absolute
        String fullName = (name.startsWith("/") ? name : getFullName(locId, name));
        return getEntityId(fullName);

    }

    /**
     * Note does not actually write anything to the file.  This is deferred until file
     * close to be sure we have the complete set.
     *
     * @param locId
     * @param fullName
     * @param value
     * @return
     */
    public int writeAttribute(int locId, String name, Object value) {
        getEntity(locId).attributes.put(name, value.toString());
        return 1;
    }

//
    public void writeDataValue(int datasetId, int rowNumber, float value) {
    }

    public void writeDataRow(int datasetId, int rowId, float[] data, int length) {
        try {
            Dataset ds = this.datasetCache.get(datasetId);
            if (ds == null) {
                // throw
            }
            ds.rowPositions[rowId] = fos.position();
            // assert ds.type = float
            // assert data.length = ds.columnCount
            write(convertToBytes(data));

        } catch (IOException iOException) {
        }
    }

    private byte[] convertToBytes(float[] data) {
        byte[] bytes = new byte[data.length * 4];
        for (int i = 0; i <
                data.length; i++) {
            int v = Float.floatToIntBits(data[i]);
            int byteIdx = i * 4;
            bytes[byteIdx++] = (byte) ((v >>> 24) & 0xFF);
            bytes[byteIdx++] = (byte) ((v >>> 16) & 0xFF);
            bytes[byteIdx++] = (byte) ((v >>> 8) & 0xFF);
            bytes[byteIdx++] = (byte) ((v >>> 0) & 0xFF);
        }

        return bytes;
    }

    private byte[] convertToBytes(int[] data) {
        byte[] bytes = new byte[data.length * 4];
        for (int i = 0; i <
                data.length; i++) {
            int v = data[i];
            int byteIdx = i * 4;
            bytes[byteIdx++] = (byte) ((v >>> 24) & 0xFF);
            bytes[byteIdx++] = (byte) ((v >>> 16) & 0xFF);
            bytes[byteIdx++] = (byte) ((v >>> 8) & 0xFF);
            bytes[byteIdx++] = (byte) ((v >>> 0) & 0xFF);
        }

        return bytes;
    }

    /**
     * TODO -- temporary implementation to maintain interface.  Redo with
     * overloaded methods.
     * @param locId
     * @param fullName
     * @param data
     */
    public void createAndWriteVectorDataset(int locId, String dsName, Object data) {
        if (data instanceof int[]) {
            createAndWriteIntDataset(locId, dsName, (int[]) data);
        } else if (data instanceof float[]) {
            createAndWriteFloatDataset(locId, dsName, (float[]) data);
        } else {
            throw new RuntimeException("Unsupported type: " + data.getClass());
        }

    }

    public void createAndWriteStringDataset(int locId, String dsName, String[] stringArray) {
        try {
            long[] dims = new long[]{stringArray.length};

            // Create the dataset
            int dsId = createDataset(locId, dsName, H5Constants.STRING, dims);
            Dataset dataset = datasetCache.get(dsId);

            // Write out the data (string array)
            dataset.rowPositions[0] = fos.position();

            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
            DataOutputStream dos = new DataOutputStream(buffer);
            for (String s : stringArray) {
                dos.writeUTF(s);
            }

            byte [] bytes = buffer.toByteArray();
            int sz = bytes.length;
            this.writeInt(sz);
            write(bytes);

        } catch (IOException ex) {
            log.error("Error creating string dataset: " + dsName, ex);
            throw new RuntimeException("Error creating string dataset: " + dsName, ex);
        }

    }

    private void createAndWriteIntDataset(int locId, String dsName, int[] data) {
        try {

            long[] dims = new long[]{data.length};

            // Create the dataset
            int dsId = createDataset(locId, dsName, H5Constants.STRING, dims);
            Dataset dataset = datasetCache.get(dsId);

            // Write out the data
            dataset.rowPositions[0] = fos.position();
            write(convertToBytes(data));

        } catch (IOException ex) {
            ex.printStackTrace();
        }

    }

    private void createAndWriteFloatDataset(int locId, String dsName, float[] data) {
        try {
            long[] dims = new long[]{data.length};

            // Create the dataset
            int dsId = createDataset(locId, dsName, H5Constants.STRING, dims);
            Dataset dataset = datasetCache.get(dsId);

            // Write out the data
            dataset.rowPositions[0] = fos.position();
            write(convertToBytes(data));

        } catch (IOException ex) {
            ex.printStackTrace();
        }

    }

    /**
     *
     * Note: assume a "square" 2-d array
     *
     * @param nodeId
     * @param dsName
     * @param data
     */
    public void createAndWriteDataset(int locId, String dsName, float[][] data) {
        try {
            long[] dims = new long[]{data.length, data[0].length};

            // Create the dataset
            int dsId = createDataset(locId, dsName, H5Constants.NATIVE_FLOAT, dims);
            Dataset dataset = datasetCache.get(dsId);

            // Write out the data
            for (int i = 0; i <
                    data.length; i++) {
                dataset.rowPositions[i] = fos.position();
                write(convertToBytes(data[i]));


            }

        } catch (IOException iOException) {
            log.error("Error creating dataset: " + dsName, iOException);
            throw new RuntimeException("Error creating dataset: " + dsName, iOException);
        }
    }


}
