"""
Contains the base class for series
Made using sequences in mind
"""

from __future__ import print_function, division

from sympy.core.expr import Expr
from sympy.core.singleton import S
from sympy.core.cache import cacheit
from sympy.core.compatibility import integer_types


class SeriesBase(Expr):
    """Base Class for series"""

    @property
    def interval(self):
        """The interval on which the series is defined"""
        raise NotImplementedError("(%s).interval" % self)

    @property
    def start(self):
        """The starting point of the series. This point is included"""
        raise NotImplementedError("(%s).start" % self)

    @property
    def stop(self):
        """The ending point of the series. This point is included"""
        raise NotImplementedError("(%s).stop" % self)

    @property
    def length(self):
        """Length of the series expansion"""
        raise NotImplementedError("(%s).length" % self)

    @property
    def variables(self):
        """Returns a tuple of variables that are bounded"""
        return ()

    @property
    def free_symbols(self):
        """
        This method returns the symbols in the object, excluding those
        that take on a specific value (i.e. the dummy symbols).
        """
        return (set(j for i in self.args for j in i.free_symbols)
                .difference(self.variables))

    @cacheit
    def term(self, pt):
        """Term at point pt of a series"""
        if pt < self.start or pt > self.stop:
            raise IndexError("Index %s out of bounds %s" % (pt, self.interval))
        return self._eval_term(pt)

    def _eval_term(self, pt):
        raise NotImplementedError("The _eval_term method should be added to"
                                  "%s to return series term so it is available"
                                  "when 'term' calls it."
                                  % self.func)

    def _ith_point(self, i):
        """
        Returns the i'th point of a series
        If start point is negative infinity, point is returned from the end.
        Assumes the first point to be indexed zero.

        Examples
        ========

        TODO
        """
        if self.start is S.NegativeInfinity:
            initial = self.stop
            step = -1
        else:
            initial = self.start
            step = 1

        return initial + i*step

    def __iter__(self):
        i = 0
        while i < self.length:
            pt = self._ith_point(i)
            yield self.term(pt)
            i += 1

    def __getitem__(self, index):
        if isinstance(index, integer_types):
            index = self._ith_point(index)
            return self.term(index)
        elif isinstance(index, slice):
            start, stop = index.start, index.stop
            if start is None:
                start = 0
            if stop is None:
                stop = self.length
            return [self.term(self._ith_point(i)) for i in
                    range(start, stop, index.step or 1)]
