
import math

import cairo
import gtk
import pango
import pangocairo

class Page:
    def __init__(self, attrs, notes):
        self.attrs = attrs
        self.notes = notes

    def draw(self, ctx, rect):
        # fill background
        ctx.rectangle(rect.x, rect.y,  rect.width, rect.height)
        ctx.set_source_rgb(*(self.attrs['background_color']))
        ctx.fill()

    def draw_page_number(self, ctx, rect, index):
        ctx.select_font_face("Sans", cairo.FONT_SLANT_NORMAL,
            cairo.FONT_WEIGHT_NORMAL)
        ctx.set_font_size(24)
        ctx.set_source_rgb(0, 0, 0)
        text = '%d' % (index + 1)
        ext = ctx.text_extents(text)
        ctx.move_to(rect.width - ext[4] - 20, rect.height - 20)
        ctx.show_text(text)

class Line:
    def __init__(self, bullet=False, weight=cairo.FONT_WEIGHT_NORMAL, size=32,
            text=None):
        self.bullet = bullet
        self.weight = weight
        self.text = text
        self.size = size

    @classmethod
    def parse(cls, line):
        if line.startswith('*'):
            return cls(bullet=True, text=line[1:].strip())
        elif line.startswith('='):
            return cls(weight=cairo.FONT_WEIGHT_BOLD, size=64,
                text=line[1:].strip())
        else:
            return cls(text=line)

    def description(self, scale):
        desc = pango.FontDescription()
        desc.set_family('Sans')
        desc.set_size(scale * self.size * pango.SCALE)

        if self.weight == cairo.FONT_WEIGHT_BOLD:
            desc.set_weight(pango.WEIGHT_BOLD)

        return desc

class TextPage(Page):
    space = 20
    padding = 20

    def __init__(self, attrs, notes, lines):
        Page.__init__(self, attrs, notes)
        self.lines = lines

    def draw(self, ctx, rect):
        Page.draw(self, ctx, rect)

        cc = pangocairo.CairoContext(ctx)
        layout = cc.create_layout()
        layout.set_width((rect.width - 2 * self.padding) * pango.SCALE)
        layout.set_alignment(pango.ALIGN_CENTER)
        cc.set_source_rgb(*(self.attrs['text_color']))

        def line_extents(line):
            desc = line.description(self.attrs['text_size_scale'])
            desc.set_family(self.attrs['text_face'])
            layout.set_font_description(desc)
            layout.set_text(line.text)
            drawn, logical = layout.get_extents()
            return logical[3] / pango.SCALE, self.space

        heights, spaces = zip(*[line_extents(line) for line in self.lines])
        total_height = sum(heights) + sum(spaces[:-1])
        y = rect.height / 2.0 - total_height / 2.0

        for line, height, space in zip(self.lines, heights, spaces):
            cc.move_to(self.padding, y)
            desc = line.description(self.attrs['text_size_scale'])
            desc.set_family(self.attrs['text_face'])
            layout.set_font_description(desc)
            layout.set_text(line.text)
            cc.show_layout(layout)
            y += height + space

class ImagePage(Page):
    def __init__(self, attrs, notes):
        Page.__init__(self, attrs, notes)
        self.width, self.height, self.pattern = self._load_image(attrs['path'])

    def _load_image(self, path):
        window = gtk.Window()
        window.realize()
        ctx = window.window.cairo_create()

        loader = gtk.gdk.PixbufLoader()
        fh = file(path)

        while True:
            data = fh.read(10240)

            if data == '':
                break

            if not loader.write(data):
                print 'error loading image %s' % attrs['path']
                break

        loader.close()
        pixbuf = loader.get_pixbuf()

        ctx.set_source_pixbuf(pixbuf, 0, 0)
        return pixbuf.get_width(), pixbuf.get_height(), ctx.get_source()

    def draw(self, ctx, rect):
        Page.draw(self, ctx, rect)
        matrix = cairo.Matrix()

        if self.attrs.get('scale') == 'fill':
            matrix.scale(
                float(self.width) / float(rect.width),
                float(self.height) / float(rect.height))

            ctx.rectangle(0, 0, rect.width, rect.height)
        else:
            print 'path:',  self.attrs['path']
            print 'image:', self.width, self.height
            print 'target:', rect.width, rect.height

            if rect.width > rect.height:
                if self.width > self.height:
                    factor = float(self.width) / rect.width
                else:
                    factor = float(self.height) / rect.height
            else:
                if self.width > self.height:
                    factor = float(self.height) / rect.height
                else:
                    factor = float(self.width) / rect.width

            x = (rect.width - self.width / factor) * factor / 2.0
            y = (rect.height - self.height / factor) * factor / 2.0

            print 'scale:', factor
            print 'translate:', (x, y)
            print

            matrix.translate(-x, -y)
            matrix.scale(factor, factor)
            ctx.rectangle(0, 0, rect.width, rect.height)

        self.pattern.set_matrix(matrix)
        ctx.set_source(self.pattern)
        ctx.fill()

class View(gtk.DrawingArea):
    def __init__(self, pages):
        gtk.DrawingArea.__init__(self)

        self.pages = pages
        self.page_index = 0

        self.connect('expose-event', self.expose_cb)

    def expose_cb(self, widget, event):
        ctx = widget.window.cairo_create()
        page = self.pages[self.page_index]

        # clip to event rectangle
        ctx.rectangle(
            event.area.x, event.area.y, event.area.width, event.area.height)
        ctx.clip()

        # draw page
        rect = self.get_allocation()
        page.draw(ctx, rect)

        # draw page number
        #page.draw_page_number(ctx, rect, self.page_index)

        return True

    def update(self):
        self.window.invalidate_rect(self.get_allocation(), False)

    def next(self):
        if self.page_index < (len(self.pages) - 1):
            self.page_index += 1
            self.update()

    def prev(self):
        if self.page_index > 0:
            self.page_index -= 1
            self.update()

    def forward(self):
        self.page_index = min(len(self.pages) - 1, self.page_index + 10)
        self.update()

    def backward(self):
        self.page_index = max(0, self.page_index - 10)
        self.update()

    def start(self):
        self.page_index = 0
        self.update()

    def end(self):
        self.page_index = len(self.pages) - 1
        self.update()

def key_release_cb(window, event, f):
    bindings = {
        'space': f.next,
        'Next': f.next,
        'Down': f.next,
        'Page_Down': f.next,
        'BackSpace': f.prev,
        'Prior': f.prev,
        'Up': f.prev,
        'Page_Up': f.prev,
        'Home': f.start,
        'End': f.end,
        'bracketleft': f.backward,
        'bracketright': f.forward,
        'Escape': gtk.main_quit,
        'q': gtk.main_quit,
        }
    key = gtk.gdk.keyval_name(event.keyval)

    if key in bindings:
        bindings[key]()

class Presentation:
    def __init__(self, pages):
        view = View(pages)

        window = gtk.Window()
        window.connect('key-release-event', key_release_cb, view)
        window.add(view)

        window.fullscreen()
        window.show_all()
        self.window = window

        # Hide the mouse cursor.
        pixmap = gtk.gdk.Pixmap(None, 1, 1, 1)
        color = gtk.gdk.Color()
        cursor = gtk.gdk.Cursor(pixmap, pixmap, color, color, 0, 0)
        window.window.set_cursor(cursor)

def parse_attr(name, value):
    if name.endswith('color'):
        value = [float(s) for s in value.split(',')]
        assert len(value) == 3
        return value
    else:
        try:
            return float(value)
        except ValueError:
            pass

    return value

default_attrs = {
    'background_color': (1, 1, 1),
    'text_color': (0, 0, 0),
    'text_alignment': 'center',
    'text_height_multiplier': 1.2,
    'text_face': 'Sans',
    'text_size': 32,
    'text_size_scale': 1,
    }

def parse_slide(s):
    s = s.strip()
    attrs = dict(default_attrs)
    lines = s.splitlines()
    type = 'text'
    text = []
    notes = []

    if lines[0].startswith('!'):
        type = lines[0][1:]
        lines = lines[1:]

    for line in lines:
        if line.startswith('@'):
            if ':' in line:
                k, v = line[1:].split(': ', 1)
                attrs[k] = parse_attr(k, v)
        elif line.startswith('>'):
            notes.append(line[1:].strip())
        else:
            text.append(line)

    if type == 'image':
        if 'path' in attrs:
            return ImagePage(attrs, notes)
    elif type == 'text':
        return TextPage(attrs, notes, map(Line.parse, text))

    return None

def parse(s):
    return filter(bool, map(parse_slide, s.split('\n---\n')))

if __name__ == '__main__':
    import sys

    pages = parse(file(sys.argv[1]).read())
    p = Presentation(pages)
    p.window.connect('destroy', gtk.main_quit)
    gtk.main()

