/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2007-2015 Broad Institute
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package org.broad.igv.bbfile;

import htsjdk.samtools.seekablestream.SeekableStream;
import org.apache.log4j.Logger;
import org.broad.igv.util.CompressionUtils;
import org.broad.igv.util.LittleEndianInputStream;

import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.DataInputStream;
import java.util.HashMap;


/**
 * Created by IntelliJ IDEA.
 * User: martind
 * Date: Jan 26, 2010
 * Time: 12:18:32 PM
 * To change this template use File | Settings | File Templates.
 */
/*
*   Container class for reading and storing a block of bed data items.
* */
public class BigBedDataBlock {

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

    // Bed data block access variables   - for reading in bed records from a file
    private long fileOffset;       // Bed data block file offset
    private long dataBlockSize;     // byte size for data block specified in the R+ leaf
    private boolean isLowToHigh;   // if true, data is low to high byte order; else high to low

    // defines the bigBed/bigWig source chromosomes
    private HashMap<Integer, String> chromosomeMap;  // map of chromosome ID's and corresponding names
    private RPTreeLeafNodeItem leafHitItem;   // R+ tree leaf item containing data block location

    // Provides uncompressed byte stream data reader
    private byte[] bedBuffer;  // buffer containing leaf block data uncompressed
    private int remDataSize;   // number of unread data bytes

    // byte stream readers
    private LittleEndianInputStream lbdis;    // low to high byte stream reader
    private DataInputStream dis;       // high to low byte stream reader

    // Bed data extraction members
    private ArrayList<BedFeature> bedFeatureList; // array of BigBed data

    /*
    *   Constructor for Bed data block reader.
    *
    *   Parameters:
    *       fis - file input stream handle
    *       leafItem - R+ tree leaf item containing chromosome region and file data location
    *       chromIDTree - B+ chromosome index tree returns chromosome ID's for names
    *       isLowToHigh - byte order is low to high if true; else high to low
    *       uncompressBufSize - byte size for decompression buffer; else 0 for uncompressed
    * */
    public BigBedDataBlock(SeekableStream fis, RPTreeLeafNodeItem leafHitItem,
                           HashMap<Integer, String> chromosomeMap, boolean isLowToHigh, int uncompressBufSize) {

        this.leafHitItem = leafHitItem;
        this.chromosomeMap = chromosomeMap;
        this.isLowToHigh = isLowToHigh;

        dataBlockSize = this.leafHitItem.geDataSize();
        byte[] buffer = new byte[(int) dataBlockSize];

        fileOffset = this.leafHitItem.getDataOffset();

        // read Bed data block into a buffer
        try {
            fis.seek(fileOffset);
            fis.readFully(buffer);

            // decompress if necessary - the buffer size is 0 for uncompressed data
            // Note:  BBFile Table C specifies a decompression buffer size
            if (uncompressBufSize > 0)
                bedBuffer = (new CompressionUtils()).decompress(buffer, uncompressBufSize);
            else
                bedBuffer = buffer;    // use uncompressed read buffer directly

        } catch (IOException ex) {
            String error = String.format("Error reading Bed data for leaf item %d \n");
            log.error(error, ex);
            throw new RuntimeException(error, ex);
        }

        // wrap the bed buffer as an input stream
        if (this.isLowToHigh)
            lbdis = new LittleEndianInputStream(new ByteArrayInputStream(bedBuffer));
        else
            dis = new DataInputStream(new ByteArrayInputStream(bedBuffer));

        // initialize unread data size
        remDataSize = bedBuffer.length;

        // use methods getBedData or getNextFeature to extract block data
    }

    /*
    *   Method returns all Bed features within the decompressed block buffer
    *
    *   Parameters:
    *       selectionRegion - chromosome region for selecting Bed features
    *       contained - indicates selected data must be contained in selection region
    *           if true, else may intersect selection region
    *
    *   Returns:
    *      Bed feature items in the data block
    *
    *   Note: Remaining bytes to data block are used to determine end of reading
    *   since a zoom record count for the data block is not known.
    * */
    public ArrayList<BedFeature> getBedData(RPChromosomeRegion selectionRegion,
                                            boolean contained) {
        int itemNumber = 0;
        int chromID, chromStart, chromEnd;
        String restOfFields;

        // chromID + chromStart + chromEnd + rest 0 byte
        // 0 byte for "restOfFields" is always present for bed data
        int minItemSize = 3 * 4 + 1;

        // allocate the bed feature array list
        bedFeatureList = new ArrayList<BedFeature>();

        try {
            for (int index = 0; remDataSize >= minItemSize; ++index) {
                itemNumber = index + 1;

                // read in BigBed item fields - BBFile Table I
                if (isLowToHigh) {
                    chromID = lbdis.readInt();
                    chromStart = lbdis.readInt();
                    chromEnd = lbdis.readInt();
                    restOfFields = lbdis.readString();
                } else {
                    chromID = dis.readInt();
                    chromStart = dis.readInt();
                    chromEnd = dis.readInt();
                    restOfFields = readHighToLowString();
                }

                int leafHitValue = selectionRegion.compareRegions(chromID, chromStart, chromID, chromEnd);
                if (leafHitValue == -2 || (contained && leafHitValue == -1)) {
                    remDataSize -= (minItemSize + restOfFields.length());
                    break;
                }
                if (leafHitValue == 2 || (contained && leafHitValue == 1)) {
                    remDataSize -= (minItemSize + restOfFields.length());
                    continue;
                } else {
                    remDataSize -= (minItemSize + restOfFields.length());
                    String chromosome = chromosomeMap.get(chromID);
                    BedFeature bbItem = new BedFeature(itemNumber, chromosome, chromStart, chromEnd, restOfFields);
                    bedFeatureList.add(bbItem);
                }
            }

        } catch (IOException ex) {

            log.error("Read error for Bed data item " + itemNumber, ex);

            // accept this as an end of block condition unless no items were read
            if (itemNumber == 1)
                throw new RuntimeException("Read error for Bed data item " + itemNumber, ex);
        }

        return bedFeatureList;
    }

    private String readHighToLowString() throws IOException {
        String restOfFields;
        ByteArrayOutputStream bis = new ByteArrayOutputStream(100);
        byte b;
        while ((b = (byte) dis.read()) != 0) {
            bis.write(b);
        }
        restOfFields = new String(bis.toByteArray());
        return restOfFields;
    }

    public void print() {

        log.debug("BigBed data for " + bedFeatureList.size() + " items");

        for (int index = 0; index <= bedFeatureList.size(); ++index) {
            // BigBed data items print themselves
            bedFeatureList.get(index).print();
        }
    }
}
