from itertools import pairwise
from math import prod

from ._flops_registry import register_npu_flop


@register_npu_flop(target="torch_npu:npu_fusion_attention", is_default=True)
def npu_fusion_attention_flops(
    query,
    key,
    value,
    head_num,
    input_layout,
    pse=None,
    padding_mask=None,
    atten_mask=None,
    scale=1.0,
    keep_prob=1.0,
    pre_tockens=2147483647,
    next_tockens=2147483647,
    inner_precise=0,
    prefix=None,
    actual_seq_qlen=None,
    actual_seq_kvlen=None,
    sparse_mode=0,
    *args,
    **kwargs,
):
    q_shape = query.shape
    k_shape = key.shape
    v_shape = value.shape
    if input_layout == "TND":
        return _calculate_tnd_layout_flops(
            q_shape, k_shape, v_shape, actual_seq_qlen, actual_seq_kvlen
        )
    kv_heads = _infer_kv_heads(q_shape, k_shape, input_layout, head_num)
    return _calculate_common_layout_flops(
        q_shape, k_shape, v_shape, input_layout, sparse_mode, head_num, kv_heads
    )


@register_npu_flop(target="torch_npu:npu_fused_infer_attention_score", is_default=True)
def npu_fused_infer_attention_score_flops(
    query,
    key,
    value,
    *,
    input_layout,
    num_heads,
    num_key_value_heads=0,
    actual_seq_lengths=None,
    actual_seq_lengths_kv=None,
    sparse_mode=0,
    **kwargs,
):
    num_key_value_heads = num_key_value_heads or num_heads
    q_shape = query.shape
    k_shape = key.shape
    v_shape = value.shape
    if input_layout == "TND":
        return _calculate_tnd_layout_flops(
            q_shape,
            k_shape,
            v_shape,
            actual_seq_lengths,
            actual_seq_lengths_kv,
            num_heads,
        )
    return _calculate_common_layout_flops(
        q_shape,
        k_shape,
        v_shape,
        input_layout,
        sparse_mode,
        num_heads,
        num_key_value_heads,
    )


@register_npu_flop(target="torch_npu:npu_all_gather_base_mm", is_default=True)
def npu_all_gather_base_mm_flops(
    x1,
    x2,
    hcom,
    world_size,
    bias=None,
    x1_scale=None,
    x2_scale=None,
    gather_index=0,
    gather_output=True,
    comm_turn=0,
    output_dtype=None,
    comm_mode=None,
    **kwargs,
):
    x1_shape = _shape(x1)
    x2_shape = _shape(x2)
    m_local, k = x1_shape[-2:]
    n = x2_shape[-1]
    return 2 * m_local * int(world_size) * k * n


@register_npu_flop(target="torch_npu:npu_transpose_batchmatmul", is_default=True)
def npu_transpose_batchmatmul_flops(
    input,
    weight,
    *,
    bias=None,
    scale=None,
    perm_x1=(0, 1, 2),
    perm_x2=(0, 1, 2),
    perm_y=(1, 0, 2),
    batch_split_factor=1,
    **kwargs,
):
    input_shape = _permute_shape(_shape(input), perm_x1)
    weight_shape = _permute_shape(_shape(weight), perm_x2)
    return _matmul_shape_flops(input_shape, weight_shape)


@register_npu_flop(target="torch_npu:npu_grouped_matmul", is_default=True)
def npu_grouped_matmul_flops(
    x,
    weight,
    *,
    bias=None,
    scale=None,
    offset=None,
    antiquant_scale=None,
    antiquant_offset=None,
    per_token_scale=None,
    group_list=None,
    activation_input=None,
    activation_quant_scale=None,
    activation_quant_offset=None,
    split_item=0,
    group_type=None,
    group_list_type=0,
    act_type=0,
    output_dtype=None,
    tuning_config=None,
    **kwargs,
):
    return _grouped_matmul_flops(x, weight, group_list)


@register_npu_flop(target="torch_npu:npu_quant_matmul_gelu", is_default=True)
def npu_quant_matmul_gelu_flops(
    x1,
    x2,
    x1_scale,
    x2_scale,
    *,
    bias=None,
    approximate="gelu_erf",
    **kwargs,
):
    return _matrix_tensor_flops(x1, x2)


@register_npu_flop(
    target="torch_npu:npu_grouped_matmul_swiglu_quant_v2", is_default=True
)
def npu_grouped_matmul_swiglu_quant_v2_flops(
    x,
    weight,
    weight_scale,
    x_scale,
    group_list,
    *,
    smooth_scale=None,
    weight_assist_matrix=None,
    bias=None,
    dequant_mode=0,
    dequant_dtype=0,
    quant_mode=0,
    quant_dtype=0,
    group_list_type=0,
    tuning_config=None,
    **kwargs,
):
    return _matrix_tensor_flops(x, weight)


@register_npu_flop(target="torch_npu:npu_alltoallv_gmm", is_default=True)
def npu_alltoallv_gmm_flops(
    gmm_x,
    gmm_weight,
    hcom,
    ep_world_size,
    send_counts,
    recv_counts,
    *,
    send_counts_tensor=None,
    recv_counts_tensor=None,
    mm_x=None,
    mm_weight=None,
    trans_gmm_weight=False,
    trans_mm_weight=False,
    permute_out_flag=False,
    **kwargs,
):
    return _gmm_with_optional_mm_flops(
        gmm_x, gmm_weight, mm_x, mm_weight, trans_gmm_weight, trans_mm_weight
    )


@register_npu_flop(target="torch_npu:npu_gmm_alltoallv", is_default=True)
def npu_gmm_alltoallv_flops(
    gmm_x,
    gmm_weight,
    hcom,
    ep_world_size,
    send_counts,
    recv_counts,
    *,
    send_counts_tensor=None,
    recv_counts_tensor=None,
    mm_x=None,
    mm_weight=None,
    trans_gmm_weight=False,
    trans_mm_weight=False,
    **kwargs,
):
    return _gmm_with_optional_mm_flops(
        gmm_x, gmm_weight, mm_x, mm_weight, trans_gmm_weight, trans_mm_weight
    )


@register_npu_flop(target="torch_npu:npu_block_sparse_attention", is_default=True)
def npu_block_sparse_attention_flops(
    query,
    key,
    value,
    block_sparse_mask,
    block_shape,
    *,
    q_input_layout="TND",
    kv_input_layout="TND",
    num_key_value_heads=1,
    scale_value=0.0,
    inner_precise=1,
    actual_seq_lengths=None,
    actual_seq_lengths_kv=None,
    softmax_lse_flag=0,
    **kwargs,
):
    q_shape = _shape(query)
    v_shape = _shape(value)
    mask = _to_nested_list(block_sparse_mask)
    q_heads = len(mask[0]) if mask else None
    _, _, q_s, q_d = _parse_attention_dims(q_shape, q_input_layout, q_heads)
    _, _, kv_s, v_d = _parse_attention_dims(
        v_shape, kv_input_layout, num_key_value_heads
    )
    batch = len(mask)
    q_lens = _parse_actual_lengths(
        actual_seq_lengths, batch, q_s, q_input_layout == "TND"
    )
    kv_lens = _parse_actual_lengths(
        actual_seq_lengths_kv, batch, kv_s, kv_input_layout == "TND"
    )
    block_x, block_y = [int(dim) for dim in block_shape]
    score_elems = _count_block_sparse_score_elems(
        mask, q_lens, kv_lens, block_x, block_y
    )
    return int(2 * score_elems * (q_d + v_d))


@register_npu_flop(target="torch:mm", is_default=True)
def mm_flops(input, other, **kwargs):
    m, k = input.shape
    _, n = other.shape
    return 2 * m * n * k


@register_npu_flop(target="torch:bmm", is_default=True)
def bmm_flops(input, other, **kwargs):
    b, m, k = input.shape
    _, _, n = other.shape
    return 2 * b * m * n * k


@register_npu_flop(target="torch:matmul", is_default=True)
def matmul_flops(input, other, **kwargs):
    input_shape = tuple(input.shape)
    other_shape = tuple(other.shape)
    if len(input_shape) == 1 and len(other_shape) == 1:
        return 2 * input_shape[0]
    if len(input_shape) == 1:
        batch_shape = other_shape[:-2]
        m, k, n = 1, input_shape[0], other_shape[-1]
    elif len(other_shape) == 1:
        batch_shape = input_shape[:-2]
        m, k, n = input_shape[-2], input_shape[-1], 1
    else:
        batch_shape = _broadcast_shapes(input_shape[:-2], other_shape[:-2])
        m, k, n = input_shape[-2], input_shape[-1], other_shape[-1]
    return 2 * prod(batch_shape) * m * n * k


@register_npu_flop(target="torch.nn.functional:linear", is_default=True)
def linear_flops(input, weight, bias=None, **kwargs):
    n, k = weight.shape
    return 2 * prod(input.shape[:-1]) * n * k


@register_npu_flop(target="torch:addmm", is_default=True)
def addmm_flops(self, mat1, mat2, beta=1, alpha=1, **kwargs):
    m, k = mat1.shape
    _, n = mat2.shape
    return 2 * m * n * k


def _shape(tensor):
    return tuple(int(dim) for dim in tensor.shape)


def _matmul_shape_flops(left_shape, right_shape, trans_right=False):
    if len(left_shape) < 2 or len(right_shape) < 2:
        raise ValueError(f"Matmul FLOPs requires rank >= 2: {left_shape}, {right_shape}")
    m = prod(left_shape[:-1])
    k = left_shape[-1]
    n = right_shape[-2] if trans_right else right_shape[-1]
    return int(2 * m * k * n)


def _matrix_tensor_flops(left, right, trans_right=False):
    return _matmul_shape_flops(_shape(left), _shape(right), trans_right)


def _as_tensor_list(tensors):
    return list(tensors) if isinstance(tensors, (list, tuple)) else [tensors]


def _grouped_matmul_flops(x, weight, group_list=None):
    x_list = _as_tensor_list(x)
    weight_list = _as_tensor_list(weight)
    if len(x_list) == len(weight_list):
        return sum(
            _matrix_tensor_flops(left, right)
            for left, right in zip(x_list, weight_list)
        )
    if len(x_list) == 1:
        left_shape = _shape(x_list[0])
        group_lengths = _parse_group_lengths(
            group_list, len(weight_list), prod(left_shape[:-1])
        )
        return sum(
            _matmul_shape_flops((group_m, left_shape[-1]), _shape(right))
            for group_m, right in zip(group_lengths, weight_list)
        )
    raise ValueError(
        f"Grouped matmul FLOPs requires matching groups: {len(x_list)}, {len(weight_list)}"
    )


def _parse_group_lengths(group_list, group_count, total_m):
    if group_count == 1:
        return [total_m]
    if group_list is None:
        raise ValueError("Grouped matmul FLOPs requires group_list for split groups")
    groups = [int(group) for group in _to_sequence(group_list)]
    if len(groups) != group_count:
        raise ValueError(f"Expected {group_count} groups, got {len(groups)}")
    if groups[-1] == total_m and sum(groups) > total_m:
        groups = [groups[0]] + [curr - prev for prev, curr in pairwise(groups)]
    if sum(groups) != total_m:
        raise ValueError("group_list does not match grouped matmul token count")
    return groups


def _gmm_with_optional_mm_flops(
    gmm_x, gmm_weight, mm_x, mm_weight, trans_gmm_weight, trans_mm_weight
):
    flops = _matrix_tensor_flops(gmm_x, gmm_weight, trans_gmm_weight)
    if mm_x is not None and mm_weight is not None:
        flops += _matrix_tensor_flops(mm_x, mm_weight, trans_mm_weight)
    return flops


def _permute_shape(tensor_shape, permutation):
    if len(tensor_shape) != len(permutation):
        raise ValueError(
            f"Permutation {permutation} does not match tensor shape {tensor_shape}"
        )
    return tuple(tensor_shape[int(index)] for index in permutation)


def _broadcast_shapes(left_shape, right_shape):
    result = []
    for left, right in zip(reversed(left_shape), reversed(right_shape)):
        if left != right and left != 1 and right != 1:
            raise ValueError(
                f"Cannot broadcast matmul batch dimensions: {left_shape}, {right_shape}"
            )
        result.append(max(left, right))
    longer = left_shape if len(left_shape) > len(right_shape) else right_shape
    result.extend(reversed(longer[: abs(len(left_shape) - len(right_shape))]))
    return tuple(reversed(result))


def _calculate_common_layout_flops(
    q_shape, k_shape, v_shape, input_layout, sparse_mode, q_heads, kv_heads
):
    q_b, q_n, q_s, q_d = _parse_dims(q_shape, input_layout, q_heads)
    _, _, k_s, k_d = _parse_dims(k_shape, input_layout, kv_heads)
    _, _, _, v_d = _parse_dims(v_shape, input_layout, kv_heads)
    attention_scores = _calculate_attention_scores(q_s, k_s, sparse_mode)
    return int(2 * q_b * q_n * attention_scores * (q_d + v_d))


def _calculate_tnd_layout_flops(
    q_shape, k_shape, v_shape, actual_seq_qlen, actual_seq_kvlen, q_heads=None
):
    if actual_seq_qlen is None or actual_seq_kvlen is None:
        raise ValueError("TND layout requires actual_seq_qlen and actual_seq_kvlen")
    _, shape_q_heads, q_d = q_shape
    _, _, v_d = v_shape
    q_lens = _parse_seq_len(actual_seq_qlen)
    kv_lens = _parse_seq_len(actual_seq_kvlen)
    if len(q_lens) != len(kv_lens) or any(length <= 0 for length in q_lens + kv_lens):
        raise ValueError("actual_seq_qlen and actual_seq_kvlen must contain valid cumulative lengths")
    attention_scores = sum(q_len * kv_len for q_len, kv_len in zip(q_lens, kv_lens))
    return int(2 * (q_heads or shape_q_heads) * (q_d + v_d) * attention_scores)


def _infer_kv_heads(q_shape, k_shape, input_layout, q_heads):
    if input_layout == "BNSD":
        return k_shape[1]
    if input_layout == "BSND":
        return k_shape[2]
    if input_layout == "BSH":
        _, _, q_hidden = q_shape
        _, _, k_hidden = k_shape
    elif input_layout == "SBH":
        _, _, q_hidden = q_shape
        _, _, k_hidden = k_shape
    else:
        return q_heads

    q_head_dim = _head_dim(q_hidden, q_heads)
    return _head_dim(k_hidden, q_head_dim)


def _calculate_attention_scores(q_s, k_s, sparse_mode):
    if sparse_mode == 0:
        return q_s * k_s
    if sparse_mode not in (2, 3):
        raise ValueError(f"Unknown FLOPs formula for sparse_mode={sparse_mode}")
    if sparse_mode == 2:
        return q_s * k_s - k_s * k_s / 2 if q_s >= k_s else q_s * q_s / 2
    return k_s * k_s / 2 if q_s >= k_s else q_s * k_s - q_s * q_s / 2


def _parse_dims(tensor_shape, input_layout, heads):
    if input_layout == "BNSD":
        return tensor_shape
    if input_layout == "BSND":
        b, s, n, d = tensor_shape
        return b, n, s, d
    if input_layout == "BSH":
        b, s, h = tensor_shape
        return b, heads, s, _head_dim(h, heads)
    if input_layout == "SBH":
        s, b, h = tensor_shape
        return b, heads, s, _head_dim(h, heads)
    raise ValueError(f"Invalid layout for FlashAttention input tensor: {input_layout}")


def _parse_attention_dims(tensor_shape, input_layout, heads):
    if input_layout == "TND":
        s, n, d = tensor_shape
        return None, n, s, d
    return _parse_dims(tensor_shape, input_layout, heads)


def _head_dim(hidden_size, heads):
    if heads <= 0 or hidden_size % heads != 0:
        raise ValueError(
            f"Hidden size {hidden_size} must be divisible by the number of heads {heads}"
        )
    return hidden_size // heads


def _parse_seq_len(original_seq_lens):
    seq_lens = [int(length) for length in original_seq_lens]
    while seq_lens and seq_lens[-1] == 0:
        seq_lens.pop()
    if not seq_lens:
        return []
    return [seq_lens[0]] + [
        curr - prev for prev, curr in pairwise(seq_lens)
    ]


def _parse_actual_lengths(seq_lens, batch, default_len, is_cumulative=False):
    if seq_lens is None:
        return [default_len] * batch
    lengths = [int(length) for length in seq_lens]
    while lengths and lengths[-1] == 0:
        lengths.pop()
    if len(lengths) != batch:
        raise ValueError(f"Expected {batch} sequence lengths, got {len(lengths)}")
    if is_cumulative:
        lengths = [lengths[0]] + [
            curr - prev for prev, curr in pairwise(lengths)
        ]
    if any(length < 0 for length in lengths):
        raise ValueError("Sequence lengths must be non-negative")
    return lengths


def _to_nested_list(value):
    return _to_sequence(value)


def _to_sequence(value):
    if isinstance(value, (list, tuple)):
        return value
    if hasattr(value, "tolist"):
        sequence = value.tolist()
        if isinstance(sequence, (list, tuple)):
            return sequence
    raise ValueError("Value must be a sequence or expose tolist() returning a sequence")


def _count_block_sparse_score_elems(mask, q_lens, kv_lens, block_x, block_y):
    score_elems = 0
    for batch_idx, heads in enumerate(mask):
        q_len = q_lens[batch_idx]
        kv_len = kv_lens[batch_idx]
        for q_blocks in heads:
            for q_block_idx, kv_blocks in enumerate(q_blocks):
                q_start = q_block_idx * block_x
                q_tokens = min(block_x, max(q_len - q_start, 0))
                if q_tokens == 0:
                    continue
                for kv_block_idx, is_valid in enumerate(kv_blocks):
                    if not is_valid:
                        continue
                    kv_start = kv_block_idx * block_y
                    kv_tokens = min(block_y, max(kv_len - kv_start, 0))
                    score_elems += q_tokens * kv_tokens
    return score_elems