/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package hivemall.utils.lang;

/**
 * A utility class to deal with half-precision floating-point. The conversion is very fast because
 * there is no conditional branch instruction in the conversion.
 * 
 * <pre>
 * |sign|       exponent          |                   mantissa                                 |
 * | 31 | 30 29 28 27 26 25 24 23 | 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 |
 * </pre>
 * 
 * @see http://en.wikipedia.org/wiki/Half-precision_floating-point_format
 * @see http://en.wikipedia.org/wiki/Single_precision_floating-point_format
 * @see ftp://www.fox-toolkit.org/pub/fasthalffloatconversion.pdf
 */
public final class HalfFloat {

    public static final short ZERO = 0;
    public static final short ONE;
    // Integers equal to or above 65520 are rounded to "infinity"
    public static final float MAX_FLOAT_INTEGER = 65520f;
    /** (2-2^-10) * 2^15 */
    public static final float MAX_FLOAT = 65504f;

    /**
     * Smallest positive e for which HalfFloat (1.0 + e) != HalfFloat (1.0)
     */
    public static final float EPSILON = 0.00097656f;

    private static final int[] mantissatable;
    private static final int[] exponenttable;
    private static final short[] offsettable;
    private static final short[] basetable;
    private static final byte[] shifttable;
    static {// lookup tables are 10 KB in total
        mantissatable = new int[2048]; // 8192 bytes
        exponenttable = new int[64]; // 256 bytes
        offsettable = new short[64]; // 128 bytes
        basetable = new short[512]; // 1024 bytes
        shifttable = new byte[512]; // 512 bytes
        populateTableEntries();
        ONE = floatToHalfFloat(1f);
    }

    private HalfFloat() {}

    public static float halfFloatToFloat(final short f16) {
        int i = ((f16 & 0xFFFF) >> 10) & 0xFF;
        int j = (offsettable[i] + (f16 & 0x3FF)) & 0x7FF;
        int bits = mantissatable[j] + exponenttable[i];
        return Float.intBitsToFloat(bits);
    }

    public static short floatToHalfFloat(final float f32) {
        int bits = Float.floatToRawIntBits(f32);
        int i = (bits >> 23) & 0x1FF;
        return (short) (basetable[i] + ((bits & 0x007FFFFF) >> shifttable[i]));
    }

    public static int halfFloatToFloatBits(final short f16) {
        int i = f16 >> 10;
        int j = offsettable[i] + (f16 & 0x3FF);
        return mantissatable[j] + exponenttable[i];
    }

    public static short floatBitsToHalfFloat(final int f32b) {
        int i = (f32b >> 23) & 0x1FF;
        return (short) (basetable[i] + ((f32b & 0x007FFFFF) >> shifttable[i]));
    }

    private static void populateTableEntries() {
        populateMantissaTable(mantissatable);
        populateExponentTable(exponenttable);
        populateOffsetTable(offsettable);

        for (int i = 0; i < 256; i++) {
            final int e = i - 127;
            if (e < -24) { // Very small numbers map to zero
                //basetable[i | 0x000] = (short) 0x0000;
                basetable[i | 0x100] = (short) 0x8000;
                shifttable[i | 0x000] = 24;
                shifttable[i | 0x100] = 24;
            } else if (e < -14) { // Small numbers map to denorms
                basetable[i | 0x000] = (short) (0x0400 >> (-e - 14));
                basetable[i | 0x100] = (short) ((0x0400 >> (-e - 14)) | 0x8000);
                shifttable[i | 0x000] = (byte) (-e - 1);
                shifttable[i | 0x100] = (byte) (-e - 1);
            } else if (e <= 15) { // Normal numbers just lose precision
                basetable[i | 0x000] = (short) ((e + 15) << 10);
                basetable[i | 0x100] = (short) (((e + 15) << 10) | 0x8000);
                shifttable[i | 0x000] = 13;
                shifttable[i | 0x100] = 13;
            } else if (e < 128) { // Large numbers map to Infinity
                basetable[i | 0x000] = (short) 0x7C00;
                basetable[i | 0x100] = (short) 0xFC00;
                shifttable[i | 0x000] = 24;
                shifttable[i | 0x100] = 24;
            } else { // Infinity and NaN's stay Infinity and NaN's
                basetable[i | 0x000] = (short) 0x7C00;
                basetable[i | 0x100] = (short) 0xFC00;
                shifttable[i | 0x000] = 13;
                shifttable[i | 0x100] = 13;
            }
        }
    }

    private static void populateMantissaTable(final int[] mantissatable) {
        mantissatable[0] = 0;
        for (int i = 1; i < 1024; i++) {
            mantissatable[i] = convertMantissa(i);
        }
        for (int i = 1024; i < 2048; i++) {
            mantissatable[i] = 0x38000000 + ((i - 1024) << 13);
        }
    }

    private static int convertMantissa(final int i) {
        int m = i << 13; // Zero pad mantissa bits
        int e = 0; // Zero exponent

        while ((m & 0x00800000) == 0) {// While not normalized
            e -= 0x00800000; // Decrement exponent (1<<23)
            m <<= 1; // Shift mantissa
        }

        m &= ~0x00800000; // Clear leading 1 bit
        e += 0x38800000; // Adjust bias ((127-14)<<23)
        return m | e; // Return combined number
    }

    private static void populateExponentTable(final int[] exponenttable) {
        exponenttable[0] = 0;
        for (int i = 1; i < 31; i++) {
            exponenttable[i] = i << 23;
        }
        exponenttable[31] = 0x47800000;
        exponenttable[32] = 0x80000000;
        for (int i = 33; i < 63; i++) {
            exponenttable[i] = 0x80000000 + ((i - 32) << 23);
        }
        exponenttable[63] = 0xC7800000;
    }

    private static void populateOffsetTable(final short[] offsettable) {
        offsettable[0] = 0;
        for (int i = 1; i < 64; i++) {
            offsettable[i] = 1024;
        }
        offsettable[32] = 0;
    }

    public static boolean isRepresentable(final float f) {
        return Math.abs(f) <= HalfFloat.MAX_FLOAT_INTEGER;
    }

    public static boolean isRepresentable(final float f, final boolean strict) {
        if (strict) {
            return Math.abs(f) <= HalfFloat.MAX_FLOAT;
        } else {
            return Math.abs(f) <= HalfFloat.MAX_FLOAT_INTEGER;
        }
    }

    public static void checkRange(final float f) {
        if (Math.abs(f) > HalfFloat.MAX_FLOAT) {
            throw new IllegalArgumentException(
                "Acceptable maximum weight is " + HalfFloat.MAX_FLOAT + ": " + f);
        }
    }

}
