/*
* Author: Andrew Robberts
*
* Copyright (C) 2003 Atomic Blue (info@planeshift.it, http://www.atomicblue.org)
*
*
* 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 (version 2 of the License)
* 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.
*
*/

#include <psconfig.h>

#include <csutil/xmltiny.h>
#include <iengine/engine.h>
#include <iengine/material.h>
#include <ivideo/material.h>
#include <iengine/mesh.h>
#include <iengine/movable.h>
#include <iengine/camera.h>
#include <cstool/csview.h>
#include <iengine/region.h>
#include <ivideo/graph2d.h>
#include <iengine/sector.h>
#include <imesh/particle.h>
#include <imesh/objmodel.h>
#include <csutil/flags.h>

#include "effects/pseffectobjtext.h"
#include "effects/pseffectanchor.h"
#include "util/pscssetup.h"
#include "util/log.h"

#define SCALE_X 500.0
#define SCALE_Y 500.0


psEffectObjText::psEffectObjText(iView *parentView)
               : psEffectObj(parentView)
{
}

psEffectObjText::~psEffectObjText()
{
    if (!staticText && meshFact)
        engine->RemoveObject(meshFact);

    if (mat)
        engine->RemoveObject(mat);

    if (tex)
        engine->RemoveObject(tex);
}

bool psEffectObjText::SetText(const char * newText)
{
    if (!sprite)
        return false;

    bool ret = false;
    if (!staticText)
    {
        ret = UnprotectedSetText(newText);
        CalcTextureCoords();
        mesh->GetMeshObject()->SetMaterialWrapper(mat);
    }
    return ret;
}

bool psEffectObjText::SetText(int rows, ...)
{
    if (!sprite || staticText)
        return false;

    csArray<psEffectObjText::EffectTextElement> textElems;

    psEffectObjText::EffectTextElement newElem;
    
    int x = 5;
    int y = 5;

    psEffectObjText::EffectTextRow* row = NULL;

    va_list arg;
    va_start(arg, rows);

    // Loop through all rows
    for(int i = 0; i < rows; i++)
    {
        row =  va_arg( arg, EffectTextRow*);

        // Text and Formatting
        newElem.colour = row->colour;
        newElem.hasOutline = row->hasOutline;
        newElem.hasShadow = row->hasShadow;
        newElem.shadowColour = row->shadowColour;
        newElem.outlineColour = row->outlineColour;
        newElem.text = row->text;
        newElem.align = row->align;

        // Positioning
        font->GetDimensions(newElem.text, newElem.width, newElem.height);
        newElem.x = x;
        newElem.y = y;

        textElems.Push(newElem);
        y += newElem.height;
    }
    va_end(arg);

    //now we have our elements..
    if ( RenderText(textElems) )
    {
    
        CalcTextureCoords();
        mesh->GetMeshObject()->SetMaterialWrapper(mat);
        return true;
    }
    return false;
}

bool psEffectObjText::IsStaticText() const
{
    return staticText;
}

bool psEffectObjText::Load(iDocumentNode *node)
{
    // get the attributes
    name = "";
    materialName = "";
    fontName = "/this/data/ttf/verdana.ttf";
    fontSize = 20;
    text = "";
    staticText = true;
    textColour = csColor(1,1,1);
    antiAlias = false;
    growUpwards = false;
    yOffset = 0.00f;

    csRef<iDocumentAttributeIterator> attribIter = node->GetAttributes();
    while (attribIter->HasNext())
    {
        csRef<iDocumentAttribute> attr = attribIter->Next();
        csString attrName = attr->GetName();
        attrName.Downcase();

        if (attrName == "name")
            name = attr->GetValue();
        else if (attrName == "font")
            fontName = attr->GetValue();
        else if (attrName == "fontquality")
            fontSize = attr->GetValueAsInt();
        else if (attrName == "static")
            staticText = attr->GetValueAsBool();
        else if (attrName == "antialias")
            antiAlias = attr->GetValueAsBool();
        else if (attrName == "growupwards")
            growUpwards = attr->GetValueAsBool();
    }
    if (name == "")
    {
        csReport(psCSSetup::object_reg, CS_REPORTER_SEVERITY_ERROR, "planeshift_effects", "Attempting to create an effect obj with no name.\n");
        return false;
    }

    csRef<iDocumentNode> dataNode = node->GetNode("text");
    if (dataNode)
    {
        text = dataNode->GetContentsValue();
    }

    dataNode = node->GetNode("textColour");
    if (dataNode)
    {
        textColour = csColor(dataNode->GetAttributeValueAsFloat("r")/255.0f, 
                             dataNode->GetAttributeValueAsFloat("g")/255.0f, 
                             dataNode->GetAttributeValueAsFloat("b")/255.0f);
    }

    if (!psEffectObj::Load(node))
        return false;
    
    return PostSetup();
}

bool psEffectObjText::Render(const csVector3 &up)
{
    static unsigned long nextUniqueID = 0;
    csString effectID = "effect_text_";
    effectID += nextUniqueID++;

    // if we don't have static text, then we need a separate mesh for each instance
    if (!staticText)
    {
        CreateMesh();
        UnprotectedSetText(text);
    }

    // create a mesh wrapper from the factory we just created
    mesh = engine->CreateMeshWrapper(meshFact, effectID.GetData());

    // do the up vector
    objUp = up;
    csReversibleTransform rt;
    rt.LookAt(csVector3(up.x, up.z, up.y), csVector3(0,2,1));
    matUp = rt.GetT2O();
    matBase = matUp;

    // common flags
    mesh->GetFlags().Set(CS_ENTITY_NOHITBEAM);
    mesh->SetZBufMode(zFunc);
    mesh->SetRenderPriority(priority);

    // disable culling
    mesh->GetMeshObject()->GetObjectModel()->SetPolygonMeshViscull(0);

    // obj specific
    // create the sprite
    sprite = SCF_QUERY_INTERFACE(mesh->GetMeshObject(), iSprite2DState);

    lastScale = 1.0f;
    lastSpin = 0.0f;

    CalcTextureCoords();

    mesh->GetMeshObject()->SetMaterialWrapper(mat);

    particle = SCF_QUERY_INTERFACE(mesh->GetMeshObject(), iParticle);

    return true;
}

bool psEffectObjText::Update(csTicks elapsed)
{
    if (!anchor || !anchor->IsReady()) // wait for anchor to be ready
        return true;

    life += (float)elapsed;
    while (life > animLength && killTime <= 0)
        life -= animLength;

    if (life >= birth && !isAlive)
        isAlive = true;

    if (isAlive && anchorMesh && anchor->IsReady())
    {
        mesh->GetMovable()->SetSector(anchorMesh->GetMovable()->GetSectors()->Get(0));
        mesh->GetMovable()->SetPosition(anchorMesh->GetMovable()->GetFullPosition());
        if (dir == DT_NONE)
            matBase = anchorMesh->GetMovable()->GetFullTransform().GetT2O();
    }

    if (keyFrames->Length() == 0)
    {
        mesh->GetMovable()->SetPosition(anchorMesh->GetMovable()->GetFullPosition());
    }
    else
    {
        currKeyFrame = FindKeyFrameByTime(life);
        nextKeyFrame = currKeyFrame + 1;
        if (nextKeyFrame >= keyFrames->Length())
            nextKeyFrame = 0;

        // scale
        /////////
        float lerpScale = LERP_KEY(KA_SCALE);
        particle->ScaleBy(lerpScale / lastScale);
        lastScale = lerpScale;

        // position
        ////////////
        csVector3 objOffset = LERP_VEC_KEY(KA_POS);
        csVector3 lerpRot = LERP_VEC_KEY(KA_ROT);
        objOffset = csZRotMatrix3(lerpRot.z) * csYRotMatrix3(lerpRot.y) * csXRotMatrix3(lerpRot.x) *
            csVector3(-objOffset.x, objOffset.y, -objOffset.z);

        if (growUpwards)
        {
            objOffset += csVector3(0.f, yOffset, 0.f);
        }
        
        mesh->GetMovable()->SetPosition(anchorMesh->GetMovable()->GetFullPosition() + objOffset);

        // spin
        ////////
        float lerpSpin = LERP_KEY(KA_SPIN_Y);
        particle->Rotate(lerpSpin-lastSpin);
        lastSpin = lerpSpin;

        // colour
        //////////
        csVector3 lerpColour = LERP_VEC_KEY(KA_COLOUR);
        mesh->GetMeshObject()->SetColor(csColor(lerpColour.x, lerpColour.y, lerpColour.z));
    }
    mesh->GetMovable()->UpdateMove();

    if (killTime <= 0)
        return true; // this effect obj will last forever until some divine intervention

    // age the effect obj
    killTime -= (int)elapsed;
    if (killTime <= 0)
        return false;
    
    return true;
}

psEffectObj * psEffectObjText::Clone() const
{
    psEffectObjText * newObj = new psEffectObjText(view);
    CloneBase(newObj);

    // text specific
    newObj->font = font;
    newObj->fontName = fontName;
    newObj->fontSize = fontSize;
    newObj->staticText = staticText;
    newObj->textColour = textColour;
    newObj->text = text;
    newObj->antiAlias = antiAlias;
    newObj->growUpwards = growUpwards;
    newObj->mat = mat;

    newObj->g3d = g3d;
    newObj->g2d = g2d;
    newObj->txtmgr = txtmgr;
    newObj->texWidth = texWidth;
    newObj->texHeight = texHeight;
    newObj->paddingWidth = paddingWidth;
    newObj->paddingHeight = paddingHeight;

    return newObj;
}

bool psEffectObjText::CreateMesh()
{
    static unsigned int uniqueID = 0;
    csString facName = "effect_text_fac_";
    facName += uniqueID++;
    
    meshFact = engine->CreateMeshFactory ("crystalspace.mesh.object.sprite.2d", facName.GetData());
    region->Add(meshFact->QueryObject());

    return true;
}

bool psEffectObjText::PostSetup()
{
    g3d = CS_QUERY_REGISTRY(psCSSetup::object_reg, iGraphics3D);
    if (!g3d)
    {
        csReport(psCSSetup::object_reg, CS_REPORTER_SEVERITY_ERROR, "planeshift_effects", "Couldn't get iGraphics3D plugin!");
        return false;
    }
    g2d = g3d->GetDriver2D();

    txtmgr = g3d->GetTextureManager();
    if (!txtmgr)
    {
        csReport(psCSSetup::object_reg, CS_REPORTER_SEVERITY_ERROR, "planeshift_effects", "Couldn't get iTextureManager!");
        return false;
    }

    // load the font
    font = g2d->GetFontServer()->LoadFont(fontName, fontSize);

    // if we have static text, then lets create the mesh and set the text here
    if (staticText)
    {
        CreateMesh();
        UnprotectedSetText(text);
    }

    animLength = 10;
    if (keyFrames->Length() > 0)
        animLength += keyFrames->Get(keyFrames->Length()-1)->time;

    return true;
}

bool psEffectObjText::UnprotectedSetText(const char * newText)
{
    text = newText;
    
    csArray<psEffectObjText::EffectTextElement> textElems;

    // build the list of text elements
    size_t a=0;
    psEffectObjText::EffectTextElement newElem;
    int textIndex = 0;

    int x = 5;
    int y = 5;

    csArray<int> colourStack;
    colourStack.Push(g2d->FindRGB((int)(textColour.red*255), (int)(textColour.green*255), (int)(textColour.blue*255)));

    int maxWidth = 5;

    csArray<int> shadowStack;
    csArray<int> outlineStack;

    csArray<int> alignStack;
    alignStack.Push(ETA_LEFT);
    char tag[512] = "";
    char tagParam[512] = "";

    size_t len = text.Length();
    while (a < len)
    {
        bool drawElem = false;
        bool newLine = false;
        newElem.colour = colourStack.Top();
        newElem.hasShadow = (shadowStack.Length() > 0);
        if (newElem.hasShadow)
            newElem.shadowColour = shadowStack.Top();
        newElem.hasOutline = (outlineStack.Length() > 0);
        if (newElem.hasOutline)
            newElem.outlineColour = outlineStack.Top();
        newElem.align = alignStack.Top();

        if (newText[a] == '[' && newText[a+1] != '[')
        {
            // it's a tag
            ++a;
            int tagIndex = 0;
            bool hasTagParam = false;
            tag[0] = '\0';
            tagParam[0] = '\0';
            while (newText[a] != ']' && newText[a+1] != '\0')
            {
                if (newText[a] == ':')
                {
                    hasTagParam = true;
                    tag[tagIndex] = '\0';
                    tagIndex = 0;
                }
                else if (!hasTagParam)
                {
                    tag[tagIndex] = newText[a];
                    ++tagIndex;
                }
                else
                {
                    tagParam[tagIndex] = newText[a];
                    ++tagIndex;
                }
                ++a;
            }
            if (!hasTagParam)
                tag[tagIndex] = '\0';

            if (strcmp(tag, "center") == 0)
            {
                alignStack.Push(ETA_CENTER);
                drawElem = true;
                newLine = false;
            }
            else if (strcmp(tag, "/center") == 0)
            {
                if (alignStack.Top() == ETA_CENTER)
                {
                    alignStack.Pop();
                    drawElem = true;
                    newLine = true;
                }
            }
            else if (strcmp(tag, "left") == 0)
            {
                alignStack.Push(ETA_LEFT);
                drawElem = true;
                newLine = false;
            }
            else if (strcmp(tag, "/left") == 0)
            {
                if (alignStack.Top() == ETA_LEFT && alignStack.Length() > 1)
                {
                    alignStack.Pop();
                    drawElem = true;
                    newLine = true;
                }
            }
            else if (strcmp(tag, "right") == 0)
            {
                alignStack.Push(ETA_RIGHT);
                drawElem = true;
                newLine = false;
            }
            else if (strcmp(tag, "/right") == 0)
            {
                if (alignStack.Top() == ETA_RIGHT)
                {
                    alignStack.Pop();
                    drawElem = true;
                    newLine = true;
                }
            }
            else if (strcmp(tag, "br /") == 0 || strcmp(tag, "br/") == 0)
            {
                drawElem = true;
                newLine = true;
            }
            else if (strcmp(tag, "colour") == 0)
            {
                int r = (HexToInt(tagParam[0]) << 4) + HexToInt(tagParam[1]);
                int g = (HexToInt(tagParam[2]) << 4) + HexToInt(tagParam[3]);
                int b = (HexToInt(tagParam[4]) << 4) + HexToInt(tagParam[5]);
                colourStack.Push(g2d->FindRGB(r, g, b));
                drawElem = true;
                newLine = false;
            }
            else if (strcmp(tag, "/colour") == 0)
            {
                if (colourStack.Length() > 1)
                    colourStack.Pop();
                drawElem = true;
                newLine = false;
            }
            else if (strcmp(tag, "shadow") == 0)
            {
                int r = 0, g = 0, b = 0;
                if (tagParam[0] != '\0')
                {
                    r = (HexToInt(tagParam[0]) << 4) + HexToInt(tagParam[1]);
                    g = (HexToInt(tagParam[2]) << 4) + HexToInt(tagParam[3]);
                    b = (HexToInt(tagParam[4]) << 4) + HexToInt(tagParam[5]);
                }
                shadowStack.Push(g2d->FindRGB(r, g, b));
                drawElem = true;
                newLine = false;
            }
            else if (strcmp(tag, "/shadow") == 0)
            {
                if (shadowStack.Length() > 0)
                    shadowStack.Pop();
                drawElem = true;
                newLine = false;
            }
            else if (strcmp(tag, "outline") == 0)
            {
                int r = 0, g = 0, b = 0;
                if (tagParam[0] != '\0')
                {
                    r = (HexToInt(tagParam[0]) << 4) + HexToInt(tagParam[1]);
                    g = (HexToInt(tagParam[2]) << 4) + HexToInt(tagParam[3]);
                    b = (HexToInt(tagParam[4]) << 4) + HexToInt(tagParam[5]);
                }
                outlineStack.Push(g2d->FindRGB(r, g, b));
                drawElem = true;
                newLine = false;
            }
            else if (strcmp(tag, "/outline") == 0)
            {
                if (outlineStack.Length() > 0)
                    outlineStack.Pop();
                drawElem = true;
                newLine = false;
            }
        }
        else
        {
            if (newText[a] == '[' && newText[a+1] == '[')
                ++a;
            newElem.text += newText[a];
            ++textIndex;
        }
        ++a;

        if (a == len)
        {
            drawElem = true;
            newLine = true;
        }

        if (drawElem)
        {
            newElem.text += '\0';
            int start = 0, end;
            while (newElem.text.GetAt(start) == 9 || newElem.text.GetAt(start) == 10 || newElem.text.GetAt(start) == 13 || newElem.text.GetAt(start) == 32)
                ++start;
            end = start;
            int c=start;
            while (newElem.text.GetData()[c] != '\0')
            {
                if (newElem.text.GetAt(c) != 9 && newElem.text.GetAt(c) != 10 && newElem.text.GetAt(c) != 13 && newElem.text.GetAt(c) != 32)
                    end = c;
                ++c;
            }
            //newElem.text.SetAt(end+1, '\0');
            newElem.text.DeleteAt(0,start);
            textIndex = 0;

            newElem.x = x;
            newElem.y = y;

            font->GetDimensions(newElem.text, newElem.width, newElem.height);

            if (newElem.width > maxWidth)
                maxWidth = newElem.width;

            textElems.Push(newElem);

            if (newLine)
            {
                y += newElem.height;
                x = 5;
            }
            else
                x += newElem.width;
        }
    }

    return RenderText(textElems);
}

/*bool psEffectObjText::PrepareText()
{
    static unsigned int uniqueID = 0;
    csString matName = "effect_text_mat_";
    matName += uniqueID++;

    // setup the material
    if (mat)
        engine->RemoveObject(mat);

    if (tex)
        engine->RemoveObject(tex);

    //calculate dimensions
    int y = 0;
    int maxWidth = 0;

    // create the material
    maxWidth += 10;

    y += 5;
    texWidth = ToPowerOf2(maxWidth);
    texHeight = ToPowerOf2(y);
    paddingWidth = texWidth - maxWidth;
    paddingHeight = texHeight - y;

    csColor transp(51/255.0F,51/255.0F,254/255.0F);
    tex = engine->CreateBlackTexture(matName, texWidth, texHeight, &transp, CS_TEXTURE_3D);
    tex->Register(txtmgr);
    mat = engine->CreateMaterial(matName, tex);

    g3d->SetRenderTarget(mat->GetMaterial()->GetTexture());
    g3d->BeginDraw(CSDRAW_2DGRAPHICS);

    g2d->DrawBox(0, 0, texWidth, texHeight, g2d->FindRGB (51, 51, 254));
    return true;
}*/

bool psEffectObjText::ApplyRow(const EffectTextElement* row)
{
    // draw the text elements
    int textX = row->x;

    if (row->align == ETA_LEFT)
    {
        textX = 5;
    }
    else if (row->align == ETA_CENTER)
    {
        textX = (texWidth-paddingWidth) / 2 - row->width / 2;
    }
    else if (row->align == ETA_RIGHT)
    {
        textX = texWidth - paddingWidth - row->width - 5;
    }

    const char * drawText = row->text;

    if (row->hasOutline)
    {
        // so, um, change this
        g2d->Write(font, textX,   row->y-2, row->outlineColour, -1, drawText, (antiAlias ? 0 : CS_WRITE_NOANTIALIAS));
        g2d->Write(font, textX+2, row->y-2, row->outlineColour, -1, drawText, (antiAlias ? 0 : CS_WRITE_NOANTIALIAS));
        g2d->Write(font, textX+2, row->y,   row->outlineColour, -1, drawText, (antiAlias ? 0 : CS_WRITE_NOANTIALIAS));
        g2d->Write(font, textX+2, row->y+2, row->outlineColour, -1, drawText, (antiAlias ? 0 : CS_WRITE_NOANTIALIAS));
        g2d->Write(font, textX,   row->y+2, row->outlineColour, -1, drawText, (antiAlias ? 0 : CS_WRITE_NOANTIALIAS));
        g2d->Write(font, textX-2, row->y+2, row->outlineColour, -1, drawText, (antiAlias ? 0 : CS_WRITE_NOANTIALIAS));
        g2d->Write(font, textX-2, row->y,   row->outlineColour, -1, drawText, (antiAlias ? 0 : CS_WRITE_NOANTIALIAS));
        g2d->Write(font, textX-2, row->y-2, row->outlineColour, -1, drawText, (antiAlias ? 0 : CS_WRITE_NOANTIALIAS));
    }

    if (row->hasShadow)
        g2d->Write(font, textX+2, row->y+2, row->shadowColour, -1, drawText, (antiAlias ? 0 : CS_WRITE_NOANTIALIAS));

    g2d->Write(font, textX, row->y, row->colour, -1, drawText, (antiAlias ? 0 : CS_WRITE_NOANTIALIAS));

    return true;
}

//Renders an array of text elements to the label's texture
bool psEffectObjText::RenderText(const csArray<psEffectObjText::EffectTextElement>& textElems)
{
    static unsigned int uniqueID = 0;
    csString matName = "effect_text_mat_";
    matName += uniqueID++;
  
    // setup the material
    if (mat)
    engine->RemoveObject(mat);
  
    if (tex)
    engine->RemoveObject(tex);
    
    //calculate dimensions
    int height = 0;
    int maxWidth = 0;
    yOffset = 0.00f;

    for (size_t a=0; a<textElems.Length(); ++a)
    {
        const psEffectObjText::EffectTextElement * elem = &textElems[a];
        maxWidth = elem->width>maxWidth?elem->width:maxWidth;
        height += elem->height;

        if (a > 0)
            yOffset += (float) elem->height;
    }
    yOffset /= SCALE_Y;

    maxWidth += 10; //some magic padding numbers
    height += 5;
    // create the material
    texWidth = ToPowerOf2(maxWidth);
    texHeight = ToPowerOf2(height);
    paddingWidth = texWidth - maxWidth;
    paddingHeight = texHeight - height;

    csColor transp(51/255.0F,51/255.0F,254/255.0F);
    tex = engine->CreateBlackTexture(matName, texWidth, texHeight, &transp, CS_TEXTURE_3D);
    tex->Register(txtmgr);
    mat = engine->CreateMaterial(matName, tex);

    g3d->SetRenderTarget(mat->GetMaterial()->GetTexture());
    g3d->BeginDraw(CSDRAW_2DGRAPHICS);

    g2d->DrawBox(0, 0, texWidth, texHeight, g2d->FindRGB (51, 51, 254));
    

    // draw the text elements
    for (size_t a=0; a<textElems.Length(); ++a)
    {
        const psEffectObjText::EffectTextElement * elem = &textElems[a];

        ApplyRow( elem );

    }

    g3d->FinishDraw();

    return true;
}

void psEffectObjText::CalcTextureCoords()
{
    iColoredVertices* vertices = sprite->GetVertices();
    vertices->SetSize(4);

    float w = ((float)texWidth - (float)paddingWidth);
    float h = ((float)texHeight - (float)paddingHeight);
    float umax = w / (float)texWidth;
    float vmax = h / (float)texHeight;
   
   // w = w / (float)(texWidth > texHeight ? texWidth : texHeight) * 0.5f;
   // h = h / (float)(texWidth > texHeight ? texWidth : texHeight) * 0.5f;

    //Scaling factor chosen only because it makes the name "Harnquist" about as wide as Harnquist.
    w = w/SCALE_X;
    h = h/SCALE_Y; 

    vertices->Get(0).pos.x = -w;
    vertices->Get(0).pos.y = h;
    vertices->Get(1).pos.x = w;
    vertices->Get(1).pos.y = h;
    vertices->Get(2).pos.x = w;
    vertices->Get(2).pos.y = -h;
    vertices->Get(3).pos.x = -w;
    vertices->Get(3).pos.y = -h;

    vertices->Get(0).u = 0;
    vertices->Get(0).v = 0;
    vertices->Get(1).u = umax;
    vertices->Get(1).v = 0;
    vertices->Get(2).u = umax;
    vertices->Get(2).v = vmax;
    vertices->Get(3).u = 0;
    vertices->Get(3).v = vmax;

    vertices->Get(0).color_init.Set(1, 1, 1);
    vertices->Get(1).color_init.Set(1, 1, 1);
    vertices->Get(2).color_init.Set(1, 1, 1);
    vertices->Get(3).color_init.Set(1, 1, 1);
}
