# 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.
# pylint: disable=invalid-name,missing-function-docstring,unused-variable
"""Intrinsics for tensorization on Apple GPU."""

from typing import Literal

from tvm.script import tirx as T
from tvm.tirx import Buffer, Expr, PrimFunc, TensorIntrin

######## simdgroup matrix intrinsics ########


def get_simdgroup_index(buffer: Buffer, stride: Expr, col: int, row: int):
    """Compute simdgroup index using elem_offset of the buffer"""

    # NOTE: Need further check the usage between `col`` and `row`
    # Currently, Metal only supports 8x8, which means the values of `col` and `row` are the same
    frag_index_m = buffer.elem_offset // stride // col
    frag_index_n = buffer.elem_offset % stride // row

    num_fragments_per_row = stride // row
    return frag_index_m * num_fragments_per_row + frag_index_n


def get_make_filled_simdgroup_matrix_intrin(
    dtype: str, col: int = 8, row: int = 8
) -> tuple[PrimFunc, PrimFunc]:
    @T.prim_func(s_tir=True)
    def desc(a: T.handle) -> None:
        A = T.match_buffer(a, (col, row), dtype, scope="metal.simdgroup", offset_factor=1)
        with T.sblock("root"):
            T.reads()
            T.writes(A[0:col, 0:row])
            for i, j in T.grid(col, row):
                with T.sblock("init"):
                    vi, vj = T.axis.remap("SS", [i, j])
                    A[vi, vj] = T.float32(0)

    @T.prim_func(s_tir=True)
    def impl(a: T.handle) -> None:
        d0, d1 = T.int32(), T.int32()
        A = T.match_buffer(
            a, (col, row), dtype, scope="metal.simdgroup", strides=[d1, d0], offset_factor=1
        )
        with T.sblock("root"):
            T.reads()
            T.writes(A[0:col, 0:row])
            T.metal.make_filled_simdgroup_matrix(
                A.data,
                index=get_simdgroup_index(A, d1, col, row),
                value=T.float32(0),
                col=col,
                row=row,
            )

    return desc, impl


def get_simdgroup_load_intrin(
    dtype: str,
    scope: Literal["global", "shared"],
    col: int = 8,
    row: int = 8,
    transpose_matrix: bool = False,
) -> tuple[PrimFunc, PrimFunc]:
    align = col * row

    @T.prim_func(s_tir=True)
    def desc(a: T.handle, c: T.handle) -> None:
        A = T.match_buffer(a, (col, row), dtype, align=align, scope=scope, offset_factor=1)
        C = T.match_buffer(
            c, (col, row), dtype, align=align, scope="metal.simdgroup", offset_factor=1
        )
        with T.sblock("root"):
            T.reads(A[0:col, 0:row])
            T.writes(C[0:col, 0:row])
            for i, j in T.grid(col, row):
                with T.sblock("load"):
                    vii, vjj = T.axis.remap("SS", [i, j])
                    if transpose_matrix:
                        # C[vii, vjj] = A[vjj, vii]
                        C[vjj, vii] = A[vii, vjj]
                    else:
                        C[vii, vjj] = A[vii, vjj]

    @T.prim_func(s_tir=True)
    def impl(a: T.handle, c: T.handle) -> None:
        s0, s1, d0, d1 = T.int32(), T.int32(), T.int32(), T.int32()
        A = T.match_buffer(
            a,
            (col, row),
            dtype,
            align=align,
            scope=scope,
            strides=[s1, s0],
            offset_factor=1,
        )
        C = T.match_buffer(
            c,
            (col, row),
            dtype,
            align=align,
            scope="metal.simdgroup",
            strides=[d1, d0],
            offset_factor=1,
        )
        with T.sblock("root"):
            T.reads(A[0:col, 0:row])
            T.writes(C[0:col, 0:row])
            T.metal.simdgroup_load(
                C.data,
                index=get_simdgroup_index(C, d1, col, row),
                ptr=A.access_ptr("r", ptr_type=dtype),
                stride=s1,
                col=col,
                row=row,
                transpose_matrix=transpose_matrix,
            )

    return desc, impl


def get_simdgroup_store_intrin(
    dtype: str,
    scope: Literal["global", "shared"],
    col: int = 8,
    row: int = 8,
    transpose_matrix: bool = False,
) -> tuple[PrimFunc, PrimFunc]:
    align = col * row

    @T.prim_func(s_tir=True)
    def desc(a: T.handle, c: T.handle) -> None:
        A = T.match_buffer(
            a, (col, row), dtype, align=align, scope="metal.simdgroup", offset_factor=1
        )
        C = T.match_buffer(c, (col, row), dtype, align=align, scope=scope, offset_factor=1)
        with T.sblock("root"):
            T.reads(A[0:col, 0:row])
            T.writes(C[0:col, 0:row])
            for i, j in T.grid(col, row):
                with T.sblock("store"):
                    vii, vjj = T.axis.remap("SS", [i, j])
                    if transpose_matrix:
                        C[vjj, vii] = A[vii, vjj]
                    else:
                        C[vii, vjj] = A[vii, vjj]

    @T.prim_func(s_tir=True)
    def impl(a: T.handle, c: T.handle) -> None:
        s0, s1, d0, d1 = T.int32(), T.int32(), T.int32(), T.int32()
        A = T.match_buffer(
            a,
            (col, row),
            dtype,
            align=align,
            scope="metal.simdgroup",
            strides=[s1, s0],
            offset_factor=1,
        )
        C = T.match_buffer(
            c, (col, row), dtype, align=align, scope=scope, strides=[d1, d0], offset_factor=1
        )
        with T.sblock("root"):
            T.reads(A[0:col, 0:row])
            T.writes(C[0:col, 0:row])
            T.metal.simdgroup_store(
                A.data,
                index=get_simdgroup_index(A, s1, col, row),
                ptr=C.access_ptr("w", ptr_type=dtype),
                stride=d1,
                col=col,
                row=row,
                transpose_matrix=transpose_matrix,
            )

    return desc, impl


def get_simdgroup_multiply_accumulate_intrin(
    m_dim: int, n_dim: int, k_dim: int, dtype: str
) -> tuple[PrimFunc, PrimFunc]:
    @T.prim_func(s_tir=True)
    def desc(a: T.handle, b: T.handle, c: T.handle) -> None:
        A = T.match_buffer(a, (m_dim, k_dim), dtype, scope="metal.simdgroup", offset_factor=1)
        B = T.match_buffer(b, (k_dim, n_dim), dtype, scope="metal.simdgroup", offset_factor=1)
        C = T.match_buffer(c, (m_dim, n_dim), dtype, scope="metal.simdgroup", offset_factor=1)
        with T.sblock("root"):
            T.reads(C[0:m_dim, 0:n_dim], A[0:m_dim, 0:k_dim], B[0:k_dim, 0:n_dim])
            T.writes(C[0:m_dim, 0:n_dim])
            for i, j, k in T.grid(m_dim, n_dim, k_dim):
                with T.sblock(""):
                    vii, vjj, vkk = T.axis.remap("SSR", [i, j, k])
                    C[vii, vjj] += A[vii, vkk] * B[vkk, vjj]

    @T.prim_func(s_tir=True)
    def impl(a: T.handle, b: T.handle, c: T.handle) -> None:
        a0, a1, b0, b1, c0, c1 = T.int32(), T.int32(), T.int32(), T.int32(), T.int32(), T.int32()
        A = T.match_buffer(
            a, (m_dim, k_dim), dtype, scope="metal.simdgroup", strides=[a1, a0], offset_factor=1
        )
        B = T.match_buffer(
            b, (k_dim, n_dim), dtype, scope="metal.simdgroup", strides=[b1, b0], offset_factor=1
        )
        C = T.match_buffer(
            c, (m_dim, n_dim), dtype, scope="metal.simdgroup", strides=[c1, c0], offset_factor=1
        )
        with T.sblock("root"):
            T.reads(C[0:m_dim, 0:n_dim], A[0:m_dim, 0:k_dim], B[0:k_dim, 0:n_dim])
            T.writes(C[0:m_dim, 0:n_dim])
            T.metal.simdgroup_multiply_accumulate(
                C.data,
                get_simdgroup_index(C, c1, m_dim, n_dim),
                A.data,
                get_simdgroup_index(A, a1, m_dim, k_dim),
                B.data,
                get_simdgroup_index(B, b1, k_dim, n_dim),
                C.data,
                get_simdgroup_index(C, c1, m_dim, n_dim),
            )

    return desc, impl


# Make filled simdgroup matrix intrinsics

SIMDGROUP_MAKE_FILLED_8x8x8_f16_INTRIN = "simdgroup_make_filled_8x8x8_f16"
TensorIntrin.register(
    SIMDGROUP_MAKE_FILLED_8x8x8_f16_INTRIN,
    *get_make_filled_simdgroup_matrix_intrin("float16", 8, 8),
)

SIMDGROUP_FILLED_8x8x8_f32_INTRIN = "simdgroup_fill_8x8x8_f32"
TensorIntrin.register(
    SIMDGROUP_FILLED_8x8x8_f32_INTRIN, *get_make_filled_simdgroup_matrix_intrin("float32", 8, 8)
)

SIMDGROUP_FILLED_8x8x8_bf16_INTRIN = "simdgroup_fill_8x8x8_bf16"
TensorIntrin.register(
    SIMDGROUP_FILLED_8x8x8_bf16_INTRIN, *get_make_filled_simdgroup_matrix_intrin("bfloat16", 8, 8)
)

# Load intrinsics

SIMDGROUP_LOAD_8x8x8_f16_SHARED_INTRIN = "simdgroup_load_8x8x8_f16_shared"
TensorIntrin.register(
    SIMDGROUP_LOAD_8x8x8_f16_SHARED_INTRIN,
    *get_simdgroup_load_intrin("float16", "shared", 8, 8, False),
)

SIMDGROUP_LOAD_8x8x8_f16_SHARED_TRANS_INTRIN = "simdgroup_load_8x8x8_f16_shared_trans"
TensorIntrin.register(
    SIMDGROUP_LOAD_8x8x8_f16_SHARED_TRANS_INTRIN,
    *get_simdgroup_load_intrin("float16", "shared", 8, 8, True),
)

# Store intrinsics

SIMDGROUP_STORE_8x8x8_f16_GLOBAL_INTRIN = "simdgroup_store_8x8x8_f16_global"
TensorIntrin.register(
    SIMDGROUP_STORE_8x8x8_f16_GLOBAL_INTRIN,
    *get_simdgroup_store_intrin("float16", "global", 8, 8, False),
)

SIMDGROUP_STORE_8x8x8_f16_SHARED_INTRIN = "simdgroup_store_8x8x8_f16_shared"
TensorIntrin.register(
    SIMDGROUP_STORE_8x8x8_f16_SHARED_INTRIN,
    *get_simdgroup_store_intrin("float16", "shared", 8, 8, False),
)
# Multiply accumulate intrinsics

SIMDGROUP_MULTI_ACC_8x8x8_f16_INTRIN = "simdgroup_multiply_accumulate_8x8x8_f16"
TensorIntrin.register(
    SIMDGROUP_MULTI_ACC_8x8x8_f16_INTRIN,
    *get_simdgroup_multiply_accumulate_intrin(8, 8, 8, "float16"),
)


def get_simdgroup_intrin_group(
    load_scope: Literal["shared"],
    store_scope: Literal["global", "shared"],
    dtype: str,
    trans_a: bool = False,
    trans_b: bool = False,
) -> dict[str, str]:
    """Get a group of intrinsics for tensorization on Apple GPU.

    Parameters
    ----------
    load_scope : Literal["shared"]
        The memory scope of the input buffer.

    store_scope : Literal["global", "shared"]
        The memory scope of the result buffer.

    dtype : str
        The data type of the input and output buffers.

    trans_a : bool
        Whether the input matrix A is transposed.

    trans_b : bool
        Whether the input matrix B is transposed.

    Returns
    -------
    ret : Dict[str, str]
        A group of tensor intrinsics.
    """
    assert load_scope in ["shared"]
    assert store_scope in ["global", "shared"]
    assert dtype in ["float16", "bfloat16", "float32"]

    shape = "8x8x8"
    dtype = "f16" if dtype == "float16" else "bf16" if dtype == "bfloat16" else "f32"
    trans_a = "_trans" if trans_a else ""
    trans_b = "_trans" if trans_b else ""

    # e.g. simdgroup_load_8x8x8_f16_shared
    load_a_intrin = f"simdgroup_load_{shape}_{dtype}_{load_scope}{trans_a}"
    # e.g. simdgroup_load_8x8x8_f16_shared_trans
    load_b_intrin = f"simdgroup_load_{shape}_{dtype}_{load_scope}{trans_b}"
    # e.g. simdgroup_multiply_accumulate_8x8x8_f16
    compute_intrin = f"simdgroup_multiply_accumulate_{shape}_{dtype}"
    # e.g. simdgroup_make_filled_8x8x8_f16
    init_intrin = f"simdgroup_make_filled_{shape}_{dtype}"
    # e.g. simdgroup_store_8x8x8_f16_global
    store_intrin = f"simdgroup_store_{shape}_{dtype}_{store_scope}"

    return {
        "init": init_intrin,
        "load_a": load_a_intrin,
        "load_b": load_b_intrin,
        "compute": compute_intrin,
        "store": store_intrin,
    }
