/*
 * 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
 *
 *      https://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 org.apache.commons.statistics.descriptive;

import java.math.BigInteger;
import java.nio.ByteBuffer;

/**
 * A mutable 192-bit unsigned integer.
 *
 * <p>This is a specialised class to implement an accumulator of squared {@code long} values.
 *
 * @since 1.1
 */
final class UInt192 {
    /** Mask for the lower 32-bits of a long. */
    private static final long MASK32 = 0xffff_ffffL;

    // Data is stored using integers to allow efficient sum-with-carry addition

    /** bits 32-1 (low 32-bits). */
    private int f;
    /** bits 64-33. */
    private int e;
    /** bits 96-65. */
    private int d;
    /** bits 128-97. */
    private int c;
    /** bits 192-129 (high 64-bits). */
    private long ab;

    /**
     * Create an instance.
     */
    private UInt192() {
        // No-op
    }

    /**
     * Create an instance using a direct binary representation.
     * This is package-private for testing.
     *
     * @param hi High 64-bits.
     * @param mid Middle 64-bits.
     * @param lo Low 64-bits.
     */
    UInt192(long hi, long mid, long lo) {
        this.f = (int) lo;
        this.e = (int) (lo >>> Integer.SIZE);
        this.d = (int) mid;
        this.c = (int) (mid >>> Integer.SIZE);
        this.ab = hi;
    }

    /**
     * Create an instance using a direct binary representation.
     *
     * @param ab bits 192-129 (high 64-bits).
     * @param c bits 128-97.
     * @param d bits 96-65.
     * @param e bits 64-33.
     * @param f bits 32-1.
     */
    private UInt192(long ab, int c, int d, int e, int f) {
        this.ab = ab;
        this.c = c;
        this.d = d;
        this.e = e;
        this.f = f;
    }

    /**
     * Create an instance. The initial value is zero.
     *
     * @return the instance
     */
    static UInt192 create() {
        return new UInt192();
    }

    /**
     * Adds the squared value {@code x * x}.
     *
     * @param x Value.
     */
    void addSquare(long x) {
        final long lo = x * x;
        final long hi = IntMath.squareHigh(x);

        // Sum with carry.
        long s = (lo & MASK32) + (f & MASK32);
        f = (int) s;
        s = (s >>> Integer.SIZE) + (lo >>> Integer.SIZE) + (e & MASK32);
        e = (int) s;
        s = (s >>> Integer.SIZE) + (hi & MASK32) + (d & MASK32);
        d = (int) s;
        s = (s >>> Integer.SIZE) + (hi >>> Integer.SIZE) + (c & MASK32);
        c = (int) s;
        ab += s >>> Integer.SIZE;
    }

    /**
     * Adds the value.
     *
     * @param x Value.
     */
    void add(UInt192 x) {
        // Avoid issues adding to itself
        final int ff = x.f;
        final int ee = x.e;
        final int dd = x.d;
        final int cc = x.c;
        final long aabb = x.ab;
        // Sum with carry.
        long s = (ff & MASK32) + (f & MASK32);
        f = (int) s;
        s = (s >>> Integer.SIZE) + (ee & MASK32) + (e & MASK32);
        e = (int) s;
        s = (s >>> Integer.SIZE) + (dd & MASK32) + (d & MASK32);
        d = (int) s;
        s = (s >>> Integer.SIZE) + (cc & MASK32) + (c & MASK32);
        c = (int) s;
        ab += (s >>> Integer.SIZE) + aabb;
    }


    /**
     * Multiply by the unsigned value.
     * Any overflow bits are lost.
     *
     * @param x Value.
     * @return the product
     */
    UInt192 unsignedMultiply(int x) {
        final long xx = x & MASK32;
        // Multiply with carry.
        long product = xx * (f & MASK32);
        final int ff = (int) product;
        product = (product >>> Integer.SIZE) + xx * (e & MASK32);
        final int ee = (int) product;
        product = (product >>> Integer.SIZE) + xx * (d & MASK32);
        final int dd = (int) product;
        product = (product >>> Integer.SIZE) + xx * (c & MASK32);
        final int cc = (int) product;
        // Possible overflow here and bits are lost
        final long aabb = (product >>> Integer.SIZE) + xx * ab;
        return new UInt192(aabb, cc, dd, ee, ff);
    }

    /**
     * Subtracts the value.
     * Any overflow bits (negative result) are lost.
     *
     * @param x Value.
     * @return the difference
     */
    UInt192 subtract(UInt128 x) {
        // Difference with carry.
        // Subtract common part.
        long diff = (f & MASK32) - (x.lo32()  & MASK32);
        final int ff = (int) diff;
        diff = (diff >> Integer.SIZE) + (e & MASK32) - (x.mid32() & MASK32);
        final int ee = (int) diff;
        diff = (diff >> Integer.SIZE) + (d & MASK32) - (x.hi64() & MASK32);
        final int dd = (int) diff;
        diff = (diff >> Integer.SIZE) + (c & MASK32) - (x.hi64() >>> Integer.SIZE);
        final int cc = (int) diff;
        // Possible overflow here and bits are lost containing info on the
        // magnitude of the true negative value
        final long aabb = (diff >> Integer.SIZE) + ab;
        return new UInt192(aabb, cc, dd, ee, ff);
    }

    /**
     * Convert to a BigInteger.
     *
     * @return the value
     */
    BigInteger toBigInteger() {
        final ByteBuffer bb = ByteBuffer.allocate(Integer.BYTES * 6)
            .putLong(ab)
            .putInt(c)
            .putInt(d)
            .putInt(e)
            .putInt(f);
        // Sign is always positive. This works for zero.
        return new BigInteger(1, bb.array());
    }

    /**
     * Convert to a double.
     *
     * @return the value
     */
    double toDouble() {
        final long h = hi64();
        final long m = mid64();
        final long l = lo64();
        if (h == 0) {
            return IntMath.uint128ToDouble(m, l);
        }
        // For correct rounding we use a sticky bit to represent magnitude
        // lost from the low 64-bits. The result is scaled by 2^64.
        return IntMath.uint128ToDouble(h, m | ((l == 0) ? 0 : 1)) * 0x1.0p64;
    }

    /**
     * Convert to an {@code int}; throwing an exception if the value overflows an {@code int}.
     *
     * @return the value
     * @throws ArithmeticException if the value overflows an {@code int}.
     * @see Math#toIntExact(long)
     */
    int toIntExact() {
        return Math.toIntExact(toLongExact());
    }

    /**
     * Convert to a {@code long}; throwing an exception if the value overflows a {@code long}.
     *
     * @return the value
     * @throws ArithmeticException if the value overflows a {@code long}.
     */
    long toLongExact() {
        // Test if we have more than 63-bits
        if ((ab | c | d) != 0 || e < 0) {
            throw new ArithmeticException("long integer overflow");
        }
        return lo64();
    }

    /**
     * Return the lower 64-bits as a {@code long} value.
     *
     * @return the low 64-bits
     */
    long lo64() {
        return (f & MASK32) | ((e & MASK32) << Integer.SIZE);
    }

    /**
     * Return the middle 64-bits as a {@code long} value.
     *
     * @return bits 128-65
     */
    long mid64() {
        return (d & MASK32) | ((c & MASK32) << Integer.SIZE);
    }

    /**
     * Return the higher 64-bits as a {@code long} value.
     *
     * @return bits 192-129
     */
    long hi64() {
        return ab;
    }
}
