# 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.

"""Attention operator in python"""

import numpy as np

from .softmax_python import softmax_python


def attention_python(
    q: np.ndarray,
    k: np.ndarray,
    v: np.ndarray,
    bias: np.ndarray | None,
    qk_scale: float,
    causal: str,
    window_size: int | None = None,
    layout: str = "BSNH",
):  # pylint: disable=too-many-arguments, too-many-locals, invalid-name
    """Attention operator in python

    Parameters
    ----------
    q : np.ndarray
        Query tensor with shape [batch, seq_length, num_heads, head_dim] in the layout specified by
        `layout`.
    k : np.ndarray
        Key tensor with shape [batch, seq_length_kv, num_kv_heads, head_dim] in the layout specified
        by `layout`.
    v : np.ndarray
        Value tensor with shape [batch, seq_length_kv, num_kv_heads, head_dim_v] in the layout
        specified by `layout`.
    bias : np.ndarray
        Bias tensor with shape [batch, num_heads, seq_length, seq_length]
    qk_scale : float
        Scale factor for the query-key product.
    causal : str
        The type of causal mask to apply. Can be "none", "TopLeft", or "BottomRight".
    window_size : Optional[int]
        The window size for the causal mask.
    layout : str
        The layout of the input tensors, e.g. "BSNH" or "BNSH".

    Returns
    -------
    np.ndarray
        The output tensor with shape [batch, seq_length, num_heads, head_dim_v] in the layout
        specified by `layout`.
    """
    assert layout in ["BSNH", "BNSH", "SBNH"]

    dim_b = layout.find("B")
    dim_s = layout.find("S")
    dim_n = layout.find("N")
    dim_h = layout.find("H")

    q = q.transpose(dim_b, dim_n, dim_s, dim_h)  # b, n, s, h
    k = k.transpose(dim_b, dim_n, dim_s, dim_h)  # b, n, s_kv, h
    kt = k.transpose(0, 1, 3, 2)  # b, n, h, s_kv
    v = v.transpose(dim_b, dim_n, dim_s, dim_h)

    num_heads = q.shape[1]
    num_kv_heads = k.shape[1]
    s = q.shape[2]
    s_kv = k.shape[2]

    if num_heads != num_kv_heads:
        assert num_heads % num_kv_heads == 0
        factor = num_heads // num_kv_heads
        kt = np.repeat(kt, factor, axis=1)
        v = np.repeat(v, factor, axis=1)

    if not qk_scale == "none":
        score = q @ kt * qk_scale  # b, n, s, s_kv
    else:
        score = q @ kt / np.sqrt(q.shape[-1])  # b, n, s, s_kv
    if bias is not None:
        score = score + bias  # b, n, s, s_kv
    if causal == "none":
        attn = softmax_python(score, -1)
    else:
        if causal == "TopLeft":
            offset = 0
        elif causal == "BottomRight":
            offset = abs(s - s_kv)
        else:
            raise ValueError(f"Unsupported causal type: {causal}")
        score_masked = np.tril(score, k=offset)

        if window_size:
            score_masked = np.triu(
                score_masked,
                -window_size + 1,  # pylint: disable=invalid-unary-operand-type
            )

        score_masked_exp = np.tril(
            np.exp(score_masked - np.max(score_masked, axis=-1, keepdims=True)), k=offset
        )

        if window_size:
            score_masked_exp = np.triu(
                score_masked_exp,
                -window_size + 1,  # pylint: disable=invalid-unary-operand-type
            )

        score_masked_sum = np.sum(score_masked_exp, axis=-1, keepdims=True)
        attn = np.divide(score_masked_exp, score_masked_sum)

    out = attn @ v  # b, n, s, h_v
    return out.transpose(*np.argsort([dim_b, dim_n, dim_s, dim_h]).tolist())
