from __future__ import annotations
import numpy as np
from datetime import datetime
from typing import Union, List, Iterator, Tuple, Any
import matplotlib.pyplot as plt

from geiger.core.base_classes import MeasurementBase, TickedMeasurement, ArrayBase, TickedArrayBase
from geiger.utilities.plotting import TickedCountsPlotter


class Counts(MeasurementBase):
    """
    Class which stores counts, live and real tick_times and errors for count data.

    Parameters
    ----------
    counts : float
        Number of counts measured within live_time by the detector.
    live_time : float
        Live_time of detector during acquisition.
    real_time : float
        Real_time of detector during acquisition.
    name : str
        Label for the count data.
    comment : str
        Comment to attach to the count data.
    start_time : datetime or None
        Time at which the acquisition started.

    Methods
    -------
    consecutive_addition(other: Counts) -> Counts
        Method which subtracts Counts data from the current Counts data, equivalent to subtracting a subsection of a
        measurement.
    """
    def __init__(self,
                 counts: float,
                 live_time: float,
                 real_time: float,
                 name: str = '',
                 error: Union[float, None] = None,
                 comment: str = '',
                 start_time: Union[datetime, None] = None):
        self.counts = counts
        self._error = error
        super(Counts, self).__init__(live_time=live_time,
                                     real_time=real_time,
                                     name=name,
                                     comment=comment,
                                     start_time=start_time)

    @property
    def error(self) -> float:
        """
        Property which returns the uncertainty in the count data.

        Returns
        -------
        float
        """
        if self._error is None:
            return self.counts**0.5
        else:
            return self._error

    @property
    def count_rate(self) -> float:
        """
        Property which returns the count rate of the measurement. I.e. counts / live_time

        Returns
        -------
        float
        """
        return (self.counts / self.live_time) if self.live_time > 0. else 0.

    def resample(self, scale: float = 1.0) -> Counts:
        """
        Method which returns a new Counts instance with the counts data resampled from a normal distribution from the
        counts and error data, scaled by the 'scale' factor.

        Parameters
        ----------
        scale : float
            Scale factor by which the counts data will be multiplied before resampling.

        Returns
        -------
        Counts
        """
        new_counts = np.random.normal(self.counts*scale, np.sqrt(scale*self.error**2))
        return Counts(new_counts, live_time=self.live_time, real_time=self.real_time, name=self.name,
                      error=np.sqrt(scale*self.error**2), comment=self.comment)

    def subtract_background(self, bg_counts: Counts) -> Counts:
        """
        Function which takes a background Counts object and subtracts it,
        scaled by the correct live time, returning the resultant Counts.

        Parameters
        ----------
        bg_counts : Counts
            Background counts to be subtracted.

        Returns
        -------
        Counts
        """
        return super(Counts, self).subtract_background(bg_counts)

    def consecutive_addition(self, other: Counts) -> Counts:
        """
        Method which adds Counts data to the current Counts data, equivalent to adding two consecutive measurements
        together, adding their counts and live and real times together.

        Parameters
        ----------
        other : Counts
            Counts to add.

        Returns
        -------
        Counts
        """
        counts = Counts(self.counts + other.counts,
                        live_time=self.live_time + other.live_time,
                        real_time=self.real_time + other.real_time,
                        error=(self.error**2 + other.error**2)**0.5,
                        name=self.name)
        return counts

    def simultaneous_addition(self, other: Counts) -> Counts:
        """
        Method which adds Counts data to the current Counts data, equivalent to adding two simultaneous measurements
        together, adding their counts together, scaling 'other' counts to have equal live time.

        Parameters
        ----------
        other : Counts
            Counts to add.

        Returns
        -------
        Counts
        """
        scale = self.live_time / other.live_time
        counts = Counts(self.counts + other.counts * scale,
                        live_time=self.live_time,
                        real_time=self.real_time,
                        error=(self.error**2 + (other.error*scale)**2)**0.5,
                        name=self.name)
        return counts

    def consecutive_subtraction(self, other: Counts) -> Counts:
        """
        Method which subtracts Counts data from the current Counts data, equivalent to subtracting a subsection of a
        measurement.

        Parameters
        ----------
        other : Counts
            Counts to subtract.

        Returns
        -------
        Counts
        """
        counts = Counts(self.counts - other.counts,
                        live_time=self.live_time - other.live_time,
                        real_time=self.real_time - other.real_time,
                        error=(self.error**2 + other.error**2)**0.5,
                        name=self.name)
        return counts

    def simultaneous_subtraction(self, other: Counts) -> Counts:
        """
        Method which subtracts Counts data from the current Counts data, equivalent to subtracting a signal present
        throughout the measurement, e.g. the background.

        Parameters
        ----------
        other : Counts
            Counts to subtract.

        Returns
        -------
        Counts
        """
        scale = self.live_time / other.live_time
        counts = Counts(self.counts - other.counts * scale,
                        live_time=self.live_time,
                        real_time=self.real_time,
                        error=(self.error**2 + (other.error*scale)**2)**0.5,
                        name=self.name)
        return counts

    def __str__(self) -> str:
        return (f"<Counts counts='{self.counts:.1f}' live_time='{self.live_time:.1f}' real_time='{self.real_time:.1f}' "
                f"error='{self.error:.1f}' name='{self.name}'  comment='{self.comment}' />")

    def __eq__(self, other: Counts) -> bool:
        return self.counts == other.counts and self.live_time == other.live_time


class CountsArray(ArrayBase):
    """
    Array of Counts objects, indexed by their 'name' attribute.

    Parameters
    ----------
    counts : List[Counts]
        List of Counts instances. These will become the members of the array.

    Methods
    -------
    consecutive_addition(other: Counts) -> Counts
        Method which subtracts Counts data from the current Counts data, equivalent to subtracting a subsection of a
        measurement.

    Notes
    -----
    Users will probably not need to create CountsArray instances, they will be used after being created inside the
    SMLReader.
    """

    def __init__(self, counts: List[Counts]):
        super(CountsArray, self).__init__(counts)

    def __getitem__(self, item) -> Counts:
        return super(CountsArray, self).__getitem__(item)

    def get(self, item: str, default_val: Any = Counts(0., 0., 0.)) -> Counts:
        return super(CountsArray, self).get(item, default_val)

    def simultaneous_sum(self) -> Counts:
        """
        Method which returns the simultaneous sum of all of the members of the array.

        Returns
        -------
        Counts
        """
        c_sum = Counts(counts=0.,
                       live_time=float(np.mean([c.live_time for c in self.elements.values()])),
                       real_time=float(np.mean([c.real_time for c in self.elements.values()])),
                       name=f'Simultaneously summed counts from ' + ', '.join(self.names))
        for c in self.elements.values():
            c_sum = c_sum.simultaneous_addition(c)
        return c_sum

    def consecutive_sum(self) -> Counts:
        """
        Method which returns the consecutive sum of all of the members of the array.

        Returns
        -------
        Counts
        """
        c_sum = Counts(counts=0., live_time=0., real_time=0.,
                       name=f'Consecutively summed counts from ' + ', '.join(self.names))
        for c in self.elements.values():
            c_sum = c_sum.consecutive_addition(c)
        return c_sum

    def subtract_backgrounds(self, backgrounds: CountsArray) -> CountsArray:
        """
        Method which subtracts the background from each member of the array, using the array of background counts
        supplied as the argument.

        Parameters
        ----------
        backgrounds : CountsArray

        Returns
        -------
        CountsArray
        """
        return super(CountsArray, self).subtract_backgrounds(backgrounds)

    def consecutive_addition(self, other: CountsArray) -> CountsArray:
        """
        Method which returns the consecutive sum of the CountsArray with another Array.

        Parameters
        ----------
        other : CountsArray
            The Array instance to add to this one.

        Returns
        -------
        CountsArray
        """
        return super(CountsArray, self).consecutive_addition(other)

    def consecutive_subtraction(self, other: CountsArray) -> CountsArray:
        """
        Method which returns the consecutive subtraction of another CountsArray from the Array.

        Parameters
        ----------
        other : Array
            The Array instance to subtract from this one.

        Returns
        -------
        CountsArray
        """
        return super(CountsArray, self).consecutive_subtraction(other)

    def simultaneous_addition(self, other: CountsArray) -> CountsArray:
        """
        Method which returns the simultaneous sum of the CountsArray with another Array.

        Parameters
        ----------
        other : Array
            The Array instance to add to this one.

        Returns
        -------
        CountsArray
        """
        return super(CountsArray, self).simultaneous_addition(other)

    def simultaneous_subtraction(self, other: CountsArray) -> CountsArray:
        """
        Method which returns the simultaneous subtraction of another CountsArray from the Array.

        Parameters
        ----------
        other : Array
            The Array instance to subtract from this one.

        Returns
        -------
        CountsArray
        """
        return super(CountsArray, self).simultaneous_subtraction(other)

    def resample(self, factor: float) -> CountsArray:
        """
        Method which returns a new array with each member resampled after scaling by the factor parameter.

        Parameters
        ----------
        factor : float
            Scale by which the measurements will be scaled before resampling.

        Returns
        -------
        CountsArray
        """
        return super(CountsArray, self).resample(factor)

    def masked_array(self, names: List[str]) -> CountsArray:
        """
        Method which returns a new array consisting only of members with the names in the given list of names.

        Parameters
        ----------
        names : List[str]

        Returns
        -------
        Array
        """
        return super(CountsArray, self).masked_array(names)

    def __str__(self):
        return f"Counts : " + ", ".join([f"{det}:{self[det].counts:.1f}" for det in self.elements])


class TickedCounts(TickedMeasurement):
    """
    Class which contains details about the counts for multiple ticks (sequential short periods of time).

    Attributes
    ----------
    ticks : np.ndarray
        Array of counts for each tick.
    tick_times : np.ndarray
        Array of datetime instances, marking the time at which each tick ended or began.
    live_times : np.ndarray
        Array of floats, labelling the live time for each tick.
    real_times : np.ndarray
        Array of floats, labelling the real time for each tick.
    msg_times : np.ndarray
        Possible array of datetime instances, marking the times at which the tick messages arrived.
    blank_rate : np.ndarray
        Possible array of floats, marking the rate of blanking pulses measured during each tick.
    name : str
        Label for the count data.
    comment : str
        Comment to attach to the count data.
    start_time : datetime or None
        Time at which the acquisition started.

    Methods
    -------
    errors() -> np.ndarray
        Property which returns the uncertainties for each tick.
    get_single_tick(tick_num: int) -> Counts
        Method which returns a single tick's worth of counts, with the appropriate live time and real time.
    sum_ticks(start: datetime = None, end: datetime = None) -> Counts
        Method which returns a Counts instance containing the summed counts for all ticks, or ticks in the range
        start:end.
    resample(scale: float = 1.0) -> TickedCounts
        Method which returns a resampled TickedCounts instance.
    plot(**kwargs) -> Tuple[plt.Figure, plt.Axes]
        Simple generator of variable length windowed spectra, equivalent to a rolling window across the time space.
    simple_windowed_counts(tick_window_width: int = 1, mode: int = 0) -> Iterator[Counts]
        Simple generator of variable length windowed spectra, equivalent to a rolling window across the time space.
    subtract_background(bg_counts: Counts) -> TickedCounts
        Method which subtracts a background signal from each of the ticks, using simultaneous subtraction.
    """
    def __init__(self,
                 ticks: np.ndarray,
                 tick_times: np.ndarray,
                 live_times: np.ndarray,
                 real_times: np.ndarray,
                 name: str = '',
                 msg_times: Union[np.ndarray, None] = None,
                 blank_rate: Union[np.ndarray, None] = None,
                 errors: Union[np.ndarray, None] = None,
                 start_time: Union[np.ndarray, None] = None,
                 comment: str = ''):
        """
        Parameters
        ----------
        ticks : np.ndarray
            Array of counts for each tick.
        tick_times : np.ndarray
            Array of datetime instances, marking the time at which each tick ended or began.
        name : str
            Label for the count data.
        live_times : np.ndarray
            Array of floats, labelling the live time for each tick.
        real_times : np.ndarray
            Array of floats, labelling the real time for each tick.
        msg_times : np.ndarray
            Optional array of datetime instances, marking the times at which the tick messages arrived.
        blank_rate : np.ndarray
            Optional array of floats, marking the rate of blanking pulses measured during each tick.
        errors : np.ndarray
            Optional array of floats, marking the uncertainties of the counts values. If None, defaults to Poisson
            uncertainties.
        start_time : datetime
            Time at which the measurement began.
        comment : str
            Optional string comment to attach to the measurement.
        """

        super().__init__(tick_times=tick_times, live_times=live_times, real_times=real_times, name=name,
                         comment=comment, start_time=start_time)

        self.ticks = ticks
        self.msg_times = msg_times
        self.blank_rate = blank_rate
        self._errors = errors

    @property
    def errors(self) -> np.ndarray:
        """
        Property which returns the uncertainties for each tick.

        Returns
        -------
        np.ndarray
        """
        if self._errors is None:
            return np.abs(self.ticks) ** 0.5
        else:
            return self._errors

    def get_single_tick(self, tick_num: int) -> Counts:
        """
        Method which returns a single tick's worth of counts, with the appropriate live time and real time.

        Parameters
        ----------
        tick_num : int
            The index for the tick to retrieve.

        Returns
        -------
        Counts
        """
        return Counts(counts=self.ticks[tick_num],
                      live_time=self.live_times[tick_num],
                      real_time=self.real_times[tick_num],
                      error=self.errors[tick_num],
                      name=self.name + (' ' if len(self.name) > 0 else '') + f'Tick {tick_num}',
                      comment=self.comment)

    def sum_ticks(self, start: Union[datetime, None] = None, end: Union[datetime, None] = None) -> Counts:
        """
        Method which returns a Counts instance containing the summed counts for all ticks, or ticks in the range
        start:end.

        Parameters
        ----------
        start : Union[datetime, None]
            The start time from which to sum the ticks. If None, sums from the first tick in the sequence.
        end : Union[datetime, None]
            The end time up to which to sum the ticks. If None, sums up to the last tick in the sequence.

        Returns
        -------
        Counts
        """
        t_mask = np.ones(self.tick_times.size, dtype=bool)
        if start is not None:
            t_mask = np.logical_and(t_mask, self.tick_times >= start)
        if end is not None:
            t_mask = np.logical_and(t_mask, self.tick_times < end)
        counts = Counts(self.ticks[t_mask].sum(axis=0),
                        live_time=self.live_times[t_mask].sum(axis=0),
                        real_time=self.real_times[t_mask].sum(axis=0),
                        name=self.name, comment=self.comment,
                        error=None if self._errors is None else np.sqrt(np.square(self.errors[t_mask]).sum()),
                        start_time=self.start_time if start is None else start)
        return counts

    def resample(self, scale: float = 1.0) -> TickedCounts:
        """
        Method which returns a resampled TickedCounts instance.

        Parameters
        ----------
        scale : float
            Factor by which to multiply the TickedCounts instance before resampling.

        Returns
        -------
        TickedCounts
        """
        new_ticks = np.random.poisson(self.ticks*scale)
        return TickedCounts(new_ticks, self.tick_times, name=self.name, live_times=self.live_times*scale,
                            real_times=self.real_times, msg_times=self.msg_times, blank_rate=self.blank_rate,
                            errors=self.errors, start_time=self.start_time)

    def plot(self, other: List[TickedCounts] = None, **kwargs) -> Tuple[plt.Figure, plt.Axes]:
        """
        Method which plots the TickedCounts as a function of time, returning the figure and axes.

        Parameters
        ----------
        other: List[TickedCounts]
            Other TickedCounts to plot together

        Returns
        -------
        Tuple[plt.Figure, plt.Axes]
        """
        if other is None:
            other = []
        return TickedCountsPlotter(ticked_counts=[self, *other], **kwargs).plot()

    def simple_windowed_counts(self, tick_window_width: int = 1, mode: int = 0) -> Iterator[Counts]:
        """
        Simple generator of variable length windowed spectra, equivalent to a rolling window across the time space.
        Modes:
        0 - windows are always full. For a TickedSpectrum with N ticks, there will be N - tick_window_width + 1
        spectra yielded.
        1 - windows are always at least half full. For a TickedSpectrum with N ticks, there will be N spectra
        yielded.
        2 - windows are always at least one tick full. For a TickedSpectrum with N ticks, there will be
        N + tick_window_width - 1 spectra yielded.

        Parameters
        ----------
        tick_window_width : int
            width of the window, in ticks. Default: 1.
        mode : int
            sets the mode of the rolling window. default: 0.

        Returns
        -------
        Iterator[Counts]
        """
        if mode == 0:
            for i in range(self.ticks.shape[0] - tick_window_width + 1):
                yield Counts(self.ticks[i:i+tick_window_width].sum(axis=0), name=self.name,
                             error=np.sqrt(np.square(self.errors[i:i+tick_window_width]).sum(axis=0)),
                             comment=self.comment, live_time=self.live_times[i:i+tick_window_width].sum(),
                             real_time=self.real_times[i:i+tick_window_width].sum(),
                             start_time=self.tick_times[i])
        elif mode == 1:
            for i in range(-int(tick_window_width / 2), self.ticks.shape[0] - int(tick_window_width / 2)):
                yield Counts(self.ticks[max(i, 0):i+tick_window_width].sum(axis=0), name=self.name,
                             error=np.sqrt(np.square(self.errors[max(i, 0):i+tick_window_width]).sum(axis=0)),
                             comment=self.comment, live_time=self.live_times[max(i, 0):i+tick_window_width].sum(),
                             real_time=self.real_times[max(i, 0):i+tick_window_width].sum(),
                             start_time=self.tick_times[max(i, 0)])
        elif mode == 2:
            for i in range(-tick_window_width+1, self.ticks.shape[0]):
                yield Counts(self.ticks[max(i, 0):i+tick_window_width].sum(axis=0), name=self.name,
                             error=np.sqrt(np.square(self.errors[max(i, 0):i+tick_window_width]).sum(axis=0)),
                             comment=self.comment, live_time=self.live_times[max(i, 0):i+tick_window_width].sum(),
                             real_time=self.real_times[max(i, 0):i+tick_window_width].sum(),
                             start_time=self.tick_times[max(i, 0)])

    def subtract_background(self, bg_counts: Counts) -> TickedCounts:
        """
        Method which subtracts a background signal from each of the ticks, using simultaneous subtraction.

        Note: Since ticks are short (~0.2s) the noise in the counts is often of the order of the counts themselves, and
        hence the background subtracted counts are often negative.

        Parameters
        ----------
        bg_counts : Counts
            Counts instance containing information about the background count rate.

        Returns
        -------
        TickedCounts
        """
        return self.simultaneous_subtraction(bg_counts)

    def _simultaneous_op_counts(self, other: Counts, add: bool):
        """
        Not sure why you'd want to do this, but it makes logical sense, so here it is.

        Parameters
        ----------
        other : Counts

        Returns
        -------
        TickedCounts
        """
        scale = self.live_times / other.live_time
        return TickedCounts(ticks=self.ticks + (1. if add else -1.)*other.counts * scale,
                            tick_times=self.tick_times,
                            name=self.name,
                            live_times=self.live_times,
                            real_times=self.real_times,
                            msg_times=self.msg_times,
                            blank_rate=self.blank_rate,
                            errors=np.sqrt(self.errors ** 2 + other.error ** 2 * scale),
                            start_time=self.start_time)

    def _simultaneous_op_ticked_counts(self, other: TickedCounts, add: bool):
        """
        Simultaneous sum of two CountsArrays.

        Parameters
        ----------
        other : TickedCounts

        Returns
        -------
        TickedCounts
        """
        if self.n_ticks != other.n_ticks:
            self.log.warning(f'Simultaneous Sum: {self.name} has {self.n_ticks} ticks, but {other.name} has '
                             f'{other.n_ticks} ticks. The longer measurement will be shortened.')
        n_ticks = min(self.n_ticks, other.n_ticks)
        scale = self.live_times[:n_ticks] / other.live_times[:n_ticks]
        return TickedCounts(ticks=self.ticks[:n_ticks] + (1. if add else -1.)*other.ticks[:n_ticks] * scale,
                            tick_times=self.tick_times[:n_ticks],
                            name=self.name,
                            live_times=self.live_times[:n_ticks],
                            real_times=self.real_times[:n_ticks],
                            msg_times=None if self.msg_times is None else self.msg_times[:n_ticks],
                            blank_rate=None if self.blank_rate is None else self.blank_rate[:n_ticks],
                            errors=np.sqrt(self.errors[:n_ticks] ** 2 + other.errors[:n_ticks] ** 2 * scale),
                            start_time=self.start_time)

    def simultaneous_addition(self, other: Union[Counts, TickedCounts]):
        """
        Method which simultaneously adds a measurement to the TickedCounts.

        Parameters
        ----------
        other : Counts or TickedCounts
            Signal to be added.

        Returns
        -------
        TickedCounts
        """
        if isinstance(other, Counts):
            return self._simultaneous_op_counts(other, add=True)
        else:
            return self._simultaneous_op_ticked_counts(other, add=True)

    def simultaneous_subtraction(self, other: Union[Counts, TickedCounts]):
        """
        Method which simultaneously subtracts a signal from the TickedCounts. E.g. background subtraction.

        Parameters
        ----------
        other : Counts
            Signal to be subtracted.

        Returns
        -------
        TickedCounts
        """
        if isinstance(other, Counts):
            return self._simultaneous_op_counts(other, add=False)
        else:
            return self._simultaneous_op_ticked_counts(other, add=False)

    def consecutive_addition(self, other: TickedCounts) -> TickedCounts:
        """
        Method which consecutively sums two TickedCounts instances into a new TickedCounts instance.

        Parameters
        ----------
        other : TickedCounts
            TickedCounts instance to sum onto the end of this TickedCounts instance.

        Returns
        -------
        TickedCounts
        """
        if isinstance(other, Counts):
            raise TypeError('Consecutive addition between TickedCounts and Counts instances makes no real sense...')
        return TickedCounts(ticks=np.concatenate((self.ticks, other.ticks)),
                            tick_times=np.concatenate((self.tick_times, other.tick_times)),
                            live_times=np.concatenate((self.live_times, other.live_times)),
                            real_times=np.concatenate((self.real_times, other.real_times)),
                            name=self.name,
                            msg_times=None if self.msg_times is None or other.msg_times is None else np.concatenate(
                                (self.msg_times, other.msg_times)
                            ),
                            blank_rate=None if self.blank_rate is None or other.blank_rate is None else np.concatenate(
                                (self.blank_rate, other.blank_rate)
                            ),
                            errors=None if self._errors is None and other._errors is None else np.concatenate(
                                (self.errors, other.errors)
                            ),
                            start_time=self.start_time,
                            comment=self.comment)


class TickedCountsArray(TickedArrayBase):
    """
    Array of TickedCounts instances.

    Attributes
    ----------
    elements : OrderedDict[str, Measurement]
        OrderedDict, mapping measurements by their names.

    Methods
    -------
    tick_times() -> Array[datetime]
        Property which returns a list of datetime instances labelling the time at which each tick occurred.
    n_ticks() -> int
        Property which returns the number of ticks in the first member of the array.
    n_ticks_all() -> List[int]
        Property which, like n_ticks, returns the number of ticks, but returns a list of values for each member, in
        case you suspect they may not all contain equal numbers.
    names() -> List[str]
        Property which returns a list of the measurement names which are keys to the mapping.
    number_of_members() -> int
        Property which returns the number of members of the array.
    start_times() -> List[datetime]
        Property which returns a list of datetime instances which give the times at which each member of the array
        began to be collected.
    contains_all(names: List[str]) -> bool
        Method which returns True if all the strings in the argument match the names of elements in the array.
    names_match(other: ArrayBase) -> bool
        Method which returns True if all the names of the other array are present in this array, and vice-versa.
    consecutive_addition(other: TickedCountsArray) -> TickedCountsArray
        Method which returns the consecutive sum of the CountsArray with another Array.
    simultaneous_addition(other: TickedCountsArray) -> TickedCountsArray
        Method which returns the simultaneous sum of the CountsArray with another Array.
    consecutive_subtraction(other: CountsArray) -> TickedCountsArray
        Method which returns the consecutive subtraction of another CountsArray from the Array.
    simultaneous_subtraction(other: CountsArray) -> TickedCountsArray:
        Method which returns the simultaneous subtraction of another CountsArray from the Array.
    subtract_backgrounds(self, backgrounds: CountsArray) -> TickedCountsArray:
        Method which subtracts the background from each member of the array, using the array of background counts
        supplied as the argument.
    simultaneous_sum() -> TickedCounts
        Simultaneously sum all members of the array.
    consecutive_sum() -> CountsArray
        Sum all ticks into a CountsArray.
    resample(factor: float) -> TickedCountsArray
        Method which returns a new array with each member resampled after scaling by the factor parameter.
    masked_array(names: List[str]) -> TickedCountsArray
        Method which returns a sub-array of this TickedCountsArray. The method returns a new TickedCountsArray
        containing only TickedCounts instances with the names given.
    get_single_tick(tick_num: int) -> CountsArray
        Method which returns a single tick of Counts from each TickedCounts instance in the array, returning a
        CountsArray.
    sum_ticks(start: datetime, end: datetime) -> CountsArray
        Method which sums the ticks in the TickedCountsArray into a CountsArray.
    simple_windowed_counts_arrays(tick_window_width: int = 1, mode: int = 0) -> Iterator[CountsArray]
        Generator for rolling fixed-length windows. Generator yields CountsArrays.
    plot()
        Method which plots each TickedCounts as a function of time, returning the figure and axes.
    """
    def __init__(self, t_counts: List[TickedCounts]):
        super().__init__(t_counts)

    def __getitem__(self, item: str) -> TickedCounts:
        return super().__getitem__(item)

    def get_single_tick(self, tick_num: int) -> CountsArray:
        """
        Method which returns a single tick of Counts from each TickedCounts instance in the array, returning a
        CountsArray.

        Parameters
        ----------
        tick_num : int
            Index of the tick to be returned.

        Returns
        -------
        CountsArray
        """
        return CountsArray([self[det].get_single_tick(tick_num) for det in self.names])

    def sum_ticks(self, start: Union[datetime, None] = None, end: Union[datetime, None] = None) -> CountsArray:
        """
        Method which sums the ticks in the TickedCountsArray into a CountsArray.

        Parameters
        ----------
        start : datetime
        end : datetime

        Returns
        -------
        CountsArray
        """
        return CountsArray([tc.sum_ticks(start, end) for tc in self])

    def simple_windowed_counts_arrays(self, tick_window_width: int = 1, mode: int = 0) -> Iterator[CountsArray]:
        """
        Generator for rolling fixed-length windows. Generator yields CountsArrays.

        Parameters
        ----------
        tick_window_width : int
            Length of the rolling window, in numbers of ticks.
        mode : int
            Mode (0, 1, or 2)  of the rolling windows.
                0 - windows are always full. For a TickedSpectrum with N ticks, there will be N - tick_window_width + 1
                    spectra yielded.
                    |====================================================| Ticks
                    |====|----------------------------------------->|====| Windows
                1 - windows are always at least half full. For a TickedSpectrum with N ticks, there will be N spectra
                    yielded.
                      |====================================================| Ticks
                    |====|--------------------------------------------->|====| Windows
                2 - windows are always at least one tick full. For a TickedSpectrum with N ticks, there will be
                    N + tick_window_width - 1 spectra yielded.
                         |====================================================| Ticks
                    |====|--------------------------------------------------->|====| Windows

        Yields
        ------
        CountsArray
        """
        generators = [self[d].simple_windowed_counts(tick_window_width, mode) for d in self.names]
        while 1:
            try:
                spectra = [next(gen) for gen in generators]
            except StopIteration:
                return
            yield CountsArray(spectra)

    def masked_array(self, names: List[str]) -> TickedCountsArray:
        """
        Method which returns a sub-array of this TickedCountsArray. The method returns a new TickedCountsArray
        containing only TickedCounts instances with the names given.

        Parameters
        ----------
        names : List[str]
            List of detector names.

        Returns
        -------
        TickedCountsArray
        """
        return self.__class__([self[name] for name in names])

    def plot(self, **kwargs) -> Tuple[plt.Figure, plt.Axes]:
        """
        Method which plots each TickedCounts as a function of time, returning the figure and axes.

        Parameters
        ----------
        xmin : Union[float, int, None]
            Lower bound of the X axis.
        xmax : Union[float, int, None]
            Upper bound of the X axis.
        ymin : Union[float, int, None]
            Lower bound of the Y axis.
        ymax : Union[float, int, None]
            Upper bound of the Y axis.
        axes : Union[plt.Axes, None]
            Optional axes on which to plot the data. If None, the method generates a new figure and axes.
        figsize : Tuple[float, float]
            Tuple containing the dimensions of the figure if the figure is generated in the method.
        xlabel : str
            Text label for the X axis.
        ylabel : str
            Text label for the Y axis.
        title : Union[str, None]
            Optional title for the axes.
        dpi : int
            Dots per inch value for the figure, if generated by the method.
        logy : bool
            False by default. Set to true to scale the Y-axis logarithmically.
        show : bool
            True by default. Set to False to not automatically show the figure.

        Returns
        -------
        Tuple[plt.Figure, plt.Axes]
        """
        return TickedCountsPlotter(ticked_counts=self.elements.values(), **kwargs).plot()

    def subtract_backgrounds(self, bg_array: CountsArray) -> TickedCountsArray:
        """
        Method which subtracts a background CountsArray.

        Parameters
        ----------
        bg_array : CountsArray

        Returns
        -------
        TickedCountsArray
        """
        return self.simultaneous_subtraction(bg_array)

    def simultaneous_addition(self, other: Union[CountsArray, TickedCountsArray]) -> TickedCountsArray:
        """
        Method which simultaneously adds a CountsArray or TickedCountsArray to this TickedCountsArray.
        Not sure why you'd want to though...

        Parameters
        ----------
        other : CountsArray

        Returns
        -------
        TickedCountsArray
        """
        assert self.names_match(other)
        return TickedCountsArray([self[det].simultaneous_addition(other[det]) for det in self.names])

    def simultaneous_subtraction(self, other: Union[CountsArray, TickedCountsArray]) -> TickedCountsArray:
        """
        Method which simultaneously subtracts a CountsArray from this TickedCountsArray. E.g. Background subtraction.

        Parameters
        ----------
        other : CountsArray

        Returns
        -------
        TickedCountsArray
        """
        assert self.names_match(other)
        return TickedCountsArray([self[det].simultaneous_subtraction(other[det]) for det in self.names])

    def consecutive_addition(self, other: TickedCountsArray) -> TickedCountsArray:
        """
        Method which consecutively adds a TickedCountsArray to this TickedCountsArray. Not sure why you'd want to
        though...

        Parameters
        ----------
        other : CountsArray

        Returns
        -------
        TickedCountsArray
        """
        return super(TickedCountsArray, self).consecutive_addition(other)

    def consecutive_sum(self) -> TickedCounts:
        """
        Consecutive sum of each member of the array.

        Returns
        -------
        TickedCounts
        """
        result: Union[TickedCounts, None] = None
        for ticked_counts in self:
            if result is None:
                result = ticked_counts
            else:
                result = result.consecutive_addition(ticked_counts)
        return result

    def simultaneous_sum(self) -> TickedCounts:
        """
        Simultaneous sum tick-wise of each member of the array.

        Returns
        -------
        TickedCounts
        """
        result: Union[TickedCounts, None] = None
        for ticked_counts in self:
            if result is None:
                result = ticked_counts
            else:
                result = result.simultaneous_addition(ticked_counts)
        return result

    def resample(self, factor: float) -> TickedCountsArray:
        """
        Method which returns a new array with each member resampled after scaling by the factor parameter.

        Parameters
        ----------
        factor : float
            Scale by which the measurements will be scaled before resampling.

        Returns
        -------
        TickedCountsArray
        """
        return super(TickedCountsArray, self).resample(factor)
