#include <cstddef>
#include "app.h"
#include "util.h"

#include "StaticMesh.h"

struct SceneUniformBuffer
{
    DKMatrix4 projectionMatrix;
    DKMatrix4 modelMatrix;
    DKMatrix4 viewMatrix;
};

class DKGLDemo : public SampleApp
{
    DKObject<DKWindow> window;
    DKObject<DKThread> renderThread;
    DKAtomicNumber32 runningRenderThread;
    DKObject<StaticMesh> sampleMesh;

    DKObject<DKGpuBuffer> sceneUniformBuffer;
    SceneUniformBuffer* sceneBufferData = nullptr;

public:
    void LoadResource()
    {
        DKString vertexShaderPath
            = resourcePool.ResourceFilePath("shaders/ObjMeshShader/objmesh.vert.spv");
        DKString fragMentShaderPath
            = resourcePool.ResourceFilePath("shaders/ObjMeshShader/objmesh.frag.spv");
        DKString texturePath
            = resourcePool.ResourceFilePath("meshes/VikingRoom/viking_room.png");

        sampleMesh->LoadShaderFromFile(resourcePool, vertexShaderPath, DKShaderStage::Vertex);
        sampleMesh->LoadShaderFromFile(resourcePool, fragMentShaderPath, DKShaderStage::Fragment);
        sampleMesh->LoadTextureFromFile(resourcePool, texturePath);
    }

    void LoadRenderResource(DKObject<DKCommandQueue> queue)
    {
        DKLog("Loading Static Meshes");
        DKString path = resourcePool.ResourceFilePath("meshes/VikingRoom/viking_room.obj");
        sampleMesh->LoadMeshResourceFromFile(queue->Device(), resourcePool, path);
        sampleMesh->LoadRenderResourceShader(queue->Device(), DKShaderStage::Vertex);
        sampleMesh->LoadRenderResourceShader(queue->Device(), DKShaderStage::Fragment);
        sampleMesh->LoadRenderResourceTexture(queue);
    }

    void SetupPipeline(DKRenderPipelineDescriptor& pipelineDesc)
    {
        sampleMesh->SetupPipelineDecriptor(pipelineDesc);
    }

    void SetupMaterial(DKObject<DKGraphicsDevice> device)
    {
        sampleMesh->SetupMaterial(device);
    }

    void SetupSceneUniformBuffer(DKObject<DKGraphicsDevice> device)
    {
        // should Go to Scene Uniform Buffer
        sceneUniformBuffer = device->CreateBuffer(sizeof(SceneUniformBuffer)
            , DKGpuBuffer::StorageModeShared, DKCpuCacheModeReadWrite);

        sceneBufferData = reinterpret_cast<SceneUniformBuffer*>(sceneUniformBuffer->Contents());
        sceneBufferData->projectionMatrix = DKMatrix4::identity;
        sceneBufferData->modelMatrix = DKMatrix4::identity;
        sceneBufferData->viewMatrix = DKMatrix4::identity;
        sceneUniformBuffer->Flush();

        //DKAffineTransform3 trans;
        //trans.Multiply(DKLinearTransform3().Scale(0.5).Rotate(DKVector3(0,1,0), DKGL_PI * 0.5));
        //ubo.modelMatrix.Multiply(trans.Matrix4());

        //memcpy(uboBuffer->Contents(), &ubo, sizeof(ubo));

        sampleMesh->SetupExternalUniformBuffer(sceneUniformBuffer, sizeof(SceneUniformBuffer), 0);
    }

    void EncodeScene(DKObject<DKRenderCommandEncoder> encoder)
    {
        sampleMesh->EncodeRenderCommand(encoder);
    }

    void RenderThread(void)
    {
        DKObject<DKGraphicsDevice> device = DKGraphicsDevice::SharedInstance();
        DKObject<DKCommandQueue> queue = device->CreateCommandQueue(DKCommandQueue::Graphics);

        LoadRenderResource(queue);

        DKObject<DKSwapChain> swapChain = queue->CreateSwapChain(window);

        DKRenderPipelineDescriptor pipelineDescriptor;
        // setup color-attachment render-targets
        pipelineDescriptor.colorAttachments.Resize(1);
        pipelineDescriptor.colorAttachments.Value(0).pixelFormat = swapChain->ColorPixelFormat();
        pipelineDescriptor.colorAttachments.Value(0).blendState.enabled = false;
        pipelineDescriptor.colorAttachments.Value(0).blendState.sourceRGBBlendFactor = DKBlendFactor::SourceAlpha;
        pipelineDescriptor.colorAttachments.Value(0).blendState.destinationRGBBlendFactor = DKBlendFactor::OneMinusSourceAlpha;
        // setup depth-stencil
        pipelineDescriptor.depthStencilAttachmentPixelFormat = DKPixelFormat::D32Float;
        pipelineDescriptor.depthStencilDescriptor.depthWriteEnabled = true;
        pipelineDescriptor.depthStencilDescriptor.depthCompareFunction = DKCompareFunctionLessEqual;
        // setup topology and rasterization
        pipelineDescriptor.primitiveTopology = DKPrimitiveType::Triangle;
        pipelineDescriptor.frontFace = DKFrontFace::CCW;
        pipelineDescriptor.triangleFillMode = DKTriangleFillMode::Fill;
        pipelineDescriptor.depthClipMode = DKDepthClipMode::Clip;
        pipelineDescriptor.cullMode = DKCullMode::Back;
        pipelineDescriptor.rasterizationEnabled = true;

        SetupPipeline(pipelineDescriptor);

        DKPipelineReflection reflection;
        DKObject<DKRenderPipelineState> pipelineState = device->CreateRenderPipeline(pipelineDescriptor, &reflection);
        if (pipelineState)
        {
            PrintPipelineReflection(&reflection, DKLogCategory::Verbose);
        }

        SetupMaterial(device);
        SetupSceneUniformBuffer(device);

        DKObject<DKTexture> depthBuffer = nullptr;

        DKCamera camera;
        DKVector3 cameraPosition = { 0, 5, 10 };
        DKVector3 cameraTartget = { 0, 0, 0 };

        DKAffineTransform3 tm(DKLinearTransform3().Scale(5).Rotate(DKVector3(-1, 0, 0), DKGL_PI * 0.5));

        DKTimer timer;
        timer.Reset();

        DKLog("Render thread begin");
        while (!runningRenderThread.CompareAndSet(0, 0))
        {
            DKRenderPassDescriptor rpd = swapChain->CurrentRenderPassDescriptor();
            double t = timer.Elapsed();
            double waveT = (cos(t) + 1.0) * 0.5;
            rpd.colorAttachments.Value(0).clearColor = DKColor(waveT, 0.0, 0.0, 0.0);

            int width = rpd.colorAttachments.Value(0).renderTarget->Width();
            int height = rpd.colorAttachments.Value(0).renderTarget->Height();
            if (depthBuffer)
            {
                if (depthBuffer->Width() != width ||
                    depthBuffer->Height() != height)
                    depthBuffer = nullptr;
            }
            if (depthBuffer == nullptr)
            {
                // create depth buffer
                DKTextureDescriptor texDesc = {};
                texDesc.textureType = DKTexture::Type2D;
                texDesc.pixelFormat = DKPixelFormat::D32Float;
                texDesc.width = width;
                texDesc.height = height;
                texDesc.depth = 1;
                texDesc.mipmapLevels = 1;
                texDesc.sampleCount = 1;
                texDesc.arrayLength = 1;
                texDesc.usage = DKTexture::UsageRenderTarget;
                depthBuffer = device->CreateTexture(texDesc);
            }
            rpd.depthStencilAttachment.renderTarget = depthBuffer;
            rpd.depthStencilAttachment.loadAction = DKRenderPassAttachmentDescriptor::LoadActionClear;
            rpd.depthStencilAttachment.storeAction = DKRenderPassAttachmentDescriptor::StoreActionDontCare;

            DKObject<DKCommandBuffer> buffer = queue->CreateCommandBuffer();
            DKObject<DKRenderCommandEncoder> encoder = buffer->CreateRenderCommandEncoder(rpd);
            if (encoder)
            {
                if (sceneUniformBuffer)
                {
                    camera.SetView(cameraPosition, cameraTartget - cameraPosition, DKVector3(0, 1, 0));
                    camera.SetPerspective(DKGL_DEGREE_TO_RADIAN(90), float(width) / float(height), 1, 1000);

                    sceneBufferData->projectionMatrix = camera.ProjectionMatrix();
                    sceneBufferData->viewMatrix = camera.ViewMatrix();

                    DKQuaternion quat(DKVector3(0, 1, 0), t);
                    DKAffineTransform3 trans = tm * DKAffineTransform3(quat);
                    sceneBufferData->modelMatrix = trans.Matrix4();
                    sceneUniformBuffer->Flush();

                    sampleMesh->SetupExternalUniformBuffer(sceneUniformBuffer, sizeof(SceneUniformBuffer), 0);
                }

                encoder->SetRenderPipelineState(pipelineState);

                EncodeScene(encoder);

                buffer->Commit();
                swapChain->Present();
            }
            else
            {
            }
            DKThread::Sleep(0.01);
        }
        DKLog("RenderThread terminating...");
    }

    void OnInitialize(void) override
    {
        SampleApp::OnInitialize();
        DKLogD("%s", DKGL_FUNCTION_NAME);

        // create window
        window = DKWindow::Create("DefaultWindow");
        window->SetOrigin({ 0, 0 });
        window->Resize({ 320, 240 });
        window->Activate();

        window->AddEventHandler(this, DKFunction([this](const DKWindow::WindowEvent& e)
            {
                if (e.type == DKWindow::WindowEvent::WindowClosed)
                    DKApplication::Instance()->Terminate(0);
            }), NULL, NULL);

        sampleMesh = DKObject<StaticMesh>::New();
        LoadResource();

        runningRenderThread = 1;
        renderThread = DKThread::Create(DKFunction(this, &DKGLDemo::RenderThread)->Invocation());
    }
    void OnTerminate(void) override
    {
        DKLogD("%s", DKGL_FUNCTION_NAME);

        runningRenderThread = 0;
        renderThread->WaitTerminate();
        renderThread = NULL;
        window = NULL;

        SampleApp::OnTerminate();
    }
};


#ifdef _WIN32
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPWSTR    lpCmdLine,
    _In_ int       nCmdShow)
#else
int main(int argc, const char* argv[])
#endif
{
    DKGLDemo app;
    DKPropertySet::SystemConfig().SetValue("AppDelegate", "AppDelegate");
    DKPropertySet::SystemConfig().SetValue("GraphicsAPI", "Vulkan");
    return app.Run();
}