/*
 * 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 htsjdk.variant.bcf2;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.EnumSet;

/**
 * BCF2 types and associated information
 *
 * @author depristo
 * @since 05/12
 */
public enum BCF2Type {
    // the actual values themselves
    MISSING(0, 0, 0x00) {
        @Override
        public int read(final InputStream in) throws IOException {
            throw new IllegalArgumentException("Cannot read MISSING type");
        }

        @Override
        public void write(final int value, final OutputStream out) throws IOException {
            throw new IllegalArgumentException("Cannot write MISSING type");
        }
    },

    INT8(1, 1, 0xFFFFFF80, -127, 127) {
        @Override
        public int read(final InputStream in) throws IOException {
            return BCF2Utils.readByte(in);
        }

        @Override
        public void write(final int value, final OutputStream out) throws IOException {
            out.write(0xFF & value); // TODO -- do we need this operation?
        }
    },

    INT16(2, 2, 0xFFFF8000, -32767, 32767) {
        @Override
        public int read(final InputStream in) throws IOException {
            final int b2 = BCF2Utils.readByte(in) & 0xFF;
            final int b1 = BCF2Utils.readByte(in) & 0xFF;
            return (short) ((b1 << 8) | b2);
        }

        @Override
        public void write(final int value, final OutputStream out) throws IOException {
            // TODO -- optimization -- should we put this in a local buffer?
            out.write((0x00FF & value));
            out.write((0xFF00 & value) >> 8);
        }
    },

    INT32(3, 4, 0x80000000, -2147483647, 2147483647) {
        @Override
        public int read(final InputStream in) throws IOException {
            final int b4 = BCF2Utils.readByte(in) & 0xFF;
            final int b3 = BCF2Utils.readByte(in) & 0xFF;
            final int b2 = BCF2Utils.readByte(in) & 0xFF;
            final int b1 = BCF2Utils.readByte(in) & 0xFF;
            return (int) (b1 << 24 | b2 << 16 | b3 << 8 | b4);
        }

        @Override
        public void write(final int value, final OutputStream out) throws IOException {
            out.write((0x000000FF & value));
            out.write((0x0000FF00 & value) >> 8);
            out.write((0x00FF0000 & value) >> 16);
            out.write((0xFF000000 & value) >> 24);
        }
    },

    FLOAT(5, 4, 0x7F800001) {
        @Override
        public int read(final InputStream in) throws IOException {
            return INT32.read(in);
        }

        @Override
        public void write(final int value, final OutputStream out) throws IOException {
            INT32.write(value, out);
        }
    },

    CHAR(7, 1, 0x00000000) {
        @Override
        public int read(final InputStream in) throws IOException {
            return INT8.read(in);
        }

        @Override
        public void write(final int value, final OutputStream out) throws IOException {
            INT8.write(value, out);
        }
    };

    private final int id;
    private final Object missingJavaValue;
    private final int missingBytes;
    private final int sizeInBytes;
    private final long minValue, maxValue;

    BCF2Type(final int id, final int sizeInBytes, final int missingBytes) {
        this(id, sizeInBytes, missingBytes, 0, 0);
    }

    BCF2Type(final int id, final int sizeInBytes, final int missingBytes, final long minValue, final long maxValue) {
        this.id = id;
        this.sizeInBytes = sizeInBytes;
        this.missingJavaValue = null;
        this.missingBytes = missingBytes;
        this.minValue = minValue;
        this.maxValue = maxValue;
    }

    /**
     * How many bytes are used to represent this type on disk?
     * @return
     */
    public int getSizeInBytes() {
        return sizeInBytes;
    }

    /**
     * The ID according to the BCF2 specification
     * @return
     */
    public int getID() {
        return id;
    }

    /**
     * Can we encode value v in this type, according to its declared range.
     *
     * Only makes sense for integer values
     *
     * @param v
     * @return
     */
    public final boolean withinRange(final long v) {
        return v >= minValue && v <= maxValue;
    }

    /**
     * Return the java object (aka null) that is used to represent a missing value for this
     * type in Java
     *
     * @return
     */
    public Object getMissingJavaValue() {
        return missingJavaValue;
    }

    /**
     * The bytes (encoded as an int) that are used to represent a missing value
     * for this type in BCF2
     *
     * @return
     */
    public int getMissingBytes() {
        return missingBytes;
    }

    /**
     * An enum set of the types that might represent Integer values
     */
    private static final EnumSet<BCF2Type> INTEGERS = EnumSet.of(INT8, INT16, INT32);

    /**
     * @return true if this BCF2Type corresponds to the magic "MISSING" type (0x00)
     */
    public boolean isMissingType() {
        return this == MISSING;
    }

    public boolean isIntegerType() {
        return INTEGERS.contains(this);
    }

    /**
     * Read a value from in stream of this BCF2 type as an int [32 bit] collection of bits
     *
     * For intX and char values this is just the int / byte value of the underlying data represented as a 32 bit int
     * For a char the result must be converted to a char by (char)(byte)(0x0F &amp; value)
     * For doubles it's necessary to convert subsequently this value to a double via Double.bitsToDouble()
     *
     * @param in
     * @return
     * @throws IOException
     */
    public int read(final InputStream in) throws IOException {
        throw new IllegalArgumentException("Not implemented");
    }

    public void write(final int value, final OutputStream out) throws IOException {
        throw new IllegalArgumentException("Not implemented");
    }
}
