/*
 *  Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

#include <memory>

#include "webrtc/common_audio/blocker.h"

#include "webrtc/base/arraysize.h"
#include "webrtc/test/gtest.h"

namespace {

// Callback Function to add 3 to every sample in the signal.
class PlusThreeBlockerCallback : public webrtc::BlockerCallback {
 public:
  void ProcessBlock(const float* const* input,
                    size_t num_frames,
                    size_t num_input_channels,
                    size_t num_output_channels,
                    float* const* output) override {
    for (size_t i = 0; i < num_output_channels; ++i) {
      for (size_t j = 0; j < num_frames; ++j) {
        output[i][j] = input[i][j] + 3;
      }
    }
  }
};

// No-op Callback Function.
class CopyBlockerCallback : public webrtc::BlockerCallback {
 public:
  void ProcessBlock(const float* const* input,
                    size_t num_frames,
                    size_t num_input_channels,
                    size_t num_output_channels,
                    float* const* output) override {
    for (size_t i = 0; i < num_output_channels; ++i) {
      for (size_t j = 0; j < num_frames; ++j) {
        output[i][j] = input[i][j];
      }
    }
  }
};

}  // namespace

namespace webrtc {

// Tests blocking with a window that multiplies the signal by 2, a callback
// that adds 3 to each sample in the signal, and different combinations of chunk
// size, block size, and shift amount.
class BlockerTest : public ::testing::Test {
 protected:
  void RunTest(Blocker* blocker,
               size_t chunk_size,
               size_t num_frames,
               const float* const* input,
               float* const* input_chunk,
               float* const* output,
               float* const* output_chunk,
               size_t num_input_channels,
               size_t num_output_channels) {
    size_t start = 0;
    size_t end = chunk_size - 1;
    while (end < num_frames) {
      CopyTo(input_chunk, 0, start, num_input_channels, chunk_size, input);
      blocker->ProcessChunk(input_chunk,
                            chunk_size,
                            num_input_channels,
                            num_output_channels,
                            output_chunk);
      CopyTo(output, start, 0, num_output_channels, chunk_size, output_chunk);

      start += chunk_size;
      end += chunk_size;
    }
  }

  void ValidateSignalEquality(const float* const* expected,
                              const float* const* actual,
                              size_t num_channels,
                              size_t num_frames) {
    for (size_t i = 0; i < num_channels; ++i) {
      for (size_t j = 0; j < num_frames; ++j) {
        EXPECT_FLOAT_EQ(expected[i][j], actual[i][j]);
      }
    }
  }

  void ValidateInitialDelay(const float* const* output,
                            size_t num_channels,
                            size_t num_frames,
                            size_t initial_delay) {
    for (size_t i = 0; i < num_channels; ++i) {
      for (size_t j = 0; j < num_frames; ++j) {
        if (j < initial_delay) {
          EXPECT_FLOAT_EQ(output[i][j], 0.f);
        } else {
          EXPECT_GT(output[i][j], 0.f);
        }
      }
    }
  }

  static void CopyTo(float* const* dst,
                     size_t start_index_dst,
                     size_t start_index_src,
                     size_t num_channels,
                     size_t num_frames,
                     const float* const* src) {
    for (size_t i = 0; i < num_channels; ++i) {
      memcpy(&dst[i][start_index_dst],
             &src[i][start_index_src],
             num_frames * sizeof(float));
    }
  }
};

TEST_F(BlockerTest, TestBlockerMutuallyPrimeChunkandBlockSize) {
  const size_t kNumInputChannels = 3;
  const size_t kNumOutputChannels = 2;
  const size_t kNumFrames = 10;
  const size_t kBlockSize = 4;
  const size_t kChunkSize = 5;
  const size_t kShiftAmount = 2;

  const float kInput[kNumInputChannels][kNumFrames] = {
      {1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
      {2, 2, 2, 2, 2, 2, 2, 2, 2, 2},
      {3, 3, 3, 3, 3, 3, 3, 3, 3, 3}};
  ChannelBuffer<float> input_cb(kNumFrames, kNumInputChannels);
  input_cb.SetDataForTesting(kInput[0], sizeof(kInput) / sizeof(**kInput));

  const float kExpectedOutput[kNumInputChannels][kNumFrames] = {
      {6, 6, 12, 20, 20, 20, 20, 20, 20, 20},
      {6, 6, 12, 28, 28, 28, 28, 28, 28, 28}};
  ChannelBuffer<float> expected_output_cb(kNumFrames, kNumInputChannels);
  expected_output_cb.SetDataForTesting(
      kExpectedOutput[0], sizeof(kExpectedOutput) / sizeof(**kExpectedOutput));

  const float kWindow[kBlockSize] = {2.f, 2.f, 2.f, 2.f};

  ChannelBuffer<float> actual_output_cb(kNumFrames, kNumOutputChannels);
  ChannelBuffer<float> input_chunk_cb(kChunkSize, kNumInputChannels);
  ChannelBuffer<float> output_chunk_cb(kChunkSize, kNumOutputChannels);

  PlusThreeBlockerCallback callback;
  Blocker blocker(kChunkSize,
                  kBlockSize,
                  kNumInputChannels,
                  kNumOutputChannels,
                  kWindow,
                  kShiftAmount,
                  &callback);

  RunTest(&blocker,
          kChunkSize,
          kNumFrames,
          input_cb.channels(),
          input_chunk_cb.channels(),
          actual_output_cb.channels(),
          output_chunk_cb.channels(),
          kNumInputChannels,
          kNumOutputChannels);

  ValidateSignalEquality(expected_output_cb.channels(),
                         actual_output_cb.channels(),
                         kNumOutputChannels,
                         kNumFrames);
}

TEST_F(BlockerTest, TestBlockerMutuallyPrimeShiftAndBlockSize) {
  const size_t kNumInputChannels = 3;
  const size_t kNumOutputChannels = 2;
  const size_t kNumFrames = 12;
  const size_t kBlockSize = 4;
  const size_t kChunkSize = 6;
  const size_t kShiftAmount = 3;

  const float kInput[kNumInputChannels][kNumFrames] = {
      {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
      {2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2},
      {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}};
  ChannelBuffer<float> input_cb(kNumFrames, kNumInputChannels);
  input_cb.SetDataForTesting(kInput[0], sizeof(kInput) / sizeof(**kInput));

  const float kExpectedOutput[kNumOutputChannels][kNumFrames] = {
      {6, 10, 10, 20, 10, 10, 20, 10, 10, 20, 10, 10},
      {6, 14, 14, 28, 14, 14, 28, 14, 14, 28, 14, 14}};
  ChannelBuffer<float> expected_output_cb(kNumFrames, kNumOutputChannels);
  expected_output_cb.SetDataForTesting(
      kExpectedOutput[0], sizeof(kExpectedOutput) / sizeof(**kExpectedOutput));

  const float kWindow[kBlockSize] = {2.f, 2.f, 2.f, 2.f};

  ChannelBuffer<float> actual_output_cb(kNumFrames, kNumOutputChannels);
  ChannelBuffer<float> input_chunk_cb(kChunkSize, kNumInputChannels);
  ChannelBuffer<float> output_chunk_cb(kChunkSize, kNumOutputChannels);

  PlusThreeBlockerCallback callback;
  Blocker blocker(kChunkSize,
                  kBlockSize,
                  kNumInputChannels,
                  kNumOutputChannels,
                  kWindow,
                  kShiftAmount,
                  &callback);

  RunTest(&blocker,
          kChunkSize,
          kNumFrames,
          input_cb.channels(),
          input_chunk_cb.channels(),
          actual_output_cb.channels(),
          output_chunk_cb.channels(),
          kNumInputChannels,
          kNumOutputChannels);

  ValidateSignalEquality(expected_output_cb.channels(),
                         actual_output_cb.channels(),
                         kNumOutputChannels,
                         kNumFrames);
}

TEST_F(BlockerTest, TestBlockerNoOverlap) {
  const size_t kNumInputChannels = 3;
  const size_t kNumOutputChannels = 2;
  const size_t kNumFrames = 12;
  const size_t kBlockSize = 4;
  const size_t kChunkSize = 4;
  const size_t kShiftAmount = 4;

  const float kInput[kNumInputChannels][kNumFrames] = {
      {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
      {2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2},
      {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}};
  ChannelBuffer<float> input_cb(kNumFrames, kNumInputChannels);
  input_cb.SetDataForTesting(kInput[0], sizeof(kInput) / sizeof(**kInput));

  const float kExpectedOutput[kNumOutputChannels][kNumFrames] = {
      {10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10},
      {14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14}};
  ChannelBuffer<float> expected_output_cb(kNumFrames, kNumOutputChannels);
  expected_output_cb.SetDataForTesting(
      kExpectedOutput[0], sizeof(kExpectedOutput) / sizeof(**kExpectedOutput));

  const float kWindow[kBlockSize] = {2.f, 2.f, 2.f, 2.f};

  ChannelBuffer<float> actual_output_cb(kNumFrames, kNumOutputChannels);
  ChannelBuffer<float> input_chunk_cb(kChunkSize, kNumInputChannels);
  ChannelBuffer<float> output_chunk_cb(kChunkSize, kNumOutputChannels);

  PlusThreeBlockerCallback callback;
  Blocker blocker(kChunkSize,
                  kBlockSize,
                  kNumInputChannels,
                  kNumOutputChannels,
                  kWindow,
                  kShiftAmount,
                  &callback);

  RunTest(&blocker,
          kChunkSize,
          kNumFrames,
          input_cb.channels(),
          input_chunk_cb.channels(),
          actual_output_cb.channels(),
          output_chunk_cb.channels(),
          kNumInputChannels,
          kNumOutputChannels);

  ValidateSignalEquality(expected_output_cb.channels(),
                         actual_output_cb.channels(),
                         kNumOutputChannels,
                         kNumFrames);
}

TEST_F(BlockerTest, InitialDelaysAreMinimum) {
  const size_t kNumInputChannels = 3;
  const size_t kNumOutputChannels = 2;
  const size_t kNumFrames = 1280;
  const size_t kChunkSize[] =
      {80, 80, 80, 80, 80, 80, 160, 160, 160, 160, 160, 160};
  const size_t kBlockSize[] =
      {64, 64, 64, 128, 128, 128, 128, 128, 128, 256, 256, 256};
  const size_t kShiftAmount[] =
      {16, 32, 64, 32, 64, 128, 32, 64, 128, 64, 128, 256};
  const size_t kInitialDelay[] =
      {48, 48, 48, 112, 112, 112, 96, 96, 96, 224, 224, 224};

  float input[kNumInputChannels][kNumFrames];
  for (size_t i = 0; i < kNumInputChannels; ++i) {
    for (size_t j = 0; j < kNumFrames; ++j) {
      input[i][j] = i + 1;
    }
  }
  ChannelBuffer<float> input_cb(kNumFrames, kNumInputChannels);
  input_cb.SetDataForTesting(input[0], sizeof(input) / sizeof(**input));

  ChannelBuffer<float> output_cb(kNumFrames, kNumOutputChannels);

  CopyBlockerCallback callback;

  for (size_t i = 0; i < arraysize(kChunkSize); ++i) {
    std::unique_ptr<float[]> window(new float[kBlockSize[i]]);
    for (size_t j = 0; j < kBlockSize[i]; ++j) {
      window[j] = 1.f;
    }

    ChannelBuffer<float> input_chunk_cb(kChunkSize[i], kNumInputChannels);
    ChannelBuffer<float> output_chunk_cb(kChunkSize[i], kNumOutputChannels);

    Blocker blocker(kChunkSize[i],
                    kBlockSize[i],
                    kNumInputChannels,
                    kNumOutputChannels,
                    window.get(),
                    kShiftAmount[i],
                    &callback);

    RunTest(&blocker,
            kChunkSize[i],
            kNumFrames,
            input_cb.channels(),
            input_chunk_cb.channels(),
            output_cb.channels(),
            output_chunk_cb.channels(),
            kNumInputChannels,
            kNumOutputChannels);

    ValidateInitialDelay(output_cb.channels(),
                         kNumOutputChannels,
                         kNumFrames,
                         kInitialDelay[i]);
  }
}

}  // namespace webrtc
