import os
import struct
import tarfile
import wave

import numpy as np
from PyQt5.QtCore import pyqtSignal, QObject, QDir, Qt
from PyQt5.QtWidgets import QApplication

import urh.cythonext.signalFunctions as signal_functions
from urh import constants
from urh.util import FileOperator
from urh.util.Logger import logger


class Signal(QObject):
    """
    Representation of a loaded signal (complex file).
    """


    MODULATION_TYPES = ["ASK", "FSK", "PSK", "QAM"]

    bit_len_changed = pyqtSignal(int)
    tolerance_changed = pyqtSignal(int)
    noise_threshold_changed = pyqtSignal()
    qad_center_changed = pyqtSignal(float)
    name_changed = pyqtSignal(str)
    sample_rate_changed = pyqtSignal(float)
    modulation_type_changed = pyqtSignal()

    saved_status_changed = pyqtSignal()
    protocol_needs_update = pyqtSignal()
    data_edited = pyqtSignal()  # On Crop/Mute/Delete etc.

    def __init__(self, filename: str, name: str, wav_is_qad_demod=False,
                 modulation: str = None, sample_rate: float = 1e6, parent=None):
        super().__init__(parent)
        self.__name = name
        self.__tolerance = 5
        self.__bit_len = 100
        self._qad = None
        self.__qad_center = 0
        self._noise_threshold = 0
        self.__sample_rate = sample_rate
        self.noise_min_plot = 0
        self.noise_max_plot = 0
        self.block_protocol_update = False

        self.auto_detect_on_modulation_changed = True
        self.wav_mode = filename.endswith(".wav")
        self.__changed = False
        self.qad_demod_file_loaded = wav_is_qad_demod
        if modulation is None:
            modulation = "FSK"
        self.__modulation_type = self.MODULATION_TYPES.index(modulation)
        self.__parameter_cache = {mod: {"qad_center": None, "bit_len": None} for mod in self.MODULATION_TYPES}

        if len(filename) > 0:
            # Daten auslesen
            if not self.wav_mode:
                if not filename.endswith(".coco"):
                    if filename.endswith(".complex16u"):
                        # two 8 bit unsigned integers
                        raw = np.fromfile(filename, dtype=[('r', np.uint8), ('i', np.uint8)])
                        self._fulldata = np.empty(raw.shape[0], dtype=np.complex64)
                        self._fulldata.real = (raw['r'] / 127.5) - 1.0
                        self._fulldata.imag = (raw['i'] / 127.5) - 1.0
                    elif filename.endswith(".complex16s"):
                        # two 8 bit signed integers
                        raw = np.fromfile(filename, dtype=[('r', np.int8), ('i', np.int8)])
                        self._fulldata = np.empty(raw.shape[0], dtype=np.complex64)
                        self._fulldata.real = (raw['r'] + 0.5) / 127.5
                        self._fulldata.imag = (raw['i'] + 0.5) / 127.5
                    else:
                        self._fulldata = np.fromfile(filename, dtype=np.complex64)  # Uncompressed
                else:
                    obj = tarfile.open(filename, "r")
                    members = obj.getmembers()
                    obj.extract(members[0], QDir.tempPath())
                    extracted_filename = os.path.join(QDir.tempPath(), obj.getnames()[0])
                    self._fulldata = np.fromfile(extracted_filename, dtype=np.complex64)
                    os.remove(extracted_filename)

                self._fulldata = np.ascontiguousarray(self._fulldata, dtype=np.complex64)  # type: np.ndarray
            else:
                f = wave.open(filename, "r")
                n = f.getnframes()
                unsigned_bytes = struct.unpack('<{0:d}B'.format(n), f.readframes(n))
                if not self.qad_demod_file_loaded:
                    # Complex To Real WAV File load
                    self._fulldata = np.empty(n, dtype=np.complex64, order="C")
                    self._fulldata.real = np.multiply(1/256, np.subtract(unsigned_bytes, 128))
                    self._fulldata.imag = [-1/128] * n
                else:
                    self._fulldata = np.multiply(1 / 256, np.subtract(unsigned_bytes, 128).astype(np.int8)).astype(
                        np.float32)
                    self._fulldata = np.ascontiguousarray(self._fulldata, dtype=np.float32)

                f.close()

            self.filename = filename

            if not self.qad_demod_file_loaded:
                self.noise_threshold = self.calc_noise_threshold(int(0.99 * self.num_samples), self.num_samples)

        else:
            self.filename = ""

    @property
    def sample_rate(self):
        return self.__sample_rate

    @sample_rate.setter
    def sample_rate(self, val):
        if val != self.sample_rate:
            self.__sample_rate = val
            self.sample_rate_changed.emit(val)


    @property
    def parameter_cache(self) -> dict:
        """
        Caching bit_len and qad_center for modulations, so they do not need
        to be recalculated every time.

        :return:
        """
        return self.__parameter_cache

    @parameter_cache.setter
    def parameter_cache(self, val):
        self.__parameter_cache = val

    @property
    def modulation_type(self):
        return self.__modulation_type

    @modulation_type.setter
    def modulation_type(self, value: str):
        """
        0 - "ASK", 1 - "FSK", 2 - "PSK", 3 - "APSK (QAM)"

        :param value:
        :return:
        """
        if self.__modulation_type != value:
            self.__modulation_type = value
            self._qad = None

            if self.auto_detect_on_modulation_changed:
                self.auto_detect(emit_update=False)

            self.modulation_type_changed.emit()
            if not self.block_protocol_update:
                self.protocol_needs_update.emit()

    @property
    def modulation_type_str(self):
        return self.MODULATION_TYPES[self.modulation_type]

    @property
    def bit_len(self):
        return self.__bit_len

    @bit_len.setter
    def bit_len(self, value):
        if self.__bit_len != value:
            self.__bit_len = value
            self.bit_len_changed.emit(value)
            if not self.block_protocol_update:
                self.protocol_needs_update.emit()

    @property
    def tolerance(self):
        return self.__tolerance

    @tolerance.setter
    def tolerance(self, value):
        if self.__tolerance != value:
            self.__tolerance = value
            self.tolerance_changed.emit(value)
            if not self.block_protocol_update:
                self.protocol_needs_update.emit()

    @property
    def qad_center(self):
        return self.__qad_center

    @qad_center.setter
    def qad_center(self, value: float):
        if self.__qad_center != value:
            self.__qad_center = value
            self.qad_center_changed.emit(value)
            if not self.block_protocol_update:
                self.protocol_needs_update.emit()

    @property
    def name(self):
        return self.__name

    @name.setter
    def name(self, value):
        if value != self.__name:
            self.__name = value
            self.name_changed.emit(self.__name)

    @property
    def num_samples(self):
        return len(self.data)

    @property
    def noise_threshold(self):
        return self._noise_threshold

    @noise_threshold.setter
    def noise_threshold(self, value):
        if value != self.noise_threshold:
            self._qad = None
            self.clear_parameter_cache()
            self._noise_threshold = value
            self.noise_min_plot = -value
            self.noise_max_plot = value
            self.noise_threshold_changed.emit()
            if not self.block_protocol_update:
                self.protocol_needs_update.emit()

    @property
    def qad(self):
        if self._qad is None:
            self._qad = self.data if self.qad_demod_file_loaded else self.quad_demod()

        return self._qad

    @property
    def data(self) -> np.ndarray:
        return self._fulldata

    @property
    def real_plot_data(self):
        return self.data.real

    @property
    def wave_data(self):
        return bytearray(np.multiply(-1, (np.round(self.data.real * 127)).astype(np.int8)))

    @property
    def changed(self) -> bool:
        """
        Determines whether the signal was changed (e.g. cropped/muted) and not saved yet

        :return:
        """
        return self.__changed

    @changed.setter
    def changed(self, val: bool):
        if val != self.__changed:
            self.__changed = val
            self.saved_status_changed.emit()

    def save(self):
        if self.changed:
            self.save_as(self.filename)

    def save_as(self, filename: str):
        QApplication.setOverrideCursor(Qt.WaitCursor)
        self.filename = filename
        FileOperator.save_signal(self)
        self.name = os.path.splitext(os.path.basename(filename))[0]
        self.changed = False
        QApplication.restoreOverrideCursor()

    def get_signal_start(self) -> int:
        """
        Index ab dem das Signal losgeht (Nach Übersteuern + Pause am Anfang)

        """
        return signal_functions.find_signal_start(self.qad, self.modulation_type)

    def get_signal_end(self):
        return signal_functions.find_signal_end(self.qad, self.modulation_type)

    def quad_demod(self):
        return signal_functions.afp_demod(self.data, self.noise_threshold, self.modulation_type)

    def calc_noise_threshold(self, noise_start: int, noise_end: int):
        NDIGITS = 4
        try:
            return np.ceil(np.max(np.absolute(self.data[int(noise_start):int(noise_end)])) * 10 ** NDIGITS) / 10 ** NDIGITS
        except ValueError:
            logger.warning("Could not caluclate noise treshold for range {}-{}".format(int(noise_start),int(noise_end)))
            return self.noise_threshold

    def estimate_bitlen(self) -> int:
        bit_len = self.__parameter_cache[self.modulation_type_str]["bit_len"]
        if bit_len is None:
            bit_len = signal_functions.estimate_bit_len(self.qad, self.qad_center, self.tolerance, self.modulation_type)
            self.__parameter_cache[self.modulation_type_str]["bit_len"] = bit_len
        return bit_len

    def estimate_qad_center(self) -> float:
        center = self.__parameter_cache[self.modulation_type_str]["qad_center"]
        if center is None:
            noise_value = signal_functions.get_noise_for_mod_type(self.modulation_type)
            qad = self.qad[np.where(self.qad > noise_value)] if noise_value < 0 else self.qad
            center = signal_functions.estimate_qad_center(qad, constants.NUM_CENTERS)
            self.__parameter_cache[self.modulation_type_str]["qad_center"] = center
        return center

    def create_new(self, start:int, end:int):
        new_signal = Signal("", "New " + self.name)
        new_signal._fulldata = self.data[start:end]
        new_signal._noise_threshold = self.noise_threshold
        new_signal.noise_min_plot = self.noise_min_plot
        new_signal.noise_max_plot = self.noise_max_plot
        new_signal.__bit_len = self.bit_len
        new_signal.history = [("Crop", 0, len(self._fulldata))]
        new_signal.cur_history_index = 0
        new_signal.__qad_center = self.qad_center
        new_signal.changed = True
        return new_signal

    def auto_detect(self, emit_update=True):
        needs_update = False
        old_qad_center = self.__qad_center
        self.__qad_center = self.estimate_qad_center()
        if self.__qad_center != old_qad_center:
            self.qad_center_changed.emit(self.__qad_center)
            needs_update = True

        old_bit_len = self.__bit_len
        self.__bit_len = self.estimate_bitlen()
        if self.__bit_len != old_bit_len:
            self.bit_len_changed.emit(self.__bit_len)
            needs_update = True

        if emit_update and needs_update and not self.block_protocol_update:
            self.protocol_needs_update.emit()

    def clear_parameter_cache(self):
        for mod in self.parameter_cache.keys():
            self.parameter_cache[mod]["bit_len"] = None
            self.parameter_cache[mod]["qad_center"] = None

    def estimate_frequency(self, start: int, end: int, sample_rate: float):
        """
        Schätzt die Frequenz des Basissignals mittels FFT

        :param start: Start des Bereichs aus dem untersucht werden soll
        :param end: Ende des Bereichs aus dem untersucht werden soll
        :param sample_rate: Die Sample Rate mit der das Signal aufgenommen wurde
        :return:
        """
        data = self.data[start:end]

        w = np.fft.fft(data)
        freqs = np.fft.fftfreq(len(w))
        idx = np.argmax(np.abs(w))
        freq = freqs[idx]
        freq_in_hertz = abs(freq * sample_rate)
        return freq_in_hertz

    def destroy(self):
        self._fulldata = None
        self._qad = None

    def silent_set_modulation_type(self, mod_type: int):
        self.__modulation_type = mod_type

    def insert_data(self, index: int, data: np.ndarray):
        self._fulldata = np.insert(self._fulldata, index, data)
        self._qad = None

        self.__invalidate_after_edit()

    def delete_range(self, start: int, end: int):
        mask = np.ones(self.num_samples, dtype=bool)
        mask[start:end] = False

        try:
            self._fulldata = self._fulldata[mask]
            self._qad = self._qad[mask] if self._qad is not None else None
        except IndexError as e:
            logger.warning("Could not delete data: " + str(e))

        self.__invalidate_after_edit()

    def mute_range(self, start: int, end: int):
        self._fulldata[start:end] = 0
        if self._qad is not None:
            self._qad[start:end] = 0

        self.__invalidate_after_edit()

    def crop_to_range(self, start: int, end: int):
        self._fulldata = self._fulldata[start:end]
        self._qad = self._qad[start:end] if self._qad is not None else None

        self.__invalidate_after_edit()

    def __invalidate_after_edit(self):
        self.clear_parameter_cache()
        self.changed = True
        self.data_edited.emit()
        self.protocol_needs_update.emit()

    @staticmethod
    def from_samples(samples: np.ndarray, name: str, sample_rate: float):
        signal = Signal("", name, sample_rate=sample_rate)
        signal._fulldata = samples

        return signal
