/*
 * The MIT License
 *
 * Copyright (c) 2012 The 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 picard.illumina.parser.readers;

import picard.PicardException;
import picard.util.UnsignedTypeUtil;

import java.io.File;
import java.nio.ByteBuffer;

/**
 * The clocs file format is one of 3 Illumina formats(pos, locs, and clocs) that stores position data exclusively.
 * clocs files store position data for successive clusters, compressed in bins as follows:
 *     Byte 0   : unused
 *     Byte 1-4 : unsigned int numBins
 *     The rest of the file consists of bins/blocks, where a bin consists of an integer
 *     indicating number of blocks, followed by that number of blocks and a block consists
 *     of an x-y coordinate pair.  In otherwords:
 *
 *     for each bin
 *         byte 1: Unsigned int numBlocks
 *         for each block:
 *             byte 1 : byte xRelativeCoordinate
 *             byte 2 : byte yRelativeCoordinate
 *
 *     Actual x and y values are computed using the following algorithm
 *
 *     xOffset = yOffset = 0
 *     imageWidth = 2048
 *     blockSize = 25
 *     maxXbins:Int = Math.Ceiling((double)ImageWidth/(double)blockSize)
 *     for each bin:
 *         for each location:
 *             x = convert.ToSingle(xRelativeCoordinate/10f + xoffset)
 *             y = convert.toSingle(yRelativeCoordinate/10f + yoffset)
 *         if (binIndex > 0 && ((binIndex + 1) % maxXbins == 0)) {
 *            xOffset = 0; yOffset += blockSize
 *         } else xOffset += blockSize
 */
public class ClocsFileReader extends AbstractIlluminaPositionFileReader {

    private static final int HEADER_SIZE = 5;

    private static final int IMAGE_WIDTH = 2048;
    private static final int BLOCK_SIZE = 25;
    private static final int NUM_BINS_IN_ROW = (int)Math.ceil((double) IMAGE_WIDTH / (double) BLOCK_SIZE);

    /** Total number of bins */
    private final long numBins;

    /** An iterator through clocsFile's bytes */
    private final BinaryFileIterator<Byte> byteIterator;

    //mutable vars
    private float xOffset;
    private float yOffset;
    private long currentBin;
    private int numClustersInBin;   //MAX 255
    private long currentClusterInBin;

    public ClocsFileReader(final File clocsFile) {
        super(clocsFile);

        byteIterator = MMapBackedIteratorFactory.getByteIterator(HEADER_SIZE, clocsFile);

        final ByteBuffer hbs = byteIterator.getHeaderBytes();
        hbs.get(); //unusedByte
        numBins = UnsignedTypeUtil.uIntToLong(hbs.getInt());

        xOffset = 0;
        yOffset = 0;
        currentBin = 0;
        startBlock();

        checkAndAdvanceBin();
    }

    /**
     * Grab the next set of offset values, decompress them.
     * @return the position information of the next offset values
     */
    @Override
    protected PositionInfo unsafeNextInfo() {
        final byte xByte = byteIterator.next();
        final byte yByte = byteIterator.next();

        final float xPos = UnsignedTypeUtil.uByteToInt(xByte)/10f + xOffset;
        final float yPos = UnsignedTypeUtil.uByteToInt(yByte)/10f + yOffset;
        ++currentClusterInBin;
        checkAndAdvanceBin();

        return new PositionInfo(xPos, yPos, getLane(), getTile());
    }

    /** Compute offset for next bin and then increment the bin number and reset block information*/
    private void checkAndAdvanceBin() {
        while(currentClusterInBin >= numClustersInBin && currentBin < numBins) { //While rather than if statement to skip empty blocks
            if ((currentBin + 1) % NUM_BINS_IN_ROW == 0) {
                xOffset = 0;
                yOffset += BLOCK_SIZE;
            } else{
                xOffset += BLOCK_SIZE;
            }

            currentBin += 1;
            if(currentBin < numBins) {
                startBlock();
            }
        }
    }

    /** Start the next block by reading it's numBlocks byte and setting the currentBlock index to 0 */
    private void startBlock() {
        numClustersInBin = UnsignedTypeUtil.uByteToInt(byteIterator.next());
        currentClusterInBin = 0;
    }
    
    @Override
    protected String makeExceptionMsg() {
        return "ClocsFileReader(file=" + getFile().getName() + ", lane=" + getLane() +  ", tile=" + getTile() +
                               ", currentBin="   + currentBin +  ", numBins=" + numBins +  ", xOffset=" + xOffset  + ", yOffset" + yOffset +
                               ", currentBlock=" + currentClusterInBin + ", numBlocks=" + numClustersInBin;
    }

    @Override
    public boolean hasNext() { 
        boolean valuesRemain = currentClusterInBin < numClustersInBin || currentBin < (numBins -1);
        if(!valuesRemain && byteIterator.hasNext()) {
            throw new PicardException("Read the number of expected bins( " + numBins + ") but still had more elements in file( " + byteIterator.getFile().getAbsolutePath() + ") ");
        }
        return valuesRemain;
    }

    public void close() {
    }
}
