///Defines the terrain class
module terrain;
import std.stdio;
import syst; //get the camera object
import derelict.opengl.gl;
import derelict.opengl.glu;
import std.string:toString;
import std.format;
import std.stream;
import std.math;

alias std.string.toString strfy; //solve std.Object.toString ambiguity

private static const ubyte PatchSize=64; //must be power of 2
//The following fails because of sqrt
//const ubyte MAX_DEPTH=cast(ubyte)(2*sqrt(PatchSize)+1);
private static const MaxDepth=17;

private struct CamInfo
{
    uint x, y, z;
}

private CamInfo camInfo;

/**
The Terrain engine class.
*/
public final class Terrain
{
    ///The TreeNode class is used to store information about the patch tree
    private final class TreeNode
    {
        public TreeNode lChild, rChild, bNeigh, lNeigh, rNeigh;
        ~this()
        {
            writefln("TreeNode died");
        }
    }

    ///Patch Class. A terrain is composed of several patches
    private final class Patch
    {
        this (ushort x, ushort y)
        {
            this.x=x; this.y=y;
            visible=true;
            rNode=new TreeNode(); //these never get reassigned
            lNode=new TreeNode(); //they're const, remmember?
            assert(rNode !is null);
            assert(lNode !is null);
            //nodes come already clean
            rNode.bNeigh=lNode;
            lNode.bNeigh=rNode;
            calcError();
        }

        ///Resets the patch. It becomes undivided and ready for the next tesselation.
        void reset()
        {
            rNode.rNeigh=rNode.lNeigh=rNode.lChild=rNode.rChild=
            lNode.rNeigh=lNode.lNeigh=lNode.lChild=lNode.rChild=null;
            rNode.bNeigh=lNode;
            lNode.bNeigh=rNode;
            //TODO: check visibility here
            visible=true;
        }

        ///Tesselates this patch and it becomes ready for rendering
        void update()
        {
            cError[]=lError;
            tesselate(lNode, x, cast(ushort)(y+PatchSize), cast(ushort)(x+PatchSize), y, x, y, 1);
            cError[]=rError;
            tesselate(rNode, cast(ushort)(x+PatchSize), y, x, cast(ushort)(y+PatchSize),
                cast(ushort)(x+PatchSize), cast(ushort)(y+PatchSize), 1);
        }

        ///This patch's error measure is updated from the data on hData.
        void calcError()
        {
            Calculate(x, cast(ushort)(y+PatchSize), cast(ushort)(x+PatchSize), y, x, y, 1);
            lError[]=cError; //copy the error data
            Calculate(cast(ushort)(x+PatchSize), y, x, cast(ushort)(y+PatchSize),
                cast(ushort)(x+PatchSize), cast(ushort)(y+PatchSize), 1);
            rError[]=cError;
            return;
        }

        void render()
        {
            glPushMatrix();
            glTranslatef(x, y, 0); //move into camera's position + patch coord
            glBegin(GL_TRIANGLES);
                auxRender(lNode, x, y, 0, PatchSize, PatchSize, 0, 0, 0);
                auxRender(rNode, x, y, PatchSize, 0, 0, PatchSize, PatchSize, PatchSize);
            glEnd();
            glPopMatrix();
            return;
        }

        ~this()
        {
            delete rNode;
            delete lNode;
        }

        //no need to protect this vars because this class won't be accessed from the exterior
        const ushort x, y; //coordinates aren't expected to change
        bool visible; ///Result of lastest cull check
        ushort[MaxDepth] lError, rError;
        const TreeNode rNode, lNode;
    }

    public:
    this(in File file, float detail)
    in
    {
        assert(file !is null);
        assert(detail>=0); //negative error values make no sense. 0 values are purely debugging technices
    }
    body
    {
        debug Log("Creating terrain object");
        this.detail=detail;
        file.read(sizeX);
        file.read(sizeY);
        assert((sizeX>0)&&(sizeY>0));
        HMult=1; //TODO: Replace with resolution from file
        hData=new ushort[][](sizeX, sizeY); //initialize data
        debug Log(" * Loading terrain data...");
        foreach (stripe; hData)
            foreach (inout vertex; stripe)
            {
                file.read(vertex);
                vertex*=HMult;
            }
        maxH=0; //Maximum height present on the map. Used somewhere in debug code
        foreach (stripe; hData)
            foreach (vertex; stripe)
                if (vertex>maxH) maxH=vertex;
        xPatch=cast(ubyte)(sizeX/PatchSize);
        yPatch=cast(ubyte)(sizeY/PatchSize);
        nodes.length=(sizeX*sizeY/PatchSize);
        debug Log(" * Creating terrain nodes ("~strfy(nodes.length)~")...");
        foreach(inout node; nodes) node=new TreeNode;

        debug Log(" * Creating patches...");
        patches.length=xPatch;//how many columns will we have? Patch[col][row]
        foreach (size_t row, inout Patch[] strip; patches)
        {
            assert(strip.length==0, "Oups! Redefining a length of a strip");
            strip.length=yPatch; //How many rows?
            foreach (size_t col, inout Patch patch; strip)
                patch=new Patch(cast(ushort)(col*PatchSize), cast(ushort)(row*PatchSize));
        }
        debug Log("Terrain creation complete");
    }

    public float heightAt(in float x, in float y)
    out(result)
    {
        assert((result>=0.0f)||(result==float.nan));
    }
    body
    {
        if ((x<0.0)||(y<0.0f)) return float.nan;
        //triangulate height at the given point
        return 0.0f;
    }

    ///Makes the terrain ready for rendering. Tesselates all the patches.
    void update()
    in
    {
        assert(focus !is null, "Can't update terrain when focus is null");
    }
    body
    {
        //reset the engine first
        currNode=0; //reset current node
        foreach (size_t row, inout Patch[] strip; patches)
            foreach (size_t col, inout Patch patch; strip)
            {
                patch.reset(); //Recalculate the visibility flag
                if (patch.visible) //bail on non-visible patches
                {
                    if (col>0) patch.lNode.lNeigh=patches[col-1][row].rNode;
                    else patch.lNode.lNeigh=null;
                    if (col<(xPatch-1)) patch.rNode.lNeigh=patches[col+1][row].lNode;
                    else patch.rNode.lNeigh=null;
                    if (row>0) patch.lNode.rNeigh=patches[col][row-1].rNode;
                    else patch.lNode.rNeigh=null;
                    if (row<(yPatch-1)) patch.rNode.rNeigh=patches[col][row+1].lNode;
                    else patch.rNode.rNeigh=null;
                }
            }
        //Unfloatize camera information. Remember the case in which the coords are <0
        camInfo.x=cast(uint)(focus.posX);
        camInfo.y=cast(uint)(focus.posY);
        camInfo.z=cast(uint)(focus.height);
        foreach (Patch[] strip; patches)
            foreach (Patch patch; strip)
                if (patch.visible) patch.update();
        return;
    }

    void render()
    {
        foreach (strip; patches)
            foreach (patch; strip)
                patch.render();
        return;
    }

    ~this()
    {
        /*Since the terrain is the last thing to be deleted when a game is over, a gc.fullCollect()
        will be preformed sometime soon, so it is not necessary to clean up manually*/
        foreach (inout stripe; hData)
        {
            stripe.length=0; delete stripe;
        }
        hData.length=0;
        delete hData;
        foreach (inout Patch[] strip; patches)
        {
            foreach (inout Patch patch; strip)
                delete patch;
            strip.length=0;
            delete strip;
        }
        patches.length=0;
        delete patches;
        foreach(inout node; nodes) delete node;
        nodes.length=0;
        delete nodes;
    }

    private:
    ///Returns the next available TreeNode. Creates a new one if there are no more available.
    TreeNode newNode()
    out(result)
    {
        assert(result !is null, "newNode() attempted to return a null TreeNode.");
    }
    body
    {
        assert(currNode<=nodes.length, "currNode advanced too much");
        if (currNode==nodes.length)
        {
            nodes~=new TreeNode; //no more nodes available, create another one
            debug Log("Insufficient TreeNodes for current tesselation (detail="~strfy(detail)~"). Increasing TreeNode pool (currNode="~strfy(currNode)~").");
        }
        TreeNode ret=nodes[currNode]; //get a new node
        ret.lChild=ret.rChild=ret.bNeigh=ret.lNeigh=ret.rNeigh=null; //clear new node's affiliations
        currNode++;
        return ret;
    }

    ///To be used by functions of the class, returns raw height value.
    ushort heightAt(ushort x, ushort y)
    {
        if ((x==sizeX)||(y==sizeY)) return 0; //Out of bounds values are not drawn (taken has void in game)
        assert(x<sizeX, "Out of bounds value requested");
        assert(y<sizeY, "Out of bounds value requested");
        return hData[x][y];
    }

    ushort Calculate(ushort lX, ushort lY, ushort rX, ushort rY, ushort aX, ushort aY, ushort node)
    {
        //Calculate hipotenuse
        ushort cX=cast(ushort)((rX+lX)/2);
        ushort cY=cast(ushort)((rY+lY)/2);
        ushort cZ=heightAt(cX, cY);
        ushort rZ=heightAt(rX, rY);
        ushort lZ=heightAt(lX, lY);
        ushort errRatio=cast(ushort)abs(cZ-((lZ+rZ)/2)); //this iteraction's primary error ratio
        if (errRatio==0) return 0; //reached perfectness
        if ((abs((rX-lX))>=2)||(abs(rY-lY))>=2) //smaller than 2x2
        {
            //calculate the error of one of the child triangles
            ushort tempErrRatio=Calculate(aX, aY, lX, lY, cX, cY, cast(ushort)(node*2));
            if (errRatio<tempErrRatio) errRatio=tempErrRatio; //error is greater, adopt it
            //and then try the other child
            tempErrRatio=Calculate(rX, rY, aX, aY, cX, cY, cast(ushort)(node*2+1));
            if (errRatio<tempErrRatio) errRatio=tempErrRatio; //error is greater, adopt it
        }
        //TODO: this "if" may not need to be here: required for range check, nothing else
        if (node<MaxDepth) cError[node]=errRatio; //store error ratio as we descend on the tree
        return errRatio; //return this levels error ratio
    }

    ///Splits a TreeNode, used by Patch sub-class
    void split(inout TreeNode myNode)
    in
    {
        assert(myNode !is null, "Attempted to split a null node");
    }
    body
    {
        std.gc.disable();
        scope(exit) std.gc.enable();
        if (myNode.lChild !is null) return; //already split
        //Check to see if current triangle is diamond with bNeigh
        if ((myNode.bNeigh !is null)&&(myNode.bNeigh.bNeigh !is myNode))
        {
            split(myNode.bNeigh); //it has to be split, so that current triangle can be a diamond with it's bNeigh
            //TODO: Since Split never fails (there are always nodes available) this check is unecessary
            //if (myNode.bNeigh.lChild is null) return; //No more nodes available
            assert(myNode.bNeigh.lChild !is null, "Split failed!");
        }

        //asserts that I am a diamond with my bNeigh, or I'm a border node
        //assert ((myNode.bNeigh is null)||(myNode.bNeigh.bNeigh is myNode), "Attempted to split a node that is not part of a diamond");

        myNode.lChild=newNode(); myNode.rChild=newNode(); //Get children
        //with (myNode) //in this block, all the relations of myNode are updated
        {
            myNode.lChild.bNeigh=myNode.lNeigh; myNode.lChild.lNeigh=myNode.rChild;
            myNode.rChild.bNeigh=myNode.rNeigh; myNode.rChild.rNeigh=myNode.lChild;

            if (myNode.lNeigh !is null) //I have a left neigh
            {
                if (myNode.lNeigh.bNeigh is myNode) myNode.lNeigh.bNeigh=myNode.lChild;
                else
                {
                    if (myNode.lNeigh.lNeigh is myNode) myNode.lNeigh.lNeigh=myNode.lChild;
                    else if (myNode.lNeigh.rNeigh is myNode) myNode.lNeigh.rNeigh=myNode.lChild;
                }
            }
            assert((myNode.lChild !is null)&&(myNode.rChild !is null), "Children died at lNeigh update");

            //The following if block causes the children to become null
            if (myNode.rNeigh !is null) //I have a right neigh
            {
                if (myNode.rNeigh.bNeigh is myNode)
                {
                    assert(myNode.lChild !is null);
                    assert(myNode.rChild !is null);
                    assert(myNode.rNeigh !is null);
                    assert(myNode.rNeigh.bNeigh !is null);
                    myNode.rNeigh.bNeigh=myNode.rChild; //This causes the children (both) to die
                    assert(myNode.rNeigh.bNeigh is myNode.rChild);
                    assert(myNode.lChild !is null); //fails
                    assert(myNode.rChild !is null); //fails
                }
                else
                {
                    if (myNode.rNeigh.rNeigh is myNode) myNode.rNeigh.rNeigh=myNode.rChild;
                    else if (myNode.rNeigh.lNeigh is myNode) myNode.rNeigh.lNeigh=myNode.rChild;
                }
            }
            assert((myNode.lChild !is null)&&(myNode.rChild !is null), "Children died at rNeigh update");

            if (myNode.bNeigh !is null) //I have a base neigh
            {
                if (myNode.bNeigh.lChild !is null) //My bNeigh is already split
                {
                    assert(myNode.bNeigh.rChild !is null, "bNeigh is only partially split, cannot update relations.");
                    myNode.bNeigh.lChild.rNeigh=myNode.rChild;
                    myNode.bNeigh.rChild.lNeigh=myNode.lChild;
                    myNode.lChild.rNeigh=myNode.bNeigh.rChild;
                    myNode.rChild.lNeigh=myNode.bNeigh.lChild;
                }
                else split(myNode.bNeigh); //Split my base. He'll take care of Neigh relations
            }
            else //I don't have bNeigh
            {
                myNode.lChild.rNeigh=null;
                myNode.rChild.lNeigh=null;
            }
            assert((myNode.lChild !is null)&&(myNode.rChild !is null), "Children died at bNeigh update");
        } //updates to myNode are over
        assert((myNode.lChild !is null)&&(myNode.rChild !is null), "Node children have been lost!");
        return;
    }

    void tesselate (TreeNode myNode, ushort lX, ushort lY, ushort rX, ushort rY, ushort aX, ushort aY, ushort node)
    in
    {
        assert(myNode !is null, "Attempted to tesselate a null node");
    }
    body
    {
        //Calculate hipotenuse
        ushort cX=cast(ushort)((rX+lX)/2);
        ushort cY=cast(ushort)((rY+lY)/2);
        ushort cZ=heightAt(cX, cY);
        ulong distance=(cX-camInfo.x)*(cX-camInfo.x)+(cY-camInfo.y)*(cY-camInfo.y)+(cZ-camInfo.z)*(cZ-camInfo.z);
        ulong threshold=cError[node]*cError[node];

        if ((threshold*detail)>distance) //should this triangle be split?
        {
            split(myNode); //won't fail because TreeNodes will not go out
            //I have children and they aren't too small,
            //TODO: Unnecessary check: split never fails: (myNode.lChild !is null)&&(myNode.rChild !is null)&&
            if ((node<cast(ushort)(MaxDepth/2))&&((abs(lX-rX)>=2)||(abs(lY-rY)>=2)))
            {
                tesselate(myNode.lChild, aX, aY, lX, lY, cX, cY, cast(ushort)(node*2));
                tesselate(myNode.rChild, rX, rY, aX, aY, cX, cY, cast(ushort)(node*2+1));
            }
        }
    }

    void auxRender(TreeNode myNode, ushort fX, ushort fY, ushort lX, ushort lY, ushort rX, ushort rY, ushort aX, ushort aY)
    in
    {
        assert(myNode !is null, "Cannot render null tree node");
    }
    body
    {
        if ((myNode.lChild !is null)&&(myNode.rChild !is null)) //I'm split
        {
            ushort cX=cast(ushort)((lX+rX)/2);
            ushort cY=cast(ushort)((lY+rY)/2);
            auxRender(myNode.lChild, fX, fY, aX, aY, lX, lY, cX, cY);
            auxRender(myNode.rChild, fX, fY, rX, rY, aX, aY, cX, cY);
        }
        else
        {
            static float cor;
            //cor=heightAt(cast(ushort)(aX+fX), cast(ushort)(aY+fY))/maxH;
            //glColor3f(cor, cor, cor);
            glVertex3f(aX, aY, /*heightAt(cast(ushort)(aX+fX), cast(ushort)(aY+fY))*/0);

            //cor=heightAt(cast(ushort)(rX+fX), cast(ushort)(rY+fY))/maxH;
            //glColor3f(cor, cor, cor);
            glVertex3f(rX, rY, /*heightAt(cast(ushort)(rX+fX), cast(ushort)(rY+fY))*/0);

            //cor=heightAt(cast(ushort)(lX+fX), cast(ushort)(lY+fY))/maxH;
            //glColor3f(cor, cor, cor);
            glVertex3f(lX, lY, /*heightAt(cast(ushort)(lX+fX), cast(ushort)(lY+fY))*/0);
        }
        return;
    }

    const float maxH;
    float detail; ///Detail Threshold
    TreeNode nodes[]; ///Node pool
    size_t currNode; ///Current node being assigned
    Texture textures[]; ///Stores the Textures required by the terrain
    const ushort sizeX, sizeY;
    const ubyte xPatch, yPatch;
    ushort hData[][]; ///The Height Data
    static ushort[MaxDepth] cError; //error buffer
    const ubyte HMult; ///Height multiplier, determines the resolution of the terrain
    Patch patches[][]; ///The pathes that compose the terrain.
    //const must be commented due to bug in DMD
}

///Public terrain handler
public Terrain hTerrain;
