// Copyright (c) 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <cmath>
#include <vector>

#include "media/base/audio_bus.h"
#include "media/base/audio_shifter.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace media {

const int kSampleRate = 48000;
const int kInputPacketSize = 48;
const int kOutputPacketSize = 24;

#if GTEST_HAS_COMBINE

class AudioShifterTest :
      public ::testing::TestWithParam<::testing::tuple<int, int, int, bool> > {
 public:
  AudioShifterTest()
      : shifter_(base::TimeDelta::FromMilliseconds(2000),
                 base::TimeDelta::FromMilliseconds(3),
                 base::TimeDelta::FromMilliseconds(100),
                 kSampleRate,
                 2),
        end2end_latency_(base::TimeDelta::FromMilliseconds(30)),
        playback_latency_(base::TimeDelta::FromMilliseconds(10)),
        tag_input_(false),
        expect_smooth_output_(true),
        input_sample_n_(0),
        output_sample_(0) {
  }

  void SetupInput(int size, base::TimeDelta rate) {
    input_size_ = size;
    input_rate_ = rate;
  }

  scoped_ptr<AudioBus> CreateTestInput() {
    scoped_ptr<AudioBus> input(AudioBus::Create(2, input_size_));
    for (size_t i = 0; i < input_size_; i++) {
      input->channel(0)[i] = input->channel(1)[i] = input_sample_n_;
      input_sample_n_++;
    }
    if (tag_input_) {
      input->channel(0)[0] = 10000000.0;
      tag_input_ = false;
      expect_smooth_output_ = false;
    }
    return input.Pass();
  }

  void SetupOutput(int size, base::TimeDelta rate) {
    test_output_ = AudioBus::Create(2, size);
    output_rate_ = rate;
  }

  void SetUp() override {
    SetupInput(
        kInputPacketSize + ::testing::get<0>(GetParam()) - 1,
        base::TimeDelta::FromMicroseconds(
            1000 + ::testing::get<1>(GetParam()) * 5 - 5));
    SetupOutput(
        kOutputPacketSize,
        base::TimeDelta::FromMicroseconds(
            500 + ::testing::get<2>(GetParam()) * 3 - 3));
    if (::testing::get<3>(GetParam())) {
      end2end_latency_ = -end2end_latency_;
    }
  }

  void Run(size_t loops) {
    for (size_t i = 0; i < loops;) {
      if (now_ >= time_to_push_) {
        shifter_.Push(CreateTestInput(), now_ + end2end_latency_);
        time_to_push_ += input_rate_;
        i++;
      }
      if (now_ >= time_to_pull_) {
        shifter_.Pull(test_output_.get(), now_ + playback_latency_);
        bool silence = true;
        for (size_t j = 0;
             j < static_cast<size_t>(test_output_->frames());
             j++) {
          if (test_output_->channel(0)[j] != 0.0) {
            silence = false;
            if (test_output_->channel(0)[j] > 3000000.0) {
              marker_outputs_.push_back(
                now_ + playback_latency_ +
                base::TimeDelta::FromSeconds(j) / kSampleRate);
             } else {
               // We don't expect smooth output once we insert a tag,
               // or in the very beginning.
               if (expect_smooth_output_ && output_sample_ > 500.0) {
                 EXPECT_GT(test_output_->channel(0)[j], output_sample_ - 3)
                     << "j = " << j;
                 if (test_output_->channel(0)[j] >
                     output_sample_ + kOutputPacketSize / 2) {
                   skip_outputs_.push_back(now_ + playback_latency_);
                 }
               }
               output_sample_ = test_output_->channel(0)[j];
             }
           }
        }
        if (silence) {
          silent_outputs_.push_back(now_);
        }
        time_to_pull_ += output_rate_;
      }
      now_ += std::min(time_to_push_ - now_,
                       time_to_pull_ - now_);
    }
  }

  void RunAndCheckSync(size_t loops) {
    Run(100);
    size_t expected_silent_outputs = silent_outputs_.size();
    Run(loops);
    tag_input_ = true;
    CHECK(marker_outputs_.empty());
    base::TimeTicks expected_mark_time = time_to_push_ + end2end_latency_;
    Run(100);
    if (end2end_latency_ > base::TimeDelta()) {
      CHECK(!marker_outputs_.empty());
      base::TimeDelta actual_offset = marker_outputs_[0] - expected_mark_time;
      EXPECT_LT(actual_offset, base::TimeDelta::FromMicroseconds(100));
      EXPECT_GT(actual_offset, base::TimeDelta::FromMicroseconds(-100));
    } else {
      EXPECT_GT(marker_outputs_.size(), 0UL);
    }
    EXPECT_EQ(expected_silent_outputs, silent_outputs_.size());
  }

 protected:
  AudioShifter shifter_;
  base::TimeDelta input_rate_;
  base::TimeDelta output_rate_;
  base::TimeDelta end2end_latency_;
  base::TimeDelta playback_latency_;
  base::TimeTicks time_to_push_;
  base::TimeTicks time_to_pull_;
  base::TimeTicks now_;
  scoped_ptr<AudioBus> test_input_;
  scoped_ptr<AudioBus> test_output_;
  std::vector<base::TimeTicks> silent_outputs_;
  std::vector<base::TimeTicks> skip_outputs_;
  std::vector<base::TimeTicks> marker_outputs_;
  size_t input_size_;
  bool tag_input_;
  bool expect_smooth_output_;
  size_t input_sample_n_;
  double output_sample_;
};

TEST_P(AudioShifterTest, TestSync) {
  RunAndCheckSync(1000);
  EXPECT_EQ(0UL, skip_outputs_.size());
}

TEST_P(AudioShifterTest, TestSyncWithPush) {
  // Push some extra audio.
  shifter_.Push(CreateTestInput().Pass(), now_ - base::TimeDelta(input_rate_));
  RunAndCheckSync(1000);
  EXPECT_LE(skip_outputs_.size(), 2UL);
}

TEST_P(AudioShifterTest, TestSyncWithPull) {
  // Output should smooth out eventually, but that is not tested yet.
  expect_smooth_output_ = false;
  Run(100);
  for (int i = 0; i < 100; i++) {
    shifter_.Pull(test_output_.get(),
                  now_ + base::TimeDelta::FromMilliseconds(i));
  }
  RunAndCheckSync(1000);
  EXPECT_LE(skip_outputs_.size(), 1UL);
}

TEST_P(AudioShifterTest, UnderOverFlow) {
  expect_smooth_output_ = false;
  SetupInput(
      kInputPacketSize + ::testing::get<0>(GetParam()) * 10 - 10,
      base::TimeDelta::FromMicroseconds(
          1000 + ::testing::get<1>(GetParam()) * 100 - 100));
  SetupOutput(
      kOutputPacketSize,
      base::TimeDelta::FromMicroseconds(
          500 + ::testing::get<2>(GetParam()) * 50 - 50));
  // Sane output is not expected, but let's make sure we don't crash.
  Run(1000);
}

// Note: First argument is optional and intentionally left blank.
// (it's a prefix for the generated test cases)
INSTANTIATE_TEST_CASE_P(
    ,
    AudioShifterTest,
    ::testing::Combine(::testing::Range(0, 3),
                       ::testing::Range(0, 3),
                       ::testing::Range(0, 3),
                       ::testing::Bool()));

#endif

}  // namespace media