#include "ai.h"
#include "ai.moc"

#include <qlabel.h>
#include <qhbox.h>
#include <qvbox.h>
#include <qlayout.h>
#include <qgrid.h>

#include <klocale.h>

#include "board.h"
#include "base/piece.h"
#include "base/factory.h"


//-----------------------------------------------------------------------------
AIPiece::AIPiece()
    : _p(0)
{}

AIPiece::~AIPiece()
{
    delete _p;
}

void AIPiece::init(const Piece *p, Board *b)
{
    _piece = p;
    _board = b;
    nbRot = p->nbConfigurations() - 1;
    if ( _p==0 ) _p = new Piece;
    reset();
}

void AIPiece::reset()
{
	curPos = 0;
	curRot = 0;
    _p->copy(_piece);
    nbPos = _board->matrix().width() - _piece->size().first + 1;
}

bool AIPiece::increment()
{
	curPos++;
	if ( curPos==nbPos ) {
		if ( curRot==nbRot ) {
            reset();
            return false;
        }
        _p->rotate(true, QPoint(0, 0));
        nbPos = _board->matrix().width() - _p->size().first + 1;
        curRot++;
        curPos = 0;
	}
	return true;
}

bool AIPiece::place()
{
	if ( curRot==3 ) {
        if ( !_board->rotateRight() ) return false;
	} else for (uint i=0; i<curRot; i++)
        if ( !_board->rotateLeft() ) return false;
	curDec = curPos - _board->currentPos().first - _p->min().first;
    if ( curDec!=0 && _board->moveRight(curDec)!=(uint)kAbs(curDec) )
        return false;
	_board->dropDown();
    return !_board->isGameOver();
}

//-----------------------------------------------------------------------------
const AIElement::Data AI::ELEMENT_DATA[NB_ELEMENT_TYPES] = {
    { "occupied lines", false, nbOccupiedLines },
    { "holes",          false, nbHoles         },
    { "spaces",         false, nbSpaces        },
    { "peak-to-peak",   false, peakToPeak      },
    { "mean height",    false, mean            }
};

AI::AI(uint tTime, uint oTime)
    : timer(this), thinkTime(tTime), orderTime(oTime), stopped(false),
      board(0)
{
    elements.setAutoDelete(true);
    settingsChanged();
	connect(&timer, SIGNAL(timeout()), SLOT(timeout()));

    addElement(ELEMENT_DATA[Occupied]);
    addElement(ELEMENT_DATA[PeakToPeak]);
    addElement(ELEMENT_DATA[MeanHeight]);
    addElement(ELEMENT_DATA[Spaces]);
}

void AI::resizePieces(uint size)
{
    uint oldSize = pieces.size();
    for (uint i=size; i<oldSize; i++) delete pieces[i];
    pieces.resize(size);
    for (uint i=oldSize; i<size; i++) pieces[i] = new AIPiece;
}

AI::~AI()
{
	delete board;
    resizePieces(0);
}

void AI::addElement(const AIElement::Data &data)
{
    uint s = elements.size();
    elements.resize(s+1);
    elements.insert(s, new AIElement(data));
}

void AI::initThink()
{
	board->copy(*main);
}

void AI::launch(Board *m)
{
    main = m;
	if ( board==0 )
        board = static_cast<Board *>(bfactory->createBoard(false, 0));

	pieces[0]->init(main->currentPiece(), board);
    if ( pieces.size()==2 )
        pieces[1]->init(main->nextPiece(), board);

    state = Thinking;
    hasBestPoints = false;
	startTimer();
}

void AI::stop()
{
    timer.stop();
    stopped = true;
}

void AI::start()
{
	if (stopped) {
		startTimer();
		stopped = false;
	}
}

void AI::startTimer()
{
	switch (state) {
	case Thinking:     timer.start(thinkTime, true); break;
	case GivingOrders: timer.start(orderTime, true); break;
	}
}

void AI::timeout()
{
	switch (state) {
	case Thinking:
		if ( think() ) state = GivingOrders;
		break;
	case GivingOrders:
		if ( emitOrder() ) return;
		break;
	}

	startTimer();
}

bool AI::emitOrder()
{
    if ( bestRot==3 ) {
		bestRot = 0;
		main->pRotateRight();
	} else if (bestRot) {
		bestRot--;
        main->pRotateLeft();
	} else if ( bestDec>0 ) {
		bestDec--;
        main->pMoveRight();
	} else if ( bestDec<0 ) {
		bestDec++;
        main->pMoveLeft();
	} else {
        main->pDropDown();
        return true;
    }
    return false;
}

bool AI::think()
{
    initThink();
    bool moveOk = true;
    for (uint i=0; i<pieces.size(); i++)
        if ( !pieces[i]->place() ) {
            moveOk = false;
            break;
        }

	if (moveOk) {
        double p = points();
        if ( !hasBestPoints || p>bestPoints
             || (p==hasBestPoints && random.getBool()) ) {
            hasBestPoints = true;
            bestPoints = p;
            bestDec = pieces[0]->dec();
            bestRot = pieces[0]->rot();
        }
    }

    for (uint i=pieces.size(); i>0; i--)
        if ( pieces[i-1]->increment() ) return false;
    return true;
}

double AI::points() const
{
	double pts = 0;
    for (uint i=0; i<elements.size(); i++)
        pts += elements.at(i)->points(*main, *board);
	return pts;
}

KConfigWidget *AI::createConfigWidget()
{
    KConfigWidget *cw = new AIConfig(elements);
    connect(cw->configCollection(), SIGNAL(saved()),
            SLOT(settingsChanged()));
    return cw;
}

void AI::settingsChanged()
{
    int d = KConfigCollection::configValue("thinking depth").toInt();
    resizePieces(d);
    if ( timer.isActive() ) launch(main);
}

int AI::nbOccupiedLines(const Board &, const Board &current)
{
	return current.matrix().height() - current.nbClearLines();
}

int AI::nbHoles(const Board &, const Board &current)
{
	uint nb = 0;
	for (uint i=0; i<current.matrix().width(); i++) {
		for (int j=current.firstColumnBlock(i)-1; j>=0; j--) {
            Grid2D::Coord c(i, j);
			if ( current.matrix()[c]==0 ) nb++;
        }
	}
	return nb;
}

int AI::peakToPeak(const Board &, const Board &current)
{
	int min = current.matrix().height()-1;
	for (uint i=0; i<current.matrix().width(); i++)
		min = kMin(min, current.firstColumnBlock(i));
	return (int)current.firstClearLine()-1 - min;
}

int AI::mean(const Board &, const Board &current)
{
	uint mean = 0;
	for (uint i=0; i<current.matrix().width(); i++)
		mean += current.firstColumnBlock(i);
	return mean / current.matrix().width();
}

int AI::nbSpaces(const Board &main, const Board &current)
{
	int nb = 0;
	int m = mean(main, current);
	for (uint i=0; i<current.matrix().width(); i++) {
		int j = current.firstColumnBlock(i);
		if ( j<m ) nb += m - j;
	}
	return nb;
}

int AI::nbRemoved(const Board &main, const Board &current)
{
	return current.nbRemoved() - main.nbRemoved();
}

//-----------------------------------------------------------------------------
AIElement::AIElement(const Data &data)
    : _trigger(0), _function(data.function)
{
    Q_ASSERT(data.function);

    _coeffName = QString(data.name) + " COEF";
    _coeff = KConfigCollection::item(_coeffName.utf8());
    if (data.triggered) {
        _triggerName = QString(data.name) + " TRIG";
        _trigger = KConfigCollection::item(_triggerName.utf8());
    }
}

AIElement::~AIElement()
{}

double AIElement::points(const Board &main, const Board &current) const
{
    double c = _coeff->configValue().toDouble();
    if ( c==0 ) return 0;
    int v = _function(main, current);
    if (_trigger) {
        int t = _trigger->configValue().toInt();
        if ( v<t ) return 0;
    }
    return c * v;
}

//-----------------------------------------------------------------------------
AIConfig::AIConfig(const QPtrVector<AIElement> &elements)
    : KConfigWidget(i18n("A.I."), "A.I.")
{
    QGridLayout *top = new QGridLayout(this, 3, 2, KDialog::marginHint(),
                                       KDialog::spacingHint());

    QLabel *label = new QLabel(this);
    top->addWidget(label, 0, 0);
    KIntNumInput *in = new KIntNumInput(this);
    in->setRange(0, 100); //#### just to have a slider !
    KConfigItem *set = configCollection()->plug("thinking depth", in);
    set->setProxyLabel(label);
    top->addWidget(in, 0, 1);

    top->addRowSpacing(1, KDialog::spacingHint());

    QGrid *grid = new QGrid(2, this);
    top->addMultiCellWidget(grid, 2, 2, 0, 1);
    for (uint i=0; i<elements.size(); i++)
        addElement(elements.at(i), grid);
}

void AIConfig::addElement(const AIElement *element, QGrid *grid)
{
    QLabel *lab = new QLabel(grid);
	lab->setFrameStyle(QFrame::Panel | QFrame::Plain);

	QVBox *vb = new QVBox(grid);
	vb->setMargin(KDialog::spacingHint());
	vb->setSpacing(KDialog::spacingHint());
	vb->setFrameStyle(QFrame::Panel | QFrame::Plain);
	if ( element->triggered() ) {
         KIntNumInput *trig = new KIntNumInput(vb);
         trig->setRange(0, 100); //#### just to have a slider !
         QString s = element->triggerName();
         configCollection()->plug(s.utf8(), trig);
    }
    KDoubleNumInput *coeff = new KDoubleNumInput(vb);
    coeff->setRange(0, 100); //#### just to have a slider !
    QString s = element->coeffName();
    KConfigItem *set = configCollection()->plug(s.utf8(), coeff);
    set->setProxyLabel(lab);
}
