/**
 * This file is a part of Qtpfsgui package.
 * ---------------------------------------------------------------------- 
 * Copyright (C) 2006,2007 Giuseppe Rota
 * 
 *  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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * ---------------------------------------------------------------------- 
 *
 * @author Giuseppe Rota <grota@users.sourceforge.net>
 */

#include "gamma_and_levels.h"
#include <QCloseEvent>
#include <QLinearGradient>
#include <QPainter>
#include <cmath>
#include <cassert>

GammaAndLevels::GammaAndLevels(QWidget *parent, const QImage data) : QDialog(parent) {
	setupUi(this);
	connect(cancelButton,SIGNAL(clicked()),this,SIGNAL(closing()));
	connect(okButton,SIGNAL(clicked()),this,SIGNAL(closing()));
	LUT=new unsigned char[256];
	blackin=0;
	gamma=1.0f;
	whitein=255;
	blackout=0;
	whiteout=255;

	QVBoxLayout *qvl=new QVBoxLayout;
	qvl->setMargin(0);
	qvl->setSpacing(1);

	histogram=new HistogramLDR(this);
	histogram->setData(data);
	gb1=new GrayBar(inputStuffFrame);

	connect(black_in_spinbox,SIGNAL(valueChanged(int)),gb1,SLOT(changeBlack(int)));
	connect(gamma_spinbox,SIGNAL(valueChanged(double)),gb1,SLOT(changeGamma(double)));
	connect(white_in_spinbox,SIGNAL(valueChanged(int)),gb1,SLOT(changeWhite(int)));

	connect(gb1,SIGNAL(black_changed(int)),this,SLOT(updateBlackIn(int)));
	connect(gb1,SIGNAL(gamma_changed(double)),this,SLOT(updateGamma(double)));
	connect(gb1,SIGNAL(white_changed(int)),this,SLOT(updateWhiteIn(int)));
	connect(gb1,SIGNAL(default_gamma_black_white()),this,SLOT(defaultGammaBlackWhiteIn()));

	qvl->addWidget(histogram);
	qvl->addWidget(gb1);
	inputStuffFrame->setLayout(qvl);

	QVBoxLayout *qvl2=new QVBoxLayout;
	qvl2->setMargin(0);
	qvl2->setSpacing(1);
	gb2=new GrayBar(out_levels,true);
	connect(black_out_spinbox,SIGNAL(valueChanged(int)),gb2,SLOT(changeBlack(int)));
	connect(white_out_spinbox,SIGNAL(valueChanged(int)),gb2,SLOT(changeWhite(int)));

	connect(gb2,SIGNAL(black_changed(int)),this,SLOT(updateBlackOut(int)));
	connect(gb2,SIGNAL(white_changed(int)),this,SLOT(updateWhiteOut(int)));
	connect(gb2,SIGNAL(default_black_white()),this,SLOT(defaultBlackWhiteOut()));

	connect(ResetButton,SIGNAL(clicked()),gb1,SLOT(resetvalues()));
	connect(ResetButton,SIGNAL(clicked()),gb2,SLOT(resetvalues()));
	connect(ResetButton,SIGNAL(clicked()),this,SLOT(resetValues()));

	qvl2->addWidget(gb2);
	out_levels->setLayout(qvl2);
}

GammaAndLevels::~GammaAndLevels() {
	delete gb1; delete gb2; delete histogram;
	emit closing();
}

void GammaAndLevels::defaultGammaBlackWhiteIn() {
	qDebug("GammaAndLevels::defaultGammaBlackWhiteIn");
	blackin=0;
	gamma=1.0f;
	whitein=255;
}

void GammaAndLevels::defaultBlackWhiteOut() {
	qDebug("GammaAndLevels::defaultBlackWhiteOut");
	blackout=0;
	whiteout=255;
}

void GammaAndLevels::closeEvent(QCloseEvent *) {
// 	emit closing();
}

void GammaAndLevels::updateBlackIn(int v) {
	qDebug("GammaAndLevels::updateBlackIn");
	black_in_spinbox->setValue(v);
	blackin=v;
	refreshLUT();
}
void GammaAndLevels::updateGamma(double v) {
	qDebug("GammaAndLevels::updateGamma");
	gb1->dont_emit=true;
	gamma_spinbox->setValue(v);
	gamma=v;
	refreshLUT();
}
void GammaAndLevels::updateWhiteIn(int v) {
	qDebug("GammaAndLevels::updateWhiteIn");
	white_in_spinbox->setValue(v);
	whitein=v;
	refreshLUT();
}
void GammaAndLevels::updateBlackOut(int v) {
	qDebug("GammaAndLevels::updateBlackOut");
	black_out_spinbox->setValue(v);
	blackout=v;
	refreshLUT();
}
void GammaAndLevels::updateWhiteOut(int v) {
	qDebug("GammaAndLevels::updateWhiteOut");
	white_out_spinbox->setValue(v);
	whiteout=v;
	refreshLUT();
}

void GammaAndLevels::resetValues() {
	qDebug("GammaAndLevels::resetValues");
	blackin=0;
	gamma=1.0f;
	whitein=255;
	blackout=0;
	whiteout=255;
	gb1->dont_emit=true;
	black_in_spinbox->setValue(0);
	gamma_spinbox->setValue(1);
	white_in_spinbox->setValue(255);
	black_out_spinbox->setValue(0);
	white_out_spinbox->setValue(255);
	refreshLUT();
}

static inline unsigned char clamp( const float v, const unsigned char minV, const unsigned char maxV )
{
    if( v < minV ) return minV;
    if( v > maxV ) return maxV;
    return (unsigned char)v;
}

void GammaAndLevels::refreshLUT() {
	qDebug("refreshLUT");
	//values in 0..1 range
	float bin=(float)blackin/255.0f;
	float win=(float)whitein/255.0f;
	float expgamma=1.0f/gamma;
	for (int i=0; i<256; i++) {
		float value=powf( ( ((float)(i)/255.0f) - bin ) / (win-bin), expgamma);
		LUT[i]=clamp(blackout+value*(whiteout-blackout),0,255);
// 		qDebug("LUT[%d]=%f char=%d",i,(blackout+value*(whiteout-blackout)),LUT[i]);
	}
	qDebug("GammaAndLevels::emitting LUTrefreshed");
	emit LUTrefreshed(LUT);
}
//////////////////////////////////////////////////////////////////////////////////////

HistogramLDR::HistogramLDR(QWidget *parent, int accuracy) : QWidget(parent), accuracy(accuracy){
	P = new float[256];
	this->setSizePolicy(QSizePolicy::MinimumExpanding,QSizePolicy::MinimumExpanding);
	//initialize to 0
	for( int i = 0; i < 256; i++ )
		P[i]=0;
	
}

void HistogramLDR::setData(const QImage &data) {

	if (data.isNull()) {
		for( int i = 0; i < 256; i++ )
			P[i] = 0;
		return;
	}

	//increment bins
	for (int i=0; i<data.width()*data.height(); i+=accuracy) {
		int v=qGray(*((QRgb*)(data.bits())+i));
		assert(v>=0 && v<=255);
		P[v] += 1;
	}

	//find max
	float max=-1;
	for( int i = 0; i < 256; i++ )
		if (P[i]>max)
			max=P[i];

	//normalize to make maxvalue=1
	for( int i = 0; i < 256; i++ )
		P[i] /= max;
}

void HistogramLDR::paintEvent( QPaintEvent * ) {
	QPainter painter(this);
	for (int i=0; i<256; i++) {
		QRectF rf( i*(((float)(this->width()))/255.f), this->height()-(P[i]*(height()/*-2*/)), ((float)(this->width())/255.f), (P[i]*(height()/*-2*/)));
		painter.fillRect(rf,QBrush(Qt::black) );
	}
painter.drawRect(QRect(0,0,width()-1,height()-1));
}

QSize HistogramLDR::sizeHint () const {
	return QSize( 255, 80 );
}
QSize HistogramLDR::minimumSizeHint () const {
	return QSize( 255, 80 );
}
HistogramLDR::~HistogramLDR() {
	delete [] P;
}

//////////////////////////////////////////////////////////////////////////////////////
GrayBar::GrayBar(QWidget *parent, bool two_handles) : QWidget(parent), dont_emit(false) {
	twohandles=two_handles;
	dragging=DRAGNONE;
// 	qDebug("width=%d, height=%d",width(),height());
}

QSize GrayBar::sizeHint () const {
	return QSize( 500, 22 );
}
QSize GrayBar::minimumSizeHint () const {
	return QSize( 400, 22 );
}

void GrayBar::mouseMoveEvent( QMouseEvent * e ) {
	if (dragging==DRAGNONE)
		return;

	//here we have to make sure that we keep the "order": black,(gray),white
	if (dragging==DRAGBLACK) {
		if (e->x()<=whitepos && e->x()>=0) {
			//update graphical position of gray handle
			gammapos=e->x()+(int)(blackgrayratio*(whitepos-e->x()));
			//update graphical position of black handle
			blackpos=e->x();
			update();
		}
		return;
	}
	if (dragging==DRAGWHITE) {
		if (e->x()>=blackpos && e->x()<=width()) {
			//update graphical position of gray handle
			gammapos=e->x()-(int)((1.0f-blackgrayratio)*(e->x()-blackpos));
			//update graphical position of white handle
			whitepos=e->x();
			update();
		}
		return;
	}
	if (dragging==DRAGGRAY) {
		if (e->x()>=blackpos && e->x()<=whitepos) {
			//update graphical position of gray handle
			blackgrayratio=(float)(e->x()-blackpos)/(float)(whitepos-blackpos);
			//update graphical position of white handle
			gammapos=e->x();
			update();
		}
		return;
	}
}

void GrayBar::mousePressEvent( QMouseEvent * e ) {
	dragging=findHandle(e->x(),e->y());
}

void GrayBar::mouseReleaseEvent( QMouseEvent * e) {
	if (dragging==DRAGBLACK) {
		emit black_changed( (int)(255*((float)(blackpos)/(float)(width()))) );
	}
	if (dragging==DRAGWHITE) {
		emit white_changed( (int)(255*((float)(whitepos)/(float)(width()))) );
	}
	if (dragging==DRAGGRAY) {
		float mediumpos = (float)blackpos+ ((float)whitepos-(float)blackpos)/2.0f;
		if (e->x()>mediumpos) {
//exp10f is not defined on MinGW in windows.
#ifdef _GNU_SOURCE
			emit gamma_changed( exp10f( (mediumpos-(float)e->x())/((float)(whitepos)-mediumpos) ) );
		} else {
			emit gamma_changed( exp10f( (mediumpos-(float)e->x())/(mediumpos-(float)(blackpos)) ) );
#else
			emit gamma_changed( powf(10.0f, (mediumpos-(float)e->x())/((float)(whitepos)-mediumpos) ) );
		} else {
			emit gamma_changed( powf(10.0f, (mediumpos-(float)e->x())/(mediumpos-(float)(blackpos)) ) );
#endif
		}
	}
	dragging=DRAGNONE;
	update();
}

GrayBar::draggingT GrayBar::findHandle(int x, int y) {

	QRect black_rect(blackpos-5,1+(height()-1)/2, 10, (height()-1)-(1+(height()-1)/2));
	QRect white_rect(whitepos-5,1+(height()-1)/2, 10, (height()-1)-(1+(height()-1)/2));

	//mouse click belongs to both white and black rects, and there's some space on the left of the black coordinate
	if ( black_rect.contains(x,y,false) && white_rect.contains(x,y,false) && blackpos!=0 ) {
		return DRAGBLACK;
	}
	//mouse click belongs to both white and black rects, and there's some space on the right of the white coordinate
	if ( black_rect.contains(x,y,false) && white_rect.contains(x,y,false) && whitepos!=width() ) {
		return DRAGWHITE;
	}

	//check if we clicked on black handle
	if ( black_rect.contains(x,y,false) ) {
		return DRAGBLACK;
	}

	//check if we clicked on white handle
	if ( white_rect.contains(x,y,false) ) {
		return DRAGWHITE;
	}

	//check if we clicked on gray handle
	if (!twohandles) {
		QRect gray_rect(gammapos-5,1+(height() - 1)/2, 10, (height() - 1)-(1+(height() - 1)/2));
		if ( gray_rect.contains(x,y,false) ) {
			return DRAGGRAY;
		}
	}
	return DRAGNONE;
}

void GrayBar::resizeEvent ( QResizeEvent * ) {
	qDebug("GrayBar::resizeEvent");
	resetvalues();
//this one below does not work, we resetvalues for the time being.
// 	float factor=(float)(e->size().width())/(float)(e->oldSize().width());
// 	qDebug("factor=%f",factor);
// 	blackpos=(int)( (float)blackpos*( factor ) );
// 	whitepos=(int)( (float)whitepos*( factor ) );
// 	gammapos=(int)( (float)gammapos*( factor ) );
// 	update();
}

void GrayBar::paintEvent( QPaintEvent * ) {
	QPainter painter(this);
	QLinearGradient linearGradient(0, height()/2, width(), height()/2);
	linearGradient.setColorAt(0.0, Qt::black);
	linearGradient.setColorAt(0.5, Qt::darkGray);
	linearGradient.setColorAt(1.0, Qt::white);
	painter.setBrush(linearGradient);
	painter.setPen(Qt::NoPen);
	painter.drawRect(QRect(0, 0, width() - 1, (height() - 1)/2));

	painter.setPen(Qt::black);
	//draw black triangle
	static QPoint black_tri[3] = {
		QPoint(blackpos, 1+(height() - 1)/2),
		QPoint(blackpos-5, (height() - 1)),
		QPoint(blackpos+5, (height() - 1)),
	};
	black_tri[0].setX(blackpos);
	black_tri[1].setX(blackpos-5);
	black_tri[2].setX(blackpos+5);
	black_tri[0].setY(1+(height() - 1)/2);
	black_tri[1].setY(height()-1);
	black_tri[2].setY(height()-1);
	painter.setBrush(QBrush(Qt::black));
	painter.drawPolygon(black_tri,3);
	//draw white triangle
	static QPoint white_tri[3] = {
		QPoint(whitepos, 1+(height() - 1)/2),
		QPoint(whitepos-5, (height() - 1)),
		QPoint(whitepos+5, (height() - 1)),
	};
	white_tri[0].setX(whitepos);
	white_tri[1].setX(whitepos-5);
	white_tri[2].setX(whitepos+5);
	white_tri[0].setY(1+(height()-1)/2);
	white_tri[1].setY(height()-1);
	white_tri[2].setY(height()-1);
	painter.setBrush(QBrush(Qt::white));
	painter.drawPolygon(white_tri,3);
	//in case, draw gray triangle
	if (!twohandles) {
		static QPoint gray_tri[3] = {
			QPoint(gammapos, 1+(height() - 1)/2),
			QPoint(gammapos-5, (height() - 1)),
			QPoint(gammapos+5, (height() - 1)),
		};
		gray_tri[0].setX(gammapos);
		gray_tri[1].setX(gammapos-5);
		gray_tri[2].setX(gammapos+5);
		gray_tri[0].setY(1+(height()-1)/2);
		gray_tri[1].setY(height()-1);
		gray_tri[2].setY(height()-1);
		painter.setBrush(QBrush(Qt::darkGray));
		painter.drawPolygon(gray_tri,3);
	}
// 	qDebug("paint width=%d, height=%d",width(),height());
// 	qDebug("blackpos=%d, gammapos=%d, whitepos=%d",blackpos,gammapos,whitepos);
}

void GrayBar::resetvalues() {
	qDebug("GrayBar::resetvalues");
	blackpos=0;
	gammapos=width()/2;
	blackgrayratio=0.5f;
	whitepos=width();

	if (twohandles)
		emit default_black_white();
	else
		emit default_gamma_black_white();
	update();
}

void GrayBar::changeBlack(int v) {
	if ((int)(255*((float)(blackpos)/(float)(width()))) == v)
		return;
	qDebug("GrayBar::changeBlack");
	blackpos=(int) (v*width()/255.0f) < whitepos ? (int) (v*width()/255.0f) : blackpos;
	gammapos=blackpos+(int)(blackgrayratio*(whitepos-blackpos));
	update();
	emit black_changed(v);
}

void GrayBar::changeGamma(double v) {

	float mediumpos = (float)blackpos+ ((float)whitepos-(float)blackpos)/2.0f;
	if (v<1.0f) {
		gammapos=(int)( mediumpos-((float)(whitepos)-mediumpos)*log10f(v) );
	} else {
		gammapos=(int)( mediumpos-(mediumpos-(float)(blackpos))*log10f(v) );
	}
	qDebug("GrayBar::changeGamma %f",v);
	blackgrayratio=(float)(gammapos-blackpos)/(float)(whitepos-blackpos);
	update();
	if (dont_emit) {
		dont_emit=false;
		return;
	}
	emit gamma_changed (v);
}

void GrayBar::changeWhite(int v) {
	if ((int)(255*((float)(whitepos)/(float)(width()))) == v)
		return;
	qDebug("GrayBar::changeWhite, %d", v);
	whitepos=(int) (v*width()/255.0f) > blackpos ? (int) (v*width()/255.0f) : whitepos;
	gammapos=whitepos-(int)((1.0f-blackgrayratio)*(whitepos-blackpos));
	update();
	emit white_changed(v);
}

