# 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.
import tvm
import tvm.script
from tvm.script import tir as T


def get_before():
    @tvm.script.ir_module
    class Before:
        @T.prim_func
        def main(
            Aptr: T.handle("bfloat16"), Bptr: T.handle("bfloat16"), Dptr: T.handle("bfloat16")
        ):
            T.func_attr({"global_symbol": "main"})
            A = T.decl_buffer((100,), "bfloat16", data=Aptr)
            B = T.decl_buffer((100,), "bfloat16", data=Bptr)
            D = T.decl_buffer((100,), "bfloat16", data=Dptr)
            C = T.decl_buffer((100,), "bfloat16")
            for i in T.grid(100):
                C[i] = A[i] + B[i]
                D[i] = T.exp(C[i])

    return Before


def u16tof32(v):
    uint32_v = v.astype("uint32")
    uint32_v = uint32_v << tvm.tir.const(16, "uint32")
    return T.reinterpret("float32", uint32_v)


def bf16tof32(v):
    return u16tof32(T.reinterpret("uint16", v))


def f32tou16(v):
    uint32_v = T.reinterpret("uint32", v)
    rounding_bias = (uint32_v >> tvm.tir.const(16, "uint32")) & tvm.tir.const(1, "uint32")
    rounding_bias += tvm.tir.const(0x7FFF, "uint32")
    uint32_v = uint32_v + rounding_bias
    return (uint32_v >> tvm.tir.const(16, "uint32")).astype("uint16")


def f32tobf16(v):
    return T.reinterpret("bfloat16", f32tou16(v))


def get_after_compute_legalize():
    @tvm.script.ir_module
    class After:
        @T.prim_func
        def main(
            Aptr: T.handle("bfloat16"), Bptr: T.handle("bfloat16"), Dptr: T.handle("bfloat16")
        ):
            T.func_attr({"global_symbol": "main"})
            A = T.decl_buffer((100,), "bfloat16", data=Aptr)
            B = T.decl_buffer((100,), "bfloat16", data=Bptr)
            D = T.decl_buffer((100,), "bfloat16", data=Dptr)
            C = T.decl_buffer((100,), "float32")
            for i in T.grid(100):
                C[i] = bf16tof32(A[i]) + bf16tof32(B[i])
                D[i] = f32tobf16(T.exp(C[i]))

    return After


def get_after_storage_legalize():
    @tvm.script.ir_module
    class After:
        @T.prim_func
        def main(Aptr: T.handle("uint16"), Bptr: T.handle("uint16"), Dptr: T.handle("uint16")):
            T.func_attr({"global_symbol": "main"})
            A = T.decl_buffer((100,), "uint16", data=Aptr)
            B = T.decl_buffer((100,), "uint16", data=Bptr)
            D = T.decl_buffer((100,), "uint16", data=Dptr)
            C = T.decl_buffer((100,), "float32")
            for i in T.grid(100):
                C[i] = u16tof32(A[i]) + u16tof32(B[i])
                D[i] = f32tou16(T.exp(C[i]))

    return After


def test_bf16_compute_legalize():
    before = get_before()
    expected = get_after_compute_legalize()
    # run the transform twice to ensure we can afford to deal
    # with this repeative optimizations
    after = tvm.tir.transform.BF16ComputeLegalize()(before)
    after = tvm.tir.transform.BF16ComputeLegalize()(after)

    tvm.ir.assert_structural_equal(after, expected)


def test_bf16_storage_legalize():
    before = get_after_compute_legalize()
    after = tvm.tir.transform.BF16StorageLegalize()(before)
    expected = get_after_storage_legalize()
    tvm.ir.assert_structural_equal(after, expected)


if __name__ == "__main__":
    test_bf16_storage_legalize()
