//
// Copyright 2020 Google LLC
//
// Licensed 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.
//

package com.google.privacy.differentialprivacy;

import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.Math.exp;

import com.google.privacy.differentialprivacy.proto.SummaryOuterClass.MechanismType;
import java.security.SecureRandom;
import java.util.Random;
import javax.annotation.Nullable;

/**
 * Generates and adds Laplace noise to a raw piece of numerical data such that the result is
 * securely differentially private.
 *
 * <p>The Laplace noise is generated according to the geometric sampling mechanism described <a
 * href="https://github.com/google/differential-privacy/blob/main/common_docs/Secure_Noise_Generation.pdf">here</a>.
 * This approach is robust against unintentional privacy leaks due to artifacts of floating point
 * arithmetic.
 */
public class LaplaceNoise implements Noise {
  /**
   * This parameter determines the resolution of the numerical noise that is being generated
   * relative to the L_inf sensitivity and privacy parameter epsilon. More precisely, the
   * granularity parameter corresponds to the value 2^k as described <a
   * href="https://github.com/google/differential-privacy/blob/main/common_docs/Secure_Noise_Generation.pdf">here</a>.
   * Larger values result in more fine grained noise, but increase the chance of sampling
   * inaccuracies due to overflows. The probability of an overflow is less than 2^-1000, if the
   * granularity parameter is set to a value of 2^40 or less and the epsilon passed to addNoise is
   * at least 2^-50.
   *
   * <p>This parameter should be a power of 2.
   */
  private static final double GRANULARITY_PARAM = (double) (1L << 40);

  private final Random random;

  /** Returns a Noise instance initialized with a secure randomness source. */
  public LaplaceNoise() {
    this(new SecureRandom());
  }

  private LaplaceNoise(Random random) {
    this.random = random;
  }

  /**
   * Returns a Noise instance initialized with a specified randomness source. This should only be
   * used for testing and may only be called via the static methods in {@link TestNoiseFactory}.
   *
   * <p>This method is package-private for use by the factory.
   */
  static LaplaceNoise createForTesting(Random random) {
    return new LaplaceNoise(random);
  }

  /**
   * Adds Laplace noise to {@code x} such that the output is {@code epsilon}-differentially private,
   * with respect to the specified L_0 and L_inf sensitivities. Note that {@code delta} must be set
   * to {@code null} because it does not parameterize Laplace noise. Moreover, {@code epsilon} must
   * be at least 2^-50.
   */
  @Override
  public double addNoise(
      double x, int l0Sensitivity, double lInfSensitivity, double epsilon, @Nullable Double delta) {
    DpPreconditions.checkSensitivities(l0Sensitivity, lInfSensitivity);

    return addNoise(x, Noise.getL1Sensitivity(l0Sensitivity, lInfSensitivity), epsilon, delta);
  }

  /**
   * See {@link #addNoise(double, int, double, double, Double)}.
   *
   * <p>As opposed to the latter method, this accepts the L_1 sensitivity of {@code x} directly
   * instead of the L_0 and L_Inf proxies. This should be used in settings where it is feasible or
   * more convenient to calculate the L_1 sensitivity directly.
   */
  public double addNoise(double x, double l1Sensitivity, double epsilon, @Nullable Double delta) {
    checkParameters(l1Sensitivity, epsilon, delta);

    double granularity = getGranularity(l1Sensitivity, epsilon);
    long twoSidedGeomericSample =
        SamplingUtil.sampleTwoSidedGeometric(
            random, granularity * epsilon / (l1Sensitivity + granularity));
    return SecureNoiseMath.roundToMultipleOfPowerOfTwo(x, granularity)
        + twoSidedGeomericSample * granularity;
  }

  /**
   * Adds Laplace noise to the integer {@code x} such that the output is {@code
   * epsilon}-differentially private, with respect to the specified L_0 and L_inf sensitivities.
   * Note that {@code delta} must be set to {@code null} because it does not parameterize Laplace
   * noise. Moreover, {@code epsilon} must be at least 2^-50.
   */
  @Override
  public long addNoise(
      long x, int l0Sensitivity, long lInfSensitivity, double epsilon, @Nullable Double delta) {
    DpPreconditions.checkSensitivities(l0Sensitivity, lInfSensitivity);

    return addNoise(
        x, (long) Noise.getL1Sensitivity(l0Sensitivity, lInfSensitivity), epsilon, delta);
  }

  /**
   * See {@link #addNoise(long, int, long, double, Double)}.
   *
   * <p>As opposed to the latter method, this accepts the L_1 sensitivity of {@code x} directly
   * instead of the L_0 and L_Inf proxies. This should be used in settings where it is feasible or
   * more convenient to calculate the L_1 sensitivity directly.
   */
  public long addNoise(long x, long l1Sensitivity, double epsilon, @Nullable Double delta) {
    checkParameters(l1Sensitivity, epsilon, delta);

    double granularity = getGranularity(l1Sensitivity, epsilon);
    long twoSidedGeomericSample =
        SamplingUtil.sampleTwoSidedGeometric(
            random, granularity * epsilon / (l1Sensitivity + granularity));
    if (granularity <= 1.0) {
      return x + Math.round(twoSidedGeomericSample * granularity);
    } else {
      return SecureNoiseMath.roundToMultiple(x, (long) granularity)
          + twoSidedGeomericSample * (long) granularity;
    }
  }

  @Override
  public MechanismType getMechanismType() {
    return MechanismType.LAPLACE;
  }

  /**
   * Computes a confidence interval that contains the raw value {@code x} passed to {@link
   * #addNoise(double, int, double, double, Double)} with a probability equal to {@code 1 - alpha}
   * based on the specified {@code noisedX} and noise parameters. Note that {@code delta} must be
   * set to {@code null} because it does not parameterize Laplace noise. Moreover, {@code epsilon}
   * must be at least 2^-50.
   *
   * <p>Refer to <a
   * href="https://github.com/google/differential-privacy/tree/main/common_docs/confidence_intervals.md">this</a> doc for
   * more information.
   */
  @Override
  public ConfidenceInterval computeConfidenceInterval(
      double noisedX,
      int l0Sensitivity,
      double lInfSensitivity,
      double epsilon,
      @Nullable Double delta,
      double alpha) {
    checkConfidenceIntervalParameters(l0Sensitivity, lInfSensitivity, epsilon, delta, alpha);
    double z =
        computeQuantile(alpha / 2.0, noisedX, l0Sensitivity, lInfSensitivity, epsilon, delta);
    // Because of the symmetry of the Laplace distribution, 2 * noisedX - z corresponds to the
    // (1 - alpha/2)-quantile of the distribution, meaning that the interval [z, 2 * noisedX - z]
    // contains 1-alpha of the probability mass. Deriving the (1 - alpha/2)-quantile from the
    // (alpha/2)-quantile and not vice versa is a deliberate choice. The reason is that alpha tends
    // to be very small. Consequently, alpha/2 is more accurately representable as a double than
    // 1 - alpha/2, facilitating numerical computations.
    return ConfidenceInterval.create(z, 2.0 * noisedX - z);
  }

  /**
   * Computes a confidence interval that contains the raw integer value {@code x} passed to {@link
   * #addNoise(long, int, long, double, Double)} with a probability greater or equal to {@code 1 -
   * alpha} based on the specified {@code noisedX} and noise parameters. Note that {@code delta}
   * must be set to {@code null} because it does not parameterize Laplace noise. Moreover, {@code
   * epsilon} must be at least 2^-50.
   *
   * <p>Refer to <a
   * href="https://github.com/google/differential-privacy/tree/main/common_docs/confidence_intervals.md">this</a> doc for
   * more information.
   */
  @Override
  public ConfidenceInterval computeConfidenceInterval(
      long noisedX,
      int l0Sensitivity,
      long lInfSensitivity,
      double epsilon,
      @Nullable Double delta,
      double alpha) {
    // Computing the confidence interval around zero rather than nosiedX helps represent the
    // interval bounds more accurately. The reason is that the resolution of double values is most
    // fine grained around zero.
    ConfidenceInterval confIntAroundZero =
        computeConfidenceInterval(0.0, l0Sensitivity, lInfSensitivity, epsilon, delta, alpha);
    // Adding noisedX after converting the interval bounds to long ensures that no precision is lost
    // due to the coarse resolution of double values for large instances of noisedX.
    return ConfidenceInterval.create(
        SecureNoiseMath.nextSmallerDouble(Math.round(confIntAroundZero.lowerBound()) + noisedX),
        SecureNoiseMath.nextLargerDouble(Math.round(confIntAroundZero.upperBound()) + noisedX));
  }

  /**
   * Computes the cumulative density function, i.e. Pr[Y <= z] for a Laplace random variable Y whose
   * distribution is given by applying the Laplace mechanism to the raw value {@code x} using the
   * specified privacy parameters {@code epsilon}, {@code delta}, and {@code l1Sensitivity}.
   *
   * <p>This is inverse to {@link #computeQuantile(double, double, double, double)} with the same
   * parameters.
   */
  public static double cumulativeDensity(double z, double x, double l1Sensitivity, double epsilon) {
    DpPreconditions.checkL1Sensitivity(l1Sensitivity);
    DpPreconditions.checkEpsilon(epsilon);

    double scale = l1Sensitivity / epsilon;
    double y = z - x;
    if (y > 0) {
      return 1 - 0.5 * exp(-y / scale);
    }
    return 0.5 * exp(y / scale);
  }

  /**
   * Computes the quantile z satisfying Pr[Y <= z] = {@code rank} for a Laplace random variable Y
   * whose distribution is given by applying the Laplace mechanism to the raw value {@code x} using
   * the specified privacy parameters {@code epsilon}, {@code delta}, {@code l0Sensitivity}, and
   * {@code lInfSensitivity}.
   */
  @Override
  public double computeQuantile(
      double rank,
      double x,
      int l0Sensitivity,
      double lInfSensitivity,
      double epsilon,
      @Nullable Double delta) {
    DpPreconditions.checkNoiseComputeQuantileArguments(
        this, rank, l0Sensitivity, lInfSensitivity, epsilon, delta);
    double l1Sensitivity = Noise.getL1Sensitivity(l0Sensitivity, lInfSensitivity);
    return computeQuantile(rank, x, l1Sensitivity, epsilon);
  }

  /**
   * Computes the quantile z satisfying Pr[Y <= z] = {@code rank} for a Laplace random variable Y
   * whose distribution is given by applying the Laplace mechanism to the raw value {@code x} using
   * the specified privacy parameters {@code epsilon}, {@code delta}, and {@code l1Sensitivity}.
   *
   * <p>This is inverse to {@link #cumulativeDensity} with the same parameters.
   */
  public static double computeQuantile(
      double rank, double x, double l1Sensitivity, double epsilon) {
    DpPreconditions.checkL1Sensitivity(l1Sensitivity);
    DpPreconditions.checkEpsilon(epsilon);
    checkArgument(rank > 0 && rank < 1, "rank must be > 0 and < 1. Provided value: %s", rank);

    double lambda = l1Sensitivity / epsilon;
    if (rank < 0.5) {
      return x + lambda * Math.log(2 * rank);
    }
    return x - lambda * Math.log(2 * (1 - rank));
  }

  private void checkParameters(double l1Sensitivity, double epsilon, @Nullable Double delta) {
    DpPreconditions.checkEpsilon(epsilon);
    DpPreconditions.checkNoiseDelta(delta, this);
    DpPreconditions.checkL1Sensitivity(l1Sensitivity);
  }

  private void checkConfidenceIntervalParameters(
      int l0Sensitivity,
      double lInfSensitivity,
      double epsilon,
      @Nullable Double delta,
      double alpha) {
    DpPreconditions.checkAlpha(alpha);
    DpPreconditions.checkSensitivities(l0Sensitivity, lInfSensitivity);
    checkParameters(Noise.getL1Sensitivity(l0Sensitivity, lInfSensitivity), epsilon, delta);
  }

  /**
   * Determines the granularity of the output of {@link addNoise} based on the epsilon and
   * l1Sensitivity of the Laplace mechanism.
   */
  private static double getGranularity(double l1Sensitivity, double epsilon) {
    return SecureNoiseMath.ceilPowerOfTwo((l1Sensitivity / epsilon) / GRANULARITY_PARAM);
  }
}
