package net.sf.picard.illumina.parser.readers;

import net.sf.picard.PicardException;
import net.sf.picard.util.UnsignedTypeUtil;

import java.io.File;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.NoSuchElementException;

/**
 * Reads a TileMetricsOut file commonly found in the InterOp directory of an Illumina Run Folder.  This
 * reader DOES NOT try to interpret the metrics code or metrics value but instead returns them in what
 * is essentially a struct.
 *
 * File Format:
 * byte 0 (unsigned byte) = The version number which MUST be 2 or an exception will be thrown
 * byte 1 (unsigned byte) = The record size which must be 10 or an exception will be thrown
 * bytes 3 + (current_record * 10) to (current_record * 10 + 10) (TileMetrics Record) = The actual records each of size 10 that
 *          get converted into IlluminaPhasingMetrics objects
 *
 * TileMetrics Record Format:
 * Each 10 byte record is of the following format:
 * byte 0-1 (unsigned short) = lane number
 * byte 2-3 (unsigned short) = tile number
 * byte 4-5 (unisgned short) = metrics code, see Theory of RTA document by Illumina for definition
 * byte 6-9 (float)          = metrics value, see Theory of RTA document by Illumina for definition
 */
public class TileMetricsOutReader implements Iterator<TileMetricsOutReader.IlluminaTileMetrics> {
    private static final int HEADER_SIZE = 2;
    private static final int EXPECTED_RECORD_SIZE = 10;
    private static final int EXPECTED_VERSION = 2;

    private final BinaryFileIterator<ByteBuffer> bbIterator;

    /**
     * Return a TileMetricsOutReader for the specified file
     * @param tileMetricsOutFile The file to read
     */
    public TileMetricsOutReader(final File tileMetricsOutFile) {
        bbIterator = MMapBackedIteratorFactory.getByteBufferIterator(HEADER_SIZE, EXPECTED_RECORD_SIZE, tileMetricsOutFile);

        final ByteBuffer header = bbIterator.getHeaderBytes();

        //Get the version, should be EXPECTED_VERSION, which is 2
        final int actualVersion = UnsignedTypeUtil.uByteToInt(header.get());
        if(actualVersion != EXPECTED_VERSION) {
            throw new PicardException("TileMetricsOutReader expects the version number to be " + EXPECTED_VERSION + ".  Actual Version in Header( " + actualVersion + ")" );
        }

        final int actualRecordSize = UnsignedTypeUtil.uByteToInt(header.get());
        if(EXPECTED_RECORD_SIZE != actualRecordSize) {
            throw new PicardException("TileMetricsOutReader expects the record size to be " + EXPECTED_RECORD_SIZE + ".  Actual Record Size in Header( " + actualRecordSize + ")" );
        }
    }

    public boolean hasNext() {
        return bbIterator.hasNext();
    }

    public IlluminaTileMetrics next() {
        if(!hasNext()) {
            throw new NoSuchElementException();
        }
        return new IlluminaTileMetrics(bbIterator.next());
    }

    public void remove() {
        throw new UnsupportedOperationException();
    }

    /**
     * IlluminaPhasingMetrics corresponds to a single record in a TileMetricsOut file
     */
    public static class IlluminaTileMetrics {
        private final IlluminaLaneTileCode laneTileCode;
        private final float metricValue;

        public IlluminaTileMetrics(final ByteBuffer bb) {
            this(UnsignedTypeUtil.uShortToInt(bb.getShort()), UnsignedTypeUtil.uShortToInt(bb.getShort()),
                    UnsignedTypeUtil.uShortToInt(bb.getShort()), bb.getFloat());
        }

        public IlluminaTileMetrics(final int laneNumber, final int tileNumber, final int metricCode, final float metricValue) {
            this.laneTileCode = new IlluminaLaneTileCode(laneNumber, tileNumber, metricCode);
            this.metricValue = metricValue;
        }

        public int getLaneNumber() {
            return laneTileCode.getLaneNumber();
        }

        public int getTileNumber() {
            return laneTileCode.getTileNumber();
        }

        public int getMetricCode() {
            return laneTileCode.getMetricCode();
        }

        public float getMetricValue() {
            return metricValue;
        }

        public IlluminaLaneTileCode getLaneTileCode() {
            return laneTileCode;
        }

        @Override
        public boolean equals(final Object o) {
            if (o instanceof IlluminaTileMetrics) {
                final IlluminaTileMetrics that = (IlluminaTileMetrics) o;
                return laneTileCode == that.laneTileCode && metricValue == that.metricValue; // Identical tile data should render exactly the same float.
            } else {
                return false;
            }
        }
        
        @Override
        public int hashCode() {
            return String.format("%s:%s:%s:%s", laneTileCode.getLaneNumber(), laneTileCode.getTileNumber(), laneTileCode.getMetricCode(), metricValue).hashCode(); // Slow but adequate.
        }
    }

    /** Helper class which captures the combination of a lane, tile & metric code */
    public static class IlluminaLaneTileCode {
        private final int laneNumber;
        private final int tileNumber;
        private final int metricCode;

        public IlluminaLaneTileCode(final int laneNumber, final int tileNumber, final int metricCode) {
            this.laneNumber = laneNumber;
            this.tileNumber = tileNumber;
            this.metricCode = metricCode;
        }

        public int getLaneNumber() {
            return laneNumber;
        }

        public int getTileNumber() {
            return tileNumber;
        }

        public int getMetricCode() {
            return metricCode;
        }

        @Override
        public boolean equals(final Object o) {
            if (o instanceof IlluminaLaneTileCode) {
                final IlluminaLaneTileCode that = (IlluminaLaneTileCode) o;
                return laneNumber == that.laneNumber && tileNumber == that.tileNumber && metricCode == that.metricCode;
            } else {
                return false;
            }
        }

        @Override
        public int hashCode() {
            int result = laneNumber;
            result = 31 * result + tileNumber;
            result = 31 * result + metricCode;
            return result;
        }
    }
}
