"""CW Noise-Floor Normalizer (Stufe 3: mit Warm-up-Sperre).

Mirrors die Rauschschwellwert-Schaetzung aus SDRangel's MorseDemod
(sdrbase/dsp/morsedemod.cpp): das Minimum der Maxima ueber mehrere
aufeinanderfolgende Zeitfenster (Bins) dient als adaptiver Rauschflur,
gegen den die geglaettete Huellkurve normiert wird.

Gegenueber Stufe 2 neu: Solange die Bin-Historie noch nicht einmal
vollstaendig durchlaufen ist (Platzhalterwerte noch nicht ersetzt), gibt der
Block eine Ratio von 0 aus statt eines kuenstlich ueberhoehten Wertes -
das unterdrueckt das in Stufe 2 dokumentierte Anlauf-Artefakt (ein
falsches Zeichen zu Beginn jeder Aufnahme), weil der nachfolgende
Threshold-Block waehrend der Einschwingzeit stabil auf "Space" bleibt.
"""
import numpy as np
from gnuradio import gr


class blk(gr.sync_block):
    def __init__(self, samp_rate=1000.0, ref_wpm=30.0, num_bins=20):
        gr.sync_block.__init__(
            self, name="CW Noise-Floor Normalizer",
            in_sig=[np.float32], out_sig=[np.float32])
        self.samp_rate = samp_rate
        # ref_wpm ist bewusst NICHT die (evtl. unbekannte oder falsch geschaetzte)
        # tatsaechliche Sendegeschwindigkeit, sondern eine feste, grosszuegig
        # schnelle Referenz: Die Glaettungsfenster muessen nur kurz genug sein, um
        # auch schnelles CW nicht zu verschmieren - bei langsamerem CW ist ein zu
        # kurzes Fenster lediglich etwas weniger rauschunterdrueckend, aber nicht
        # falsch. Ein an eine falsche (zu langsame) Geschwindigkeitsannahme
        # gekoppeltes Fenster verschmiert dagegen schnelles CW komplett - genau
        # das Problem, das die adaptive Elementerkennung eigentlich loesen soll.
        dot_samples = (1.2 / ref_wpm) * samp_rate
        self.bin_len = max(1, int(dot_samples / 4))
        self.mavg_len = max(1, int(dot_samples / 3))
        self.num_bins = num_bins

        self._mavg_buf = np.zeros(self.mavg_len, dtype=np.float64)
        self._mavg_idx = 0
        self._mavg_sum = 0.0

        self._bin_max = 0.0
        self._bin_cnt = 0
        self._bin_history = np.full(num_bins, 1e-6, dtype=np.float64)
        self._bin_hist_idx = 0
        self._noise_floor = 1e-6
        self._bins_filled = 0  # zaehlt bis num_bins, dann ist die Historie "warm"

    def work(self, input_items, output_items):
        in0 = input_items[0]
        out = output_items[0]

        for i in range(len(in0)):
            # gleitender Mittelwert (kantenerhaltend) zur Rauschglaettung
            self._mavg_sum -= self._mavg_buf[self._mavg_idx]
            self._mavg_buf[self._mavg_idx] = in0[i]
            self._mavg_sum += in0[i]
            self._mavg_idx = (self._mavg_idx + 1) % self.mavg_len
            mav = self._mavg_sum / self.mavg_len

            if mav > self._bin_max:
                self._bin_max = mav
            self._bin_cnt += 1
            if self._bin_cnt >= self.bin_len:
                self._bin_history[self._bin_hist_idx] = max(self._bin_max, 1e-4)
                self._bin_hist_idx = (self._bin_hist_idx + 1) % self.num_bins
                self._noise_floor = max(float(np.min(self._bin_history)), 1e-4)
                self._bin_max = 0.0
                self._bin_cnt = 0
                if self._bins_filled < self.num_bins:
                    self._bins_filled += 1

            if self._bins_filled < self.num_bins:
                out[i] = 0.0  # Warm-up: Historie enthaelt noch Platzhalterwerte
            else:
                out[i] = mav / self._noise_floor

        return len(out)
