#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Tuomas Räsänen
#
# 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, see <http://www.gnu.org/licenses/>.

import random
import sys

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtMultimedia import *
from PyQt5.QtWidgets import *

class GraphicsGlowEffect(QGraphicsEffect):

    def __init__(self, color):
        super().__init__()

        self.__color  = color
        self.__extent = 100

    def boundingRectFor(self, rect):
        return QRectF(
            rect.left()   - 1 * self.__extent,
            rect.top()    - 1 * self.__extent,
            rect.width()  + 2 * self.__extent,
            rect.height() + 2 * self.__extent)

    def draw(self, painter):
        sourcePixmap, sourceOffset = self.sourcePixmap(Qt.LogicalCoordinates)

        colorizeEffect = QGraphicsColorizeEffect()
        colorizeEffect.setColor(self.__color)

        colorizedPixmap = self.__applyEffectToPixmap(sourcePixmap, colorizeEffect, 0)

        blurEffect = QGraphicsBlurEffect()
        blurEffect.setBlurRadius(15)

        colorizedBlurredPixmap = self.__applyEffectToPixmap(colorizedPixmap,
                                                            blurEffect,
                                                            self.__extent)

        for i in range(5):
            position = sourceOffset - QPoint(self.__extent, self.__extent)
            painter.drawPixmap(position, colorizedBlurredPixmap)
            self.drawSource(painter)

    def __applyEffectToPixmap(self, sourcePixmap, effect, extent):
        if not sourcePixmap:
            return QPixmap()

        if not effect:
            return sourcePixmap

        scene  = QGraphicsScene()
        item   = QGraphicsPixmapItem()
        pixmap = QPixmap(sourcePixmap.width()  + 2 * extent,
                         sourcePixmap.height() + 2 * extent)

        item.setPixmap(sourcePixmap)
        item.setGraphicsEffect(effect)
        scene.addItem(item);
        pixmap.fill(Qt.transparent)
        scene.render(QPainter(pixmap), QRectF(),
                     QRectF(-extent, -extent, pixmap.width(), pixmap.height()))

        return pixmap

class ColorLedButton(QPushButton):

    def __init__(self, color, radius=50):
        super().__init__()

        self.__color     = color
        self.__radius    = radius
        self.__blinkTimerId = None
        colorName        = color.name()
        darkerColorName  = color.darker(500).name()
        lighterColorName = color.lighter(175).name()
        self.setFixedSize(QSize(radius * 2, radius * 2))
        self.setStyleSheet("""
ColorLedButton {
 background    : qlineargradient(x1:0, y1:-1.5, x2:0, y2:1.3,
                                 stop:0 %s, stop: 1 %s);
 border-style  : solid;
 border-width  : 1px;
 border-radius : %spx;
 border-color  : black;
 font-size     : 24px;
}

ColorLedButton:pressed {
 background    : qlineargradient(x1:0, y1:0, x2:0, y2:3,
                                 stop:0 %s, stop: 1 %s);
 border-width  : 3px;
}

ColorLedButton[isLit="true"] {
 background    : qradialgradient(cx:0.5, cy:0.5, radius:0.8,
                                 fx:0.5, fy:0.5, stop:0 %s, stop: 1 %s);
}

ColorLedButton[isLit="true"]:pressed {
 background    : qradialgradient(cx:0.5, cy:0.5, radius:0.82,
                                 fx:0.55, fy:0.55, stop:0 %s, stop: 1 %s);
}
""" % (lighterColorName, darkerColorName, radius,
       darkerColorName, lighterColorName,
       lighterColorName, colorName,
       lighterColorName, colorName))

        self.setMask(QRegion(QRect(-2, -2, self.__radius * 2 + 6, self.__radius * 2 + 6), QRegion.Ellipse))
        self.setFocusPolicy(Qt.NoFocus)

    def switchOn(self):
        self.setGraphicsEffect(GraphicsGlowEffect(self.__color))
        self.setProperty("isLit", True)
        self.style().unpolish(self)
        self.style().polish(self)
        self.update()

    def switchOff(self):
        self.setGraphicsEffect(None)
        self.setProperty("isLit", False)
        self.style().unpolish(self)
        self.style().polish(self)
        self.update()

    def toggle(self):
        if self.isLit():
            self.switchOff()
        else:
            self.switchOn()

    def timerEvent(self, event):
        if event.timerId() == self.__blinkTimerId:
            self.toggle()
        super().timerEvent(event)

    def startBlinking(self, interval=550):
        self.stopBlinking()
        self.__blinkTimerId = self.startTimer(interval)

    def stopBlinking(self):
        if self.__blinkTimerId:
            self.killTimer(self.__blinkTimerId)
            self.__blinkTimerId = None

    def isLit(self):
        return self.property("isLit")

    def reset(self):
        self.stopBlinking()
        self.switchOff()
        self.setText('')

class ScoreCounter(QLabel):

    def __init__(self):
        super().__init__("0")

        font = QFont()
        font.setPointSize(48)
        self.setFont(font)
        self.setAlignment(Qt.AlignCenter)
        self.setFixedSize(QSize(160, 80))
        self.setStyleSheet("""
ScoreCounter {
 color            : green;
 background-color : black;
 border-style     : ridge;
 border-width     : 6px;
 border-color     : grey;
 border-radius    : 40;
}
        """)

    def decrement(self):
        self.setText(str(int(self.text()) - 1))

    def increment(self):
        self.setText(str(int(self.text()) + 1))

    def reset(self):
        self.setText("0")

    def count(self):
        return int(self.text())

class Dashboard(QWidget):

    def __init__(self):
        super().__init__()

        self.buttons      = []
        self.__levelButtons = []
        self.__controller   = Controller()
        self.scoreCounter = ScoreCounter()

        layout              = QVBoxLayout(self)
        topRowLayout        = QHBoxLayout()
        middleRowLayout     = QHBoxLayout()
        bottomRowLayout     = QHBoxLayout()

        topRowLayout.addStretch()
        for animal in  ["snail", "turtle", "runner", "rabbit",
                        "kangaroo", "ostrich", "horse", "cheetah"]:
            radius = 30
            button = ColorLedButton(QColor(255, 0, 255), radius)
            button.setIcon(QIcon(QPixmap("%s.png" % animal)))
            button.setIconSize(QSize(radius, radius))
            self.__levelButtons.append(button)
            topRowLayout.addSpacing(radius)
            topRowLayout.addWidget(button)
        topRowLayout.addSpacing(radius)
        topRowLayout.addStretch()

        middleRowLayout.addWidget(self.scoreCounter)

        bottomRowLayout.addStretch()
        for color in [QColor(255, 0, 0), QColor(0, 255, 0), QColor(0, 0, 255), QColor(255, 255, 0)]:
            button = ColorLedButton(color)
            self.buttons.append(button)
            bottomRowLayout.addSpacing(50)
            bottomRowLayout.addWidget(button)
            self.__controller.addButton(button)

        bottomRowLayout.addSpacing(50)
        bottomRowLayout.addStretch()

        layout.addSpacing(50)
        layout.addLayout(topRowLayout)
        layout.addSpacing(50)
        layout.addLayout(middleRowLayout)
        layout.addSpacing(50)
        layout.addLayout(bottomRowLayout)
        layout.addSpacing(50)

        self.__controller.setScoreCounter(self.scoreCounter)
        self.__controller.setDashboard(self)

    def clickButton(self, i):
        self.buttons[i].animateClick()

    def reset(self):
        for button in self.buttons:
            button.reset()
        self.scoreCounter.reset()
        for levelButton in self.__levelButtons:
            levelButton.reset()

    def advanceLevel(self, level):
        if level % 2:
            try:
                button = self.__levelButtons[level // 2]
            except IndexError:
                return
            button.switchOn()
            button.startBlinking()
        else:
            try:
                button = self.__levelButtons[(level - 1) // 2]
            except IndexError:
                return
            button.stopBlinking()
            button.switchOn()

class Controller(QObject):

    TIMEOUTS = [1000 - i * 35 for i in range(17)]

    def __init__(self):
        super().__init__()

        self.__buttons      = []
        self.__dashboard    = None
        self.__scoreCounter = None
        self.__failButton   = None
        self.__failTicks    = 0
        self.__failTimerId  = None
        self.__gameTimerId  = None
        self.__steps        = 0

        self.__clickSoundEffect = QSoundEffect()
        self.__clickSoundEffect.setSource(QUrl.fromLocalFile("tick.wav"))
        self.__clickSoundEffect.setVolume(0.25)

        self.__failSoundEffect = QSoundEffect()
        self.__failSoundEffect.setSource(QUrl.fromLocalFile("fail.wav"))
        self.__failSoundEffect.setVolume(0.25)

        self.__gameOverSoundEffect = QSoundEffect()
        self.__gameOverSoundEffect.setSource(QUrl.fromLocalFile("sad.wav"))
        self.__gameOverSoundEffect.setVolume(0.25)

        self.__levelSoundEffect = QSoundEffect()
        self.__levelSoundEffect.setSource(QUrl.fromLocalFile("teaspoon.wav"))
        self.__levelSoundEffect.setVolume(0.25)

    def timerEvent(self, event):
        if event.timerId() == self.__failTimerId:
            self.__failTicks += 1
            self.__failButton.toggle()
            if self.__failTicks >= 10:
                self.killTimer(self.__failTimerId)
                self.__failTimerId = None

                for button in self.__buttons:
                    button.switchOn()
                    button.setText("START")

            return

        for button in self.__buttons:
            if button.isLit():
                self.__gameOver(button)
                return

        self.__buttons[random.randrange(len(self.__buttons))].switchOn()

        level, remainder = divmod(self.__steps, 7)
        if remainder == 0 and level < len(Controller.TIMEOUTS):
            if level >= 1:
                self.__levelSoundEffect.play()
                self.__dashboard.advanceLevel(level)
            self.killTimer(self.__gameTimerId)
            self.__gameTimerId = self.startTimer(Controller.TIMEOUTS[level])

    def __gameOver(self, failButton):
        self.__gameOverSoundEffect.play()

        self.__failButton = failButton
        self.__failButton.setText("GAME\nOVER")
        self.killTimer(self.__gameTimerId)
        self.__gameTimerId = None
        self.__failTimerId = self.startTimer(500)

    def addButton(self, button):
        button.clicked.connect(lambda: self.__buttonClicked(button))
        self.__buttons.append(button)
        button.switchOn()
        button.setText("START")

    def __buttonClicked(self, button):
        if self.__failTimerId is not None:
            self.__clickSoundEffect.play()
            return

        if self.__gameTimerId is None:
            self.__clickSoundEffect.play()
            self.start()
            return

        if not button.isLit():
            self.__failSoundEffect.play()
            self.__scoreCounter.decrement()
            return

        self.__clickSoundEffect.play()
        button.switchOff()
        self.__scoreCounter.increment()
        self.__steps += 1

    def setDashboard(self, dashboard):
        self.__dashboard = dashboard

    def setScoreCounter(self, scoreCounter):
        self.__scoreCounter = scoreCounter

    def start(self):
        self.__dashboard.reset()
        self.__gameTimerId = self.startTimer(1000)
        self.__failButton = None
        self.__failTicks = 0
        self.__steps = 0

class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()

        self.__dashboard = Dashboard()

        self.setStyleSheet("background-color: #3f3f3f;")
        self.setCentralWidget(self.__dashboard)
        self.setWindowTitle("TJJR's Speed Test Game")

    def keyPressEvent(self, event):
        keys = [Qt.Key_Left, Qt.Key_Up, Qt.Key_Down, Qt.Key_Right]
        try:
            i = keys.index(event.key())
        except ValueError:
            super().keyPressEvent(event)
            return
        else:
            self.__dashboard.clickButton(i)

def main():
    app = QApplication(sys.argv)

    if len(sys.argv) > 1:
        error_message = "ERROR: invalid number of arguments ({}), expected 0"
        print(error_message.format(len(sys.argv) - 1), file=sys.stderr)
        return 1

    win = MainWindow()
    win.show()

    return app.exec_()

if __name__ == "__main__":
    sys.exit(main())
