package picard.illumina.parser.readers;

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

import java.io.File;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
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 agree with the constructor parameter 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 final BinaryFileIterator<ByteBuffer> bbIterator;
    private float density;
    private final TileMetricsVersion version;

    /**
     * Return a TileMetricsOutReader for the specified file
     * @param tileMetricsOutFile The file to read
     */
    public TileMetricsOutReader(final File tileMetricsOutFile) {

        int versionInt = UnsignedTypeUtil.uByteToInt(MMapBackedIteratorFactory.getByteIterator(1, tileMetricsOutFile).getHeaderBytes().get());
        this.version = TileMetricsVersion.findByKey(versionInt);

        bbIterator = MMapBackedIteratorFactory.getByteBufferIterator(version.headerSize, version.recordSize, tileMetricsOutFile);

        final ByteBuffer header = bbIterator.getHeaderBytes();

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

        final int actualRecordSize = UnsignedTypeUtil.uByteToInt(header.get());
        if (version.recordSize != actualRecordSize) {
            throw new PicardException("TileMetricsOutReader expects the record size to be " + version.recordSize + ".  Actual Record Size in Header(" + actualRecordSize + ")");
        }
        if (version == TileMetricsVersion.THREE) {
            this.density = UnsignedTypeUtil.uIntToFloat(header.getInt());
        }
    }

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

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

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

    public float getDensity() {
        return density;
    }

    public int getVersion() { return version.version; }

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

        public IlluminaTileMetrics(final ByteBuffer bb, TileMetricsVersion version) {
            if (version == TileMetricsVersion.THREE) {
                this.laneTileCode = new IlluminaLaneTileCode(UnsignedTypeUtil.uShortToInt(bb.getShort()), bb.getInt(), 0);
                //need to dump 9 bytes
                this.type = bb.get();
                this.metricValue = bb.getFloat();
            } else {
                this.laneTileCode = new IlluminaLaneTileCode(UnsignedTypeUtil.uShortToInt(bb.getShort()), UnsignedTypeUtil.uShortToInt(bb.getShort()),
                        UnsignedTypeUtil.uShortToInt(bb.getShort()));
                this.metricValue = 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.
        }

        public boolean isClusterRecord() {
            return type == 't';
        }
    }

    /** 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;
        }
    }

    public enum TileMetricsVersion {
        TWO(2, 2, 10),
        THREE(3, 6, 15);

        public final int version;
        private final int headerSize;
        private final int recordSize;
        private static final Map<Integer,TileMetricsVersion> versionLookupMap;

        TileMetricsVersion(int version, int headerSize, int recordSize) {
            this.version = version;
            this.headerSize = headerSize;
            this.recordSize = recordSize;
        }

        static {
            versionLookupMap = new HashMap<>();
            for (TileMetricsVersion v : TileMetricsVersion.values()) {
                versionLookupMap.put(v.version, v);
            }
        }
        public static TileMetricsVersion findByKey(int i) {
            return versionLookupMap.get(i);
        }
    }
}
