# -*- coding: utf-8 -*-
"""Raw abstract class - framework for specific devices."""

import numpy as np
from ciso8601 import parse_datetime
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import TypeVar, Any, ClassVar


@dataclass(slots=True, eq=False)
class _ActiData(ABC):
    ActiData = TypeVar("ActiData", bound="_ActiData")

    _data: np.ndarray
    _metadata: dict
    _fs: float
    _channel_names: list[str]

    def __len__(self):
        """Length of the data in samples."""
        return len(self.timestamp)

    @staticmethod
    @abstractmethod
    def load_file(path: str) -> "_ActiData":
        pass

    @abstractmethod
    def cut_by_samples(self: ActiData, start_sample: int, end_sample: int) -> ActiData:
        pass

    def cut_by_dates(self: ActiData, start_date: str, end_date: str | None) -> ActiData:
        """Create new object with the data cut by given dates.

        Args:
            start_date (str): ISO-formated date of outputa data beginning.
            end_date (str | None): ISO-formated date of output data end. If None, last sample of output data will be last sample of input data.

        Returns:
            _ActiData: Object containing the cutted data of the same type as input data.
        """
        start_ts = parse_datetime(start_date).timestamp()
        end_ts = parse_datetime(end_date).timestamp() if end_date is not None else None
        return self.cut_by_timestamp(start_ts, end_ts)

    @abstractmethod
    def cut_by_timestamp(self: ActiData, start_ts: float, end_ts: float | None) -> ActiData:
        pass

    def trim(self: ActiData, time_from_start: str | None, time_from_end: str | None) -> ActiData:
        """Removes points from the recording beginning and end by given times.

        Args:
            time_from_start (str | None): Time in HH:MM:SS.sss from the recording beginning. Microseconds can be ommited. If None, data is returned from first sample.
            time_from_end (str | None): Time in HH:MM:SS.sss from the recording end. Microseconds can be ommited. If None, data is returned to the end sample.

        Returns:
            _ActiData: Trimmed data container of the same type as input one.
        """
        def parse_input(time_string):
            out = 0
            if '.' in time_string:
                out += int(time_string.split('.')[-1])
            data = time_string.split('.')[0]
            numbers = [int(e) for e in data.split(':')]
            out += 3600 * numbers[0] + 60 * numbers[1] + numbers[2]
            return out

        if time_from_start is None:
            start_time = 0.
        else:
            start_time = parse_input(time_from_start)
        
        if time_from_end is None:
            end_time = 0.
        else:
            end_time = parse_input(time_from_end)

        return self.cut_by_samples(int(start_time * self.fs), self.data.shape[1] - int(end_time * self.fs))

    @abstractmethod
    def export(self, path: str):
        pass

    @property
    def fs(self) -> float:
        """Sampling frequency of the data (not the same as recording frequency for epoched data)."""
        return self._fs

    @property
    def metadata(self) -> dict:
        """Metadata associated with the raw recording and the device."""
        return self._metadata

    @property
    def data(self) -> np.ndarray:
        """Actigraphic data in format (n_channels, n_samples)."""
        return self._data

    @property
    @abstractmethod
    def timestamp(self) -> np.ndarray:
        """Unix timestamp of the data, relative to the recording beginning."""
        pass

    @property
    def channel_names(self) -> list[str]:
        """List of names for the channels stored in the data field."""
        return self._channel_names
    
    @property
    @abstractmethod
    def first_sample_timestamp(self) -> float:
        """Unix timestamp of first sample."""
        pass

    @property
    def last_sample_timestamp(self) -> float:
        """Unix timestamp of last sample."""
        return self.timestamp[-1] - self.timestamp[0] + self.first_sample_timestamp
    
    @property
    @abstractmethod
    def id(self) -> str:
        """ID of the recording set during device configuration."""
        pass


@dataclass(slots=True, eq=False)
class _Raw(_ActiData):

    _stationary_variance: ClassVar[float]
    _dynamic_range: ClassVar[float]

    @property
    @abstractmethod
    def to_score(self) -> np.ndarray:
        """Data to be scored by scoring algorithms."""
        pass

    @property
    @abstractmethod
    def vlen(self) -> np.ndarray:
        """Mean vector magnitude (in g) calculated as |sqrt(x**2 + y**2 + z**2) - 1|."""
        pass

    @property
    @abstractmethod
    def x(self) -> np.ndarray:
        """Acceleration (in g) along x axis."""
        pass

    @property
    @abstractmethod
    def y(self) -> np.ndarray:
        """Acceleration (in g) along y axis."""
        pass

    @property
    @abstractmethod
    def z(self) -> np.ndarray:
        """Acceleration (in g) along z axis."""
        pass

    @staticmethod
    @abstractmethod
    def _convert_timestamp(timestamp: float, metadata: dict, to_unix: bool = False) -> float:
        """Timestamp channel is relative. Use this to get absolute unix timestamp.

        Args:
            timestamp (float): To be converted.
            metadata (dict): Metadata of the object containing timestamp to be converted.
            to_unix (bool): If True, relative timestamp will be converted to unix, if False other way around. Defaults to False.

        Returns:
            float: Converted timestamp.
        """
        pass

    @property
    def first_sample_timestamp(self) -> float:
        """Unix timestamp of first sample."""
        return self._convert_timestamp(self.timestamp[0], self._metadata, True)
    
    @property
    @abstractmethod
    def stationary_variance(self) -> float:
        """Variance of actigraph laying still (transducer noise)."""
        pass

    @property
    @abstractmethod
    def dynamic_range(self) -> float:
        """Dynamic range (in g) of the digital converter."""
        pass


@dataclass(slots=True, eq=False)
class _Epoched(_ActiData):
    _epoching_method_metadata: dict

    @property
    @abstractmethod
    def to_score(self) -> np.ndarray:
        """Data to be scored by scoring algorithms."""
        pass

    @property
    def epoching_method_metadata(self) -> dict:
        """Metadata asssociated with the epoching method and its parameters."""
        return self._epoching_method_metadata


@dataclass(slots=True, eq=False)
class _ActiEEG(ABC):
    ActiEEG = TypeVar("ActiEEG", bound="_ActiEEG")

    _acti_data: _ActiData
    _eeg_data: Any

    @abstractmethod
    def cut_by_timestamp(self: ActiEEG, start_ts: float, end_ts: float | None) -> ActiEEG:
        pass

    def cut_by_dates(self: ActiEEG, start_date: str, end_date: str | None) -> ActiEEG:
        """Create new object with the data cut by given dates.

        Args:
            start_date (str): ISO-formated date of outputa data beginning.
            end_date (str): ISO-formated date of output data end.

        Returns:
            _ActiEEG: Object containing the cutted data of the same type as input data.
        """
        start_ts = parse_datetime(start_date).timestamp()
        end_ts = parse_datetime(end_date).timestamp() if end_date is not None else None
        return self.cut_by_timestamp(start_ts, end_ts)

    @property
    def acti_data(self) -> _ActiData:
        return self._acti_data
    
    @property
    def eeg_data(self) -> Any:
        return self._eeg_data
    
    @property
    def first_sample_timestamp(self) -> float:
        """Unix timestamp of first sample."""
        return self._acti_data.first_sample_timestamp
    
    @property
    def last_sample_timestamp(self) -> float:
        """Unix timestamp of last sample."""
        return self._acti_data.last_sample_timestamp
