/**
 * Copyright © 2017-2026 Wellington Wallace
 * Crosstalk Canceller plugin developed by Antti S. Lankila <alankila@bel.fi>
 *
 * 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/>.
 */

#pragma once

#include <cmath>
#include <span>
#include <string>
#include <vector>
#include "easyeffects_db_crosstalk_canceller.h"
#include "pipeline_type.hpp"
#include "plugin_base.hpp"
#include "pw_manager.hpp"

/**
 * Some standard filters from "Cookbook formulae for audio EQ biquad filter coefficients"
 * by Robert Bristow-Johnson  <rbj@audioimagination.com>, adapted for EasyEffects.
 */
class Biquad {
 private:
  float fa1 = 0;
  float fa2 = 0;
  float fb0 = 1;
  float fb1 = 0;
  float fb2 = 0;

  float x1 = 0;
  float x2 = 0;
  float y1 = 0;
  float y2 = 0;

  void set_coefficients(const double a0,
                        const double a1,
                        const double a2,
                        const double b0,
                        const double b1,
                        const double b2) {
    fa1 = static_cast<float>(a1 / a0);
    fa2 = static_cast<float>(a2 / a0);
    fb0 = static_cast<float>(b0 / a0);
    fb1 = static_cast<float>(b1 / a0);
    fb2 = static_cast<float>(b2 / a0);
  }

 public:
  void set_allpass(const double center_frequency, const double sampling_frequency, const double quality) {
    const auto w0 = 2 * M_PI * center_frequency / sampling_frequency;
    const auto alpha = std::sin(w0) / (2 * quality);
    const auto b0 = 1 - alpha;
    const auto b1 = -2 * std::cos(w0);
    const auto b2 = 1 + alpha;
    const auto a0 = 1 + alpha;
    const auto a1 = -2 * std::cos(w0);
    const auto a2 = 1 - alpha;
    set_coefficients(a0, a1, a2, b0, b1, b2);
  }

  void set_low_pass(const double center_frequency, const double sampling_frequency, const double quality) {
    const auto w0 = 2 * M_PI * center_frequency / sampling_frequency;
    const auto alpha = std::sin(w0) / (2 * quality);

    const auto b0 = (1 - std::cos(w0)) / 2;
    const auto b1 = 1 - std::cos(w0);
    const auto b2 = (1 - std::cos(w0)) / 2;
    const auto a0 = 1 + alpha;
    const auto a1 = -2 * std::cos(w0);
    const auto a2 = 1 - alpha;
    set_coefficients(a0, a1, a2, b0, b1, b2);
  }

  void set_high_pass(const double center_frequency, const double sampling_frequency, const double quality) {
    const auto w0 = 2 * M_PI * center_frequency / sampling_frequency;
    const auto alpha = std::sin(w0) / (2 * quality);
    const auto b0 = (1 + std::cos(w0)) / 2;
    const auto b1 = -(1 + std::cos(w0));
    const auto b2 = (1 + std::cos(w0)) / 2;
    const auto a0 = 1 + alpha;
    const auto a1 = -2 * std::cos(w0);
    const auto a2 = 1 - alpha;
    set_coefficients(a0, a1, a2, b0, b1, b2);
  }

  void set_high_shelf(const double center_frequency,
                      const double sampling_frequency,
                      const double db_gain,
                      const double quality) {
    const auto w0 = 2 * M_PI * center_frequency / sampling_frequency;
    const auto A = std::pow(10, db_gain / 40);
    const auto alpha = std::sin(w0) / (2 * quality);

    const auto b0 = A * ((A + 1) + ((A - 1) * std::cos(w0)) + (2 * std::sqrt(A) * alpha));
    const auto b1 = -2 * A * ((A - 1) + ((A + 1) * std::cos(w0)));
    const auto b2 = A * ((A + 1) + ((A - 1) * std::cos(w0)) - (2 * std::sqrt(A) * alpha));
    const auto a0 = (A + 1) - ((A - 1) * std::cos(w0)) + (2 * std::sqrt(A) * alpha);
    const auto a1 = 2 * ((A - 1) - ((A + 1) * std::cos(w0)));
    const auto a2 = (A + 1) - ((A - 1) * std::cos(w0)) - (2 * std::sqrt(A) * alpha);
    set_coefficients(a0, a1, a2, b0, b1, b2);
  }

  void set_peaking_band(const double center_frequency,
                        const double sampling_frequency,
                        const double db_gain,
                        const double quality) {
    const auto w0 = 2 * M_PI * center_frequency / sampling_frequency;
    const auto A = std::pow(10, db_gain / 40);
    const auto alpha = std::sin(w0) / (2 * quality);

    const auto b0 = 1 + (alpha * A);
    const auto b1 = -2 * std::cos(w0);
    const auto b2 = 1 - (alpha * A);
    const auto a0 = 1 + (alpha / A);
    const auto a1 = -2 * std::cos(w0);
    const auto a2 = 1 - (alpha / A);
    set_coefficients(a0, a1, a2, b0, b1, b2);
  }

  float process(const float x0) {
    const auto y0 = (fb0 * x0) + (fb1 * x1) + (fb2 * x2) - (y1 * fa1) - (y2 * fa2);

    y2 = y1;
    y1 = y0;

    x2 = x1;
    x1 = x0;

    return y0;
  }
};

class FilterState {
 private:
  std::vector<float> data;
  size_t data_index = 0;

  /* Feedback correction filters*/
  Biquad f1;
  Biquad f2;
  Biquad f3;
  Biquad f4;
  Biquad f5;
  Biquad f6;

 public:
  /**
   * Set up filtering line for specific delay and configure filters with sample rate.
   */
  void configure(const double delay_us, const double rate) {
    /* Configure delay line for the appropriate length (full sample precision only) */
    if (const auto samples = static_cast<size_t>(std::round(delay_us / 1.0e6 * rate)); data.size() != samples) {
      data.resize(samples);
      data_index = 0;
    }

    /*
     * Standard kemar head fixture, digitized
     * from picture at https://www.intechopen.com/chapters/45612
     *
     * The contralateral HRTF is what is perceived of the sound traversing around the head,
     * but the correction audio is received with sensitivity of the ipsilateral HRTF. Thus,
     * the magnitudes required must be subtracted: contra - ipsi, and an equalization must
     * reproduce that shape.
     *
     * This EQ was fitted in REW with -1 dB fixed offset between ipsi and contra.
     *
     * Filter  1: ON  LP Q     Fc    3245 Hz  Q  0.710
     * Filter  2: ON  PK       Fc   688.0 Hz  Gain  -2.90 dB  Q  1.000
     * Filter  3: ON  PK       Fc    1066 Hz  Gain  -6.40 dB  Q  3.352
     * Filter  4: ON  PK       Fc    2190 Hz  Gain  -5.60 dB  Q  2.037
     * Filter  5: ON  PK       Fc    3792 Hz  Gain  -4.50 dB  Q  3.232
     */
    f1.set_high_pass(250, rate, 0.710);
    f2.set_low_pass(3245, rate, 0.710);
    f3.set_peaking_band(688, rate, -2.9, 1.000);
    f4.set_peaking_band(1066, rate, -6.4, 3.352);
    f5.set_peaking_band(2190, rate, -5.6, 2.037);
    f6.set_peaking_band(3792, rate, -4.5, 3.232);
  }

  /**
   * Retrieve sample from the delay line that was set
   * some number of samples ago.
   *
   * @return historical sample from delay line
   */
  float get_sample() { return data[data_index]; }

  /**
   * Put a new sample into the delay line
   *
   * @param sample the sample to store
   */
  void put_sample(float sample) {
    sample = f1.process(sample);
    sample = f2.process(sample);
    sample = f3.process(sample);
    sample = f4.process(sample);
    sample = f5.process(sample);
    sample = f6.process(sample);
    data[data_index] = sample;
    data_index = (data_index + 1) % data.size();
  }
};

class CrosstalkCanceller : public PluginBase {
  Q_OBJECT
  QML_NAMED_ELEMENT(BackendCrosstalkCanceller)
  QML_UNCREATABLE("Use the c++ instance")

 public:
  CrosstalkCanceller(const std::string& tag, pw::Manager* pipe_manager, PipelineType pipe_type, QString instance_id);
  CrosstalkCanceller(const CrosstalkCanceller&) = delete;
  auto operator=(const CrosstalkCanceller&) -> CrosstalkCanceller& = delete;
  CrosstalkCanceller(const CrosstalkCanceller&&) = delete;
  auto operator=(const CrosstalkCanceller&&) -> CrosstalkCanceller& = delete;
  ~CrosstalkCanceller() override;

  void reset() override;

  void clear_data() override;

  void setup() override;

  void process(std::span<float>& left_in,
               std::span<float>& right_in,
               std::span<float>& left_out,
               std::span<float>& right_out) override;

  void process(std::span<float>& left_in,
               std::span<float>& right_in,
               std::span<float>& left_out,
               std::span<float>& right_out,
               std::span<float>& probe_left,
               std::span<float>& probe_right) override;

  auto get_latency_seconds() -> float override;

 private:
  std::vector<float> data;

  FilterState a;
  FilterState b;

  DbCrosstalkCanceller* settings = nullptr;
};
