#!/usr/bin/env python
#
# Script to visualize what parts of the screen are updated
# according to the xresponse output.  Requires a screenshot
# of the logged activity.
# 
# This file is part of xresponse-visualize
# 
# Copyright (C) 2007 by Eero Tamminen
# Copyright (C) 2008 by Nokia Corporation
#
# Contact: Eero Tamminen <eero.tamminen@nokia.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# version 2 as published by the Free Software Foundation.
#
# 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., 51 Franklin St, Fifth Floor, Boston, MA
# 02110-1301 USA

import os, re
import pygame

verbose = False


def usage_and_exit(name, msg):
    "print help, given error message and exit"
    print """
%s -- a script to visualize screen updates recorded
by xresponse. You cat get latest upstream xresponse from:
    http://labs.o-hand.com/xresponse/

The visualization is done so that the updates are shown at
the same intervals as they originally happened.  The time
taken by these intervals is however clamped between
0.2s - 2.0s for usability reasons (also in slow mode).

Keys (PC/N8x0):
    ESC, Back         - quit
    Enter, Select     - un/pause
    Space, Menu       - repeat from start
    Up, Left          - pause and go back one frame
    Down, Right       - pause and go forward one frame
    F, Fullscreen     - toggle fullscreen
    
usage:
    %s [options] <screenshot PNG file> <xresponse log file>
options:
    -f  fullscreen
    -s  slow (20x delay between updates)
    -v  verbose
example:
    %s -f -s use-case.png use-case.log
""" % (name, name, name)
    if msg:
        print "ERROR: %s\n" % msg
    os.sys.exit(1)


def parse_updates(name, logfile, maxw, maxh, delayfactor):
    "return list of corresponding pygame update rects for given xresponse log"
    timer = 0
    mindelay = 200
    maxdelay = 2000
    starttime = 0
    updates = []
    re_event = re.compile("^ *([0-9]+)ms.* : *(Clicked|keypress)")
    re_update = re.compile("^ *([0-9]+)ms.* damage event ([0-9]+)x([0-9]+)\+([0-9]+)\+([0-9]+)")
    for line in open(logfile).readlines():
        match = re_update.search(line)
        if match:
            st,sw,sh,sx,sy = match.groups()
            t,w,h,x,y = int(st), int(sw), int(sh), int(sx), int(sy)
            if (x+w) > maxw or (y+h) > maxh:
                usage_and_exit(name, "an update doesn't fit to given %dx%d screenshot:\n\t%s" % (size[0], size[1], line.strip()))
            if timer:
                delay = (t - timer) * delayfactor
                if delay > maxdelay:
                    delay = maxdelay
                elif delay < mindelay:
                    delay = mindelay
            else:
                if not starttime:
                    starttime = t
                delay = mindelay
            timer = t
            updates.append((timer-starttime, int(delay), pygame.Rect(x,y,w,h)))
        else:
            match = re_event.search(line)
            if match:
                starttime = int(match.group(1))
    return updates


def process_args(args):
    "process args and return (script name, screenshot image, screen updates list)"
    global verbose
    fullscreen = False
    delayfactor = 1

    name = args[0].split(os.sep)[-1]
    while len(args) > 1 and args[1][0] == '-':
        if args[1] == "-f":
            fullscreen = True
        elif args[1] == "-s":
            delayfactor = 20
        elif args[1] == "-v":
            verbose = True
        else:
            usage_and_exit(name, "unknown option '%s'" % args[1])
        args.pop(1)

    if len(args) != 3:
        usage_and_exit(name, "I have no arguments and I must scream!")
        
    pngfile = args[1]
    if not (os.path.exists(pngfile) and os.path.isfile(pngfile)):
        usage_and_exit(name, "file '%s' doesn't exist" % pngfile)
    screenshot = pygame.image.load(pngfile)
    size = screenshot.get_size()

    logfile = args[2]
    if not (os.path.exists(logfile) and os.path.isfile(logfile)):
        usage_and_exit(name, "file '%s' doesn't exist" % logfile)

    updates = parse_updates(name, logfile, size[0], size[1], delayfactor)
    if not updates:
        usage_and_exit(name, "file '%s' doesn't contain any damage events" % logfile)

    return (name, fullscreen, screenshot, updates)


def area_with_linesize(update):
    "return corrected (update rectangle, line thickness) for give update"
    minsize = min(update[2], update[3])
    if minsize < 4:
        linesize = 1
    elif minsize < 10:
        linesize = (minsize - 2) / 2
    else:
        linesize = 4
    return pygame.Rect(update[0] - linesize/2,
                       update[1] - linesize/2,
                       update[2] + linesize,
                       update[3] + linesize), linesize


def show_centered_text(message, size, screen):
    "show the given message at given size on screen as centered, return used area"
    global fontcolor
    font = pygame.font.Font(None, size)
    text = font.render(message, 1, (0xFF, 0xFF, 0xFF))
    pos = text.get_rect()
    size = screen.get_size()
    pos.centerx = size[0]/2
    pos.centery = size[1]/2
    fillarea = pygame.Rect(pos.x-2, pos.y-2, pos.width+4, pos.height+4)
    # white text on black
    screen.fill((0x00, 0x00, 0x00), fillarea)
    screen.blit(text, pos)
    return fillarea


def wait_for_update(delay, paused, frame, lastframe):
    "wait given delay, process keys and wait until paused is false, return true if repeat key is pressed"
    oldpaused = paused
    if paused:
        pygame.time.wait(120)
    else:
        pygame.time.wait(delay)
    repeat = False
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            os.sys.exit(0)
        # use KEYUP, not interested about repeats
        if event.type == pygame.KEYUP:
            if event.key == pygame.K_ESCAPE:
                os.sys.exit(0)
            elif event.key in (pygame.K_f, pygame.K_F6):
                pygame.display.toggle_fullscreen()
            elif event.key in (pygame.K_SPACE, pygame.K_F4):
                paused = False
                repeat = True
            elif event.key in (pygame.K_RETURN, pygame.K_KP_ENTER):
                paused = not paused
        # use KEYDOWN, these should repeat
        elif event.type == pygame.KEYDOWN:
            if event.key in (pygame.K_UP, pygame.K_LEFT):
                paused = True
                if frame > 0:
                    frame -= 1
            elif event.key in (pygame.K_DOWN, pygame.K_RIGHT):
                paused = True
                if frame < lastframe:
                    frame += 1
    if verbose:
        if repeat:
            print "repeat from start"
        elif paused and not oldpaused:
            print "pause"
        elif oldpaused and not paused:
            print "unpaused"
    return (frame, paused, repeat)


def print_updates(index, timer, update, changetitle):
    "index, update, whether to update title"
    if not verbose:
        return
    msg = "update %d after %dms: %dx%d+%d+%d " % (index+1, timer, update[2], update[3], update[0], update[1])
    if changetitle:
        pygame.display.set_caption(msg)
    print msg


def init_and_main(argv):
    "init script & pygame, process args, loop showing the updates"
    pygame.init()
    pygame.event.set_allowed([pygame.QUIT, pygame.KEYUP, pygame.KEYDOWN])
    pygame.key.set_repeat(250, 200)  # first 250ms, repeats 200ms

    title, fullscreen, screenshot, updates = process_args(argv)

    screensize = screenshot.get_size()
    if fullscreen:
        screen = pygame.display.set_mode(screensize, pygame.FULLSCREEN)
    else:
        screen = pygame.display.set_mode(screensize)
    screenshot = screenshot.convert()

    fillcolor = (0xFF, 0x00, 0x00) # red
    statesize  = 96 # points
    numbersize = 32 # points
    paused = False
    while True:
        if not fullscreen:
            pygame.display.set_caption(title)
        screen.blit(screenshot, (0,0))
        if paused:
            text = "paused"
        else:
            text = "start"
        show_centered_text(text, statesize, screen)
        pygame.display.flip()
        pygame.time.wait(500)
        screen.blit(screenshot, (0,0))
        index = 0
        previndex = -1
        lastindex = len(updates) - 1
        while index <= lastindex:
            if index != previndex:
                timer, delay, update = updates[index]
                # show update and frame index count
                #screen.fill(fillcolor, update)
                area,linesize = area_with_linesize(update)
                pygame.draw.rect(screen, fillcolor, update, linesize)
                dirty = show_centered_text(str(index+1), numbersize, screen)
                pygame.display.flip()
                print_updates(index, timer, update, not fullscreen)
                # and restore screenshot
                screen.blit(screenshot, area, area)
                screen.blit(screenshot, dirty, dirty)
            previndex = index
            index,paused,repeat = wait_for_update(delay, paused, index, lastindex)
            if repeat:
                break
            if not paused:
                index += 1
        if not repeat:
            paused = True


init_and_main(os.sys.argv)
