/*-----------------------------------------------------------------------------*
| headerfile TlTerrainSceneNode.h                                              |
|                                                                              |
| version 0.10                                                                 |
| date: (26.12.2006)                                                           |
|                                                                              |
| author:  Michal Švantner                                                     |
|                                                                              |
| for Irrlicht engine v. 1.2                                                   |
| tile based shifting terrain scene node                                       |
*-----------------------------------------------------------------------------*/

#ifndef TLTERRAINSCENENODE_H
#define TLTERRAINSCENENODE_H

#include <irrlicht.h>
using namespace irr;

#include "TlMeshBuffer.h"

// structure holding data about point on terrain
typedef struct tag_DataPoint
{
    f32 height;
    core::vector3df normal;
    core::vector2df tcoords[4]; // UV coordinates of tile
} DataPoint;
/*
struct DataPoint
{
    f32 height;
    core::vector3df normal;
    core::vector2df tcoords[4]; // UV coordinates of tile
};
*/
class TlTerrainSceneNode : public scene::ISceneNode
{
    // dinamic array of data points which store info about whole terrain
    DataPoint** data;

    // meshbuffer of visible part of terrain
    TlMeshBuffer* buffer;

    // size of terrain
    core::dimension2d<u32> size;

    // size of single square tile
    f32 tileSize;

    // number of tiles on X and Z axis which are visible
    u32 range;

    // if this pointer is set to some node (camera for example) visible terrain
    // will by centered around it automaticaly
    scene::ISceneNode* fnode;

public:
    // constructor
    // smgr -pointer to scene manager
    // trsize -dimensions of whole terrain
    // visibility -number of tiles which are visible in X and Z directions
    // tlsize -size of single tile
    TlTerrainSceneNode(scene::ISceneManager* smgr, core::dimension2d<u32> trsize,
        u32 visibility, f32 tlsize = 1.0f)
        : scene::ISceneNode(smgr->getRootSceneNode(), smgr, -1)
    {
        size = trsize;

        tileSize = tlsize;

        range = visibility;

        fnode = NULL;

        // create array of data points
        data = new DataPoint*[size.Width+1];
        for(u16 i=0; i<size.Width+1; i++) data[i] = new DataPoint[size.Height+1];
        for(u16 j=0; j<size.Height+1; j++)
            for(u16 i=0; i<size.Width+1; i++)
            {
                data[i][j].height = 0;
                data[i][j].normal = core::vector3df(0,1,0);
                data[i][j].tcoords[0] = core::vector2df(0,1);
                data[i][j].tcoords[1] = core::vector2df(0,0);
                data[i][j].tcoords[2] = core::vector2df(1,0);
                data[i][j].tcoords[3] = core::vector2df(1,1);
            }

        // create buffer where all data about visible part of terrain are stored
        buffer = new TlMeshBuffer(range*2, tileSize);

        // recalculate bounding box
        buffer->recalculateBoundingBox();

        // set some atributes of material used to render terrain
        buffer->Material.AmbientColor = video::SColor(255,255,255,255);
        buffer->Material.DiffuseColor = video::SColor(255,255,255,255);
        buffer->Material.SpecularColor = video::SColor(255,0,0,0);
        buffer->Material.EmissiveColor = video::SColor(255,0,0,0);
        buffer->Material.Shininess = 0;
    }

    // destructor
    ~TlTerrainSceneNode()
    {
        // remove array of data points
        for(u16 i=0; i<size.Width+1; i++) delete [] data[i];
        delete [] data;

        delete buffer;
    }

//    virtual void OnPreRender()
    virtual void OnAnimte(u32 timeMs)
	{
        // if there exist node which visible terrain should be rendered around
        // center it at the position of that node
        if(fnode) centerVisibleTerrain(fnode);

		if (IsVisible)
			SceneManager->registerNodeForRendering(this);

//		ISceneNode::OnPreRender();
		ISceneNode::OnAnimate(timeMs);
	}

	virtual void render()
	{
        video::IVideoDriver* driver = SceneManager->getVideoDriver();

		driver->setMaterial(buffer->Material);
		driver->setTransform(video::ETS_WORLD, AbsoluteTransformation);
		driver->drawIndexedTriangleList
            (&buffer->Vertices[0], buffer->getVertexCount(),
             &buffer->Indices[0], buffer->getIndexCount()/3 );
	}

	// return bounding box of visible part of terrain
	virtual const core::aabbox3d<f32>& getBoundingBox() const
	{
		return buffer->BoundingBox;
	}

    // number of materials used (only one is used)
//	virtual s32 getMaterialCount()
	virtual u32 getMaterialCount()
	{
		return 1;
	}

	// return material used to render visible part of terrain
	virtual video::SMaterial& getMaterial(s32 i)
	{
		return buffer->Material;
	}

	// set height of terrain at given coordinate
	virtual void setHeight(core::vector2d<s32> position, f32 newheight)
	{
        data[position.X][position.Y].height = newheight;
    }

    // return height of terrain at given coordinate
    virtual f32 getHeight(core::vector2d<s32> position)
	{
        return data[position.X][position.Y].height;
    }

    // set normal vector at given coordinate
    virtual void setNormal(core::vector2d<s32> position, core::vector3df newnorm)
    {
        data[position.X][position.Y].normal = newnorm;
    }

    // return normal vector at given coordinate
    virtual core::vector3df getNormal(core::vector2d<s32> position)
    {
        return data[position.X][position.Y].normal;
    }

    // set UV coordinates of tile vertices starting with lower left vertice and
    // going in clockwise order to lower right one
    virtual void setTileUVs(core::vector2d<s32> position, core::vector2df uv0,
        core::vector2df uv1, core::vector2df uv2, core::vector2df uv3)
    {
        // lower left vertex
        data[position.X][position.Y].tcoords[0] = uv0;
        // upper left vertex
        data[position.X][position.Y].tcoords[1] = uv1;
        // upper right vertex
        data[position.X][position.Y].tcoords[2] = uv2;
        // lower right vertex
        data[position.X][position.Y].tcoords[3] = uv3;
    }

    // return position of visible terrain in tiles and relative to whole terrain
    // note that origin of visible terrain is in lower left corner not center
    virtual core::vector2d<u32> getVisibleTerrainPosition()
    {
        return buffer->position;
    }

    // place visible part of terrain at given coordinates of whole terrain
    // note that origin of visible terrain is in lower left corner not center
    virtual void setVisibleTerrainPosition(core::vector2d<u32> newposition)
    {
        // prevent placing out of terrain dimensions
        if(newposition.X < 0) newposition.X = 0;
        if(newposition.Y < 0) newposition.Y = 0;
        if(newposition.X > size.Width - buffer->size)
            newposition.X = size.Width - buffer->size;
        if(newposition.Y > size.Height - buffer->size)
            newposition.Y = size.Height - buffer->size;

        // change only if positions differ
        if(buffer->position != newposition)
        {
            buffer->position = newposition;
            for(u16 j=0; j<buffer->size; j++)
                for(u16 i=0; i<buffer->size; i++)
                {
                    // lower left vertex
                    buffer->tile[i][j].vertex[0]->Pos.X = (buffer->position.X + i) * tileSize;
                    buffer->tile[i][j].vertex[0]->Pos.Z = (buffer->position.Y + j) * tileSize;
                    // upper left vertex
                    buffer->tile[i][j].vertex[1]->Pos.X = (buffer->position.X + i) * tileSize;
                    buffer->tile[i][j].vertex[1]->Pos.Z = (buffer->position.Y + j + 1) * tileSize;
                    // upper right vertex
                    buffer->tile[i][j].vertex[2]->Pos.X = (buffer->position.X + i + 1) * tileSize;
                    buffer->tile[i][j].vertex[2]->Pos.Z = (buffer->position.Y + j + 1) * tileSize;
                    // lower right vertex
                    buffer->tile[i][j].vertex[3]->Pos.X = (buffer->position.X + i + 1) * tileSize;
                    buffer->tile[i][j].vertex[3]->Pos.Z = (buffer->position.Y + j) * tileSize;
                }
            // update at new position
            update();
        }
    }

    // read data from data array to visible part of terrain
    virtual void update()
    {
        // ti and tj are position coordinates of tile on terrain
        // di and dj are position coordinates of data point
        u32 ti, tj, di, dj;

        // acces each tile of visible terrain and adjust it using data array
        for(u16 j=0; j<buffer->size; j++)
            for(u16 i=0; i<buffer->size; i++)
            {
                // find position of tile on terrain
                ti = buffer->position.X + i;
                tj = buffer->position.Y + j;

                // lower left corner of tile
                di = ti;
                dj = tj;
                buffer->tile[i][j].vertex[0]->Pos.Y = data[di][dj].height;
                buffer->tile[i][j].vertex[0]->Normal = data[di][dj].normal;

                // upper left corner of tile
                dj = tj + 1;
                buffer->tile[i][j].vertex[1]->Pos.Y = data[di][dj].height;
                buffer->tile[i][j].vertex[1]->Normal = data[di][dj].normal;

                // upper right corner of tile
                di = ti + 1;
                buffer->tile[i][j].vertex[2]->Pos.Y = data[di][dj].height;
                buffer->tile[i][j].vertex[2]->Normal = data[di][dj].normal;

                // lower right corner of tile
                dj = tj;
                buffer->tile[i][j].vertex[3]->Pos.Y = data[di][dj].height;
                buffer->tile[i][j].vertex[3]->Normal = data[di][dj].normal;
            }

        for(u16 j=0; j<buffer->size; j++)
            for(u16 i=0; i<buffer->size; i++)
            {
                // find position of tile on terrain
                ti = buffer->position.X + i;
                tj = buffer->position.Y + j;

                // texture coordinates of tile
                buffer->tile[i][j].vertex[0]->TCoords = data[ti][tj].tcoords[0];
                buffer->tile[i][j].vertex[1]->TCoords = data[ti][tj].tcoords[1];
                buffer->tile[i][j].vertex[2]->TCoords = data[ti][tj].tcoords[2];
                buffer->tile[i][j].vertex[3]->TCoords = data[ti][tj].tcoords[3];
            }

        // recalculate bounding box
        buffer->recalculateBoundingBox();
    }

    // center part of terrain which is rendered at position in 3d space
    virtual void centerVisibleTerrain(core::vector3df pos)
    {
        pos -= getPosition(); // set relative to terrain position
        core::vector2d<u32> tpos;
        tpos.X = (u32)(pos.X / tileSize) - range;
        tpos.Y = (u32)(pos.Z / tileSize) - range;

        setVisibleTerrainPosition(tpos);
    }

    // center part of terrain which is rendered at position of node
    // can be camera or player node for example
    virtual void centerVisibleTerrain(scene::ISceneNode* node)
    {
        core::vector3df pos = node->getAbsolutePosition();
        pos -= getPosition(); // set relative to terrain position
        core::vector2d<u32> tpos;
        tpos.X = (u32)(pos.X / tileSize) - range;
        tpos.Y = (u32)(pos.Z / tileSize) - range;

        setVisibleTerrainPosition(tpos);
    }

    // set node which visible terrain should be centered around automaticaly
    // can be camera or player node
    virtual void follow(scene::ISceneNode* node)
    {
        fnode = node;
    }

    // stop automatic centering
    virtual void stopFollowing()
    {
        fnode = NULL;
    }

    // get height of terrain at any position
    virtual f32 getHeight(core::vector3df pos)
    {
        // find position relative to mesh
        pos = pos - getPosition();

        if(pos.X>=0 && pos.X<(size.Width)*tileSize && pos.Z>=0 && pos.Z<(size.Height)*tileSize)
        {
            // calculatin coordinates of tile
            s32 x = (s32) (pos.X / tileSize);
            s32 y = (s32) (pos.Z / tileSize);

            // calculating position relative to tile
            f32 xtil = pos.X - (x*tileSize);
            f32 ytil = pos.Z - (y*tileSize);

            // finding trianle of tile
            core::triangle3df triangle;
            if(xtil<=ytil)
            {
                // upper left triangle of tile
                triangle.pointA = core::vector3df(0, getHeight(core::vector2d<s32>(x,y)), 0);
                triangle.pointB = core::vector3df(0, getHeight(core::vector2d<s32>(x,y+1)), tileSize);
                triangle.pointC = core::vector3df(tileSize, getHeight(core::vector2d<s32>(x+1,y+1)), tileSize);
            }
            else
            {
                // lower right triangle of tile
                triangle.pointA = core::vector3df(0, getHeight(core::vector2d<s32>(x,y)), 0);
                triangle.pointB = core::vector3df(tileSize, getHeight(core::vector2d<s32>(x+1,y+1)), tileSize);
                triangle.pointC = core::vector3df(tileSize, getHeight(core::vector2d<s32>(x+1,y)), 0);
            }

            // calculating intersection with triangle
            core::vector3df intersection;
            triangle.getIntersectionWithLine(core::vector3df(xtil,0,ytil),core::vector3df(0,1,0),intersection);

            return intersection.Y + getPosition().Y;
        }
        else
            return 0;
    }

    // this function can be used to check line of sight or bulet collision
    // is slower than getHeight so use it only when appropirate
    // returns an intersection with a 3d line -stored in "outIntersection"
    // return true if line collide with mesh, false if not
    virtual int getIntersectionWithLine( core::line3d<f32> line,  core::vector3df &outIntersection)
    {

        // set position of line relative to mesh
        line.start = line.start - getPosition();
        line.end = line.end - getPosition();

        // find tile in which line starts
        core::vector2d<u32> startTile;
        startTile.X = (u32)(line.start.X/tileSize);
        startTile.Y = (u32)(line.start.Z/tileSize);
        // find tile in which line ends
        core::vector2d<u32> endTile;
        endTile.X = (u32)(line.end.X/tileSize);
        endTile.Y = (u32)(line.end.Z/tileSize);

        // look if start and end tiles are in boundaties of terrain and adjust them if not
        if(startTile.X < 0) startTile.X = 0;
        if(startTile.Y < 0) startTile.Y = 0;
        if(startTile.X > size.Width-1) startTile.X = size.Width-1;
        if(startTile.Y > size.Height-1) startTile.Y = size.Height-1;

        if(endTile.X < 0) endTile.X = 0;
        if(endTile.Y < 0) endTile.Y = 0;
        if(endTile.X > size.Width-1) endTile.X = size.Width-1;
        if(endTile.Y > size.Height-1) endTile.Y = size.Height-1;

        // find area of tiles line cover
        // this will exclude other tiles from calculation and save time
        core::vector2d<s32> areaStart;
        core::vector2d<s32> areaEnd;

        if(startTile.X < endTile.X)
        {
            areaStart.X = startTile.X;
            areaEnd.X = endTile.X;
        }
        else
        {
            areaStart.X = endTile.X;
            areaEnd.X = startTile.X;
        }

        if(startTile.Y < endTile.Y)
        {
            areaStart.Y = startTile.Y;
            areaEnd.Y = endTile.Y;
        }
        else
        {
            areaStart.Y = endTile.Y;
            areaEnd.Y = startTile.Y;
        }

        // look if line collide with triangles of tiles inside area covered by line
        core::triangle3df triangle;
        core::vector3df intersection;
        core::vector3df tmp;    // used to store new intersection before comparing with last one
        bool collision = false; // if intersection occured turned to true
        f32 distanceTmp;        // distance from start of line to new intersection
        f32 distance;           // distance from start of line to last intersection

        for(u16 j=areaStart.Y; j<=areaEnd.Y; j++)
            for(u16 i=areaStart.X; i<=areaEnd.X; i++)
            {
                // upper left triangle of tile
                triangle.pointA = core::vector3df(i*tileSize, data[i][j].height, j*tileSize);
                triangle.pointB = core::vector3df(i*tileSize, data[i][j+1].height, (j+1)*tileSize);
                triangle.pointC = core::vector3df((i+1)*tileSize, data[i+1][j+1].height, (j+1)*tileSize);
                // calculating intersection with triangle
                if(triangle.getIntersectionWithLimitedLine(line, tmp))
                {
                    if(!collision) // if it is the firsth detected intersection
                    {
                        collision = true;
                        intersection = tmp;
                        distance = line.start.getDistanceFromSQ(intersection);
                    }
                    else // else choose intersection which ocured closer to start of line
                    {
                        distanceTmp = line.start.getDistanceFromSQ(tmp);
                        if(distanceTmp < distance)
                        {
                            intersection = tmp;
                            distance = distanceTmp;
                        }
                    }
                }

                // lower right triangle of tile
                triangle.pointA = core::vector3df(i*tileSize, data[i][j].height, j*tileSize);
                triangle.pointB = core::vector3df((i+1)*tileSize, data[i+1][j+1].height, (j+1)*tileSize);
                triangle.pointC = core::vector3df((i+1)*tileSize, data[i+1][j].height, j*tileSize);
                // calculating intersection with triangle
                if(triangle.getIntersectionWithLimitedLine(line, tmp))
                {
                    if(!collision) // if it is the firsth detected intersection
                    {
                        collision = true;
                        intersection = tmp;
                        distance = line.start.getDistanceFromSQ(intersection);
                    }
                    else // else choose intersection which ocured closer to start of line
                    {
                        distanceTmp = line.start.getDistanceFromSQ(tmp);
                        if(distanceTmp < distance)
                        {
                            intersection = tmp;
                            distance = distanceTmp;
                        }
                    }
                }
            }

        if(collision)
        {
            outIntersection = intersection + getPosition();
            return true;
        }
        else
            return false;
    }

    // load texture
    virtual void setTexture(char* filename)
    {
        video::IVideoDriver* driver = SceneManager->getVideoDriver();
        video::IImage* image = driver->createImageFromFile(filename);
        video::ITexture* texture = driver->addTexture("tlttexture", image);
//        buffer->Material.Texture1 = texture;
        buffer->Material.TextureLayer[1].Texture = texture;
    }

    // calculate normals of 6 triangles around point on terrain
    // then calculate normal of that point out of 6 normals of triangles
    virtual void calculateNormal(u32 x, u32 y)
    {
        core::vector3df v0, v1;
        core::vector3df n0, n1, n2, n3, n4, n5;

        // calculate vector to point 0,-1
        if(x>0) // check if not out of data array
            v0 = core::vector3df(0, data[x][y-1].height-data[x][y].height, -tileSize);
        else
            v0 = core::vector3df(0, 0, -tileSize);
        // calculate vector to point -1,-1
        if(x > 0 && y > 0) // check if not out of data array
            v1 = core::vector3df(-tileSize, data[x-1][y-1].height-data[x][y].height, -tileSize);
        else
            v1 = core::vector3df(-tileSize, 0, -tileSize);
        n0 = v0.crossProduct(v1);

        // calculate vector to point -1,-1
        v0 = v1;
        // calculate vector to point -1,0
        if(x > 0)
            v1 = core::vector3df(-tileSize, data[x-1][y].height-data[x][y].height, 0);
        else
            v1 = core::vector3df(-tileSize, 0, 0);
        n1 = v0.crossProduct(v1);

        // calculate vector to point -1,0
        v0 = v1;
        // calculate vector to point 0,1
        if(y < size.Height-1)
            v1 = core::vector3df(0, data[x][y+1].height-data[x][y].height, tileSize);
        else
            v1 = core::vector3df(0, 0, tileSize);
        n2 = v0.crossProduct(v1);

        // calculate vector to point 0,1
        v0 = v1;
        // calculate vector to point 1,1
        if(x < size.Width-1 && y < size.Height-1)
            v1 = core::vector3df(tileSize, data[x+1][y+1].height-data[x][y].height, tileSize);
        else
            v1 = core::vector3df(tileSize, 0, tileSize);
        n3 = v0.crossProduct(v1);

        // calculate vector to point 1,1
        v0 = v1;
        // calculate vector to point 1,0
        if(x < size.Width-1)
            v1 = core::vector3df(tileSize, data[x+1][y].height-data[x][y].height, 0);
        else
            v1 = core::vector3df(tileSize, 0, 0);
        n4 = v0.crossProduct(v1);

        // calculate vector to point 1,0
        v0 = v1;
        // calculate vector to point 0,-1
        if(y > 0)
            v1 = core::vector3df(0, data[x][y-1].height-data[x][y].height, -tileSize);
        else
            v1 = core::vector3df(0, 0, -tileSize);
        n5 = v0.crossProduct(v1);

        // calculate normals of 4 tiles around point
        core::vector3df m0, m1, m2, m3;
        m0 = (n1 - n0) /2 + n0;
        m1 = n2;
        m2 = (n4 - n3) /2 + n3;
        m3 = n5;

        // calculate normals between oposing tiles
        core::vector3df k0, k1;
        k0 = (m2 - m0) /2 + m0;
        k1 = (m3 - m1) /2 + m1;

        // calculate normal of point
        core::vector3df n = (k1 - k0) /2 + k0;
        n.normalize();
        data[x][y].normal = n;
    }

    // recalculare normals of whole terrain making it smooth
    virtual void smoothNormals()
    {
        for(u32 j=0; j<size.Height; j++)
            for(u32 i=0; i<size.Width; i++) calculateNormal(i, j);
    }

    virtual void wireframe(bool i)
    {
        buffer->Material.Wireframe = i;
    }

    virtual void lighting(bool i)
    {
		buffer->Material.Lighting = i;
    }

    virtual void backfaceCulling(bool i)
    {
        buffer->Material.BackfaceCulling = i;
    }

    virtual void fog(bool i)
    {
        buffer->Material.FogEnable = i;
    }
    virtual core::dimension2d<u32> getSize()
    {
        return size;
    }

    virtual f32 getTileSize()
    {
        return tileSize;
    }
};
#endif
