#### RemiAudio
#### Copyright (C) 2022-2024 Remilia Scarlet <remilia@posteo.jp>
####   Based on MVerb
####   Copyright (c) 2010 Martin Eastwood
####
#### This program is free software: you can redistribute it and/or modify it
#### under the terms of the GNU Affero General Public License as published by
#### the Free Software Foundation, either version 3 of the License, or (at your
#### option) any later version.
####
#### This program 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 Affero General Public
#### License for more details.
####
#### You should have received a copy of the GNU Affero General Public License
#### along with this program.  If not, see <https://www.gnu.org/licenses/>.
require "./reverb"

module RemiAudio::DSP
  class MVerb < Reverb
    # :nodoc:
    class StateVariable
      enum FilterType
        Lowpass
        Highpass
        Bandpass
        Notch
      end

      @oversampleCount : Int32 = 1
      @sampleRate : Float64 = 0.0
      @frequency : Float64 = 0.0
      @q : Float64 = 0.0
      @f : Float64 = 0.0
      @low : Float64 = 0.0
      @high : Float64 = 0.0
      @band : Float64 = 0.0
      @notch : Float64 = 0.0
      property type : FilterType = FilterType::Lowpass

      def initialize(@oversampleCount : Int32)
        self.sampleRate = 44100.0
        self.frequency = 1000.0
        self.resonance = 0.0
        self.type = FilterType::Lowpass
        self.reset
      end

      @[AlwaysInline]
      def process(input : Float64) : Float64
        @oversampleCount.times do |_|
          @low += @f * @band + 1.0e-25
          @high = input - @low - @q * @band
          @band += @f * @high
          @notch = @low + @high
        end

        case @type
        in .lowpass? then @low
        in .highpass? then @high
        in .bandpass? then @band
        in .notch? then @notch
        end
      end

      def reset
        @low = 0.0
        @high = 0.0
        @band = 0.0
        @notch = 0.0
      end

      def sampleRate=(value : Float64) : Float64
        @sampleRate = value * @oversampleCount
        updateCoefficients
        @sampleRate
      end

      def frequency=(value : Float64) : Float64
        @frequency = value
        updateCoefficients
        @frequency
      end

      def resonance=(value : Float64) : Float64
        @q = 2.0 - 2.0 * value
      end

      @[AlwaysInline]
      private def updateCoefficients : Nil
        @f = 2.0 * Math.sin(Math::PI * @frequency / @sampleRate)
      end
    end

    private macro defineDelayLine(name, ntaps)
      # :nodoc:
      class {{name.id}}
        @maxLength : Int32 = 1
        @buffer : Array(Float64)
        getter length : Int32 = 0

        {% for i in (0...ntaps) %}
          @index{{i.id}} : Int32 = 0
        {% end %}

        def initialize(@maxLength : Int32)
          @buffer = Array(Float64).new(@maxLength, 0.0)
          self.length = @maxLength - 1
          clear
        end

        @[AlwaysInline]
        def process(input : Float64) : Float64
          ret : Float64 = @buffer.unsafe_fetch(@index0)
          @buffer.unsafe_put(@index0, input)
          {% for i in (0...ntaps) %}
            @index{{i.id}} = 0 if (@index{{i.id}} += 1) >= @length
          {% end %}
          ret
        end

        @[AlwaysInline]
        def length=(len : Int32) : Int32
          @length = len.clamp(0, @maxLength)
        end

        def clear : Nil
          @buffer.fill(0.0)
          {% for i in (0...ntaps) %}
            @index{{i.id}} = 0
          {% end %}
        end

        {% unless ntaps == 1 %}
          @[AlwaysInline]
          def setIndex(
               {% for i in (0...ntaps) %}
                 @index{{i.id}} : Int32 {% unless i == ntaps - 1 %},{% end %}
               {% end %}
             ) : Nil
          end
        {% end %}

        {% for i in 0...ntaps %}
          @[AlwaysInline]
          def idx{{i.id}} : Float64
            @buffer.unsafe_fetch(@index{{i.id}})
          end
        {% end %}
      end
    end

    # :nodoc:
    class StaticAllpassFourTap
      @maxLength : Int32 = 1
      @buffer : Array(Float64)
      @index0 : Int32 = 0
      @index1 : Int32 = 0
      @index2 : Int32 = 0
      @index3 : Int32 = 0
      property feedback : Float64 = 0.5
      getter length : Int32 = 0

      def initialize(@maxLength : Int32)
        @buffer = Array(Float64).new(@maxLength, 0.0)
        self.length = @maxLength - 1
        clear
      end

      @[AlwaysInline]
      def process(input : Float64) : Float64
        bufout : Float64 = @buffer[@index0]
        tmp : Float64 = input * -@feedback
        ret : Float64 = bufout + tmp
        @buffer.unsafe_put(@index0, input + ((bufout + tmp) * @feedback))
        @index0 = 0 if (@index0 += 1) >= @length
        @index1 = 0 if (@index1 += 1) >= @length
        @index2 = 0 if (@index2 += 1) >= @length
        @index3 = 0 if (@index3 += 1) >= @length
        ret
      end

      @[AlwaysInline]
      def setIndex(@index0 : Int32, @index1 : Int32, @index2 : Int32, @index3 : Int32) : Nil
      end

      {% begin %}
        {% for i in 0..3 %}
          @[AlwaysInline]
          def idx{{i.id}} : Float64
            @buffer.unsafe_fetch(@index{{i.id}})
          end
        {% end %}
      {% end %}

      @[AlwaysInline]
      def length=(value : Int32) : Int32
        @length = value.clamp(0, @maxLength)
      end

      def clear : Nil
        @buffer.fill(0.0)
        @index0 = 0
        @index1 = 0
        @index2 = 0
        @index3 = 0
      end
    end

    # :nodoc:
    class Allpass
      @maxLength : Int32 = 1
      @buffer : Array(Float64)
      @index : Int32 = 0
      property feedback : Float64 = 0.5
      getter length : Int32 = 0

      def initialize(@maxLength : Int32)
        @buffer = Array(Float64).new(@maxLength, 0.0)
        self.length = @maxLength - 1
        clear
      end

      @[AlwaysInline]
      def process(input : Float64) : Float64
        bufout : Float64 = @buffer.unsafe_fetch(@index)
        tmp : Float64 = input * -@feedback
        ret : Float64 = bufout + tmp
        @buffer.unsafe_put(@index, input + ((bufout + tmp) * @feedback))
        @index = 0 if (@index += 1) >= @length
        ret
      end

      @[AlwaysInline]
      def length=(value : Int32) : Int32
        @length = value.clamp(0, @maxLength)
      end

      @[AlwaysInline]
      def clear : Nil
        @buffer.fill(0.0)
        @index = 0
      end
    end

    defineDelayLine(StaticDelayLine, 1)
    defineDelayLine(StaticDelayLineFourTap, 4)
    defineDelayLine(StaticDelayLineEightTap, 8)
  end
end
