from __future__ import annotations
from typing import Iterator, Optional

import numpy
from numpy import ndarray

from .timing import GraphTime

class AnalogData:
    raw_samples: ndarray
    _samples: Optional[ndarray]

    def __init__(self, raw_samples: ndarray, voltage_transform_gain: float, voltage_transform_offset: float, start_time: GraphTime, end_time: GraphTime):
        self.raw_samples = raw_samples
        self.voltage_transform = VoltageTransform(voltage_transform_gain, voltage_transform_offset)
        self.start_time = start_time
        self.end_time = end_time
        self._samples = None

    def __iter__(self) -> Iterator[float]:
        '''
        Iterates over the samples in this instance as voltage values.
        '''

        for sample in self.raw_samples:
            yield self._convert_sample(sample)

    @property
    def sample_count(self) -> int:
        '''
        The number of samples contained in this instance.
        '''

        return len(self.raw_samples)

    def _convert_sample(self, sample: int) -> float:
        return self.voltage_transform.gain * float(sample) + self.voltage_transform.offset

    def slice_samples(self, r: slice) -> AnalogData:
        '''
        Allows creating an :py:class:`~.AnalogData` from a subset of this one's samples.
        '''

        if r.step is not None:
            raise ValueError("step is not supported for slicing AnalogData")

        if r.start is None:
            start_sample = 0
        elif r.start >= 0:
            start_sample = r.start
        else:
            start_sample = len(self.raw_samples) + r.start

        if r.stop is None:
            stop_sample = len(self.raw_samples)
        elif r.stop >= 0:
            stop_sample = r.stop
        else:
            stop_sample = len(self.raw_samples) + r.stop


        new_start_time = self.start_time + (self.end_time - self.start_time) * (float(start_sample) / len(self.raw_samples))
        new_end_time = self.end_time - (self.end_time - self.start_time) * (float(len(self.raw_samples) - stop_sample) / len(self.raw_samples))

        return AnalogData(self.raw_samples[r], self.voltage_transform.gain, self.voltage_transform.offset, new_start_time, new_end_time)

    @property
    def samples(self) -> ndarray:
        '''
        Samples after applying voltage scaling.
        '''
        if self._samples is None:
            # Cast to floating point, and allocate a new writable array
            samples = self.raw_samples.astype(numpy.float32)

            # Apply voltage transform
            samples *= self.voltage_transform.gain
            samples += self.voltage_transform.offset
            self._samples = samples
        return self._samples


class VoltageTransform:
    gain: float
    offset: float

    def __init__(self, gain, offset):
        self.gain = gain
        self.offset = offset
