from collections import Iterable
import tempfile
import subprocess as sp
from StringIO import StringIO
try:
    from IPython.core.display import Image
    ipython = True
except ImportError:
    ipython = False

from sebastian.lilypond import write_lilypond


class UnificationError(Exception):
    pass


class Point(dict):
    
    def unify(self, other):
        new = self.copy()
        for key, value in other.items():
            if key in new:
                if new[key] != value:
                    raise UnificationError(key)
            else:
                new[key] = value
        return Point(new)
    
    def tuple(self, *attributes):
        return tuple(self.get(attribute) for attribute in attributes)
    
    __mod__ = unify


class SeqBase:
    
    def __init__(self, *elements):
        if len(elements) == 1:
            if isinstance(elements[0], Point):
                elements = [elements[0]]
            elif isinstance(elements[0], Iterable):
                elements = list(elements[0])
        else:
            elements = list(elements)
        self._elements = []
        
        for point in elements:
            self.append(point)
    
    def __getitem__(self, item):
        return self._elements[item]
    
    def __len__(self):
        return len(self._elements)
    
    def __iter__(self):
        return iter(self._elements)

    def __eq__(self, other):
        return isinstance(other, self.__class__) and self._elements == other._elements
    
    def __ne__(self, other):
        return not (isinstance(other, self.__class__) and self._elements == other._elements)
    
    def map_points(self, func):
        return self.__class__([func(Point(point)) for point in self._elements])
    
    def transform(self, func):
        """
        applies function to a sequence to produce a new sequence
        """
        return func(self)
    
    __or__ = transform

    def _repr_png_(self):
        """
        Return a PNG representation of this sequence for IPython Notebook.
        """
        if not ipython:
            return None

        from sebastian.core.transforms import lilypond
        seq = HSeq(self) | lilypond()
        f = tempfile.NamedTemporaryFile(suffix=".preview.png")
        basename = f.name[:-12] # everything except ".preview.png"

        p = sp.Popen(["lilypond", "--png", "-dno-print-pages", 
            "-dpreview", "-o"+basename, "-"], stdin=sp.PIPE)
        p.communicate(write_lilypond.output(seq))
        if p.returncode != 0:
            # there was an error
            return None

        return f.read()


def OSeq(offset_attr, duration_attr):
    
    class _OSeq(SeqBase):
        
        def last_point(self):
            if len(self._elements) == 0:
                return Point({offset_attr: 0, duration_attr: 0})
            else:
                return sorted(self._elements, key=lambda x: x[offset_attr])[-1]
        
        def next_offset(self):
            point = self.last_point()
            return point[offset_attr] + point.get(duration_attr, 0)
        
        def append(self, point):
            """
            appends a copy of the given point to this sequence, calculating
            the next offset to use for it if it doesn't have one
            """
            point = Point(point)
            if offset_attr not in point:
                point[offset_attr] = self.next_offset()
            self._elements.append(point)
        
        def concatenate(self, next_seq):
            """
            concatenates two sequences to produce a new sequence
            """
            offset = self.next_offset()
            
            new_seq = _OSeq(self._elements)
            for point in next_seq._elements:
                new_point = Point(point)
                new_point[offset_attr] = new_point[offset_attr] + offset
                new_seq._elements.append(new_point)
            return new_seq
        
        def repeat(self, count):
            """
            repeat sequence given number of times to produce a new sequence
            """
            x = _OSeq()
            for i in range(count):
                x = x.concatenate(self)
            return x
        
        def merge(self, parallel_seq):
            """
            combine the points in two sequences, putting them in offset order
            """
            return _OSeq(sorted(self._elements + parallel_seq._elements, key=lambda x: x.get(offset_attr, 0)))
        
        __add__ = concatenate
        __mul__ = repeat
        __floordiv__ = merge
    
    return _OSeq


class HSeq(SeqBase):
    """
    a horizontal sequence where each element follows the previous
    """
    
    def append(self, point):
        """
        appends a copy of the given point to this sequence
        """
        point = Point(point)
        self._elements.append(point)
    
    def concatenate(self, next_seq):
        """
        concatenates two sequences to produce a new sequence
        """
        return HSeq(self._elements + next_seq._elements)
    
    def repeat(self, count):
        """
        repeat sequence given number of times to produce a new sequence
        """
        x = HSeq()
        for i in range(count):
            x = x.concatenate(self)
        return x
    
    __add__ = concatenate
    __mul__ = repeat


class VSeq(SeqBase):
    """
    a vertical sequence where each element is coincident with the others
    """
    
    def append(self, point):
        """
        appends a copy of the given point to this sequence
        """
        point = Point(point)
        self._elements.append(point)
    
    def merge(self, parallel_seq):
        """
        combine the points in two sequences
        """
        return VSeq(self._elements + parallel_seq._elements)
    
    __floordiv__ = merge

@@ -2594,16 +2594,710 @@
 read()%0A%0A
+    def _repr_svg_(self):%0A        %22%22%22%0A        Return a SVG representation of this sequence for IPython Notebook.%0A        %22%22%22%0A        if not ipython:%0A            return None%0A%0A        from sebastian.core.transforms import lilypond%0A        seq = HSeq(self) %7C lilypond()%0A        f = tempfile.NamedTemporaryFile(suffix=%22.preview.svg%22)%0A        basename = f.name%5B:-12%5D # everything except %22.preview.svg%22%0A%0A        p = sp.Popen(%5B%22lilypond%22, %22-dbackend=svg%22, %22-dno-print-pages%22, %0A            %22-dpreview%22, %22-o%22+basename, %22-%22%5D, stdin=sp.PIPE)%0A        p.communicate(write_lilypond.output(seq))%0A        if p.returncode != 0:%0A            # there was an error%0A            return None%0A%0A        return f.read()%0A%0A
 %0Adef OSe

Output in SVG for iPython Notebook