/**
 * Copyright © 2017-2026 Wellington Wallace
 *
 * This file is part of Easy Effects.
 *
 * Easy Effects is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Easy Effects is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Easy Effects. If not, see <https://www.gnu.org/licenses/>.
 */

#include "fir_filter_base.hpp"
#include <sched.h>
#include <sys/types.h>
#include <zita-convolver.h>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstddef>
#include <format>
#include <mutex>
#include <numbers>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#include "util.hpp"

namespace {

constexpr auto CONVPROC_SCHEDULER_PRIORITY = 0;

constexpr auto CONVPROC_SCHEDULER_CLASS = SCHED_FIFO;

}  // namespace

FirFilterBase::FirFilterBase(std::string tag) : log_tag(std::move(tag)) {}

FirFilterBase::~FirFilterBase() {
  std::scoped_lock<std::mutex> lock(util::fftw_lock());

  zita_ready = false;

  free_zita();
}

void FirFilterBase::free_zita() {
  if (conv != nullptr) {
    conv->stop_process();

    while (!conv->check_stop()) {
      std::this_thread::sleep_for(std::chrono::milliseconds(1));
    }

    delete conv;

    conv = nullptr;
  }
}

void FirFilterBase::set_rate(const uint& value) {
  rate = value;
}

void FirFilterBase::set_n_samples(const uint& value) {
  n_samples = value;
}

void FirFilterBase::set_min_frequency(const float& value) {
  min_frequency = value;
}

void FirFilterBase::set_max_frequency(const float& value) {
  max_frequency = value;
}

void FirFilterBase::set_transition_band(const float& value) {
  transition_band = value;
}

void FirFilterBase::setup() {}

auto FirFilterBase::create_lowpass_kernel(const float& cutoff, const float& transition_band) const
    -> std::vector<float> {
  std::vector<float> output;

  if (rate == 0U) {
    return output;
  }

  // transition band frequency as a fraction of the sample rate

  const float b = transition_band / static_cast<float>(rate);

  /**
   * The kernel size must be odd: M + 1 where M is even. This is done so it can
   * be symmetric around the main lobe
   * https://www.dspguide.com/ch16/1.htm
   *
   * The kernel size is related to the transition bandwidth M = 4/BW
   */

  auto M = static_cast<uint>(std::ceil(4.0F / b));

  M = (M % 2U == 0U) ? M : M + 1U;  // checking if M is even

  output.resize(M + 1U);

  // cutoff frequency as a fraction of the sample rate

  const float fc = cutoff / static_cast<float>(rate);

  float sum = 0.0F;

  for (size_t n = 0U; n < output.size(); n++) {
    // windowed-sinc kernel https://www.dspguide.com/ch16/1.htm

    if (n == M / 2U) {
      output[n] = 2.0F * std::numbers::pi_v<float> * fc;
    } else {
      output[n] = std::sin(2.0F * std::numbers::pi_v<float> * fc * static_cast<float>(n - static_cast<uint>(M / 2U))) /
                  static_cast<float>(n - static_cast<uint>(M / 2U));
    }

    // Blackman window https://www.dspguide.com/ch16/1.htm

    const float w =
        0.42F - (0.5F * std::cos(2.0F * std::numbers::pi_v<float> * static_cast<float>(n) / static_cast<float>(M))) +
        (0.08F * std::cos(4.0F * std::numbers::pi_v<float> * static_cast<float>(n) / static_cast<float>(M)));

    output[n] *= w;

    sum += output[n];
  }

  // Normalizing so that we have unit gain at zero frequency

  std::ranges::for_each(output, [&](auto& v) { v /= sum; });

  return output;
}

void FirFilterBase::setup_zita() {
  std::scoped_lock<std::mutex> lock(util::fftw_lock());

  zita_ready = false;

  if (n_samples == 0U || kernel.empty()) {
    return;
  }

  free_zita();

  conv = new Convproc();

  conv->set_options(0);

  float density = 0.5F;

  int ret = conv->configure(2, 2, kernel.size(), n_samples, n_samples, Convproc::MAXPART, density);

  if (ret != 0) {
    util::warning(std::format("{}can't initialise zita-convolver engine: {}", log_tag, ret));

    return;
  }

  ret = conv->impdata_create(0, 0, 1, kernel.data(), 0, static_cast<int>(kernel.size()));

  if (ret != 0) {
    util::warning(std::format("{}left impdata_create failed: {}", log_tag, ret));

    return;
  }

  ret = conv->impdata_create(1, 1, 1, kernel.data(), 0, static_cast<int>(kernel.size()));

  if (ret != 0) {
    util::warning(std::format("{}right impdata_create failed: {}", log_tag, ret));

    return;
  }

  ret = conv->start_process(CONVPROC_SCHEDULER_PRIORITY, CONVPROC_SCHEDULER_CLASS);

  if (ret != 0) {
    util::warning(std::format("{}start_process failed: {}", log_tag, ret));

    conv->stop_process();
    conv->cleanup();

    return;
  }

  // conv->print();

  zita_ready = true;
}

void FirFilterBase::direct_conv(const std::vector<float>& a, const std::vector<float>& b, std::vector<float>& c) {
  const uint M = (c.size() + 1U) / 2U;

  for (uint n = 0U; n < c.size(); n++) {
    c[n] = 0.0F;

    for (uint m = 0U; m < M; m++) {
      if (n > m && n - m < M) {
        c[n] += a[n - m] * b[m];
      }
    }
  }
}

auto FirFilterBase::get_delay() const -> float {
  return delay;
}
