"""A centralized data management structure that allows multiple named FIFO streams.

SensorGraph is organized around data streams, which are named FIFOs that hold
readings.  For the purposes of embedding SensorGraph into a low resource setting
you can limit the number of readings stored in any given FIFO as needed to put
a hard cap on storage requirements.
"""

import copy
from .engine import InMemoryStorageEngine
from .stream import DataStreamSelector, DataStream
from .walker import VirtualStreamWalker, CounterStreamWalker, BufferedStreamWalker
from .exceptions import StreamEmptyError, StorageFullError
from iotile.sg.model import DeviceModel
from iotile.core.exceptions import ArgumentError


class SensorLog(object):
    """A storage engine holding multiple named FIFOs.

    Args:
        engine (StorageEngine): The engine used for storing
            persistent data generated by this sensor graph.
            This can either be a simple, in memory data store, or
            a more complicated persistent storage setup depending on
            needs.  If not specified, a temporary in memory engine
            is used.

        model (DeviceModel): An optional device model specifying the
            constraints of the device that we are emulating.  If not
            specified, no specific constraints are imposed on the sensor
            log.
    """

    def __init__(self, engine=None, model=None):
        self._monitors = {}
        self._last_values = {}
        self._virtual_walkers = []
        self._queue_walkers = []

        if model is None:
            model = DeviceModel()

        if engine is None:
            engine = InMemoryStorageEngine(model=model)

        self._engine = engine
        self._model = model

    def watch(self, selector, callback):
        """Call a function whenever a stream changes.

        Args:
            selector (DataStreamSelector): The selector to watch
            callback (callable): The function to call when a new
                reading is pushed.  Callback is called as:
                callback(stream, value)
        """

        if selector not in self._monitors:
            self._monitors[selector] = set()

        self._monitors[selector].add(callback)

    def create_walker(self, selector):
        """Create a stream walker based on the given selector.
`
        This function returns a StreamWalker subclass that will
        remain up to date and allow iterating over and popping readings
        from the stream(s) specified by the selector.

        When the stream walker is done, it should be passed to
        destroy_walker so that it is removed from internal lists that
        are used to always keep it in sync.

        Args:
            selector (DataStreamSelector): The selector describing the
                streams that we want to iterate over.
        """

        if selector.buffered:
            walker = BufferedStreamWalker(selector, self._engine)
            self._queue_walkers.append(walker)
            return walker

        if selector.match_type == DataStream.CounterType:
            walker = CounterStreamWalker(selector)
        else:
            walker = VirtualStreamWalker(selector)

        self._virtual_walkers.append(walker)

        return walker

    def destroy_walker(self, walker):
        """Destroy a previously created stream walker.

        Args:
            walker (StreamWalker): The walker to remove from internal updating
                lists.
        """

        if walker.buffered:
            self._queue_walkers.remove(walker)
        else:
            self._virtual_walkers.remove(walker)

    def clear(self):
        """Clear all data from this sensor_log.

        All readings in all walkers are skipped and buffered data is
        destroyed.
        """

        for walker in self._virtual_walkers:
            walker.skip_all()

        self._engine.clear()

        for walker in self._queue_walkers:
            walker.skip_all()

    def push(self, stream, reading):
        """Push a reading into a stream, updating any associated stream walkers.

        Args:
            stream (DataStream): the stream to push the reading into
            reading (IOTileReading): the reading to push
        """

        # Make sure the stream is correct
        reading = copy.copy(reading)
        reading.stream = stream.encode()

        if stream.buffered:
            output_buffer = stream.output

            try:
                self._engine.push(reading)
            except StorageFullError:
                self._erase_buffer(stream.output)
                self._engine.push(reading)

            for walker in self._queue_walkers:
                # Only notify the walkers that are on this queue
                if walker.selector.output == output_buffer:
                    walker.notify_added(stream)

        # Activate any monitors we have for this stream
        for selector in self._monitors:
            if selector.matches(stream):
                for callback in self._monitors[selector]:
                    callback(stream, reading)

        # Virtual streams live only in their walkers, so update each walker
        # that contains this stream.
        for walker in self._virtual_walkers:
            if walker.matches(stream):
                walker.push(stream, reading)

        self._last_values[stream] = reading

    def _erase_buffer(self, output_buffer):
        """Erase readings in the specified buffer to make space."""

        erase_size = self._model.get(u'buffer_erase_size')

        buffer_type = u'storage'
        if output_buffer:
            buffer_type = 'streaming'

        old_readings = self._engine.popn(buffer_type, erase_size)

        # Now go through all of our walkers that could match and
        # update their availability counts and data buffer pointers
        for reading in old_readings:
            stream = DataStream.FromEncoded(reading.stream)

            for walker in self._queue_walkers:
                # Only notify the walkers that are on this queue
                if walker.selector.output == output_buffer:
                    walker.notify_rollover(stream)

    def inspect_last(self, stream):
        """Return the last value pushed into a stream.

        This function works even if the stream is virtual and no
        virtual walker has been created for it.  It is primarily
        useful to aid in debugging sensor graphs.

        Args:
            stream (DataStream): The stream to inspect

        Returns:
            IOTileReading: The data in the stream

        Raises:
            StreamEmptyError: if there has never been data written to
                the stream.
        """

        if stream in self._last_values:
            return self._last_values[stream]

        raise StreamEmptyError(u"inspect_last called on stream that has never been written to", stream=stream)
