///////////////////////////////////////////////////////////////////////////////////////////
// SpriteManager.cpp
//
//  Implementation of the SpriteManager class, which handles the drawing of sprites.
//
//  SpriteManager uses an ArrayList of ObjectListElements. An ObjectListElement contains
// texture and vertex information for each sprite. When a sprite is 'drawn' (actually,
// just flagged for drawing) its information is added to the list. When FinishDraw is 
// called, the SpriteManager creates a vertex buffer (if necessary) and copies the
// vertices into it. It then draws the sprites as triangle strips, switching textures
// each time, and empties the list.
//
//  We use video-memory buffers (not managed buffers) since we're not going to use
// the contents of the vertex buffer once we've finished drawing. Therefore, we hook 
// into the Device object's Lost event.
//
//  Finally, some would say I should have used point sprites - but... point sprites
// require a programmable vertex pipeline, which is in software on a pre-GeForce2 card,
// and are therefore unsupported on the majority of older cards. Textured triangles,
// however, are supported in hardware by just about everything. When it's this easy,
// why complain?

#include "StdAfx.h"
#include "SpriteManager.h"
#include "Sprite.h"

using namespace System::Collections;

#define LIST_GRANULARITY    10

namespace Sunlight
{
    namespace DirectX
    {
        namespace Graphics
        {
            SpriteManager::ObjectListElement::ObjectListElement(Sprite *pSprite)
            {
                TextureObject = pSprite->TextureObject;

                SpriteVertex __pin *pVertices = &Vertices[0];

                pSprite->FillVertexArray(pVertices);
                Shadowed = false;
            }

            SpriteManager::ObjectListElement::ObjectListElement(Sprite *pSprite, int nShadowDepth)
            {
                TextureObject = pSprite->TextureObject;

                SpriteVertex __pin *pVertices = &Vertices[0];

                pSprite->FillVertexArray(pVertices);

                if (nShadowDepth > 0)
                {
                    for (int i = 0; i < 4; i++)
                    {
                        pVertices[i].x += nShadowDepth;
                        pVertices[i].y += nShadowDepth;
                        // Note that the alpha part (high byte) of this colour specifies the shadow darkness. FF is black, 0 is transparent.
                        pVertices[i].dwColor = 0xC0000000;
                    }
                    Shadowed = true;
                }
                else
                    Shadowed = false;
            }

            SpriteManager::SpriteManager() : 
                m_pDevice(NULL),
                m_vb(NULL), 
                m_plistObjects(new ArrayList(LIST_GRANULARITY)), 
                m_nVBSize(0)
            {
            }

            SpriteManager::~SpriteManager()
            {
                Destroy();
            }

            void SpriteManager::Destroy()
            {
                if (m_vb != NULL)
                {
                    m_vb->Release();
                    m_vb = NULL;
                }

                m_nVBSize = 0;
            }
            
            void SpriteManager::OnDeviceRelease(Object * /*sender*/, EventArgs * /*e*/)
            {
                Destroy();
            }

            // Draw a Sprite object with this SpriteManager.
            void SpriteManager::Draw(Sprite *sprite)
            {
                // Draw shadow first...
                if (sprite->ShadowDepth > 0)
                    m_plistObjects->Add(new ObjectListElement(sprite, sprite->ShadowDepth));
                m_plistObjects->Add(new ObjectListElement(sprite));
            }
            
            // Complete the drawing of all the Sprite objects.
            void SpriteManager::FinishDraw()
            {
                if (m_pDevice == NULL)
                    throw new ArgumentNullException(S"DeviceObject");
                
                if (!m_pDevice->IsCreated)
                    return;

                IDirect3DDevice8    *dev = m_pDevice->Direct3DDevice;

                if ((m_nVBSize < m_plistObjects->Count) || (m_vb == NULL))
                {
                    if (m_vb != NULL)
                    {
                        m_vb->Release();
                        m_vb = NULL;
                    }

                    // Vertex buffer is too small or not initialized

                    IDirect3DVertexBuffer8 __nogc *pVB;

                    // 10 sprites margin for next time
                    HRESULT h = dev->CreateVertexBuffer((m_plistObjects->Count + LIST_GRANULARITY) * 4 * sizeof(SpriteVertex),
                        D3DUSAGE_WRITEONLY | D3DUSAGE_DYNAMIC, VertexFormat, 
                        D3DPOOL_DEFAULT, &pVB);
                    if (FAILED(h))
                        throw new Sunlight::DirectX::DirectXException(S"IDirect3DDevice8::CreateVertexBuffer", h);

                    m_vb = pVB;
                    
                    m_nVBSize = m_plistObjects->Count + LIST_GRANULARITY;
                }

                if (m_nVBSize == 0)
                    return;

                SpriteVertex *pVertices;

                // Lock the vertex buffer and copy all the vertices into it
                m_vb->Lock(0, 0, (BYTE **)&pVertices, D3DLOCK_DISCARD);
                for (int i = 0; i < m_plistObjects->Count; i++)
                {
                    ObjectListElement *s = static_cast<ObjectListElement *>(m_plistObjects->Item[i]);
                    SpriteVertex __pin *pSrcVertices = &s->Vertices[0];
                    memcpy(pVertices, pSrcVertices, 4 * sizeof(SpriteVertex));
                    pVertices += 4;
                }
                m_vb->Unlock();

                // Set the device to get data from this vertex buffer
                dev->SetStreamSource(0, m_vb, sizeof(SpriteVertex));
                dev->SetVertexShader(VertexFormat);

                // For each sprite, set the texture then draw the vertices
                for (int i = 0; i < m_plistObjects->Count; i++)
                {
                    ObjectListElement *s = static_cast<ObjectListElement *>(m_plistObjects->Item[i]);
                    dev->SetTexture(0, s->TextureObject->Direct3DTexture);
                    if (s->Shadowed)
                    {
                        // Get the alpha information from the texture multiplied by the diffuse colour.
                        dev->SetTextureStageState(0, D3DTSS_ALPHAOP,   D3DTOP_MODULATE);
                        dev->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);
                        dev->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE);

                        // Get the colour information from the inverted alpha.
                        dev->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
                        dev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_ALPHAREPLICATE | D3DTA_COMPLEMENT | D3DTA_TEXTURE);
                    }
                    else
                    {
                        // Get the alpha information from the texture multiplied by the diffuse colour.
                        dev->SetTextureStageState(0, D3DTSS_ALPHAOP,   D3DTOP_SELECTARG1);
                        dev->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);

                        // Get the colour information solely from the texture.
                        dev->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
                        dev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
                    }
                    dev->DrawPrimitive(D3DPT_TRIANGLESTRIP, i * 4, 2);
                }

                // Clear the drawing list for next time
                m_plistObjects->Clear();
            }

            // Abort drawing of all the Sprite objects, without displaying any.
            void SpriteManager::AbortDraw()
            {
                // Clear the drawing list for next time
                m_plistObjects->Clear();
            }

            Device *SpriteManager::get_DeviceObject()
            {
                return m_pDevice;
            }

            void SpriteManager::set_DeviceObject(Device *pDevice)
            {
                if (pDevice == m_pDevice)
                    return;
                
                if (m_vb != NULL)
                    Destroy();

                if (m_pDevice != NULL)
                    // Unhook from the current device
                    __unhook(&Device::Lost, m_pDevice, &SpriteManager::OnDeviceRelease, this);

                m_pDevice = pDevice;
                
                // Hook the device's Lost event so we can release our texture when it is reset
                __hook(&Device::Lost, m_pDevice, &SpriteManager::OnDeviceRelease, this);
            }
        }
    }
}