# Copyright (C) 2020-2021 Arm Limited or its affiliates. All rights reserved.
#
# Copyright 2015 The Gemmlowp Authors. All Rights Reserved.
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed 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.
#
# Description:
# Contains various fixed point math functions based on the gemmlowp fixed
# point implementation.
import numpy as np


# Convert floating point to fixed point, default Q5.26
def from_float(x, integer_bits=5):
    i32info = np.iinfo(np.int32)
    fractional_bits = i32info.bits - integer_bits - 1
    return min(max(round(x * (1 << fractional_bits)), i32info.min), i32info.max)


# Convert fixed point to floating point, default Q5.26
def to_float(x, integer_bits=5):
    fractional_bits = np.iinfo(np.int32).bits - integer_bits - 1
    return x / (1 << fractional_bits)


def saturating_rounding_mul32(a, b):
    assert np.int32(a) == a
    assert np.int32(b) == b
    if a == b and a == np.iinfo(np.int32).min:
        return np.int32(np.iinfo(np.int32).max)
    divider = 1 << 31
    ab = np.int64(a) * np.int64(b)

    if ab >= 0:
        nudge = 1 << 30
        return (ab + nudge) // divider
    else:
        nudge = 1 - (1 << 30)
        ab_plus_nudge = ab + nudge
        result = ab_plus_nudge // divider
        # Python uses floor, the reference uses truncation
        # so we need to compensate for that.
        if result * divider < ab_plus_nudge:
            result += 1
        return result


def saturating_rounding_mul16(a, b):
    assert np.int16(a) == a
    assert np.int16(b) == b
    if a == b and a == np.iinfo(np.int16).min:
        return np.int16(np.iinfo(np.int16).max)
    divider = 1 << 15
    ab = np.int32(a) * np.int32(b)

    if ab >= 0:
        nudge = 1 << 14
        return (ab + nudge) // divider
    else:
        nudge = 1 - (1 << 14)
        ab_plus_nudge = ab + nudge
        result = ab_plus_nudge // divider
        # Python uses floor, the reference uses truncation
        # so we need to compensate for that.
        if result * divider < ab_plus_nudge:
            result += 1
        return result


# Similar to saturating_rounding_mul16 except rounding to zero instead of to nearest
# Only supports 16bit
def saturating_mul16(a, b):
    assert np.int16(a) == a
    assert np.int16(b) == b
    if a == b and a == np.iinfo(np.int16).min:
        return np.int16(np.iinfo(np.int16).max)
    ab = np.int32(a) * np.int32(b)
    divider = 1 << 15
    if ab >= 0:
        return ab // divider
    else:
        result = ab // divider
        # Python uses floor, the reference uses truncation
        # so we need to compensate for that.
        if result * divider < ab:
            result += 1
        return result


def shift_left32(a, offset):
    assert offset >= 0
    assert np.int32(a) == a
    shifted = a * (1 << offset)
    if shifted < np.iinfo(np.int32).min:
        return np.int32(np.iinfo(np.int32).min)
    elif shifted > np.iinfo(np.int32).max:
        return np.int32(np.iinfo(np.int32).max)
    else:
        return np.int32(shifted)


def shift_left16(a, offset):
    assert offset >= 0
    assert np.int16(a) == a
    shifted = a * (1 << offset)
    if shifted < np.iinfo(np.int16).min:
        return np.int16(np.iinfo(np.int16).min)
    elif shifted > np.iinfo(np.int16).max:
        return np.int16(np.iinfo(np.int16).max)
    else:
        return np.int16(shifted)


def downscale_multiplier_int32_to_int16(a):
    assert np.int32(a) == a
    rounding_offset = 1 << 15
    if a >= np.iinfo(np.int32).max - rounding_offset:
        return np.iinfo(np.int16).max
    else:
        return np.int16((a + rounding_offset) >> 16)


def rounding_divide_by_pot(x, exponent):
    assert np.int32(x) == x
    assert np.int32(exponent) == exponent
    mask = (1 << exponent) - 1
    remainder = x & mask
    threshold = mask >> 1
    if x < 0:
        threshold += 1
    result = x >> exponent
    if remainder > threshold:
        result += 1
    return result


def saturating_rounding_multiply_by_pot(x, exponent):
    assert np.int32(x) == x
    assert np.int32(exponent) == exponent
    threshold = (1 << (np.iinfo(np.int32).bits - 1 - exponent)) - 1
    if x > threshold:
        return np.iinfo(np.int32).max
    elif x < -threshold:
        return np.iinfo(np.int32).min
    else:
        return shift_left32(x, exponent)


def rescale(integer_bits_src, integer_bits_dst, x):
    assert np.int32(integer_bits_src) == integer_bits_src
    assert np.int32(integer_bits_dst) == integer_bits_dst
    assert np.int32(x) == x
    exponent = integer_bits_src - integer_bits_dst
    if exponent < 0:
        result = rounding_divide_by_pot(x, -exponent)
    else:
        result = saturating_rounding_multiply_by_pot(x, exponent)
    return result


# Input Q0.31
def exp_on_interval_between_negative_one_quarter_and_0_excl(a):
    assert np.int32(a) == a
    assert -1 << (31 - 2) <= a < 0
    offset = 28
    constant_term = 1895147668
    constant_1_over_3 = 715827883
    x = a + (1 << offset)
    x2 = saturating_rounding_mul32(x, x)
    x3 = saturating_rounding_mul32(x2, x)
    x4 = saturating_rounding_mul32(x2, x2)
    x4_over_4 = rounding_divide_by_pot(x4, 2)
    x4_over_24_plus_x3_over_6_plus_x2_over_2 = rounding_divide_by_pot(
        saturating_rounding_mul32((x4_over_4 + x3), constant_1_over_3) + x2, 1
    )

    return np.int32(
        constant_term + saturating_rounding_mul32(constant_term, x + x4_over_24_plus_x3_over_6_plus_x2_over_2)
    )


# Input Q5.26
def exp_on_negative_values(a):
    assert np.int32(a) == a
    assert a <= 0
    one_quarter = np.int32(16777216)
    mask = np.int32(16777215)
    a_mod_quarter_minus_one_quarter = np.int32((a & mask) - one_quarter)

    result = exp_on_interval_between_negative_one_quarter_and_0_excl(rescale(5, 0, a_mod_quarter_minus_one_quarter))
    remainder = np.int32(a_mod_quarter_minus_one_quarter - a)

    def exp_barrel_shifter(exponent, multiplier, result):
        fractional_bits = 26
        integer_bits = 5
        shift = fractional_bits + exponent if integer_bits > exponent else 0
        if remainder & (1 << shift):
            return saturating_rounding_mul32(result, multiplier)
        else:
            return result

    result = exp_barrel_shifter(-2, 1672461947, result)
    result = exp_barrel_shifter(-1, 1302514674, result)
    result = exp_barrel_shifter(+0, 790015084, result)
    result = exp_barrel_shifter(+1, 290630308, result)
    result = exp_barrel_shifter(+2, 39332535, result)
    result = exp_barrel_shifter(+3, 720401, result)
    result = exp_barrel_shifter(+4, 242, result)

    if a == 0:
        return np.iinfo(np.int32).max
    else:
        return result


def multiply_by_quantized_multiplier(x, scale, shift):
    # Multiplies x (int32) by (scale, shift) which have obtained by a call to scaling.quantize_scale,
    # returns rounded result
    shift = 31 - shift
    left_shift = shift if shift > 0 else 0
    right_shift = -shift if shift < 0 else 0
    mul = saturating_rounding_mul32(x * (1 << left_shift), scale)
    return rounding_divide_by_pot(mul, right_shift)
