///////////////////////////////////////////////////////////////////////////////////////////
// Device.cpp
//
//  Implementation of the Device class, which wraps a Direct3D 8 device.
//
//  Device implements the standard Direct3D device loss logic. If Paused is true, the app
// should pause, not executing any logic or drawing functions. This represents the case
// that the app is inactive, even if the window has not yet been deactivated.
//
//  Pause should be checked frequently if it has returned true. When it is possible to
// restore the device, the object will first call its Lost event. This is a signal
// to all video-memory objects (primarily unmanaged textures and vertex buffers) to release.
// If this is not done, the app will never recover! To avoid this, release *all* video-
// memory objects in response to this event. The object will reset, then call its 
// Initialized event. At this time, it is safe to recreate video-memory objects.
// Managed textures and vertex buffers are not subject to this constraint, since they
// will be recreated by Direct3D.
//
//  The Created and Destroy events should be trapped by *all* objects holding Direct3D
// objects (managed or otherwise). These objects *must* be released on Destroy, and
// recreated in Create. This handles the app clean shutdown, among other things.

#include "StdAfx.h"
#include "device.h"

#pragma comment(lib, "winmm.lib")

namespace Sunlight
{
    namespace DirectX
    {
        namespace Graphics
        {
            Device::Device(Direct3D *d3d) :
                Width(640), 
                Height(480), 
                BitsPerPixel(16), 
                Windowed(false),
                Direct3DObject(d3d),
                FrameRateFilterTime(100),
                m_bCreated(false)
            {
            }
            
            Device::~Device()
            {
                Destroy();
            }

            void Device::Create()
            {
                Create(false);
            }

            // Perform the device initialisation or reset.
            void Device::Create(bool bReset)
            {
                if (m_bCreated && !bReset)
                    return;

                if (m_pParentWindow == NULL)
                    throw new ArgumentNullException(S"ParentWindow");

                HRESULT                 h;
                D3DPRESENT_PARAMETERS   d3dpp;

                FillD3DPP(&d3dpp);
                if (bReset)
                {
                    if (!Windowed)
                        SetupWindow();

                    h = m_pDevice->Reset(&d3dpp);
                    if (FAILED(h))
                        throw new Sunlight::DirectX::DirectXException(S"IDirect3D8::Reset", h);

                    if (Windowed)
                        SetupWindow();

                    SetupDevice();
                }
                else
                {
                    IDirect3DDevice8 __nogc *pDevice = m_pDevice;
                    HWND                    hWndMain = (HWND)m_pParentWindow->Handle.ToPointer();

                    SetupWindow();

                    h = ((LPDIRECT3D8)Direct3DObject->GetDirect3D())->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWndMain,
                                            D3DCREATE_HARDWARE_VERTEXPROCESSING, &d3dpp, &pDevice);
                    if (FAILED(h))
                    {
                        h = ((LPDIRECT3D8)Direct3DObject->GetDirect3D())->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWndMain,
                            D3DCREATE_MIXED_VERTEXPROCESSING, &d3dpp, &pDevice);
                    }
                    if (FAILED(h))
                    {
                        h = ((LPDIRECT3D8)Direct3DObject->GetDirect3D())->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWndMain,
                            D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &pDevice);
                    }
                    if (FAILED(h))
                        throw new Sunlight::DirectX::DirectXException(S"IDirect3D8::CreateDevice", h);

                    m_pDevice = pDevice;

                    SetupDevice();

                    // Attach to the parent window's Closed event, to shut down the thread.
                    __hook(&System::Windows::Forms::Form::Closed, m_pParentWindow, &Device::OnFormClosed, this);

                    m_bCreated = true;

                    // Created triggered when device is first created
                    __raise Created(this, new EventArgs());
                }

                D3DDISPLAYMODE  mode;
                m_pDevice->GetDisplayMode(&mode);
                if (mode.RefreshRate != 0)
                    m_nFrameRate = m_nRefreshRate = mode.RefreshRate;
                else
                    m_nFrameRate = m_nRefreshRate = 60;             // 60Hz is a reasonable guess

                m_dwFilteredFrameStartTime = m_dwFrameStartTime = timeGetTime();
                m_nFilterFrames = 0;
                
                // Initialized triggered when device is created or reset
                __raise Initialized(this, new EventArgs());
            }

            // Sets the window styles.
            void Device::SetupWindow()
            {
                m_pParentWindow->AutoScale = false;
                if (!Windowed)
                {
                    // We can't change the style using Control::SetStyle, because it's protected. We must
                    // use the Win32 API instead...
                    SetWindowLong((HWND)m_pParentWindow->Handle.ToPointer(), GWL_STYLE, WS_POPUP);

                    // Set up main window to cover the screen.
                    m_pParentWindow->Location = Drawing::Point(0, 0);
                    m_pParentWindow->Size = Drawing::Size(GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN));
                }
                else
                {
                    // Set up main window to be widthxheight, not sizable
                    DWORD   dwStyle = WS_POPUP | WS_CAPTION | WS_BORDER | WS_SYSMENU | WS_MINIMIZEBOX;

                    m_pParentWindow->Hide();
                    //SetWindowLong((HWND)m_pParentWindow->Handle.ToPointer(), GWL_STYLE, dwStyle);

                    int cxScreen = GetSystemMetrics(SM_CXSCREEN);
                    int cyScreen = GetSystemMetrics(SM_CYSCREEN);

                    RECT    r;
                    
                    r.left = (cxScreen - Width) / 2;
                    r.top = (cyScreen - Height) / 2;
                    r.right = r.left + Width;
                    r.bottom = r.top + Height;
                    // Adjust the rectangle to allow for the borders
                    AdjustWindowRect(&r, dwStyle, FALSE);

                    m_pParentWindow->Size = Drawing::Size(r.right - r.left, r.bottom - r.top);
                    m_pParentWindow->Location = Drawing::Point(r.left, r.top);
                }
                m_pParentWindow->Show();
            }
            // Sets the device render states.
            void Device::SetupDevice()
            {
                // Turn off culling
                m_pDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);

                // Turn off D3D lighting
                m_pDevice->SetRenderState(D3DRS_LIGHTING, FALSE);

                // Turn off the zbuffer
                m_pDevice->SetRenderState(D3DRS_ZENABLE, FALSE);

                // Turn on alpha-blending
                m_pDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
                
                // Set the render state up for source alpha blending.
                m_pDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
                m_pDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);

                // Get the alpha information solely from the texture.
                m_pDevice->SetTextureStageState(0, D3DTSS_ALPHAOP,   D3DTOP_SELECTARG1);
                m_pDevice->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);

                // Get the colour information solely from the texture.
                m_pDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
                m_pDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
            }
            // Fills a D3DPRESENT_PARAMETERS for the given parameters.
            void Device::FillD3DPP(D3DPRESENT_PARAMETERS *pd3dpp)
            {
                ZeroMemory(pd3dpp, sizeof(D3DPRESENT_PARAMETERS));
                if (!Windowed)
                {
                    pd3dpp->Windowed = FALSE;
                    pd3dpp->BackBufferCount = 2;
                    pd3dpp->BackBufferFormat = D3DFMT_R5G6B5;
                    pd3dpp->BackBufferWidth = Width;
                    pd3dpp->BackBufferHeight = Height;
                    pd3dpp->FullScreen_RefreshRateInHz = D3DPRESENT_RATE_DEFAULT;
                    pd3dpp->FullScreen_PresentationInterval = D3DPRESENT_INTERVAL_ONE;
                }
                else
                {
                    // Set up the structure used to create the D3DDevice. Most parameters are
                    // zeroed out. We set Windowed to TRUE, since we want to do D3D in a
                    // window, and then set the SwapEffect to "discard", which is the most
                    // efficient method of presenting the back buffer to the display.  And 
                    // we request a back buffer format that matches the default desktop display 
                    // format.
                    pd3dpp->Windowed = TRUE;
                    pd3dpp->BackBufferFormat = Direct3DObject->DefaultDisplayMode.Format;
                }
                pd3dpp->SwapEffect = D3DSWAPEFFECT_DISCARD;
                pd3dpp->hDeviceWindow = (HWND)m_pParentWindow->Handle.ToPointer();
                pd3dpp->EnableAutoDepthStencil = TRUE;
                pd3dpp->AutoDepthStencilFormat = D3DFMT_D16;

                m_nFormat = pd3dpp->BackBufferFormat;
            }
            void Device::Reset()
            {
                __raise Lost(this, new EventArgs());
                Create(true);
            }

            // Called when the ParentWindow is closed.
            void Device::OnFormClosed(Object *sender, EventArgs* e)
            {
                Destroy();
            }

            // Destroys this device and related video-memory objects.
            void Device::Destroy()
            {
                if (m_pDevice != NULL)
                {
                    // Lost triggered when device lost
                    __raise Lost(this, new EventArgs());
                    // Destroy triggered when device released
                    __raise Releasing(this, new EventArgs());
                    m_pDevice->Release();
                    m_pDevice = NULL;

                    ParentWindow->Hide();
                }
                m_bCreated = false;
            }

            // Begin a scene, after which drawing commands may occur.
            void Device::BeginScene()
            {
                Create();
                m_pDevice->BeginScene();
            }

            // End a scene, after which drawing commands may not occur.
            void Device::EndScene()
            {
                m_pDevice->BeginScene();
            }

            // Draw the current scene to the device.
            void Device::Flip()
            {
                DWORD dwFrameEndTime = timeGetTime();

                m_nFrameRate = 1000.0 / (double)(dwFrameEndTime - m_dwFrameStartTime);
                m_dwFrameStartTime = dwFrameEndTime;
                if (m_nFrameRate < 1)
                    m_nFrameRate = 1;
                
                m_nFilterFrames++;
                if ((dwFrameEndTime - m_dwFilteredFrameStartTime) > (DWORD)FrameRateFilterTime)
                {
                    m_nFilteredFrameRate = (1000 * m_nFilterFrames) / (dwFrameEndTime - m_dwFilteredFrameStartTime);
                    m_nFilterFrames = 0;
                    m_dwFilteredFrameStartTime = dwFrameEndTime;
                }

                m_pDevice->Present(NULL, NULL, NULL, NULL);
            }

            // The window which will represent this device.
            System::Windows::Forms::Form *Device::get_ParentWindow()
            {
                return m_pParentWindow;
            }
            void Device::set_ParentWindow(System::Windows::Forms::Form *pParent)
            {
                if (m_pDevice != NULL)
                {
                    Destroy();
                    m_pParentWindow = pParent;
                    Create(false);
                }
                else
                    m_pParentWindow = pParent;
            }
            // The Direct3D device object underlying this object.
            IDirect3DDevice8 *Device::get_Direct3DDevice()
            {
                return m_pDevice;
            }
            // Returns if it is safe for the application to execute its logic and drawing.
            bool Device::get_Paused()
            {
                HRESULT h = m_pDevice->TestCooperativeLevel();

                switch (h)
                {
                case D3D_OK:
                    return false;

                case D3DERR_DEVICELOST:
                    return true;

                case D3DERR_DEVICENOTRESET:
                    Reset();
                    return true;
                }

                throw new DirectXException(S"IDirect3DDevice8::TestCooperativeLevel", h);
            }
            // Returns the format of the device.
            DWORD Device::get_Format()
            {
                return m_nFormat;
            }
            // Returns the current frame rate in frames per second.
            double Device::get_FrameRate()
            {
                return m_nFrameRate;
            }
            // Returns the filtered frame rate in frames per second.
            double Device::get_FilteredFrameRate()
            {
                return m_nFilteredFrameRate;
            }
            // Returns the display refresh rate in Hz.
            int Device::get_RefreshRate()
            {
                return m_nRefreshRate;
            }
            // Returns whether the device has been successfully created.
            bool Device::get_IsCreated()
            {
                return m_bCreated;
            }
        }
    }
}