/***************************************************************************
 *   Copyright 2008 by Thomas Gillespie <tomjamesgillespie@googlemail.com> *
 *   Copyright 2010 by Enrico Ros <enrico.ros@gmail.com>                   *
 *                                                                         *
 *   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.,                                       *
 *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA .        *
 ***************************************************************************/

#include "bball.h"

#include <QtGui/QPainter>
#include <QtGui/QDesktopWidget>
#include <QtGui/QGraphicsScene>
#include <KSharedConfig>
#include <KLocale>
#include <KStandardDirs>
#include <KIO/NetAccess>
#include <KMessageBox>
#include <KImageIO>

#include <stdlib.h>

// default values
static const int initial_ball_radius = 64;

using namespace Plasma;

bballApplet::bballApplet(QObject * parent, const QVariantList & args):
    Plasma::Applet(parent, args),
    // keep this in sync with readConfiguration's default values
    m_overlay_enabled(false),
    m_overlay_opacity(0),
    m_gravity(1.5),
    m_friction(0.03),
    m_restitution(0.8),
    m_sound_enabled(false),
    m_sound_player(0),
    m_auto_bounce_enabled(false),
    m_auto_bounce_strength(0),
    // more status
    m_radius(initial_ball_radius),
    m_angle(0),
    m_angularVelocity(0),
    m_mousePressed(false)
{
    setHasConfigurationInterface(true);
    //TODO figure out why it is not good enough to set it here
    // but that it needs to be set in constraintsEvent
    // see also the icon applet
    // update: apparently it gets reset when the formfactor changes
    // this can be caught in constraintsEvent with Plasma::FormFactorConstraint
    setBackgroundHints(NoBackground);
    resize(contentSizeHint());
}

bballApplet::~bballApplet()
{
    if (m_sound_player) {
        m_sound_player->deleteLater();
    }
}

void bballApplet::init()
{
    configChanged();
    // monitor the scene for size changes (so we change the bouncing rect)
    if (scene())
        connect(scene(), SIGNAL(sceneRectChanged(QRectF)), this, SLOT(updateScreenRect()));
    m_timer.start(25, this);
}

void bballApplet::paintInterface(QPainter * p, const QStyleOptionGraphicsItem * option, const QRect & contentsRect)
{
    Q_UNUSED(option);
    Q_UNUSED(contentsRect);

    if (m_ballPixmap.isNull())
        return;

    if (m_angle) {
        p->translate(m_radius, m_radius);
        p->rotate(360 * m_angle / 6.28);
        p->translate(-m_radius, -m_radius);
        if (m_velocity.length() < 300) {
            p->setRenderHint(QPainter::SmoothPixmapTransform);
            p->setRenderHint(QPainter::Antialiasing);
        }
    }
    p->drawPixmap(QPoint(0, 0), m_ballPixmap);
}

QSizeF bballApplet::contentSizeHint() const
{
    return QSizeF(m_radius * 2, m_radius * 2);
}

void bballApplet::createConfigurationInterface(KConfigDialog *parent)
{
    QWidget *widget = new QWidget;
    ui.setupUi(widget);

    // Appearance
    ui.imageUrl->setFilter(KImageIO::pattern(KImageIO::Reading));
    ui.imageUrl->setUrl(KUrl::fromPath(m_image_url));
    ui.colourizeEnabled->setChecked(m_overlay_enabled);
    ui.colourizeLabel->setEnabled(m_overlay_enabled);
    ui.colourize->setEnabled(m_overlay_enabled);
    ui.colourize->setColor(m_overlay_colour);
    ui.colourizeOpacityLabel->setEnabled(m_overlay_enabled);
    ui.colourizeOpacitySlider->setEnabled(m_overlay_enabled);
    ui.colourizeOpacitySlider->setSliderPosition(static_cast< int >(m_overlay_opacity/2.55)-1);

    // Physics
    ui.gravity->setSliderPosition(static_cast< int >(m_gravity * 100));
    ui.friction->setSliderPosition(static_cast< int >(m_friction * 100));
    ui.resitution->setSliderPosition(static_cast < int >(m_restitution * 100));

    // Sound
    ui.soundEnabled->setChecked(m_sound_enabled);
    ui.soundFileLabel->setEnabled(m_sound_enabled);
    ui.soundFile->setEnabled(m_sound_enabled);
    ui.soundFile->setUrl(KUrl::fromPath(m_sound_url));

    // Misc
    ui.autoBounceEnabled->setChecked(m_auto_bounce_enabled);
    ui.autoBounceStrengthLabel->setEnabled(m_auto_bounce_enabled);
    ui.autoBounceStrength->setValue(static_cast < int >(m_auto_bounce_strength));
    ui.autoBounceStrength->setEnabled(m_auto_bounce_enabled);

    parent->addPage(widget, i18n("General"), icon());
    connect(ui.imageUrl, SIGNAL(textChanged(QString)), parent, SLOT(settingsModified()));
    connect(ui.colourizeEnabled, SIGNAL(stateChanged(int)), parent, SLOT(settingsModified()));
    connect(ui.colourize, SIGNAL(changed(QColor)), parent, SLOT(settingsModified()));
    connect(ui.colourizeOpacitySlider, SIGNAL(valueChanged(int)), parent, SLOT(settingsModified()));
    connect(ui.gravity, SIGNAL(valueChanged(int)), parent, SLOT(settingsModified()));
    connect(ui.friction, SIGNAL(valueChanged(int)), parent, SLOT(settingsModified()));
    connect(ui.resitution, SIGNAL(valueChanged(int)), parent, SLOT(settingsModified()));
    connect(ui.soundEnabled, SIGNAL(stateChanged(int)), parent, SLOT(settingsModified()));
    connect(ui.soundFile, SIGNAL(textChanged(QString)), parent, SLOT(settingsModified()));
    connect(ui.autoBounceEnabled, SIGNAL(stateChanged(int)), parent, SLOT(settingsModified()));
    connect(ui.autoBounceStrength, SIGNAL(valueChanged(int)), parent, SLOT(settingsModified()));
    connect(parent, SIGNAL(accepted()), this, SLOT(configurationChanged()));
}

void bballApplet::mousePressEvent(QGraphicsSceneMouseEvent * event)
{
    if (immutability() != Plasma::Mutable)
        return;

    if (m_geometry.isNull())
        syncGeometry();

    // reset timing
    m_timer.stop();
    m_time = QTime();
    update();

    // reset physics
    m_velocity = QVector2D();
    m_angularVelocity = 0;

    // save mouse position
    m_mouseScenePos = event->scenePos();
    m_mousePressed = true;

    event->accept();
}

void bballApplet::mouseReleaseEvent(QGraphicsSceneMouseEvent * event)
{
    if (immutability() != Plasma::Mutable)
        return;

    // TODO: use real timing instead of the fixed step (1/0.025)
    m_velocity = QVector2D(m_mouseScenePos - m_prevMouseScenePos) / 0.025;
    m_mousePressed = false;
    m_timer.start(25, this);
    event->accept();
}

void bballApplet::mouseMoveEvent(QGraphicsSceneMouseEvent * event)
{
    if (immutability() != Plasma::Mutable)
        return;

    m_prevMouseScenePos = m_mouseScenePos;
    m_mouseScenePos = event->scenePos();
    m_geometry.translate(m_mouseScenePos - m_prevMouseScenePos);
    setGeometry(m_geometry);
    event->accept();
}

void bballApplet::timerEvent(QTimerEvent *event)
{
    if (event->timerId() == m_timer.timerId()) {
        updatePhysics();
        return;
    }
    Applet::timerEvent(event);
}

void bballApplet::constraintsEvent(Plasma::Constraints constraints)
{
    if (constraints & Plasma::LocationConstraint)
        m_geometry = QRectF();

    if (constraints & Plasma::FormFactorConstraint)
        setBackgroundHints(NoBackground);

    if (constraints & Plasma::SizeConstraint)
        syncGeometry();
}

void bballApplet::updateScreenRect()
{
    m_screenRect = QDesktopWidget().availableGeometry();
    m_timer.start(25, this);
}

void bballApplet::configurationChanged()
{
    KConfigGroup cg = config();

    // Appearance
    if (KIO::NetAccess::exists(ui.imageUrl->url(), KIO::NetAccess::SourceSide, NULL)) {
        m_image_url = ui.imageUrl->url().path();
        cg.writeEntry("ImgURL", m_image_url);
        m_ballImage.load(m_image_url);
    } else
        KMessageBox::error(0, i18n("The given image could not be loaded. The image will not be changed."));
    m_overlay_enabled = ui.colourizeEnabled->checkState() == Qt::Checked;
    cg.writeEntry("OverlayEnabled", m_overlay_enabled);
    m_overlay_colour = ui.colourize->color();
    cg.writeEntry("OverlayColour", m_overlay_colour);
    m_overlay_opacity = static_cast< int >(ui.colourizeOpacitySlider->value() * 2.55);
    cg.writeEntry("OverlayOpacity", m_overlay_opacity);
    updateScaledBallImage();

    // Physics
    m_gravity = (qreal)ui.gravity->value() / 100.0;
    cg.writeEntry("Gravity", m_gravity);
    m_friction = (qreal)ui.friction->value() / 100.0;
    cg.writeEntry("Friction", 1.0 - m_friction); // RETROCOMP
    m_restitution = ui.resitution->value () / 100.0;
    cg.writeEntry("Resitution", m_restitution);

    // Sound
    m_sound_enabled = ui.soundEnabled->checkState() == Qt::Checked;
    cg.writeEntry("SoundEnabled", m_sound_enabled);
    if (m_sound_enabled) {
        if (KIO::NetAccess::exists(ui.soundFile->url(), KIO::NetAccess::SourceSide, NULL)) {
            m_sound_url = ui.soundFile->url().path();
            cg.writeEntry("SoundURL", m_sound_url);
        } else
            KMessageBox::error(0, i18n("The given sound could not be loaded. The sound will not be changed."));
    }

    // Misc
    m_auto_bounce_enabled = ui.autoBounceEnabled->checkState() == Qt::Checked;
    cg.writeEntry("AutoBounceEnabled", m_auto_bounce_enabled);
    m_auto_bounce_strength = ui.autoBounceStrength->value();
    cg.writeEntry ("AutoBounceStrength", m_auto_bounce_strength);
    if (m_auto_bounce_enabled || m_gravity > 0)
        m_timer.start(25, this);

    // mouse - undo the mouse clicked
    m_mousePressed = false;

    update();
}

void bballApplet::configChanged()
{
    KConfigGroup cg = config ();

    // Appearance
    m_image_url = cg.readEntry("ImgURL", KStandardDirs::locate("data", QLatin1String( "bball/bball.png" )));
    m_overlay_enabled = cg.readEntry("OverlayEnabled", false);
    m_overlay_colour = cg.readEntry("OverlayColour", QColor(Qt::white));
    m_overlay_opacity = cg.readEntry("OverlayOpacity", 0);
    m_ballImage.load(m_image_url);
    updateScaledBallImage();

    // Physics
    m_gravity = cg.readEntry("Gravity", 1.5);
    m_friction = 1.0 - cg.readEntry("Friction", 0.97); // RETROCOMP
    m_restitution = cg.readEntry("Resitution", 0.8);

    // Sound
    m_sound_enabled = cg.readEntry("SoundEnabled", false);
    m_sound_url = cg.readEntry("SoundURL", KStandardDirs::locate ("data", QLatin1String( "bball/bounce.ogg" )));

    // Misc
    m_auto_bounce_enabled = cg.readEntry("AutoBounceEnabled", false);
    m_auto_bounce_strength = cg.readEntry("AutoBounceStrength", 0);
}

void bballApplet::updatePhysics()
{
    // find out the delta-time since the last call
    if (m_time.isNull())
        m_time.start();
    qreal dT = qMin((qreal)m_time.restart() / 1000.0, 0.5);

    // skip progessing if externally moved (disabled because plasma moves this too frequently)
    //if (m_geometry != geometry())
    //    m_geometry = QRectF();

    // skip if dragging
    if (m_mousePressed || m_geometry.isNull() || m_radius < 1)
        return;

    if (m_screenRect.isNull())
        updateScreenRect();

    // add some randomness if autobouncing
    if (m_auto_bounce_enabled && rand() < RAND_MAX/35) {
        m_velocity += QVector2D(
                (rand() - RAND_MAX/2) * m_auto_bounce_strength * 0.0000005,
                (rand() - RAND_MAX/2) * m_auto_bounce_strength * 0.0000005);
    }

    // update velocity and position
    m_velocity += QVector2D(0, (qreal)m_screenRect.height() * m_gravity * dT);
    m_velocity *= (1.0 - 2 * m_friction * dT);
    m_geometry.translate((m_velocity * dT).toPointF());

    // floor
    bool collision = false;
    bool bottom = false;
    if (m_geometry.bottom() >= m_screenRect.bottom() && m_velocity.y() > 0) {
        m_geometry.moveBottom(m_screenRect.bottom());
        m_velocity *= QVector2D(1, -m_restitution);
        m_angularVelocity = m_velocity.x() / m_radius;
        collision = true;
        bottom = true;
    }

    // ceiling
    if (m_geometry.top() <= m_screenRect.top() && m_velocity.y() < 0) {
        m_geometry.moveTop(m_screenRect.top ());
        m_velocity *= QVector2D(1, -m_restitution);
        m_angularVelocity = -m_velocity.x() / m_radius;
        collision = true;
    }

    // right
    if (m_geometry.right() >= m_screenRect.right() && m_velocity.x() > 0) {
        m_geometry.moveRight(m_screenRect.right() - 0.1);
        m_velocity *= QVector2D(-m_restitution, 1);
        m_angularVelocity = -m_velocity.y() / m_radius;
        if (bottom)
            m_velocity.setX(0);
        collision = true;
    }

    // left
    if (m_geometry.left() <= m_screenRect.left() && m_velocity.x() < 0) {
        m_geometry.moveLeft(m_screenRect.left () + 0.1);
        m_velocity *= QVector2D(-m_restitution, 1);
        m_angularVelocity = m_velocity.y() / m_radius;
        if (bottom)
            m_velocity.setX(0);
        collision = true;
    }

    m_angularVelocity *= (0.9999 - 2 * m_friction * dT);
    m_angle += m_angularVelocity * dT;

    // stop animation if reached bottom and still
    if (m_velocity.length() < 10.0 && qAbs(m_angularVelocity) < 0.1 && !m_auto_bounce_enabled) {
        m_timer.stop();
        update();
        return;
    }

    // move this and update graphics
    setGeometry(m_geometry);
    update();

    if (collision)
        playBoingSound();
}

void bballApplet::playBoingSound()
{
    // TODO: throttle based on sound duration, configurable or both?
    if (!m_sound_enabled || m_velocity.isNull() || !m_sound_throttle.hasExpired(300))
        return;

    // create the player if missing
    if (!m_sound_player) {
        m_sound_player = new QDBusInterface("org.kde.kded", "/modules/kaudioplayer", "org.kde.kaudioplayer");
    }

    m_sound_throttle.restart();
    // play the sound
    m_sound_player->call("play", m_sound_url);
}

void bballApplet::syncGeometry()
{
    m_geometry = geometry();
    m_radius = static_cast<int>(geometry().width()) / 2;
    updateScaledBallImage();
}

void bballApplet::updateScaledBallImage()
{
    // regen m_ballPixmap
    m_ballPixmap = QPixmap::fromImage(m_ballImage.scaled(contentSizeHint().toSize()));

    // tint the pixmap if requested
    if (m_overlay_enabled) {
        QPainter p(&m_ballPixmap);
        p.setRenderHint(QPainter::Antialiasing, true);
        p.setPen(Qt::NoPen);
        QColor brush = m_overlay_colour;
        brush.setAlpha(m_overlay_opacity);
        p.setBrush(brush);
        p.drawEllipse(QRectF(0, 0, m_radius * 2, m_radius * 2));
    }
}

#include "moc_bball.cpp"
