#! /usr/bin/env python
#
#    automatic word-wrapping markup font interface for pygame
#    Copyright (C) 2002  Michael Urman
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#

import pygame

class _markup:
    def __init__(self, val):
        self.val = val

class _markup_bold(_markup):
    def __repr__(self): return ['</B>','<B>'][self.val]

class _markup_italic(_markup):
    def __repr__(self): return ['</I>','<I>'][self.val]

class _markup_underline(_markup):
    def __repr__(self): return ['</U>','<U>'][self.val]

def _markup_split(text):
    import re
    blocks = re.split('(</?[biuBIU]>)', text)
    blocks = [ s for s in blocks if s != '' ]
    for i in range(len(blocks)):
        if blocks[i] in ('<b>', '<B>'):
            blocks[i] = _markup_bold(1)
        elif blocks[i] in ('</b>', '</B>'):
            blocks[i] = _markup_bold(0)
        elif blocks[i] in ('<i>', '<I>'):
            blocks[i] = _markup_italic(1)
        elif blocks[i] in ('</i>', '</I>'):
            blocks[i] = _markup_italic(0)
        elif blocks[i] in ('<u>', '<U>'):
            blocks[i] = _markup_underline(1)
        elif blocks[i] in ('</u>', '</U>'):
            blocks[i] = _markup_underline(0)
        else:
            blocks[i] = re.sub('&lt;', '<', blocks[i])
            blocks[i] = re.sub('&amp;', '&', blocks[i])

    return blocks


class MarkFont:

    def __init__(self, *args, **kvargs):
        self._font = pygame.font.Font(*args, **kvargs)
        self._spacewidth, self._lineheight = self._font.size(' ')
        self._lineheight += self._font.get_linesize()

    def __getattr__(self, attr):
        """__getattr__(self, attr) -> attribute
        Pass everything but render to the pygame.Font instance"""

        return getattr(self._font, attr)

    def render(self, text, antialias, fore_RGBA, back_RGBA=None, width=0, height=0):
        """MarkFont.render(text, antialias, fore_RGBA, [back_RGBA],
        [width], [height]) -> Surface

        Render the given text into a surface no wider than the width of the
        passed surface, and no taller than the height.  Newlines work as you
        might expect."""

        if width < 0 or height < 0:
            raise ValueError("Arguments width and height must not be negative")

        font = self._font
        spacewidth = self._spacewidth
        lineheight = self._lineheight

        args = (antialias, fore_RGBA)
        if back_RGBA is not None:
            args += (back_RGBA,)

        lines = text.splitlines();
        line_images = []

        top = 0
        maxwidth = 0

        for line in lines:
            if width:
                # try to fit the whole line in at once
                if font.size(line)[0] < width:
                    if len(line):
                        surf = font.render(line, *args)
                    else:
                        surf = font.render(' ', *args)
                    line_images.append(surf)
                    top += lineheight

                # otherwise try a word at a time
                else:
                    aline = []
                    for word in line.split():
                        aline.append(word)
                        linewidth = font.size(' '.join(aline))[0]

                        # write all but last when the last pushes it over
                        if linewidth > width:
                            surf = font.render(' '.join(aline[:-1]), *args)
                            line_images.append(surf)
                            top += lineheight
                            aline = aline[-1:]

                    # write the final portion (could be all of it)
                    else:
                        surf = font.render(' '.join(aline), *args)
                        line_images.append(surf)
                        top += lineheight

            # if no requested width, just render the lines
            else:
                surf = font.render(line, *args)
                line_images.append(surf)

                if line_images[-1].get_width() > maxwidth:
                    maxwidth = line_images[-1].get_width()


        if len(line_images) == 1:
            return line_images[0]

        useheight = top
        usewidth = maxwidth
        if width > usewidth:
            usewidth = width
        if useheight > height > 0:
            useheight = height

        # oddly convert_alpha() gives us a better alpha channel than a SRCALPHA
        # flag to the surface creation
        final_render = pygame.surface.Surface((usewidth, useheight)).convert_alpha()

        # worse, due to the way this all defaults and works, i need to make the
        # image all clear or i lose the alpha channel.  makes sense, but what a
        # gotcha!  by reading from the line_image, it should support a bgcolor.
        img = line_images[0]
        color = img.get_at((img.get_width()-1, 0))
        final_render.fill(color)
        #final_render.fill((0,0,0))
        #final_render.fill(line_images[0].get_at((0,0)), (1, 1, usewidth-2, useheight-2))
        top = 0
        for i in line_images:
            final_render.blit(i, (0,top))
            top += lineheight

        return final_render

    def render_markup(self, text, antialias, fore_RGBA, back_RGBA=None, width=0, height=0):
        """WrapFont.render_markup(text, antialias, fore_RGBA, [back_RGBA],
        [width], [height]) -> Surface

        Render the given text into a surface no wider than the width of the
        passed surface, and no taller than the height.  Newlines work as you
        might expect.  Furthermore, simple html style tags control the styles
        available: <b>bold</b> <i>italic</i> <u>underline</u>.  They can be
        nested, and currently proper nesting is not checked. 

        Note: Due to the lack of kerning information available and the high
        interface at which this works, rendering a single word in more than one
        manner (<b>foo<i>bar</i></b>) is ugly."""

        if width < 0 or height < 0:
            raise ValueError("Arguments width and height must not be negative")

        font = self._font
        spacewidth = self._spacewidth
        lineheight = self._lineheight

        try:
            self._chartable
        except AttributeError:
            self._chartable = {}
            for i in range(128):
                c1 = chr(i)
                s1 = font.size(c1)[0]
                for j in range(128):
                    c2 = chr(j)
                    s2 = font.size(c2)[0]
                    s = c1+c2
                    self._chartable[s] = font.size(s)[0] - s1 - s2


        args = (antialias, fore_RGBA)
        if back_RGBA is not None:
            args += (back_RGBA,)

        lines = text.splitlines();
        images = []

        left = 0
        top = 0-lineheight
        maxwidth = 0

        for line in lines:
            top += lineheight
            if left > maxwidth:
                maxwidth = left
            left = 0
            lastchar = None
            for block in _markup_split(line):
                # handle markup
                if isinstance(block, _markup):
                    if isinstance(block, _markup_bold):
                        font.set_bold(block.val)
                    elif isinstance(block, _markup_italic):
                        font.set_italic(block.val)
                    elif isinstance(block, _markup_underline):
                        font.set_underline(block.val)
                    elif isinstance(block, _markup_partialspace):
                        left += self.val
                else:
                    if lastchar is not None and len(block):
                        left += self._chartable[lastchar + block[0]]
                    softspace = None
                    # try to draw the line
                    if 0 < width <= left + self.size(block)[0]:
                        # break it up if it's too big
                        for word in block.split():
                            surf = font.render(word, *args)
                            wordwidth = surf.get_size()[0]
                            if softspace is not None:
                                softspace += self._chartable[ ' '+word[0] ]
                                left += softspace + spacewidth
                            if width <= left + wordwidth:
                                top += lineheight
                                left = 0
                            images.append((left, top, surf,word))
                            left += wordwidth
                            softspace = self._chartable[ word[-1]+' ' ]
                    elif len(block):
                        surf = font.render(block, *args)
                        blockwidth = surf.get_size()[0]
                        images.append((left, top, surf,block))
                        left += blockwidth
                        lastchar = block[-1]

        if len(images) == 1:
            return images[0][2]

        useheight = top+lineheight
        usewidth = maxwidth
        if width > usewidth:
            usewidth = width
        if useheight > height > 0:
            useheight = height

        # oddly convert_alpha() gives us a better alpha channel than a SRCALPHA
        # flag to the surface creation
        final_render = pygame.surface.Surface((usewidth, useheight)).convert_alpha()

        # worse, due to the way this all defaults and works, i need to make the
        # image all clear or i lose the alpha channel.  makes sense, but what a
        # gotcha!  by reading from the line_image, it should support a bgcolor.
        img = images[0][2]
        color = img.get_at((img.get_width()-1, 0))
        final_render.fill(color)
        for x,y,i,t in images:
            # uncomment the following to get red boxes around the pieces
            #w,h = i.get_size()
            #surf = pygame.surface.Surface((w,h)).convert_alpha()
            #surf.fill((255,0,0))
            #surf.fill((0,0,0,0), (1,1,w-2,h-2))
            #final_render.blit(surf, (x,y))
            final_render.blit(i, (x,y))
            top += lineheight

        return final_render

if __name__ == '__main__':
    teststring = """This is a <b>test</b> of <b><i>MarkFont</i></b>

What's better than a quick little <i>markup</i> string to get good yet simple font effects in your game?

<b>Nothing</b>"""

    pygame.display.init()
    pygame.font.init()
    screen = pygame.display.set_mode((640,480))
    back = pygame.surface.Surface((640,480)).convert()
    font = MarkFont(None, 24)
    text1 = font.render(teststring, 1, (0,0,0), None, 300)
    text2 = font.render_markup(teststring, 1, (0,0,0), None, 300)

    w1, h1 = text1.get_size()
    x1, y1 = 0,0
    dx1, dy1 = 1, 1
    w2, h2 = text2.get_size()
    x2, y2 = h1,w1
    dx2, dy2 = -1, 1
    for i in range(0, 100, 10):
        back.fill((155+i, 155+i, 155+i), (i, i, 639-2*i, 479-2*i))

    noquit = 1
    while noquit:
        screen.blit(back, (0,0))
        screen.blit(text1, (x1,y1))
        screen.blit(text2, (x2,y2))
        pygame.display.update(screen.get_rect())

        if not 0 <= x1 + dx1 < x1 + dx1 + w1 < 640: dx1 = -dx1
        if not 0 <= y1 + dy1 < y1 + dy1 + h1 < 480: dy1 = -dy1
        x1 += dx1
        y1 += dy1

        if not 0 <= x2 + dx2 < x2 + dx2 + w2 < 640: dx2 = -dx2
        if not 0 <= y2 + dy2 < y2 + dy2 + h2 < 480: dy2 = -dy2
        x2 += dx2
        y2 += dy2

        events = pygame.event.get()
        for event in events:
            if event and event.type in (pygame.QUIT, pygame.KEYDOWN):
                noquit = 0