from pioneer.common import platform
from pioneer.das.api.interpolators import from_float_index
from pioneer.das.api.datasources import AbstractDatasource
from pioneer.das.api.sources import FileSource

from collections import deque
from typing import Optional, List, Callable, Dict, Tuple, Any, Iterable, Union

import numbers
import numpy as np

class DatasourceWrapper(AbstractDatasource):

    def __init__(self, sensor:'Sensor', ds_type:str, ds:FileSource, sample_interp:Tuple[Any, Any], cache_size:int=100):
        super(DatasourceWrapper, self).__init__(sensor, ds_type)
        self.sensor = sensor
        self.ds = ds
        self.ds_type = ds_type
        self._cache = {}
        self._cache_keys = deque(maxlen = cache_size)
        self.sample_factory, self.interpolator = sample_interp
        self.use_cache = cache_size > 0
        
    def invalidate_caches(self):
        self._cache = {}
        self._cache_keys = deque(maxlen = self._cache_keys.maxlen)
        self._interpolated = {}
        self._interpolated_keys = deque(maxlen = self._cache_keys.maxlen)

    @property
    def interpolator(self):
        return self._interpolator

    @interpolator.setter
    def interpolator(self, interpolator):
        self._interpolator = interpolator
        self._interpolated = {}
        self._interpolated_keys = deque(maxlen = self._cache_keys.maxlen)

    @property
    def timestamps(self):
        return self.ds.timestamps

    @property
    def time_of_issues(self):
        return self.ds.time_of_issues

    def _add_interpolated_from_float_index(self, float_index:float):
        if self.interpolator is None:
            raise RuntimeError('Index is float {} : No inperpolator found'.format(float_index))
        raw = self.interpolator(self, float_index)
        self._interpolated_keys.append(float_index)
        v =  self._interpolated[float_index] = self.sample_factory(float_index, self.sensor[self.ds_type], raw, self.to_timestamp(float_index))
        return v

    def to_timestamp(self, float_index:float) -> int:
        i, t = from_float_index(float_index)
        if i == 0: i += 1
        ts_from = self.timestamps[i - 1]
        return int(ts_from + t * (self.timestamps[i] - ts_from))

    def _add_interpolated_from_timestamp(self, timestamp:int) -> float:
        float_index = self.to_float_index(timestamp)
        self._add_interpolated_from_float_index(float_index)
        return float_index

    def get_at_timestamp(self, timestamp:Union[int, Iterable], interpolator:Callable=None) -> Union['Sample', List['Sample']]:
        try: #is timestamp iterable?
            return [self.get_at_timestamp(t, interpolator) for t in timestamp]
        except:
            float_index = self.to_float_index(timestamp)
            if interpolator is None:
                return self[float_index]
            return self.sample_factory(float_index, self.sensor[self.ds_type], interpolator(self, float_index), timestamp)

    def __getitem__(self, key:Any) -> Union['Sample', List['Sample']]:

        def try_get_from_cache_or_make_room(i, cache, cache_keys):
            if not self.use_cache:
                return None
            n = len(cache_keys)
            if i not in cache_keys:
                if n >= cache_keys.maxlen:
                    k = cache_keys.popleft()
                    s = cache.pop(k)
                    del s
                return None
            if n > 1:
                cache_keys.append(cache_keys.popleft()) #make latest access the most recent
            return cache[i]

        def instantiate(i):

            i = self.to_positive_index(i)
               
            if i >= len(self):
                raise IndexError("Out of bound")
            
            v = try_get_from_cache_or_make_room(i, self._cache, self._cache_keys)

            if v is None:
                if self.use_cache:
                    self._cache_keys.append(i)
                    v = self._cache[i] = self.sample_factory(i, self.sensor[self.ds_type])
                else:
                    v = self.sample_factory(i, self.sensor[self.ds_type])
                if self.sensor.platform.is_live():
                    # When the platform is live the fifo could fill up before data is accessed, 
                    # so we must avoid relying on it to get Sample.raw, Sample.timestamp
                    v.virtual_raw = v.raw
                    v.virtual_ts = v.timestamp

            return v

            self._cache_keys.append(self._cache_keys.popleft()) #make latest access the most recent
            return self._cache[i]

        try: #is key iterable?
            return [self[index] for index in key]
        except:
            if isinstance(key, slice):
                return self[platform.slice_to_range(key, len(self))]
            elif isinstance(key, numbers.Integral):
                return instantiate(key)
            elif isinstance(key, numbers.Real) and float(key).is_integer():
                return instantiate(int(key))
            elif isinstance(key, numbers.Real) and not float(key).is_integer():
                v = try_get_from_cache_or_make_room(key, self._interpolated, self._interpolated_keys)
                if v is None:
                    v = self._add_interpolated_from_float_index(key)

                return v

        raise KeyError('Invalid key: {}'.format(key))

    @property
    def label(self):
        return "{}_{}".format(self.sensor.name, self.ds_type)

    def get_timestamp_slice(self, timestamp:int, half_window_size:Union[int,tuple]):
        """Get an index slice that represent a time interval.

        The time interval is parametrized by a timestamp and a half window
        size.

        Args:
            timestamp: The center of the time interval
            half_window_size: Half of the time interval. If tuple provided -> (start, end).

        Returns:
            slice -- The indices slice representing all the elements that fit
            in the time interval
        """
        try: #is 2-tuple?
            left = int(timestamp) + int(half_window_size[0])
            right = int(timestamp) + int(half_window_size[1])
        except:
            left = int(timestamp) - int(half_window_size)
            right = int(timestamp) + int(half_window_size)
        left_index = int(np.searchsorted(self.timestamps, left, side='left'))
        right_index = int(np.searchsorted(self.timestamps, right, side='right'))
        return slice(left_index, right_index, 1)

