# 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.
"""Batch Normalization implemented in Numpy."""

import numpy as np


def batch_norm(
    x: np.ndarray,
    gamma: np.ndarray,
    beta: np.ndarray,
    moving_mean: np.ndarray,
    moving_var: np.ndarray,
    axis: int,
    epsilon: float,
    center: bool,
    scale: bool,
    training: bool,
    momentum: float,
):
    """Batch Normalization operator implemented in Numpy.

    Parameters
    ----------
    data : np.ndarray
        Input to be batch-normalized.

    gamma : np.ndarray
        Scale factor to be applied to the normalized tensor.

    beta : np.ndarray
        Offset to be applied to the normalized tensor.

    moving_mean : np.ndarray
        Running mean of input.

    moving_var : np.ndarray
        Running variance of input.

    axis : int
        Specify along which shape axis the normalization should occur.

    epsilon : float
        Small float added to variance to avoid dividing by zero.

    center : bool
        If True, add offset of beta to normalized tensor, If False,
        beta is ignored.

    scale : bool
        If True, scale normalized tensor by gamma. If False, gamma
        is ignored.

    training : bool
        Indicating whether it is in training mode. If True, update
        moving_mean and moving_var.

    momentum : float
        The value used for the moving_mean and moving_var update

    Returns
    -------
    output : np.ndarray
        Normalized data with same shape as input

    moving_mean : np.ndarray
        Running mean of input.

    moving_var : np.ndarray
        Running variance of input.
    """
    shape = [1] * len(x.shape)
    shape[axis] = x.shape[axis]

    if training:
        reduce_axes = list(range(len(x.shape)))
        reduce_axes.remove(axis)
        reduce_axes = tuple(reduce_axes)
        data_mean = np.mean(x, axis=reduce_axes)
        data_var = np.var(x, axis=reduce_axes)
        data_mean_rs = np.reshape(data_mean, shape)
        data_var_rs = np.reshape(data_var, shape)
        out = (x - data_mean_rs) / np.sqrt(data_var_rs + epsilon)
    else:
        moving_mean_rs = moving_mean.reshape(shape)
        moving_var_rs = moving_var.reshape(shape)
        out = (x - moving_mean_rs) / np.sqrt(moving_var_rs + epsilon)

    if scale:
        out = out * gamma.reshape(shape)
    if center:
        out = out + beta.reshape(shape)

    if training:
        return [
            out,
            (1 - momentum) * moving_mean + momentum * data_mean,
            (1 - momentum) * moving_var + momentum * data_var,
        ]

    return [out, moving_mean, moving_var]
