#!/usr/bin/env python


#############################################################################
##
## Copyright (C) 2010 Riverbank Computing Limited.
## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
## All rights reserved.
##
## This file is part of the examples of PyQt.
##
## $QT_BEGIN_LICENSE:BSD$
## You may use this file under the terms of the BSD license as follows:
##
## "Redistribution and use in source and binary forms, with or without
## modification, are permitted provided that the following conditions are
## met:
##   * Redistributions of source code must retain the above copyright
##     notice, this list of conditions and the following disclaimer.
##   * Redistributions in binary form must reproduce the above copyright
##     notice, this list of conditions and the following disclaimer in
##     the documentation and/or other materials provided with the
##     distribution.
##   * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
##     the names of its contributors may be used to endorse or promote
##     products derived from this software without specific prior written
##     permission.
##
## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
## $QT_END_LICENSE$
##
#############################################################################


# This is only needed for Python v2 but is harmless for Python v3.
import sip
sip.setapi('QVariant', 2)

import math

from PyQt4 import QtCore, QtGui

import stickman_rc


class Node(QtGui.QGraphicsObject):
    positionChanged = QtCore.pyqtSignal()

    def __init__(self, pos, parent=None):
        super(Node, self).__init__(parent)

        self.m_dragging = False

        self.setPos(pos)
        self.setFlag(QtGui.QGraphicsItem.ItemSendsGeometryChanges)

    def boundingRect(self):
        return QtCore.QRectF(-6.0, -6.0, 12.0, 12.0)

    def paint(self, painter, option, widget):
        painter.setPen(QtCore.Qt.white)
        painter.drawEllipse(QtCore.QPointF(0.0, 0.0), 5.0, 5.0)

    def itemChange(self, change, value):
        if change == QtGui.QGraphicsItem.ItemPositionChange:
            self.positionChanged.emit()

        return super(Node, self).itemChange(change, value)

    def mousePressEvent(self, event):
        self.m_dragging = True

    def mouseMoveEvent(self, event):
        if self.m_dragging:
            self.setPos(self.mapToParent(event.pos()))

    def mouseReleaseEvent(self, event):
        self.m_dragging = False


Coords = (
    # Head: 0
    (0.0, -150.0),

    # Body pentagon, top->bottom, left->right: 1 - 5
    (0.0, -100.0),
    (-50.0, -50.0),
    (50.0, -50.0),
    (-25.0, 50.0),
    (25.0, 50.0),

    # Right arm: 6 - 7
    (-100.0, 0.0),
    (-125.0, 50.0),

    # Left arm: 8 - 9
    (100.0, 0.0),
    (125.0, 50.0),

    # Lower body: 10 - 11
    (-35.0, 75.0),
    (35.0, 75.0),

    # Right leg: 12 - 13
    (-25.0, 200.0),
    (-30.0, 300.0),

    # Left leg: 14 - 15
    (25.0, 200.0),
    (30.0, 300.0))


Bones = (
    # Neck.
    (0, 1),

    # Body.
    (1, 2),
    (1, 3),
    (1, 4),
    (1, 5),
    (2, 3),
    (2, 4),
    (2, 5),
    (3, 4),
    (3, 5),
    (4, 5),

    # Right arm.
    (2, 6),
    (6, 7),

    # Left arm.
    (3, 8),
    (8, 9),

    # Lower body.
    (4, 10),
    (4, 11),
    (5, 10),
    (5, 11),
    (10, 11),

    # Right leg.
    (10, 12),
    (12, 13),

    # Left leg.
    (11, 14),
    (14, 15))


class StickMan(QtGui.QGraphicsObject):
    def __init__(self):
        super(StickMan, self).__init__()

        self.m_sticks = True
        self.m_isDead = False
        self.m_pixmap = QtGui.QPixmap('images/head.png')
        self.m_penColor = QtGui.QColor(QtCore.Qt.white)
        self.m_fillColor = QtGui.QColor(QtCore.Qt.black)

        # Set up start position of limbs.
        self.m_nodes = []
        for x, y in Coords:
            node = Node(QtCore.QPointF(x, y), self)
            node.positionChanged.connect(self.childPositionChanged)
            self.m_nodes.append(node)

        self.m_perfectBoneLengths = []
        for n1, n2 in Bones:
            node1 = self.m_nodes[n1]
            node2 = self.m_nodes[n2]

            dist = node1.pos() - node2.pos()
            self.m_perfectBoneLengths.append(math.hypot(dist.x(), dist.y()))

        self.startTimer(10)

    def childPositionChanged(self):
        self.prepareGeometryChange()

    def setDrawSticks(self, on):
        self.m_sticks = on

        for node in self.m_nodes:
            node.setVisible(on)

    def drawSticks(self):
        return self.m_sticks

    def boundingRect(self):
        # Account for head radius of 50.0 plus pen which is 5.0.
        return self.childrenBoundingRect().adjusted(-55.0, -55.0, 55.0, 55.0)

    def nodeCount(self):
        return len(self.m_nodes)

    def node(self, idx):
        if idx >= 0 and idx < len(self.m_nodes):
            return self.m_nodes[idx]

        return None

    def timerEvent(self, e):
        self.update()

    def stabilize(self):
        threshold = 0.001

        for i, (n1, n2) in enumerate(Bones):
            node1 = self.m_nodes[n1]
            node2 = self.m_nodes[n2]

            pos1 = node1.pos()
            pos2 = node2.pos()

            dist = pos1 - pos2
            length = math.hypot(dist.x(), dist.y())
            diff = (length - self.m_perfectBoneLengths[i]) / length

            p = dist * (0.5 * diff)
            if p.x() > threshold and p.y() > threshold:
                pos1 -= p
                pos2 += p

                node1.setPos(pos1)
                node2.setPos(pos2)

    def posFor(self, idx):
        return self.m_nodes[idx].pos()

    @QtCore.pyqtProperty(QtGui.QColor)
    def penColor(self):
        return QtGui.QColor(self.m_penColor)

    @penColor.setter
    def penColor(self, color):
        self.m_penColor = QtGui.QColor(color)

    @QtCore.pyqtProperty(QtGui.QColor)
    def fillColor(self):
        return QtGui.QColor(self.m_fillColor)

    @fillColor.setter
    def fillColor(self, color):
        self.m_fillColor = QtGui.QColor(color)

    @QtCore.pyqtProperty(bool)
    def isDead(self):
        return self.m_isDead

    @isDead.setter
    def isDead(self, isDead):
        self.m_isDead = isDead

    def paint(self, painter, option, widget):
        self.stabilize()

        if self.m_sticks:
            painter.setPen(QtCore.Qt.white)

            for n1, n2 in Bones:
                node1 = self.m_nodes[n1]
                node2 = self.m_nodes[n2]

                painter.drawLine(node1.pos(), node2.pos())
        else:
            # First bone is neck and will be used for head.

            path = QtGui.QPainterPath()
            path.moveTo(self.posFor(0))
            path.lineTo(self.posFor(1))

            # Right arm.
            path.lineTo(self.posFor(2))
            path.lineTo(self.posFor(6))
            path.lineTo(self.posFor(7))

            # Left arm.
            path.moveTo(self.posFor(3))
            path.lineTo(self.posFor(8))
            path.lineTo(self.posFor(9))

            # Body.
            path.moveTo(self.posFor(2))
            path.lineTo(self.posFor(4))
            path.lineTo(self.posFor(10))
            path.lineTo(self.posFor(11))
            path.lineTo(self.posFor(5))
            path.lineTo(self.posFor(3))
            path.lineTo(self.posFor(1))

            # Right leg.
            path.moveTo(self.posFor(10))
            path.lineTo(self.posFor(12))
            path.lineTo(self.posFor(13))

            # Left leg.
            path.moveTo(self.posFor(11))
            path.lineTo(self.posFor(14))
            path.lineTo(self.posFor(15))

            painter.setPen(QtGui.QPen(self.m_penColor, 5.0, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap))
            painter.drawPath(path)

            n1, n2 = Bones[0]
            node1 = self.m_nodes[n1]
            node2 = self.m_nodes[n2]

            dist = node2.pos() - node1.pos()

            sinAngle = dist.x() / math.hypot(dist.x(), dist.y())
            angle = math.degrees(math.asin(sinAngle))

            headPos = node1.pos()
            painter.translate(headPos)
            painter.rotate(-angle)

            painter.setBrush(self.m_fillColor)
            painter.drawEllipse(QtCore.QPointF(0, 0), 50.0, 50.0)

            painter.setBrush(self.m_penColor)
            painter.setPen(QtGui.QPen(self.m_penColor, 2.5, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap))

            # Eyes.
            if self.m_isDead:
                painter.drawLine(-30.0, -30.0, -20.0, -20.0)
                painter.drawLine(-20.0, -30.0, -30.0, -20.0)

                painter.drawLine(20.0, -30.0, 30.0, -20.0)
                painter.drawLine(30.0, -30.0, 20.0, -20.0)
            else:
                painter.drawChord(QtCore.QRectF(-30.0, -30.0, 25.0, 70.0), 30.0 * 16, 120.0 * 16)
                painter.drawChord(QtCore.QRectF(5.0, -30.0, 25.0, 70.0), 30.0 * 16, 120.0 * 16)

            # Mouth.
            if self.m_isDead:
                painter.drawLine(-28.0, 2.0, 29.0, 2.0)
            else:
                painter.setBrush(QtGui.QColor(128, 0, 64 ))
                painter.drawChord(QtCore.QRectF(-28.0, 2.0 - 55.0 / 2.0, 57.0, 55.0), 0.0, -180.0 * 16)

            # Pupils.
            if not self.m_isDead:
                painter.setPen(QtGui.QPen(self.m_fillColor, 1.0, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap))
                painter.setBrush(self.m_fillColor)
                painter.drawEllipse(QtCore.QPointF(-12.0, -25.0), 5.0, 5.0)
                painter.drawEllipse(QtCore.QPointF(22.0, -25.0), 5.0, 5.0)


class GraphicsView(QtGui.QGraphicsView):
    keyPressed = QtCore.pyqtSignal(int)

    def keyPressEvent(self, e):
        if e.key() == QtCore.Qt.Key_Escape:
            self.close()

        self.keyPressed.emit(QtCore.Qt.Key(e.key()))


class Frame(object):
    def __init__(self):
        self.m_nodePositions = []

    def nodeCount(self):
        return len(self.m_nodePositions)

    def setNodeCount(self, nodeCount):
        while nodeCount > len(self.m_nodePositions):
            self.m_nodePositions.append(QtCore.QPointF())

        while nodeCount < len(self.m_nodePositions):
            self.m_nodePositions.pop()

    def nodePos(self, idx):
        return QtCore.QPointF(self.m_nodePositions[idx])

    def setNodePos(self, idx, pos):
        self.m_nodePositions[idx] = QtCore.QPointF(pos)


class Animation(object):
    def __init__(self):
        self.m_currentFrame = 0
        self.m_frames = [Frame()]
        self.m_name = ''

    def setTotalFrames(self, totalFrames):
        while len(self.m_frames) < totalFrames:
            self.m_frames.append(Frame())

        while totalFrames < len(self.m_frames):
            self.m_frames.pop()

    def totalFrames(self):
        return len(self.m_frames)

    def setCurrentFrame(self, currentFrame):
        self.m_currentFrame = max(min(currentFrame, self.totalFrames() - 1), 0)

    def currentFrame(self):
        return self.m_currentFrame

    def setNodeCount(self, nodeCount):
        frame = self.m_frames[self.m_currentFrame]
        frame.setNodeCount(nodeCount)

    def nodeCount(self):
        frame = self.m_frames[self.m_currentFrame]
        return frame.nodeCount()

    def setNodePos(self, idx, pos):
        frame = self.m_frames[self.m_currentFrame]
        frame.setNodePos(idx, pos)

    def nodePos(self, idx):
        frame = self.m_frames[self.m_currentFrame]
        return frame.nodePos(idx)

    def name(self):
        return self.m_name

    def setName(self, name):
        self.m_name = name

    def save(self, device):
        stream = QtCore.QDataStream(device)
        stream.writeQString(self.m_name)
        stream.writeInt(len(self.m_frames))

        for frame in self.m_frames:
            stream.writeInt(frame.nodeCount())

            for i in range(frame.nodeCount()):
                stream << frame.nodePos(i)

    def load(self, device):
        self.m_frames = []

        stream = QtCore.QDataStream(device)
        self.m_name = stream.readQString()
        frameCount = stream.readInt()

        for i in range(frameCount):
            nodeCount = stream.readInt()

            frame = Frame()
            frame.setNodeCount(nodeCount)

            for j in range(nodeCount):
                pos = QtCore.QPointF()
                stream >> pos

                frame.setNodePos(j, pos)

            self.m_frames.append(frame)


class KeyPressTransition(QtCore.QSignalTransition):
    def __init__(self, receiver, key, target=None):
        super(KeyPressTransition, self).__init__(receiver.keyPressed)

        self.m_key = key

        if target is not None:
            self.setTargetState(target)

    def eventTest(self, e):
        if super(KeyPressTransition, self).eventTest(e):
            key = e.arguments()[0]
            return key == self.m_key

        return False


class LightningStrikesTransition(QtCore.QEventTransition):
    def __init__(self, target):
        super(LightningStrikesTransition, self).__init__()

        self.setEventSource(self)
        self.setEventType(QtCore.QEvent.Timer)
        self.setTargetState(target)
        QtCore.qsrand(QtCore.QDateTime.currentDateTime().toTime_t())
        self.startTimer(1000)

    def eventTest(self, e):
        return (super(LightningStrikesTransition, self).eventTest(e) and
                (QtCore.qrand() % 50) == 0)


class LifeCycle(object):
    def __init__(self, stickMan, keyReceiver):
        self.m_stickMan = stickMan
        self.m_keyReceiver = keyReceiver

        # Create animation group to be used for all transitions.
        self.m_animationGroup = QtCore.QParallelAnimationGroup()
        stickManNodeCount = self.m_stickMan.nodeCount()
        self._pas = []
        for i in range(stickManNodeCount):
            pa = QtCore.QPropertyAnimation(self.m_stickMan.node(i), 'pos')
            self._pas.append(pa)
            self.m_animationGroup.addAnimation(pa)

        # Set up intial state graph.
        self.m_machine = QtCore.QStateMachine()
        self.m_machine.addDefaultAnimation(self.m_animationGroup)

        self.m_alive = QtCore.QState(self.m_machine)
        self.m_alive.setObjectName('alive')

        # Make it blink when lightning strikes before entering dead animation.
        lightningBlink = QtCore.QState(self.m_machine)
        lightningBlink.assignProperty(self.m_stickMan.scene(),
                'backgroundBrush', QtCore.Qt.white)
        lightningBlink.assignProperty(self.m_stickMan, 'penColor',
                QtCore.Qt.black)
        lightningBlink.assignProperty(self.m_stickMan, 'fillColor',
                QtCore.Qt.white)
        lightningBlink.assignProperty(self.m_stickMan, 'isDead', True)

        timer = QtCore.QTimer(lightningBlink)
        timer.setSingleShot(True)
        timer.setInterval(100)
        lightningBlink.entered.connect(timer.start)
        lightningBlink.exited.connect(timer.stop)

        self.m_dead = QtCore.QState(self.m_machine)
        self.m_dead.assignProperty(self.m_stickMan.scene(), 'backgroundBrush',
                QtCore.Qt.black)
        self.m_dead.assignProperty(self.m_stickMan, 'penColor',
                QtCore.Qt.white)
        self.m_dead.assignProperty(self.m_stickMan, 'fillColor',
                QtCore.Qt.black)
        self.m_dead.setObjectName('dead')

        # Idle state (sets no properties).
        self.m_idle = QtCore.QState(self.m_alive)
        self.m_idle.setObjectName('idle')

        self.m_alive.setInitialState(self.m_idle)

        # Lightning strikes at random.
        self.m_alive.addTransition(LightningStrikesTransition(lightningBlink))
        lightningBlink.addTransition(timer.timeout, self.m_dead)

        self.m_machine.setInitialState(self.m_alive)

    def setDeathAnimation(self, fileName):
        deathAnimation = self.makeState(self.m_dead, fileName)
        self.m_dead.setInitialState(deathAnimation)

    def start(self):
        self.m_machine.start()

    def addActivity(self, fileName, key):
        state = self.makeState(self.m_alive, fileName)
        self.m_alive.addTransition(KeyPressTransition(self.m_keyReceiver, key, state))

    def makeState(self, parentState, animationFileName):
        topLevel = QtCore.QState(parentState)

        animation = Animation()

        file = QtCore.QFile(animationFileName)
        if file.open(QtCore.QIODevice.ReadOnly):
            animation.load(file)

        frameCount = animation.totalFrames()
        previousState = None
        for i in range(frameCount):
            animation.setCurrentFrame(i)

            frameState = QtCore.QState(topLevel)
            nodeCount = animation.nodeCount()
            for j in range(nodeCount):
                frameState.assignProperty(self.m_stickMan.node(j), 'pos',
                        animation.nodePos(j))

            frameState.setObjectName('frame %d' % i)

            if previousState is None:
                topLevel.setInitialState(frameState)
            else:
                previousState.addTransition(previousState.propertiesAssigned,
                        frameState)

            previousState = frameState

        previousState.addTransition(previousState.propertiesAssigned,
                topLevel.initialState())

        return topLevel


if __name__ == '__main__':

    import sys

    app = QtGui.QApplication(sys.argv)

    stickMan = StickMan()
    stickMan.setDrawSticks(False)

    textItem = QtGui.QGraphicsTextItem()
    textItem.setHtml("<font color=\"white\"><b>Stickman</b>"
        "<p>"
        "Tell the stickman what to do!"
        "</p>"
        "<p><i>"
        "<li>Press <font color=\"purple\">J</font> to make the stickman jump.</li>"
        "<li>Press <font color=\"purple\">D</font> to make the stickman dance.</li>"
        "<li>Press <font color=\"purple\">C</font> to make him chill out.</li>"
        "<li>When you are done, press <font color=\"purple\">Escape</font>.</li>"
        "</i></p>"
        "<p>If he is unlucky, the stickman will get struck by lightning, and never jump, dance or chill out again."
        "</p></font>")

    w = textItem.boundingRect().width()
    stickManBoundingRect = stickMan.mapToScene(stickMan.boundingRect()).boundingRect()
    textItem.setPos(-w / 2.0, stickManBoundingRect.bottom() + 25.0)

    scene = QtGui.QGraphicsScene()
    scene.addItem(stickMan)
    scene.addItem(textItem)
    scene.setBackgroundBrush(QtCore.Qt.black)

    view = GraphicsView()
    view.setRenderHints(QtGui.QPainter.Antialiasing)
    view.setTransformationAnchor(QtGui.QGraphicsView.NoAnchor)
    view.setScene(scene)
    view.show()
    view.setFocus()

    # Make enough room in the scene for stickman to jump and die.
    sceneRect = scene.sceneRect()
    view.resize(sceneRect.width() + 100, sceneRect.height() + 100)
    view.setSceneRect(sceneRect)

    cycle = LifeCycle(stickMan, view)
    cycle.setDeathAnimation(':/animations/dead')

    cycle.addActivity(':/animations/jumping', QtCore.Qt.Key_J)
    cycle.addActivity(':/animations/dancing', QtCore.Qt.Key_D)
    cycle.addActivity(':/animations/chilling', QtCore.Qt.Key_C)
    cycle.start()

    sys.exit(app.exec_())
