// Description:
//   Block Model
//
// Copyright (C) 2003 Frank Becker
//
// 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
//
#include <list>
#include <fstream>

#include <Trace.hpp>
#include <Point.hpp>
#include <Timer.hpp>
#include <Tokenizer.hpp>
#include <ResourceManager.hpp>
#include <Audio.hpp>
#include <Quaternion.hpp>

#include "BlockModel.hpp"

//TODO: clean up please
#include <BlockViewBase.hpp>

using namespace std;

/*

Terminology used:
Element/Point: The smallest piece.
Block: Made up of a number of elements.
Plane: A Z-slice of elements
Shaft: Like an elevator shaft; the playing area.

Difficulty:
level 1: 2.0 sec, element count < (width*height*5*1) 
level 2: 1.9 sec, element count < (width*height*5*2)
...

Scoring:
For each locked element: _score += 5+ (_level*5);
For cleared plane(s): _score += (subscore * ((_level+planeCount)/5));

TODO:
Bonus blocks:
- point multiplier
- eraser
erases blocks
- filler
fills planes
- crater
block explodes and removes elements

 */

BlockModel::BlockModel( int w, int h, int d, int level, R250 &r): 
    _width(w), _height(h), _depth(d), _level(level), _r250(r),
    _numBlocks(0),
    _orientation(0,0,1),
    _offset(0,0,d-1), _freefall(false),
    _score(0), _elementCount(0)
{
    LOG_INFO << "New BlockModel:" 
	<< " w=" << _width
	<< " h=" << _height
	<< " d=" << _depth << "\n";

    _planes = new unsigned char*[_depth];
    _planesLockCount = new unsigned char[_depth];
    memset( _planesLockCount, 0, _depth);
    _planesmem = new unsigned char[ _depth*_width*_height];
    memset( _planesmem, 0, _depth*_width*_height);

    for( int i=0; i<_depth; i++)
    {
	_planes[ i] = &(_planesmem[i*_width*_height]);
    }

    _elementListNorm = new ElementList;
    _elementList = new ElementList;
    _tmpElementList = new ElementList;
    _lockedElementList = new ElementList;

    updateDropDelay();
}

bool BlockModel::init( void)
{
    if( !loadBlocks())
    {
	return false;
    }

    addBlock();
    verifyAndAdjust();
    updateNextDrop( _dropDelay);

    return true;
}

BlockModel::~BlockModel()
{
    delete [] _planes;
    delete [] _planesLockCount;
    delete [] _planesmem;

    ElementList::iterator i;
    ElementList::iterator j;
    ElementList::iterator k;

    for( i=_elementList->begin(), 
	 j=_elementListNorm->begin(),
	 k=_tmpElementList->begin(); i!=_elementList->end(); i++, j++, k++)
    {
	delete (*i);
	delete (*j);
	delete (*k);
    }
    _elementList->clear();
    _elementListNorm->clear();
    _tmpElementList->clear();	

    delete _elementList;
    delete _elementListNorm;
    delete _tmpElementList;

    for( i=_lockedElementList->begin(); i!=_lockedElementList->end(); i++)
    {
	delete (*i);
    }
    _lockedElementList->clear();
    delete _lockedElementList;

    for( unsigned int c=0; c<_numBlocks; c++)
    {
	ElementList *el = _blockList[c];

	for( i=el->begin(); i!=el->end(); i++)
	{
	    delete (*i);
	}
	delete el;
    }
}

void BlockModel::addBlock( void)
{
    if( !_numBlocks) return;

    ElementList::iterator i;
    ElementList::iterator j;
    ElementList::iterator k;

    for( i=_elementList->begin(), 
	 j=_elementListNorm->begin(),
	 k=_tmpElementList->begin(); i!=_elementList->end(); i++, j++, k++)
    {
	delete (*i);
	delete (*j);
	delete (*k);
    }
    _elementListNorm->clear();
    _elementList->clear();
    _tmpElementList->clear();	

    int nextBlock = _r250.random()%_numBlocks;
    ElementList *el = _blockList[nextBlock];

    for( i=el->begin(); i!=el->end(); i++)
    {
	Point3Di *p = *i;
	_elementListNorm->insert( _elementListNorm->begin(), new Point3Di(p->x,p->y,p->z));
	_elementList->insert( _elementList->begin(), new Point3Di(p->x,p->y,p->z));
	_tmpElementList->insert( _tmpElementList->begin(), new Point3Di(p->x,p->y,p->z));
    }
    _offset = Point3Di(0,0,_depth-1);
    _orientation = Point3Di(0,0,1);
}

bool BlockModel::loadBlocks( void)
{ 
    string filename = "system/blocks.txt";

    if( ! ResourceManagerS::instance()->selectResource( string(filename)))
    {
	LOG_ERROR << "Unable to open: [" << filename << "]" << endl;
	return false;
    }
    ziStream &infile = ResourceManagerS::instance()->getInputStream();

    LOG_INFO << "Loading block info from [" << filename << "]" << endl;

    string line;
    int linecount = 0;
    while( !getline( infile, line).eof())
    {
	linecount++;
	//		LOG_INFO << "[" <<  line << "]\n";
	if( line[0] == '#') continue;

	Tokenizer  t( line);
	string token = t.next();
	if( token == "Elements")
	{
	    int numElems = atoi( t.next().c_str());
	    if( !readBlock( infile, numElems, linecount))
	    {
		LOG_ERROR << "Error reading blocks. Line: " 
		    << linecount << "\n";
		return false;
	    }
	}
    }
    return true;
}

bool BlockModel::readBlock( ziStream &infile, int numElems, int &linecount)
{
    ElementList *el = new ElementList;
    _blockList[_numBlocks++] = el;

    string line;
    for( int i=0; i<numElems; i++)
    {
	if( getline( infile, line).eof())
	{
	    return false;
	}
	linecount++;

	Tokenizer t( line);

	Point3Di p;
	p.x = atoi( t.next().c_str());
	p.y = atoi( t.next().c_str());
	p.z = atoi( t.next().c_str());

	if( t.tokensReturned() != 3) return false;

	Point3Di *np = new Point3Di(p.x,p.y,p.z);
	el->insert( el->begin(), np);
	//		LOG_INFO << "[" << p.x << "," << p.y << "," << p.z << "]" << endl;
    }
    return true;
}

void BlockModel::updateDropDelay( void)
{
    _dropDelay = 2.1f - (float)_level*0.1;
    if( _dropDelay < 0.1f) _dropDelay = 0.1f;
}

void BlockModel::moveBlock( Direction dir)
{
    switch( dir)
    {
	case eLeft:
	    _offset.x--;
	    break;
	case eRight:
	    _offset.x++;
	    break;
	case eUp:
	    _offset.y++;
	    break;
	case eDown:
	    _offset.y--;
	    break;
	case eOut:
	    _offset.z++;
	    break;
	case eIn:
	    _freefall = true;
	    break;
	default:
	    break;
    }

    if( !verifyAndAdjust())
    {
	//failed verify, undo move
	switch( dir)
	{
	    case eLeft:
		_offset.x++;
		break;
	    case eRight:
		_offset.x--;
		break;
	    case eUp:
		_offset.y--;
		break;
	    case eDown:
		_offset.y++;
		break;
	    case eOut:
	    case eIn:
	    default:
		break;
	}
    }
}

void BlockModel::rotateBlock( const Point3Di &r1, const Point3Di &r2, const Point3Di &r3, const Point3Di &axis)
{
    ElementList::iterator i;
    ElementList::iterator j;
    for( i=_elementList->begin(), j=_tmpElementList->begin(); i!=_elementList->end(); i++, j++)
    {
	Point3Di p1 = **i;
	Point3Di *p2 = *j;

	p2->x = p1.x*r1.x + p1.y*r1.y + p1.z*r1.z;
	p2->y = p1.x*r2.x + p1.y*r2.y + p1.z*r2.z;
	p2->z = p1.x*r3.x + p1.y*r3.y + p1.z*r3.z;
    }

    ElementList *tmp = _elementList;
    _elementList = _tmpElementList;

    if( verifyAndAdjust())
    {
	//accept new element list and complete ptr swap
	_tmpElementList = tmp;

	Quaternion q;
	q.set( 90.0f, Point3D(axis.x,axis.y,axis.z));
	_view->notifyNewRotation( q);
    }
    else
    {
	//reject and restore original ptr
	_elementList = tmp;
    }
}

bool BlockModel::verifyAndAdjust( void)
{
    int minx = 0;
    int maxx = 0;
    int miny = 0;
    int maxy = 0;

    ElementList::iterator i;
    for( i=_elementList->begin(); i!=_elementList->end(); i++)
    {
	Point3Di *p = *i;
	Point3Di a = *p + _offset;

	if( -a.x > minx) minx = -a.x;
	if( a.x >= (_width+maxx)) maxx = (a.x-_width+1);
	if( -a.y > miny) miny = -a.y;
	if( a.y >= (_height+maxy)) maxy = (a.y-_height+1);

	if( a.z < 0) return false;
    }

    if( (minx+miny+maxx+maxy) > 0)
    {
	_offset.x += minx;
	_offset.x -= maxx;
	_offset.y += miny;
	_offset.y -= maxy;
    }

    for( i=_elementList->begin(); i!=_elementList->end(); i++)
    {
	Point3Di *p = *i;
	Point3Di a = *p + _offset;

	if( elementLocked( a)) return false;
    }

    return true;
}

bool BlockModel::canDrop( void)
{
    ElementList::iterator i;
    for( i=_elementList->begin(); i!=_elementList->end(); i++)
    {
	Point3Di *p = *i;
	Point3Di a = *p + _offset;
	a.z--;
	if( a.z < 0) return false; 

	if( elementLocked(a)) return false;
    }
    return true;
}

bool BlockModel::update( void)
{
    if( _freefall)
    {
	if( canDrop())
	{
	    updateNextDrop( 0.5f);
	    _offset.z--;
	}
	else 
	{
	    _freefall = false;
	}
    }
    else
    {
	float now = Timer::getTime();
	if( now >= _nextDrop)
	{
	    if( canDrop())
	    {
		_offset.z--;
	    }
	    else 
	    {
		//lock block
		ElementList::iterator i;
		for( i=_elementList->begin(); i!=_elementList->end(); i++)
		{
		    Point3Di *p = *i;
		    Point3Di a = *p + _offset;
		    if( !lockElement( a))
		    {
			//failed to lock block, game over!
			return false;
		    }
		    _elementCount++;
		    if( _elementCount > (_width*_height*5*_level))
		    {
			_level++;
			updateDropDelay();
			LOG_INFO << "New level is " << _level << "\n";
		    }

		    _score += 5+ (_level*5);
		}
		AudioS::instance()->playSample( "sounds/katoung.wav");
		checkPlanes();

		addBlock();

		_view->notifyNewBlock();

		if( ! verifyAndAdjust())
		{
		    LOG_INFO << "Can't fit new block!\n";
		    return false;
		}
	    }

	    updateNextDrop( _dropDelay);
	}
    }
    return true;
}

void BlockModel::checkPlanes(void)
{
    int planeElems = _width*_height;
    unsigned int subscore = planeElems*_level;
    int planeCount = 0;

    for( int d=0; d<_depth; d++)
    {
	if( _planesLockCount[d] < planeElems) continue;
	if( _planesLockCount[d] == 0) break;

	planeCount++;
	subscore += subscore;

	// collapse
	unsigned char *tmpplane = _planes[d];
	for( int i=d; i<(_depth-1); i++)
	{
	    _planesLockCount[i] = _planesLockCount[i+1];
	    _planes[i] = _planes[i+1];
	}
	_planesLockCount[_depth-1] = 0;
	_planes[_depth-1] = tmpplane;

	memset( tmpplane, 0, planeElems);

	ElementList::iterator i;
	for( i=_lockedElementList->begin(); i!=_lockedElementList->end(); )
	{
	    Point3Di *e = *i;
	    if( e->z == d ) 
	    {
		i = _lockedElementList->erase(i);
		delete e;
	    }
	    else if( e->z > d)
	    {
		e->z--;
		i++;
	    }
	    else
	    {
		i++;
	    }
	}

	//we moved everything down, so do this depth again
	d--;
    }

    switch( planeCount)
    {
	case 0:
	    break;
	case 1:
	    AudioS::instance()->playSample( "sounds/drop1.wav");
	    break;
	case 2:
	    AudioS::instance()->playSample( "sounds/xdoubleplay.wav");
	    break;
	case 3:
	    AudioS::instance()->playSample( "sounds/xtripleplay.wav");
	    break;

	default:
	    AudioS::instance()->playSample( "sounds/xmonsterplay.wav");
	    break;
    }
    _score += (subscore * planeCount);
}

bool BlockModel::lockElement( Point3Di &a)
{
    if( a.z >= _depth) 
    {
	LOG_ERROR << "Out of bounds!\n";
	return false;
    }

    _planes[ a.z][ a.y*_width + a.x]++;

    unsigned char val = _planes[ a.z][ a.y*_width + a.x];
    if( val > 1)
    {
	LOG_ERROR << "Bad locked element! (" 
	    << a.x << "," << a.y << "," << a.z << " = " << (int)val << ")\n";
	return false;
    }

    _planesLockCount[ a.z]++;
    _lockedElementList->insert( _lockedElementList->begin(), new Point3Di(a.x,a.y,a.z));

    return true;
}

bool BlockModel::elementLocked( Point3Di &a)
{
    //elements outside the shaft are never locked
    if( a.z >= _depth) return false;

    unsigned char val = _planes[ a.z][ a.y*_width + a.x];
    return (val > 0);
}

void BlockModel::updateNextDrop( float delta)
{
    _nextDrop = Timer::getTime() + delta; //50000.0f; //delta; //FIXME:
}
