/*
 * Copyright (c) 2018, 2020, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * The Universal Permissive License (UPL), Version 1.0
 *
 * Subject to the condition set forth below, permission is hereby granted to any
 * person obtaining a copy of this software, associated documentation and/or
 * data (collectively the "Software"), free of charge and under any and all
 * copyright rights in the Software, and any and all patent rights owned or
 * freely licensable by each licensor hereunder covering either (i) the
 * unmodified Software as contributed to or provided by such licensor, or (ii)
 * the Larger Works (as defined below), to deal in both
 *
 * (a) the Software, and
 *
 * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
 * one is included with the Software each a "Larger Work" to which the Software
 * is contributed by such licensors),
 *
 * without restriction, including without limitation the rights to copy, create
 * derivative works of, display, perform, and distribute the Software and make,
 * use, sell, offer for sale, import, export, have made, and have sold the
 * Software and the Larger Work(s), and to sublicense the foregoing rights on
 * either these or other terms.
 *
 * This license is subject to the following condition:
 *
 * The above copyright notice and either this complete permission notice or at a
 * minimum a reference to the UPL must 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 com.oracle.truffle.js.builtins.math;

import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.profiles.BranchProfile;
import com.oracle.truffle.api.profiles.ConditionProfile;
import com.oracle.truffle.js.nodes.JavaScriptNode;
import com.oracle.truffle.js.nodes.cast.JSToNumberNode;
import com.oracle.truffle.js.nodes.function.JSBuiltin;
import com.oracle.truffle.js.runtime.JSContext;
import com.oracle.truffle.js.runtime.JSRuntime;

public abstract class RoundNode extends MathOperation {

    RoundNode(JSContext context, JSBuiltin builtin) {
        super(context, builtin);
    }

    public static RoundNode create(JSContext context, JSBuiltin builtin, JavaScriptNode[] arguments) {
        return RoundNodeGen.create(context, builtin, createCast(arguments));
    }

    protected static JavaScriptNode[] createCast(JavaScriptNode[] argumentNodes) {
        argumentNodes[0] = JSToNumberNode.create(argumentNodes[0]);
        return argumentNodes;
    }

    protected static boolean isCornercase(double d) {
        return Double.isNaN(d) || JSRuntime.isNegativeZero(d);
    }

    @Specialization
    protected static int roundInt(int a) {
        return a;
    }

    @Specialization(guards = "isCornercase(value)")
    protected static double roundCornercase(double value) {
        return value;
    }

    private final ConditionProfile shiftProfile = ConditionProfile.createBinaryProfile();
    private final BranchProfile negativeLongBitsProfile = BranchProfile.create();

    // Copied from sun.misc.DoubleConsts
    private static final int EXP_BIAS = 1023;
    private static final int SIGNIFICAND_WIDTH = 53;
    private static final long EXP_BIT_MASK = 9218868437227405312L;
    private static final long SIGNIF_BIT_MASK = 4503599627370495L;

    // Copy of Math.round() with added profiles
    private long round(double a) {
        long longBits = Double.doubleToRawLongBits(a);
        long biasedExp = (longBits & EXP_BIT_MASK) >> (SIGNIFICAND_WIDTH - 1);
        long shift = (SIGNIFICAND_WIDTH - 2 + EXP_BIAS) - biasedExp;
        if (shiftProfile.profile((shift & -64) == 0)) { // shift >= 0 && shift < 64
            // a is a finite number such that pow(2,-64) <= ulp(a) < 1
            long r = ((longBits & SIGNIF_BIT_MASK) | (SIGNIF_BIT_MASK + 1));
            if (longBits < 0) {
                negativeLongBitsProfile.enter();
                r = -r;
            }
            // In the comments below each Java expression evaluates to the value
            // the corresponding mathematical expression:
            // (r) evaluates to a / ulp(a)
            // (r >> shift) evaluates to floor(a * 2)
            // ((r >> shift) + 1) evaluates to floor((a + 1/2) * 2)
            // (((r >> shift) + 1) >> 1) evaluates to floor(a + 1/2)
            return ((r >> shift) + 1) >> 1;
        } else {
            // a is either
            // - a finite number with abs(a) < exp(2,SIGNIFICAND_WIDTH-64) < 1/2
            // - a finite number with ulp(a) >= 1 and hence a is a mathematical integer
            // - an infinity or NaN
            return (long) a;
        }
    }

    @Specialization(guards = {"!isCornercase(value)", "isDoubleInInt32Range(value)"}, rewriteOn = ArithmeticException.class)
    protected int roundDoubleInt(double value) {
        long longValue = round(value);
        if (longValue == 0 && value < 0) {
            CompilerDirectives.transferToInterpreterAndInvalidate();
            throw new ArithmeticException();
        } else {
            assert JSRuntime.longIsRepresentableAsInt(longValue);
            return (int) longValue;
        }
    }

    @Specialization(guards = {"!isCornercase(value)"}, replaces = "roundDoubleInt")
    protected double roundDouble(double value,
                    @Cached("createBinaryProfile()") ConditionProfile profileA,
                    @Cached("createBinaryProfile()") ConditionProfile profileB) {
        long longValue = round(value);
        if (profileA.profile(longValue == Long.MIN_VALUE || longValue == Long.MAX_VALUE)) {
            // The value is too large to have a fractional part (i.e. is rounded already)
            return value;
        } else if (profileB.profile(longValue == 0 && value < 0)) {
            return -0.0;
        } else {
            return longValue;
        }
    }

}