/*
 * knob.cpp - powerful knob-widget
 *
 * Copyright (c) 2004-2011 Tobias Doerffel <tobydox/at/users.sourceforge.net>
 *
 * This file is part of Linux MultiMedia Studio - http://lmms.sourceforge.net
 *
 * 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 (see COPYING); if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301 USA.
 *
 */

#include <QtGui/QApplication>
#include <QtGui/QBitmap>
#include <QtGui/QFontMetrics>
#include <QtGui/QInputDialog>
#include <QtGui/QMouseEvent>
#include <QtGui/QPainter>
#include <QtGui/QPalette>

#ifndef __USE_XOPEN
#define __USE_XOPEN
#endif
#include <math.h>

#include "knob.h"
#include "caption_menu.h"
#include "config_mgr.h"
#include "ControllerConnection.h"
#include "embed.h"
#include "engine.h"
#include "gui_templates.h"
#include "MainWindow.h"
#include "ProjectJournal.h"
#include "song.h"
#include "string_pair_drag.h"
#include "templates.h"
#include "text_float.h"


textFloat * knob::s_textFloat = NULL;



knob::knob( int _knob_num, QWidget * _parent, const QString & _name ) :
	QWidget( _parent ),
	FloatModelView( new FloatModel( 0, 0, 0, 1, NULL, _name, true ), this ),
	m_knobNum( _knob_num ),
	m_label( "" ),
	m_knobPixmap( NULL ),
	m_volumeKnob( false ),
	m_mouseOffset( 0.0f ),
	m_buttonPressed( false ),
	m_magneticDecay( 0 ),
	m_angle( -10 ),
	m_outerColor( NULL )
{
	if( s_textFloat == NULL )
	{
		s_textFloat = new textFloat;
	}

	setWindowTitle( _name );
	
	if( m_knobNum != knobStyled )
	{
		m_knobPixmap = new QPixmap( embed::getIconPixmap( QString( "knob0" +
			QString::number( m_knobNum + 1 ) ).toUtf8().constData() ) );

		setFixedSize( m_knobPixmap->width(), m_knobPixmap->height() );
	}
	setTotalAngle( 270.0f );
	setInnerRadius( 1.0f );
	setOuterRadius( 10.0f );
	setFocusPolicy( Qt::ClickFocus );
	doConnections();
}




knob::~knob()
{
	if( m_knobPixmap )
	{
		delete m_knobPixmap;
	}
}




void knob::setLabel( const QString & _txt )
{
	m_label = _txt;
	if( m_knobPixmap )
	{
		setFixedSize( qMax<int>( m_knobPixmap->width(),
					QFontMetrics( pointSizeF( font(), 6
							) ).width( m_label ) ),
						m_knobPixmap->height() + 10 );
	}
	update();
}




void knob::setTotalAngle( float _angle )
{
	if( _angle < 10.0 )
	{
		m_totalAngle = 10.0;
	}
	else
	{
		m_totalAngle = _angle;
	}

	update();
}




float knob::innerRadius() const
{
	return m_innerRadius;
}



void knob::setInnerRadius( float _r )
{
	m_innerRadius = _r;
}



float knob::outerRadius() const
{
	return m_outerRadius;
}



void knob::setOuterRadius( float _r )
{
	m_outerRadius = _r;
}



QPointF knob::centerPoint() const
{
	return m_centerPoint;
}



float knob::centerPointX() const
{
	return m_centerPoint.x();
}



void knob::setCenterPointX( float _c )
{
	m_centerPoint.setX( _c );
}



float knob::centerPointY() const
{
	return m_centerPoint.y();
}



void knob::setCenterPointY( float _c )
{
	m_centerPoint.setY( _c );
}



float knob::lineWidth() const
{
	return m_lineWidth;
}



void knob::setLineWidth( float _w )
{
	m_lineWidth = _w;
}



QColor knob::outerColor() const
{
	if( m_outerColor )
	{
		return *m_outerColor;
	}
	else
	{
		return QColor();
	}
}



void knob::setOuterColor( const QColor & _c )
{
	if( m_outerColor )
	{
		*m_outerColor = _c;
	}
	else
	{
		m_outerColor = new QColor( _c );
	}
}




QLineF knob::calculateLine( const QPointF & _mid, float _radius, float _innerRadius ) const
{
	const float rarc = m_angle * M_PI / 180.0;
	const float ca = cos( rarc );
	const float sa = -sin( rarc );

	return QLineF( _mid.x() - sa*_innerRadius, _mid.y() - ca*_innerRadius,
					_mid.x() - sa*_radius, _mid.y() - ca*_radius );
}



bool knob::updateAngle()
{
	int angle = 0;
	if( model() && model()->maxValue() != model()->minValue() )
	{
		float a = ( model()->value() - 0.5 * ( model()->minValue() +
						model()->maxValue() ) ) /
				( model()->maxValue() - model()->minValue() ) *
								m_totalAngle;
		angle = static_cast<int>( a ) % 360;
	}
	if( qAbs( angle - m_angle ) > 3 )
	{
		m_angle = angle;
		return true;
	}
	return false;
}




void knob::drawKnob( QPainter * _p )
{
	if( updateAngle() == false && !m_cache.isNull() )
	{
		_p->drawImage( 0, 0, m_cache );
		return;
	}

	m_cache = QImage( size(), QImage::Format_ARGB32 );
	m_cache.fill( qRgba( 0, 0, 0, 0 ) );

	QPainter p( &m_cache );

	QPoint mid;

	if( m_knobNum == knobStyled )
	{
		p.setRenderHint( QPainter::Antialiasing );

		// Perhaps this can move to setOuterRadius()
		if( m_outerColor )
		{
			QRadialGradient gradient( centerPoint(), outerRadius() );
			gradient.setColorAt(0.4, _p->pen().brush().color() );
			gradient.setColorAt(1, *m_outerColor );

			p.setPen( QPen( gradient, lineWidth(),
						Qt::SolidLine, Qt::RoundCap ) );
		}
		else {
			QPen pen = p.pen();
			pen.setWidth( (int) lineWidth() );
			pen.setCapStyle( Qt::RoundCap );
			
			p.setPen( pen );
		}

		p.drawLine( calculateLine( centerPoint(), outerRadius(),
							innerRadius() ) );
		p.end();
		_p->drawImage( 0, 0, m_cache );
		return;
	}


	// Old-skool knobs
	const float radius = m_knobPixmap->width() / 2.0f - 1;
	mid = QPoint( width() / 2, m_knobPixmap->height() / 2 );

	p.drawPixmap( static_cast<int>( 
				width() / 2 - m_knobPixmap->width() / 2 ), 0,
				*m_knobPixmap );

	p.setPen( QPen( QColor( 200, 0, 0 ), 2 ) );
	p.setRenderHint( QPainter::Antialiasing );

	switch( m_knobNum )
	{
		case knobSmall_17:
		{
			p.drawLine( calculateLine( mid, radius ) );
			break;
		}
		case knobBright_26:
		{
			p.drawLine( calculateLine( mid, radius-5 ) );
			break;
		}
		case knobDark_28:
		{
			const float rb = qMax<float>( ( radius - 10 ) / 3.0,
									0.0 );
			const float re = qMax<float>( ( radius - 4 ), 0.0 );
			QLineF ln = calculateLine( mid, re, rb );
			ln.translate( 1, 1 );
			p.drawLine( ln );
			break;
		}
		case knobGreen_17:
		{
			p.setPen( QPen( QColor( 0, 200, 0 ), 2 ) );
			p.drawLine( calculateLine( mid, radius ) );
			break;
		}
	}
	p.end();
	_p->drawImage( 0, 0, m_cache );
}




float knob::getValue( const QPoint & _p )
{
	if( engine::mainWindow()->isShiftPressed() )
	{
		return( ( _p.y() - m_origMousePos.y() ) * model()->step<float>() );
	}
	return( ( _p.y() - m_origMousePos.y() ) * pageSize() );
}




void knob::contextMenuEvent( QContextMenuEvent * )
{
	// for the case, the user clicked right while pressing left mouse-
	// button, the context-menu appears while mouse-cursor is still hidden
	// and it isn't shown again until user does something which causes
	// an QApplication::restoreOverrideCursor()-call...
	mouseReleaseEvent( NULL );

	captionMenu contextMenu( model()->displayName() );
	addDefaultActions( &contextMenu );
	contextMenu.addSeparator();
	contextMenu.addAction( embed::getIconPixmap( "help" ), tr( "&Help" ),
						this, SLOT( displayHelp() ) );
	contextMenu.exec( QCursor::pos() );
}




void knob::dragEnterEvent( QDragEnterEvent * _dee )
{
	stringPairDrag::processDragEnterEvent( _dee, "float_value,"
							"automatable_model" );
}




void knob::dropEvent( QDropEvent * _de )
{
	QString type = stringPairDrag::decodeKey( _de );
	QString val = stringPairDrag::decodeValue( _de );
	if( type == "float_value" )
	{
		model()->setValue( val.toFloat() );
		_de->accept();
	}
	else if( type == "automatable_model" )
	{
		AutomatableModel * mod = dynamic_cast<AutomatableModel *>(
				engine::projectJournal()->
					journallingObject( val.toInt() ) );
		if( mod != NULL )
		{
			AutomatableModel::linkModels( model(), mod );
			mod->setValue( model()->value() );
		}
	}
}




void knob::mousePressEvent( QMouseEvent * _me )
{
	if( _me->button() == Qt::LeftButton &&
			! ( _me->modifiers() & Qt::ControlModifier ) &&
			! ( _me->modifiers() & Qt::ShiftModifier ) )
	{
		model()->prepareJournalEntryFromOldVal();

		const QPoint & p = _me->pos();
		m_origMousePos = p;

		emit sliderPressed();

		QApplication::setOverrideCursor( Qt::BlankCursor );
		s_textFloat->setText( displayValue() );
		s_textFloat->moveGlobal( this,
				QPoint( width() + 2, 0 ) );
		s_textFloat->show();
		m_buttonPressed = true;
	}
	else if( _me->button() == Qt::LeftButton &&
			engine::mainWindow()->isShiftPressed() == true )
	{
		new stringPairDrag( "float_value",
					QString::number( model()->value() ),
							QPixmap(), this );
	}
	else
	{
		FloatModelView::mousePressEvent( _me );
	}
}




void knob::mouseMoveEvent( QMouseEvent * _me )
{
	if( m_buttonPressed == true && _me->pos() != m_origMousePos )
	{
		setPosition( _me->pos() );
		emit sliderMoved( model()->value() );
		QCursor::setPos( mapToGlobal( m_origMousePos ) );
	}

	s_textFloat->setText( displayValue() );
}




void knob::mouseReleaseEvent( QMouseEvent * /* _me*/ )
{
	model()->addJournalEntryFromOldToCurVal();

	m_buttonPressed = false;

	m_mouseOffset = 0;
	emit sliderReleased();

	QApplication::restoreOverrideCursor();

	s_textFloat->hide();
}




void knob::focusOutEvent( QFocusEvent * _fe )
{
	// make sure we don't loose mouse release event
	mouseReleaseEvent( NULL );
	QWidget::focusOutEvent( _fe );
}




void knob::mouseDoubleClickEvent( QMouseEvent * )
{
	enterValue();
}




void knob::paintEvent( QPaintEvent * _me )
{
	QPainter p( this );

	drawKnob( &p );
	if( !m_label.isEmpty() )
	{
		p.setFont( pointSizeF( p.font(), 6 ) );
/*		p.setPen( QColor( 64, 64, 64 ) );
		p.drawText( width() / 2 -
			p.fontMetrics().width( m_label ) / 2 + 1,
				height() - 1, m_label );*/
		p.setPen( QColor( 255, 255, 255 ) );
		p.drawText( width() / 2 -
				p.fontMetrics().width( m_label ) / 2,
				height() - 2, m_label );
	}
}




void knob::wheelEvent( QWheelEvent * _we )
{
	_we->accept();
	const int inc = ( _we->delta() > 0 ) ? 1 : -1;
	model()->incValue( inc );


	s_textFloat->setText( displayValue() );
	s_textFloat->moveGlobal( this, QPoint( width() + 2, 0 ) );
	s_textFloat->setVisibilityTimeOut( 1000 );

	emit sliderMoved( model()->value() );
}




void knob::setPosition( const QPoint & _p )
{
	const float current = model()->value();
	const float next = current - getValue( _p );

	if( model()->initValue() == current )
	{
		if( ++m_magneticDecay > 20 )
		{
			m_magneticDecay = 0;
			model()->setValue( next );
		}

		return;
	}

	const bool current_sign = model()->initValue() - current < 0;
	const bool next_sign = model()->initValue() - next < 0;

	if( current_sign != next_sign )
	{
		model()->setValue( model()->initValue() );
	}
	else
	{
		model()->setValue( next );
	}
}




void knob::enterValue()
{
	bool ok;
	float new_val;
	if( isVolumeKnob() &&
		configManager::inst()->value( "app", "displaydbv" ).toInt() )
	{
		new_val = QInputDialog::getDouble(
			this, windowTitle(),
			tr( "Please enter a new value between "
					"-96.0 dBV and 6.0 dBV:" ),
				20.0 * log10( model()->value() / 100.0 ),
							-96.0, 6.0, 4, &ok );
		if( new_val <= -96.0 )
		{
			new_val = 0.0f;
		}
		else
		{
			new_val = pow( 10.0, ( new_val / 20.0 ) ) * 100.0;
		}
	}
	else
	{
		new_val = QInputDialog::getDouble(
				this, windowTitle(),
				tr( "Please enter a new value between "
						"%1 and %2:" ).
						arg( model()->minValue() ).
						arg( model()->maxValue() ),
					model()->value(),
					model()->minValue(),
					model()->maxValue(), 4, &ok );
	}

	if( ok )
	{
		model()->setValue( new_val );
	}
}




void knob::friendlyUpdate()
{
	if( model()->getControllerConnection() == NULL ||
		model()->getControllerConnection()->getController()->
						frequentUpdates() == false ||
				Controller::runningFrames() % (256*4) == 0 )
	{
		update();
	}
}




QString knob::displayValue() const
{
	if( isVolumeKnob() &&
		configManager::inst()->value( "app", "displaydbv" ).toInt() )
	{
		return m_description.trimmed() + QString( " %1 dBV" ).
				arg( 20.0 * log10( model()->value() / 100.0 ),
								3, 'f', 2 );
	}
	return m_description.trimmed() + QString( " %1" ).
					arg( model()->value() ) + m_unit;
}




void knob::doConnections()
{
	if( model() != NULL )
	{
		QObject::connect( model(), SIGNAL( dataChanged() ),
					this, SLOT( friendlyUpdate() ) );

		QObject::connect( model(), SIGNAL( propertiesChanged() ),
						this, SLOT( update() ) );
	}
}




void knob::displayHelp()
{
	QWhatsThis::showText( mapToGlobal( rect().bottomRight() ),
								whatsThis() );
}




#include "moc_knob.cxx"

