def _projection(
        values: "tf.Tensor",
        eps: int | float | np.ndarray,
        norm_p: int | float | str,
        *,
        suboptimal: bool = True,
    ) -> "tf.Tensor":
        """
        Project `values` on the L_p norm ball of size `eps`.

        :param values: Values to clip.
        :param eps: If a scalar, the norm of the L_p ball onto which samples are projected. Equivalently in general,
                    can be any array of non-negatives broadcastable with `values`, and the projection occurs onto the
                    unit ball for the weighted L_{p, w} norm with `w = 1 / eps`. Currently, for any given sample,
                    non-uniform weights are only supported with infinity norm. Example: To specify sample-wise scalar,
                    you can provide `eps.shape = (n_samples,) + (1,) * values[0].ndim`.
        :param norm_p: Lp norm to use for clipping, with `norm_p > 0`. Only 2, `np.inf` and "inf" are supported
                       with `suboptimal=False` for now.
        :param suboptimal: If `True` simply projects by rescaling to Lp ball. Fast but may be suboptimal for
                           `norm_p != 2`.
                       Ignored when `norm_p in [np.inf, "inf"]` because optimal solution is fast. Defaults to `True`.
        :return: Values of `values` after projection.
        """
        import tensorflow as tf

        norm = np.inf if norm_p == "inf" else float(norm_p)
        assert norm > 0

        values_tmp = tf.reshape(values, (len(values), -1))  # (n_samples, d)

        eps = np.broadcast_to(eps, values.shape)
        eps = eps.reshape(len(eps), -1)  # (n_samples, d)
        assert np.all(eps >= 0)
        if norm != np.inf and not np.all(eps == eps[:, [0]]):
            raise NotImplementedError(
                "Projection onto the weighted L_p ball is currently not supported with finite `norm_p`."
            )

        if (suboptimal or norm == 2) and norm != np.inf:  # Simple rescaling
            values_norm = tf.norm(values_tmp, ord=norm, axis=1, keepdims=True)  # (n_samples, 1)
            values_tmp = values_tmp * tf.where(values_norm == 0, 0, tf.minimum(1, eps / values_norm))
        else:  # Optimal
            if norm == np.inf:  # Easy exact case
                values_tmp = tf.sign(values_tmp) * tf.minimum(tf.abs(values_tmp), eps)
            elif norm >= 1:  # Convex optim
                raise NotImplementedError(
                    "Finite values of `norm_p >= 1` are currently not supported with `suboptimal=False`."
                )
            else:  # Non-convex optim
                raise NotImplementedError("Values of `norm_p < 1` are currently not supported with `suboptimal=False`")

        values = tf.cast(tf.reshape(values_tmp, values.shape), values.dtype)

        return values

        Project `values` on the L_p norm ball of size `eps`.

        :param values: Values to clip.
        :param eps: If a scalar, the norm of the L_p ball onto which samples are projected. Equivalently in general,
                    can be any array of non-negatives broadcastable with `values`, and the projection occurs onto the
                    unit ball for the weighted L_{p, w} norm with `w = 1 / eps`. Currently, for any given sample,
                    non-uniform weights are only supported with infinity norm. Example: To specify sample-wise scalar,
                    you can provide `eps.shape = (n_samples,) + (1,) * values[0].ndim`.
        :param norm_p: Lp norm to use for clipping, with `norm_p > 0`. Only 2, `np.inf` and "inf" are supported
                       with `suboptimal=False` for now.
        :param suboptimal: If `True` simply projects by rescaling to Lp ball. Fast but may be suboptimal for
                           `norm_p != 2`.
                       Ignored when `norm_p in [np.inf, "inf"]` because optimal solution is fast. Defaults to `True`.
        :return: Values of `values` after projection.
        
art/attacks/evasion/projected_gradient_descent/projected_gradient_descent_tensorflow_v2.py