"""Autogenerated module for the SHFQC QCoDeS driver."""
from typing import Any, Dict, List, Tuple, Union
import numpy as np
from zhinst.toolkit import CommandTable, Waveforms, Sequence
from zhinst.toolkit.interface import AveragingMode, SHFQAChannelMode
from zhinst.utils.shfqa.multistate import QuditSettings
from zhinst.qcodes.driver.devices.base import ZIBaseInstrument
from zhinst.qcodes.qcodes_adaptions import ZINode, ZIChannelList


class CommandTableNode(ZINode):
    """CommandTable node.

    This class implements the basic functionality of the command table allowing
    the user to load and upload their own command table.

    A dedicated class called ``CommandTable`` exists that is the preferred way
    to create a valid command table. For more information about the
    ``CommandTable`` refer to the corresponding example or the documentation
    of that class directly.

    Args:
        root: Node used for the upload of the command table
        tree: Tree (node path as tuple) of the current node
        device_type: Device type.
    """

    def __init__(self, parent, tk_object, snapshot_cache=None, zi_node=None):
        ZINode.__init__(
            self, parent, "commandtable", snapshot_cache=snapshot_cache, zi_node=zi_node
        )
        self._tk_object = tk_object

    def check_status(self) -> bool:
        """Check status of the command table.

        Returns:
            Flag if a valid command table is loaded into the device.

        Raises:
            RuntimeError: If the command table upload into the device failed.
        """
        return self._tk_object.check_status()

    def load_validation_schema(self) -> Dict[str, Any]:
        """Load device command table validation schema.

        Returns:
            JSON validation schema for the device command tables.
        """
        return self._tk_object.load_validation_schema()

    def upload_to_device(
        self,
        ct: Union[CommandTable, str, dict],
        *,
        validate: bool = False,
        check_upload: bool = True,
    ) -> None:
        """Upload command table into the device.

        The command table can either be specified through the dedicated
        ``CommandTable`` class or in a raw format, meaning a json string or json
        dict. In the case of a json string or dict the command table is
        validated by default against the schema provided by the device.

        Args:
            ct: Command table.
            validate: Flag if the command table should be validated. (Only
                applies if the command table is passed as a raw json string or
                json dict)
            check_upload: Flag if the upload should be validated by calling
                `check_status`. This is not mandatory bat strongly recommended
                since the device does not raise an error when it rejects the
                command table. This Flag is ignored when called from within a
                transaction.

        Raises:
            RuntimeError: If the command table upload into the device failed.
            zhinst.toolkit.exceptions.ValidationError: Incorrect schema.

        .. versionchanged:: 0.4.2

            New Flag `check_upload` that makes the upload check optional.
            `check_status` is only called when not in a ongoing transaction.
        """
        return self._tk_object.upload_to_device(
            ct=ct, validate=validate, check_upload=check_upload
        )

    def load_from_device(self) -> CommandTable:
        """Load command table from the device.

        Returns:
            command table.
        """
        return self._tk_object.load_from_device()


class AWGCore(ZINode):
    """AWG Core Node."""

    def __init__(self, parent, tk_object, snapshot_cache=None, zi_node=None):
        ZINode.__init__(
            self, parent, "awg", snapshot_cache=snapshot_cache, zi_node=zi_node
        )
        self._tk_object = tk_object
        if self._tk_object.commandtable:

            self.add_submodule(
                "commandtable",
                CommandTableNode(
                    self,
                    self._tk_object.commandtable,
                    zi_node=self._tk_object.commandtable.node_info.path,
                    snapshot_cache=self._snapshot_cache,
                ),
            )

    def enable_sequencer(self, *, single: bool) -> None:
        """Starts the sequencer of a specific channel.

        Warning:
            This function is synchronous and blocks until the sequencer is enabled.
            When working with multiple instruments this function is the wrong
            approach and the sequencer should be enabled asynchronously.
            (For more information please take a look at the awg example in the
            toolkit documentation.)

        Args:
            single: Flag if the sequencer should be disabled after finishing
            execution.

        Raises:
            RuntimeError: If the sequencer could not be enabled.

        .. versionchanged:: 0.5.0

            Check the acknowledged value instead of using `wait_for_state_change`.
        """
        return self._tk_object.enable_sequencer(single=single)

    def wait_done(self, *, timeout: float = 10, sleep_time: float = 0.005) -> None:
        """Wait until the AWG is finished.

        Args:
            timeout: The maximum waiting time in seconds for the generator
                (default: 10).
            sleep_time: Time in seconds to wait between requesting generator
                state

        Raises:
            RuntimeError: If continuous mode is enabled
            TimeoutError: If the sequencer program did not finish within
                the specified timeout time
        """
        return self._tk_object.wait_done(timeout=timeout, sleep_time=sleep_time)

    def compile_sequencer_program(
        self, sequencer_program: Union[str, Sequence], **kwargs: Union[str, int]
    ) -> Tuple[bytes, Dict[str, Any]]:
        """Compiles a sequencer program for the specific device.

        Args:
            sequencer_program: The sequencer program to compile.

        Keyword Args:
            samplerate: Target sample rate of the sequencer. Only allowed/
                necessary for HDAWG devices. Must correspond to the samplerate
                used by the device (device.system.clocks.sampleclock.freq()).
                If not specified the function will get the value itself from
                the device. It is recommended passing the samplerate if more
                than one sequencer code is uploaded in a row to speed up the
                execution time.
            wavepath: path to directory with waveforms. Defaults to path used
                by LabOne UI or AWG Module.
            waveforms: waveform CSV files separated by ';'
            output: name of embedded ELF filename.

        Returns:
            elf: Binary ELF data for sequencer.
            extra: Extra dictionary with compiler output.

        Examples:
            >>> elf, compile_info = device.awgs[0].compile_sequencer_program(seqc)
            >>> device.awgs[0].elf.data(elf)
            >>> device.awgs[0].ready.wait_for_state_change(1)
            >>> device.awgs[0].enable(True)

        Raises:
            RuntimeError: `sequencer_program` is empty.
            RuntimeError: If the compilation failed.

        .. versionadded:: 0.4.0
        """
        return self._tk_object.compile_sequencer_program(
            sequencer_program=sequencer_program, **kwargs
        )

    def load_sequencer_program(
        self, sequencer_program: Union[str, Sequence], **kwargs: Union[str, int]
    ) -> Dict[str, Any]:
        """Compiles the given sequencer program on the AWG Core.

        Warning:
            After uploading the sequencer program one needs to wait before for
            the awg core to become ready before it can be enabled.
            The awg core indicates the ready state through its `ready` node.
            (device.awgs[0].ready() == True)

        Args:
            sequencer_program: Sequencer program to be uploaded.

        Keyword Args:
            samplerate: Target sample rate of the sequencer. Only allowed/
                necessary for HDAWG devices. Must correspond to the samplerate
                used by the device (device.system.clocks.sampleclock.freq()).
                If not specified the function will get the value itself from
                the device. It is recommended passing the samplerate if more
                than one sequencer code is uploaded in a row to speed up the
                execution time.
            wavepath: path to directory with waveforms. Defaults to path used
                by LabOne UI or AWG Module.
            waveforms: waveform CSV files separated by ';'
            output: name of embedded ELF filename.

        Examples:
            >>> compile_info = device.awgs[0].load_sequencer_program(seqc)
            >>> device.awgs[0].ready.wait_for_state_change(1)
            >>> device.awgs[0].enable(True)

        Raises:
            RuntimeError: `sequencer_program` is empty.
            RuntimeError: If the upload or compilation failed.

        .. versionadded:: 0.3.4

            `sequencer_program` does not accept empty strings

        .. versionadded:: 0.4.0

            Use offline compiler instead of AWG module to compile the sequencer
            program. This speeds of the compilation and also enables parallel
            compilation/upload.
        """
        return self._tk_object.load_sequencer_program(
            sequencer_program=sequencer_program, **kwargs
        )

    def write_to_waveform_memory(
        self, waveforms: Waveforms, indexes: list = None
    ) -> None:
        """Writes waveforms to the waveform memory.

        The waveforms must already be assigned in the sequencer program.

        Args:
            waveforms: Waveforms that should be uploaded.
            indexes: Specify a list of indexes that should be uploaded. If
                nothing is specified all available indexes in waveforms will
                be uploaded. (default = None)

        .. versionchanged:: 0.4.2

            Removed `validate` flag and functionality. The validation check is
            now done in the `Waveforms.validate` function.
        """
        return self._tk_object.write_to_waveform_memory(
            waveforms=waveforms, indexes=indexes
        )

    def read_from_waveform_memory(self, indexes: List[int] = None) -> Waveforms:
        """Read waveforms from the waveform memory.

        Args:
            indexes: List of waveform indexes to read from the device. If not
                specified all assigned waveforms will be downloaded.

        Returns:
            Waveform object with the downloaded waveforms.
        """
        return self._tk_object.read_from_waveform_memory(indexes=indexes)

    def configure_marker_and_trigger(
        self, *, trigger_in_source: str, trigger_in_slope: str, marker_out_source: str
    ) -> None:
        """Configures the trigger inputs and marker outputs of the AWG.

        Args:
            trigger_in_source: Alias for the trigger input used by the
                sequencer. For a list of available values use:
                `available_trigger_inputs`
            trigger_in_slope: Alias for the slope of the input trigger
                used by sequencer. For a list of available values use
                `available_trigger_inputs`
            marker_out_source: Alias for the marker output source used by
                the sequencer. For a list of available values use
                `available_trigger_slopes`
        """
        return self._tk_object.configure_marker_and_trigger(
            trigger_in_source=trigger_in_source,
            trigger_in_slope=trigger_in_slope,
            marker_out_source=marker_out_source,
        )

    @property
    def available_trigger_inputs(self) -> List:
        """List the available trigger sources for the sequencer."""
        return self._tk_object.available_trigger_inputs

    @property
    def available_trigger_slopes(self) -> List:
        """List the available trigger slopes for the sequencer."""
        return self._tk_object.available_trigger_slopes

    @property
    def available_marker_outputs(self) -> List:
        """List the available trigger marker outputs for the sequencer."""
        return self._tk_object.available_marker_outputs


class SGChannel(ZINode):
    """Signal Generator Channel for the SHFSG.

    :class:`SGChannel` implements basic functionality to configure SGChannel
    settings of the :class:`SHFSG` instrument.

    Args:
        device: SHFQA device object.
        session: Underlying session.
        tree: Node tree (node path as tuple) of the corresponding node.
    """

    def __init__(self, parent, tk_object, index, snapshot_cache=None, zi_node=None):
        ZINode.__init__(
            self,
            parent,
            f"sgchannel_{index}",
            snapshot_cache=snapshot_cache,
            zi_node=zi_node,
        )
        self._tk_object = tk_object
        if self._tk_object.awg:

            self.add_submodule(
                "awg",
                AWGCore(
                    self,
                    self._tk_object.awg,
                    zi_node=self._tk_object.awg.node_info.path,
                    snapshot_cache=self._snapshot_cache,
                ),
            )

    def configure_channel(
        self, *, enable: bool, output_range: int, center_frequency: float, rf_path: bool
    ) -> None:
        """Configures the RF input and output.

        Args:
            enable: Flag if the signal output should be enabled.
            output_range: Maximal range of the signal output power in dBm
            center_frequency: Center frequency before modulation
            rf_path: Flag if the RF(True) or LF(False) path should be
                configured.
        """
        return self._tk_object.configure_channel(
            enable=enable,
            output_range=output_range,
            center_frequency=center_frequency,
            rf_path=rf_path,
        )

    def configure_pulse_modulation(
        self,
        *,
        enable: bool,
        osc_index: int = 0,
        osc_frequency: float = 100000000.0,
        phase: float = 0.0,
        global_amp: float = 0.5,
        gains: tuple = (1.0, -1.0, 1.0, 1.0),
        sine_generator_index: int = 0,
    ) -> None:
        """Configure the pulse modulation.

        Configures the sine generator to digitally modulate the AWG output, for
        generating single sideband AWG signals

        Args:
            enable: Flag if the modulation should be enabled.
            osc_index: Selects which oscillator to use
            osc_frequency: Oscillator frequency used to modulate the AWG
                outputs. (default = 100e6)
            phase: Sets the oscillator phase. (default = 0.0)
            global_amp: Global scale factor for the AWG outputs. (default = 0.5)
            gains: Sets the four amplitudes used for single sideband generation.
                Default values correspond to upper sideband with a positive
                oscillator frequency. (default = (1.0, -1.0, 1.0, 1.0))
            sine_generator_index: Selects which sine generator to use on a
                given channel.
        """
        return self._tk_object.configure_pulse_modulation(
            enable=enable,
            osc_index=osc_index,
            osc_frequency=osc_frequency,
            phase=phase,
            global_amp=global_amp,
            gains=gains,
            sine_generator_index=sine_generator_index,
        )

    def configure_sine_generation(
        self,
        *,
        enable: bool,
        osc_index: int = 0,
        osc_frequency: float = 100000000.0,
        phase: float = 0.0,
        gains: tuple = (0.0, 1.0, 1.0, 0.0),
        sine_generator_index: int = 0,
    ) -> None:
        """Configures the sine generator output.

        Configures the sine generator output of a specified channel for generating
        continuous wave signals without the AWG.

        Args:
            enable: Flag if the sine generator output should be enabled.
            osc_index: Selects which oscillator to use
            osc_frequency: Oscillator frequency used by the sine generator
                (default = 100e6)
            phase: Sets the oscillator phase. (default = 0.0)
            gains: Sets the four amplitudes used for single sideband
                generation. Default values correspond to upper sideband with a
                positive oscillator frequency.
                Gains are set in the following order I/sin, I/cos, Q/sin, Q/cos.
                (default = (0.0, 1.0, 1.0, 0.0))
            sine_generator_index: Selects which sine generator to use on a given
                channel
        """
        return self._tk_object.configure_sine_generation(
            enable=enable,
            osc_index=osc_index,
            osc_frequency=osc_frequency,
            phase=phase,
            gains=gains,
            sine_generator_index=sine_generator_index,
        )

    @property
    def awg_modulation_freq(self) -> float:
        """Modulation frequency of the AWG.

        Depends on the selected oscillator.
        """
        return self._tk_object.awg_modulation_freq


class Generator(ZINode):
    """Generator node.

    Implements basic functionality of the generator allowing the user to write
    and upload their *'.seqC'* code.

    In contrast to other AWG Sequencers, e.g. from the HDAWG, SHFSG
    it does not provide writing access to the Waveform Memories
    and hence does not come with predefined waveforms such as `gauss`
    or `ones`. Therefore, all waveforms need to be defined in Python
    and uploaded to the device using `upload_waveforms` method.

    Args:
        root: Root of the nodetree
        tree: Tree (node path as tuple) of the current node
        daq_server: Instance of the ziDAQServer
        serial: Serial of the device.
        index: Index of the corresponding awg channel
        max_qubits_per_channel: Max qubits per channel
    """

    def __init__(self, parent, tk_object, snapshot_cache=None, zi_node=None):
        ZINode.__init__(
            self, parent, "generator", snapshot_cache=snapshot_cache, zi_node=zi_node
        )
        self._tk_object = tk_object

    def enable_sequencer(self, *, single: bool) -> None:
        """Starts the sequencer of a specific channel.

        Warning:
            This function is synchronous and blocks until the sequencer is enabled.
            When working with multiple instruments this function is the wrong
            approach and the sequencer should be enabled asynchronously.
            (For more information please take a look at the awg example in the
            toolkit documentation.)

        Args:
            single: Flag if the sequencer should be disabled after finishing
            execution.

        Raises:
            RuntimeError: If the sequencer could not be enabled.

        .. versionchanged:: 0.5.0

            Check the acknowledged value instead of using `wait_for_state_change`.
        """
        return self._tk_object.enable_sequencer(single=single)

    def wait_done(self, *, timeout: float = 10, sleep_time: float = 0.005) -> None:
        """Wait until the AWG is finished.

        Args:
            timeout: The maximum waiting time in seconds for the generator
                (default: 10).
            sleep_time: Time in seconds to wait between requesting generator
                state

        Raises:
            RuntimeError: If continuous mode is enabled
            TimeoutError: If the sequencer program did not finish within
                the specified timeout time
        """
        return self._tk_object.wait_done(timeout=timeout, sleep_time=sleep_time)

    def compile_sequencer_program(
        self, sequencer_program: Union[str, Sequence], **kwargs: Union[str, int]
    ) -> Tuple[bytes, Dict[str, Any]]:
        """Compiles a sequencer program for the specific device.

        Args:
            sequencer_program: The sequencer program to compile.

        Keyword Args:
            samplerate: Target sample rate of the sequencer. Only allowed/
                necessary for HDAWG devices. Must correspond to the samplerate
                used by the device (device.system.clocks.sampleclock.freq()).
                If not specified the function will get the value itself from
                the device. It is recommended passing the samplerate if more
                than one sequencer code is uploaded in a row to speed up the
                execution time.
            wavepath: path to directory with waveforms. Defaults to path used
                by LabOne UI or AWG Module.
            waveforms: waveform CSV files separated by ';'
            output: name of embedded ELF filename.

        Returns:
            elf: Binary ELF data for sequencer.
            extra: Extra dictionary with compiler output.

        Examples:
            >>> elf, compile_info = device.awgs[0].compile_sequencer_program(seqc)
            >>> device.awgs[0].elf.data(elf)
            >>> device.awgs[0].ready.wait_for_state_change(1)
            >>> device.awgs[0].enable(True)

        Raises:
            RuntimeError: `sequencer_program` is empty.
            RuntimeError: If the compilation failed.

        .. versionadded:: 0.4.0
        """
        return self._tk_object.compile_sequencer_program(
            sequencer_program=sequencer_program, **kwargs
        )

    def load_sequencer_program(
        self, sequencer_program: Union[str, Sequence], **kwargs: Union[str, int]
    ) -> Dict[str, Any]:
        """Compiles the given sequencer program on the AWG Core.

        Warning:
            After uploading the sequencer program one needs to wait before for
            the awg core to become ready before it can be enabled.
            The awg core indicates the ready state through its `ready` node.
            (device.awgs[0].ready() == True)

        Args:
            sequencer_program: Sequencer program to be uploaded.

        Keyword Args:
            samplerate: Target sample rate of the sequencer. Only allowed/
                necessary for HDAWG devices. Must correspond to the samplerate
                used by the device (device.system.clocks.sampleclock.freq()).
                If not specified the function will get the value itself from
                the device. It is recommended passing the samplerate if more
                than one sequencer code is uploaded in a row to speed up the
                execution time.
            wavepath: path to directory with waveforms. Defaults to path used
                by LabOne UI or AWG Module.
            waveforms: waveform CSV files separated by ';'
            output: name of embedded ELF filename.

        Examples:
            >>> compile_info = device.awgs[0].load_sequencer_program(seqc)
            >>> device.awgs[0].ready.wait_for_state_change(1)
            >>> device.awgs[0].enable(True)

        Raises:
            RuntimeError: `sequencer_program` is empty.
            RuntimeError: If the upload or compilation failed.

        .. versionadded:: 0.3.4

            `sequencer_program` does not accept empty strings

        .. versionadded:: 0.4.0

            Use offline compiler instead of AWG module to compile the sequencer
            program. This speeds of the compilation and also enables parallel
            compilation/upload.
        """
        return self._tk_object.load_sequencer_program(
            sequencer_program=sequencer_program, **kwargs
        )

    def write_to_waveform_memory(
        self, pulses: Union[Waveforms, dict], *, clear_existing: bool = True
    ) -> None:
        """Writes pulses to the waveform memory.

        Args:
            pulses: Waveforms that should be uploaded.
            clear_existing: Flag whether to clear the waveform memory before the
                present upload. (default = True)
        """
        return self._tk_object.write_to_waveform_memory(
            pulses=pulses, clear_existing=clear_existing
        )

    def read_from_waveform_memory(self, slots: List[int] = None) -> Waveforms:
        """Read pulses from the waveform memory.

        Args:
            slots: List of waveform indexes to read from the device. If not
                specified all assigned waveforms will be downloaded.

        Returns:
            Mutable mapping of the downloaded waveforms.
        """
        return self._tk_object.read_from_waveform_memory(slots=slots)

    def configure_sequencer_triggering(
        self, *, aux_trigger: str, play_pulse_delay: float = 0.0
    ) -> None:
        """Configure the sequencer triggering.

        Args:
            aux_trigger: Alias for the trigger source used in the sequencer.
                For the list of available values, use `available_aux_trigger_inputs`
            play_pulse_delay: Delay in seconds before the start of waveform playback.
        """
        return self._tk_object.configure_sequencer_triggering(
            aux_trigger=aux_trigger, play_pulse_delay=play_pulse_delay
        )

    @property
    def available_aux_trigger_inputs(self) -> List:
        """List of available aux trigger sources for the generator."""
        return self._tk_object.available_aux_trigger_inputs


class Qudit(ZINode):
    """Single Qudit node.

    Implements basic functionality of a single qudit node, e.g applying the
    basic configuration.

    Args:
        root: Root of the nodetree.
        tree: Tree (node path as tuple) of the current node.
        serial: Serial of the device.
        readout_channel: Index of the readout channel this qudit belongs to.
    """

    def __init__(self, parent, tk_object, index, snapshot_cache=None, zi_node=None):
        ZINode.__init__(
            self,
            parent,
            f"qudit_{index}",
            snapshot_cache=snapshot_cache,
            zi_node=zi_node,
        )
        self._tk_object = tk_object

    def configure(self, qudit_settings: QuditSettings, enable: bool = True) -> None:
        """Compiles a list of transactions to apply the qudit settings to the device.

        Args:
            qudit_settings: The qudit settings to be configured.
            enable: Whether to enable the qudit. (default: True)

        """
        return self._tk_object.configure(qudit_settings=qudit_settings, enable=enable)


class MultiState(ZINode):
    """MultiState node.

    Implements basic functionality of the MultiState node.

    Args:
        root: Root of the nodetree.
        tree: Tree (node path as tuple) of the current node.
        serial: Serial of the device.
        index: Index of the corresponding readout channel.
    """

    def __init__(self, parent, tk_object, snapshot_cache=None, zi_node=None):
        ZINode.__init__(
            self, parent, "multistate", snapshot_cache=snapshot_cache, zi_node=zi_node
        )
        self._tk_object = tk_object
        if self._tk_object.qudits:

            channel_list = ZIChannelList(
                self,
                "qudits",
                Qudit,
                zi_node=self._tk_object.qudits.node_info.path,
                snapshot_cache=self._snapshot_cache,
            )
            for i, x in enumerate(self._tk_object.qudits):
                channel_list.append(
                    Qudit(
                        self,
                        x,
                        i,
                        zi_node=self._tk_object.qudits[i].node_info.path,
                        snapshot_cache=self._snapshot_cache,
                    )
                )
            # channel_list.lock()
            self.add_submodule("qudits", channel_list)

    def get_qudits_results(self) -> Dict[int, np.ndarray]:
        """Downloads the qudit results from the device and group them by qudit.

        This function accesses the multistate nodes to determine which
        integrators were used for which qudit to able to group the results by
        qudit.

        Returns:
            A dictionary with the qudit index keys and result vector values.
        """
        return self._tk_object.get_qudits_results()


class Readout(ZINode):
    """Readout node.

    Implements basic functionality of the readout, e.g allowing the user to
    write the integration weight.

    Args:
        root: Root of the nodetree
        tree: Tree (node path as tuple) of the current node
        serial: Serial of the device.
        index: Index of the corresponding awg channel
        max_qubits_per_channel: Max qubits per channel
    """

    def __init__(self, parent, tk_object, snapshot_cache=None, zi_node=None):
        ZINode.__init__(
            self, parent, "readout", snapshot_cache=snapshot_cache, zi_node=zi_node
        )
        self._tk_object = tk_object
        if self._tk_object.multistate:

            self.add_submodule(
                "multistate",
                MultiState(
                    self,
                    self._tk_object.multistate,
                    zi_node=self._tk_object.multistate.node_info.path,
                    snapshot_cache=self._snapshot_cache,
                ),
            )

    def configure_result_logger(
        self,
        *,
        result_source: str,
        result_length: int,
        num_averages: int = 1,
        averaging_mode: AveragingMode = AveragingMode.CYCLIC,
    ) -> None:
        """Configures the result logger for readout mode.

        Args:
            result_source: String-based tag to select the result source in readout
                mode, e.g. "result_of_integration" or "result_of_discrimination".
            result_length: Number of results to be returned by the result logger
            num_averages: Number of averages, will be rounded to 2^n
            averaging_mode: Select the averaging order of the result, with
                0 = cyclic and 1 = sequential.
        """
        return self._tk_object.configure_result_logger(
            result_source=result_source,
            result_length=result_length,
            num_averages=num_averages,
            averaging_mode=averaging_mode,
        )

    def run(self) -> None:
        """Reset and enable the result logger."""
        return self._tk_object.run()

    def stop(self, *, timeout: float = 10, sleep_time: float = 0.05) -> None:
        """Stop the result logger.

        Args:
            timeout: The maximum waiting time in seconds for the Readout
                (default: 10).
            sleep_time: Sleep interval in seconds. (default = 0.05)

        Raises:
            TimeoutError: The result logger could not been stopped within the
                given time.
        """
        return self._tk_object.stop(timeout=timeout, sleep_time=sleep_time)

    def wait_done(self, *, timeout: float = 10, sleep_time: float = 0.05) -> None:
        """Wait until the readout is finished.

        Args:
            timeout: The maximum waiting time in seconds for the Readout
                (default: 10).
            sleep_time: Sleep interval in seconds. (default = 0.05)

        Raises:
            TimeoutError: if the readout recording is not completed within the
                given time.
        """
        return self._tk_object.wait_done(timeout=timeout, sleep_time=sleep_time)

    def read(self, *, timeout: float = 10) -> np.array:
        """Waits until the logger finished recording and returns the measured data.

        Args:
            timeout: Maximum time to wait for data in seconds (default = 10s)

        Returns:
            Result logger data.
        """
        return self._tk_object.read(timeout=timeout)

    def write_integration_weights(
        self,
        weights: Union[Waveforms, dict],
        *,
        integration_delay: float = 0.0,
        clear_existing: bool = True,
    ) -> None:
        """Configures the weighted integration.

        Args:
            weights: Dictionary containing the complex weight vectors, where
                keys correspond to the indices of the integration units to be
                configured.
            integration_delay: Delay in seconds before starting the readout.
                (default = 0.0)
            clear_existing: Flag whether to clear the waveform memory before
                the present upload. (default = True)
        """
        return self._tk_object.write_integration_weights(
            weights=weights,
            integration_delay=integration_delay,
            clear_existing=clear_existing,
        )

    def read_integration_weights(self, slots: List[int] = None) -> Waveforms:
        """Read integration weights from the waveform memory.

        Args:
            slots: List of weight slots to read from the device. If not specified
                all available weights will be downloaded.

        Returns:
            Mutable mapping of the downloaded weights.
        """
        return self._tk_object.read_integration_weights(slots=slots)


class Spectroscopy(ZINode):
    """Spectroscopy node.

    Implements basic functionality of the spectroscopy, e.g allowing the user to
    read the result logger data.

    Args:
        root: Root of the nodetree
        tree: Tree (node path as tuple) of the current node
        serial: Serial of the device.
        index: Index of the corresponding awg channel
    """

    def __init__(self, parent, tk_object, snapshot_cache=None, zi_node=None):
        ZINode.__init__(
            self, parent, "spectroscopy", snapshot_cache=snapshot_cache, zi_node=zi_node
        )
        self._tk_object = tk_object

    def configure_result_logger(
        self,
        *,
        result_length: int,
        num_averages: int = 1,
        averaging_mode: AveragingMode = AveragingMode.CYCLIC,
    ) -> None:
        """Configures the result logger for spectroscopy mode.

        Args:
            result_length: Number of results to be returned by the result logger
            num_averages: Number of averages, will be rounded to 2^n.
            averaging_mode: Averaging order of the result.
        """
        return self._tk_object.configure_result_logger(
            result_length=result_length,
            num_averages=num_averages,
            averaging_mode=averaging_mode,
        )

    def run(self) -> None:
        """Resets and enables the spectroscopy result logger."""
        return self._tk_object.run()

    def stop(self, *, timeout: float = 10, sleep_time: float = 0.05) -> None:
        """Stop the result logger.

        Args:
            timeout: The maximum waiting time in seconds for the
                Spectroscopy (default: 10).
            sleep_time: Time in seconds to wait between
                requesting Spectroscopy state
        Raises:
            TimeoutError: If the result logger could not been stopped within the
                given time.

        """
        return self._tk_object.stop(timeout=timeout, sleep_time=sleep_time)

    def wait_done(self, *, timeout: float = 10, sleep_time: float = 0.05) -> None:
        """Wait until spectroscopy is finished.

        Args:
            timeout (float): The maximum waiting time in seconds for the
                Spectroscopy (default: 10).
            sleep_time (float): Time in seconds to wait between
                requesting Spectroscopy state

        Raises:
            TimeoutError: if the spectroscopy recording is not completed within the
                given time.

        """
        return self._tk_object.wait_done(timeout=timeout, sleep_time=sleep_time)

    def read(self, *, timeout: float = 10) -> np.array:
        """Waits until the logger finished recording and returns the measured data.

        Args:
            timeout: Maximum time to wait for data in seconds (default = 10s)

        Returns:
            An array containing the result logger data.

        """
        return self._tk_object.read(timeout=timeout)


class QAChannel(ZINode):
    """Quantum Analyzer Channel for the SHFQA.

    :class:`QAChannel` implements basic functionality to configure QAChannel
    settings of the :class:`SHFQA` instrument.
    Besides the :class:`Generator`, :class:`Readout` and :class:`Sweeper`
    modules it also provides an easy access to commonly used `QAChannel` parameters.

    Args:
        device: SHFQA device object.
        session: Underlying session.
        tree: Node tree (node path as tuple) of the corresponding node.
    """

    def __init__(self, parent, tk_object, index, snapshot_cache=None, zi_node=None):
        ZINode.__init__(
            self,
            parent,
            f"qachannel_{index}",
            snapshot_cache=snapshot_cache,
            zi_node=zi_node,
        )
        self._tk_object = tk_object
        if self._tk_object.generator:

            self.add_submodule(
                "generator",
                Generator(
                    self,
                    self._tk_object.generator,
                    zi_node=self._tk_object.generator.node_info.path,
                    snapshot_cache=self._snapshot_cache,
                ),
            )

        if self._tk_object.readout:

            self.add_submodule(
                "readout",
                Readout(
                    self,
                    self._tk_object.readout,
                    zi_node=self._tk_object.readout.node_info.path,
                    snapshot_cache=self._snapshot_cache,
                ),
            )

        if self._tk_object.spectroscopy:

            self.add_submodule(
                "spectroscopy",
                Spectroscopy(
                    self,
                    self._tk_object.spectroscopy,
                    zi_node=self._tk_object.spectroscopy.node_info.path,
                    snapshot_cache=self._snapshot_cache,
                ),
            )

    def configure_channel(
        self,
        *,
        input_range: int,
        output_range: int,
        center_frequency: float,
        mode: SHFQAChannelMode,
    ) -> None:
        """Configures the RF input and output of a specified channel.

        Args:
            input_range: Maximal range of the signal input power in dBm
            output_range: Maximal range of the signal output power in dBm
            center_frequency: Center frequency of the analysis band [Hz]
            mode: Select between spectroscopy and readout mode.
        """
        return self._tk_object.configure_channel(
            input_range=input_range,
            output_range=output_range,
            center_frequency=center_frequency,
            mode=mode,
        )


class SHFScope(ZINode):
    """SHFQA Scope Node.

    Implements basic functionality of the scope node, e.g allowing the user to
    read the data.

    Args:
        root: Root of the nodetree
        tree: Tree (node path as tuple) of the current node
        daq_server: Instance of the ziDAQServer
        serial: Serial of the device.
    """

    def __init__(self, parent, tk_object, index, snapshot_cache=None, zi_node=None):
        ZINode.__init__(
            self,
            parent,
            f"shfscope_{index}",
            snapshot_cache=snapshot_cache,
            zi_node=zi_node,
        )
        self._tk_object = tk_object

    def run(
        self, *, single: bool = True, timeout: float = 10, sleep_time: float = 0.005
    ) -> None:
        """Run the scope recording.

        Args:
            timeout: The maximum waiting time in seconds for the Scope
                (default = 10).
            sleep_time: Time in seconds to wait between requesting the progress
                and records values (default = 0.005).

        Raises:
            TimeoutError: The scope did not start within the specified
                timeout.
        """
        return self._tk_object.run(
            single=single, timeout=timeout, sleep_time=sleep_time
        )

    def stop(self, *, timeout: float = 10, sleep_time: float = 0.005) -> None:
        """Stop the scope recording.

        Args:
            timeout: The maximum waiting time in seconds for the scope
                (default = 10).
            sleep_time: Time in seconds to wait between requesting the progress
                and records values (default = 0.005).

        Raises:
            TimeoutError: The scope did not stop within the specified
                timeout.
        """
        return self._tk_object.stop(timeout=timeout, sleep_time=sleep_time)

    def wait_done(self, *, timeout: float = 10, sleep_time: float = 0.005) -> None:
        """Wait until the scope recording is finished.

        Args:
            timeout: The maximum waiting time in seconds for the Scope
                (default = 10).
            sleep_time: Time in seconds to wait between requesting the progress
                and records values (default = 0.005).

        Raises:
            TimeoutError: The scope did not finish within the specified
                timeout.
        """
        return self._tk_object.wait_done(timeout=timeout, sleep_time=sleep_time)

    def configure(
        self,
        *,
        input_select: Dict[int, str],
        num_samples: int,
        trigger_input: str,
        num_segments: int = 1,
        num_averages: int = 1,
        trigger_delay: float = 0,
    ) -> None:
        """Configures the scope for a measurement.

        Args:
            input_select: Map of a specific scope channel an their signal
                source, e.g. "channel0_signal_input". (For a list of available
                values use `available_inputs`)
            num_samples: Number samples to recorded in a scope shot.
            trigger_input: Specifies the trigger source of the scope
                acquisition - if set to None, the self-triggering mode of the
                scope becomes active, which is useful e.g. for the GUI.
                For a list of available trigger values use
                `available_trigger_inputs`.
            num_segments: Number of distinct scope shots to be returned after
                ending the acquisition.
            num_averages: Specifies how many times each segment should be
                averaged on hardware; to finish a scope acquisition, the number
                of issued triggers must be equal to num_segments * num_averages.
            trigger_delay: delay in samples specifying the time between the
                start of data acquisition and reception of a trigger.
        """
        return self._tk_object.configure(
            input_select=input_select,
            num_samples=num_samples,
            trigger_input=trigger_input,
            num_segments=num_segments,
            num_averages=num_averages,
            trigger_delay=trigger_delay,
        )

    def read(self, *, timeout: float = 10) -> tuple:
        """Read out the recorded data from the scope.

        Args:
            timeout: The maximum waiting time in seconds for the
                Scope (default: 10).

        Returns:
            (recorded_data, recorded_data_range, scope_time)

        Raises:
            TimeoutError: if the scope recording is not completed before
                timeout.
        """
        return self._tk_object.read(timeout=timeout)

    @property
    def available_trigger_inputs(self) -> List:
        """List of the available trigger sources for the scope."""
        return self._tk_object.available_trigger_inputs

    @property
    def available_inputs(self) -> List:
        """List of the available signal sources for the scope channels."""
        return self._tk_object.available_inputs


class SHFQC(ZIBaseInstrument):
    """QCoDeS driver for the Zurich Instruments SHFQC."""

    def _init_additional_nodes(self):
        """Init class specific modules and parameters."""
        if self._tk_object.sgchannels:

            channel_list = ZIChannelList(
                self,
                "sgchannels",
                SGChannel,
                zi_node=self._tk_object.sgchannels.node_info.path,
                snapshot_cache=self._snapshot_cache,
            )
            for i, x in enumerate(self._tk_object.sgchannels):
                channel_list.append(
                    SGChannel(
                        self,
                        x,
                        i,
                        zi_node=self._tk_object.sgchannels[i].node_info.path,
                        snapshot_cache=self._snapshot_cache,
                    )
                )
            # channel_list.lock()
            self.add_submodule("sgchannels", channel_list)

        if self._tk_object.qachannels:

            channel_list = ZIChannelList(
                self,
                "qachannels",
                QAChannel,
                zi_node=self._tk_object.qachannels.node_info.path,
                snapshot_cache=self._snapshot_cache,
            )
            for i, x in enumerate(self._tk_object.qachannels):
                channel_list.append(
                    QAChannel(
                        self,
                        x,
                        i,
                        zi_node=self._tk_object.qachannels[i].node_info.path,
                        snapshot_cache=self._snapshot_cache,
                    )
                )
            # channel_list.lock()
            self.add_submodule("qachannels", channel_list)

        if self._tk_object.scopes:

            channel_list = ZIChannelList(
                self,
                "scopes",
                SHFScope,
                zi_node=self._tk_object.scopes.node_info.path,
                snapshot_cache=self._snapshot_cache,
            )
            for i, x in enumerate(self._tk_object.scopes):
                channel_list.append(
                    SHFScope(
                        self,
                        x,
                        i,
                        zi_node=self._tk_object.scopes[i].node_info.path,
                        snapshot_cache=self._snapshot_cache,
                    )
                )
            # channel_list.lock()
            self.add_submodule("scopes", channel_list)

    def factory_reset(self, *, deep: bool = True) -> None:
        """Load the factory default settings.

        Args:
            deep: A flag that specifies if a synchronization
                should be performed between the device and the data
                server after loading the factory preset (default: True).
        """
        return self._tk_object.factory_reset(deep=deep)

    def start_continuous_sw_trigger(
        self, *, num_triggers: int, wait_time: float
    ) -> None:
        """Issues a specified number of software triggers.

        Issues a specified number of software triggers with a certain wait time
        in between. The function guarantees reception and proper processing of
        all triggers by the device, but the time between triggers is
        non-deterministic by nature of software triggering. Only use this
        function for prototyping and/or cases without strong timing requirements.

        Args:
            num_triggers: Number of triggers to be issued
            wait_time: Time between triggers in seconds
        """
        return self._tk_object.start_continuous_sw_trigger(
            num_triggers=num_triggers, wait_time=wait_time
        )

    @property
    def max_qubits_per_channel(self) -> int:
        """Maximum number of supported qubits per channel."""
        return self._tk_object.max_qubits_per_channel
