#include "ResourceManager.h"
#include "AppConfig.h"
#include <stdio.h>
#include "util.h"

int ResourceManager::MAX_THREAD_NUM = 4;

class ResourceManager::LoaderThread : public Thread {
public:
    LoaderThread(ResourceManager* resourceManager, int threadIndex) 
    : Thread(), _resourceManager(resourceManager), _threadIndex(threadIndex) {}

    Texture* loadTexture(uint32 width, uint32 height, float* data);

    Texture* loadTexture(const wchar_t* fileName);
    Node* loadNode(const wchar_t* fileName);


protected:
    void operator()();

    int _threadIndex;
    ResourceManager* _resourceManager; 
};

Texture* ResourceManager::LoaderThread::loadTexture(uint32 width, uint32 height, float* data)
{
    Texture* texture = new Texture();

    texture->setResourceHandle(_resourceManager->_renderDevice->loadTextureFromFloatArray(width, height, data));

    return texture;
}

Texture* ResourceManager::LoaderThread::loadTexture(const wchar_t* fileName)
{
    Texture* texture = 0;

    _resourceManager->_textures.lock();
    stdext::hash_map<std::wstring, Texture*>::iterator iTexture = _resourceManager->_textures.find(fileName);

    if (iTexture == _resourceManager->_textures.end()) {
        texture = new Texture(fileName);

        _resourceManager->_textures.insert(std::pair<std::wstring, Texture*>(fileName, texture));
        _resourceManager->_textures.unlock();

        FILE* file = _wfopen(fileName, L"rb");

        if (file != NULL) {
            byte* data = 0;
            uint32 size = 0;

            fseek(file, 0, SEEK_END);
            size = ftell(file);
            fseek(file, 0, SEEK_SET);
            data = new byte[size];
            fread(data, 1, size, file);
            fclose(file);

            texture->setResourceHandle(_resourceManager->_renderDevice->loadTextureFromMemory(data, size));

            delete [] data;
        }

        if (texture->getResourceHandle() == 0) {
            _resourceManager->_textures.lock();

            stdext::hash_map<std::wstring, Texture*>::iterator iErrorTexture = _resourceManager->_textures.find(L"textures/default.dds");

            if (iErrorTexture != _resourceManager->_textures.end()) {
                texture->setResourceHandle(iErrorTexture->second->getResourceHandle());
                _resourceManager->_textures.unlock();
            }
            else if (wcscmp(fileName, L"textures/default.dds") != 0) {
                _resourceManager->_textures.unlock();
                Texture* errorTexture = loadTexture(L"textures/default.dds");
                texture->setResourceHandle(errorTexture->getResourceHandle());
            }
            else {
                _resourceManager->_textures.unlock();
            }
        }
    }
    else {
        texture = iTexture->second;

        _resourceManager->_textures.unlock();
    }

    return texture;
}

Node* ResourceManager::LoaderThread::loadNode(const wchar_t* fileName)
{
    Geode* node = 0;

    _resourceManager->_nodes.lock();
    stdext::hash_map<std::wstring, Node*>::iterator iNode = _resourceManager->_nodes.find(fileName);

    if (iNode == _resourceManager->_nodes.end()) {
        node = new Geode(fileName);

        _resourceManager->_nodes.insert(std::pair<std::wstring, Node*>(fileName, node));
        _resourceManager->_nodes.unlock();

        Assimp::Importer importer;

        const char* utf8FileName = util::convertUtf16ToUtf8(fileName);
        
        const aiScene* scene = importer.ReadFile(
            utf8FileName,
            aiProcess_GenNormals                |
            aiProcess_CalcTangentSpace          | 
            aiProcess_Triangulate               |
            aiProcess_JoinIdenticalVertices     |
            aiProcess_SortByPType               | 
            aiProcess_SplitLargeMeshes          |
            aiProcess_ImproveCacheLocality      |
            aiProcess_RemoveRedundantMaterials  |
            aiProcess_OptimizeMeshes            |
            aiProcess_OptimizeGraph             |
            aiProcess_GenUVCoords               |
            aiProcess_ConvertToLeftHanded);

        delete [] utf8FileName;

        if (scene == 0) {
            MessageBoxA(NULL, importer.GetErrorString(), "Error", MB_OK | MB_ICONERROR);
        }

        aiMesh** rawMeshes = scene->mMeshes;
        aiMaterial** materials = scene->mMaterials;
        aiAnimation** animations = scene->mAnimations;

        void** meshResourceHandles = _resourceManager->_renderDevice->loadMeshArrayFromMemory(rawMeshes, scene->mNumMeshes);

        //for (unsigned int i = 0; i < scene->mNumMeshes; ++i) {
            //MessageBox(0, util::to_string(rawMeshes[i]->mMaterialIndex).c_str(), L"rawMesh material index", MB_OK | MB_ICONINFORMATION);
        //}

        std::list<aiNode*> ainodes;
        std::list<Geode*> geodes;

        ainodes.push_back(scene->mRootNode);
        geodes.push_back(node);


        while(!ainodes.empty()) {
            aiNode* ainode = ainodes.front();
            ainodes.pop_front();

            Geode* geode = geodes.front();
            geodes.pop_front();

            aiVector3D scaling, position;
            aiQuaternion rotation;
            ainode->mTransformation.Decompose(scaling, rotation, position);

            geode->setPosition(Vector3(position.x, position.y, position.z));
            geode->setOrientation(Quaternion(rotation.w, rotation.x, rotation.y, rotation.z));
            geode->setScale(Vector3(scaling.x, scaling.y, scaling.z));

            for (unsigned int i = 0; i < ainode->mNumMeshes; ++i) {

                //MessageBox(GetActiveWindow(), util::to_string(rawMeshes[ainode->mMeshes[i]]->mMaterialIndex).c_str(), L"Material ID", MB_OK | MB_ICONINFORMATION);

                Geode::Drawable* drawable = new Geode::Drawable();

                drawable->setResourceHandle(meshResourceHandles[ainode->mMeshes[i]]);

                aiColor4D diffuseMaterial;
                materials[rawMeshes[ainode->mMeshes[i]]->mMaterialIndex]->Get(AI_MATKEY_COLOR_DIFFUSE, diffuseMaterial);

                aiColor4D specularMaterial;
                materials[rawMeshes[ainode->mMeshes[i]]->mMaterialIndex]->Get(AI_MATKEY_COLOR_SPECULAR, specularMaterial);

                aiColor4D ambientMaterial;
                materials[rawMeshes[ainode->mMeshes[i]]->mMaterialIndex]->Get(AI_MATKEY_COLOR_AMBIENT, ambientMaterial);

                float shininess;
                materials[rawMeshes[ainode->mMeshes[i]]->mMaterialIndex]->Get(AI_MATKEY_SHININESS, shininess);

                Material* material = new Material();
                material->setDiffuse(diffuseMaterial.r, diffuseMaterial.g, diffuseMaterial.b, 0.0f);
                material->setSpecular(specularMaterial.r, specularMaterial.g, specularMaterial.b, shininess);
                material->setAmbient(ambientMaterial.r, ambientMaterial.g, ambientMaterial.b, 0.0f);


                aiString textureFileName;

                if (AI_SUCCESS == materials[rawMeshes[ainode->mMeshes[i]]->mMaterialIndex]->Get(AI_MATKEY_TEXTURE(aiTextureType_DIFFUSE, 0), textureFileName)) {
                    wchar_t* path = new wchar_t[wcslen(fileName) + 1];
                    wcscpy(path, fileName);
                    
                    wchar_t* p = &path[wcslen(fileName) - 1];
                    while((*p != L'\\') && (*p != L'/')) {
                        *p = 0;
                        p--;
                        if (p < path) break;
                    }
 

                    const wchar_t* wcsTexFileName = util::convertUtf8ToUtf16(textureFileName.data);
                    wchar_t* wcsTexFilePath = new wchar_t[wcslen(path) + wcslen(wcsTexFileName) + 1];
                    wcscpy(wcsTexFilePath, path);
                    wcscat(wcsTexFilePath, wcsTexFileName);
                   
                    //MessageBox(GetActiveWindow(), wcsTexFilePath, L"wcsTexFilePath | diffuse texture", MB_OK | MB_ICONINFORMATION);
                    material->setDiffuseTexture(loadTexture(wcsTexFilePath));

                    delete [] wcsTexFilePath;
                    delete [] wcsTexFileName;
                    delete [] path;
                }
                else {
                    material->setDiffuseTexture(loadTexture(L"textures/default.dds"));
                }


                if (AI_SUCCESS == materials[rawMeshes[ainode->mMeshes[i]]->mMaterialIndex]->Get(AI_MATKEY_TEXTURE(aiTextureType_SPECULAR, 0), textureFileName)) {
                    wchar_t* path = new wchar_t[wcslen(fileName) + 1];
                    wcscpy(path, fileName);
                    
                    wchar_t* p = &path[wcslen(fileName) - 1];
                    while((*p != L'\\') && (*p != L'/')) {
                        *p = 0;
                        p--;
                        if (p < path) break;
                    }
 

                    const wchar_t* wcsTexFileName = util::convertUtf8ToUtf16(textureFileName.data);
                    wchar_t* wcsTexFilePath = new wchar_t[wcslen(path) + wcslen(wcsTexFileName) + 1];
                    wcscpy(wcsTexFilePath, path);
                    wcscat(wcsTexFilePath, wcsTexFileName);
                   

                    material->setSpecularTexture(loadTexture(wcsTexFilePath));

                    delete [] wcsTexFilePath;
                    delete [] wcsTexFileName;
                    delete [] path;
                }
                else {
                    material->setSpecularTexture(loadTexture(L"textures/default.dds"));
                }

                if (AI_SUCCESS == materials[rawMeshes[ainode->mMeshes[i]]->mMaterialIndex]->Get(AI_MATKEY_TEXTURE(aiTextureType_AMBIENT, 0), textureFileName)) {
                    wchar_t* path = new wchar_t[wcslen(fileName) + 1];
                    wcscpy(path, fileName);
                    
                    wchar_t* p = &path[wcslen(fileName) - 1];
                    while((*p != L'\\') && (*p != L'/')) {
                        *p = 0;
                        p--;
                        if (p < path) break;
                    }
 

                    const wchar_t* wcsTexFileName = util::convertUtf8ToUtf16(textureFileName.data);
                    wchar_t* wcsTexFilePath = new wchar_t[wcslen(path) + wcslen(wcsTexFileName) + 1];
                    wcscpy(wcsTexFilePath, path);
                    wcscat(wcsTexFilePath, wcsTexFileName);
                   

                    material->setAmbientTexture(loadTexture(wcsTexFilePath));

                    delete [] wcsTexFilePath;
                    delete [] wcsTexFileName;
                    delete [] path;
                }
                else {
                    material->setAmbientTexture(loadTexture(L"textures/default.dds"));
                }


                textureFileName = "";
                materials[rawMeshes[ainode->mMeshes[i]]->mMaterialIndex]->Get(AI_MATKEY_TEXTURE(aiTextureType_NORMALS, 0), textureFileName);
                
                if (
                    (AI_SUCCESS == materials[rawMeshes[ainode->mMeshes[i]]->mMaterialIndex]->Get(AI_MATKEY_TEXTURE(aiTextureType_NORMALS, 0), textureFileName)) ||
                    (AI_SUCCESS == materials[rawMeshes[ainode->mMeshes[i]]->mMaterialIndex]->Get(AI_MATKEY_TEXTURE(aiTextureType_HEIGHT, 0), textureFileName))
                ) {
                    wchar_t* path = new wchar_t[wcslen(fileName) + 1];
                    wcscpy(path, fileName);
                    
                    wchar_t* p = &path[wcslen(fileName) - 1];
                    while((*p != L'\\') && (*p != L'/')) {
                        *p = 0;
                        p--;
                        if (p < path) break;
                    }
 

                    const wchar_t* wcsTexFileName = util::convertUtf8ToUtf16(textureFileName.data);
                    wchar_t* wcsTexFilePath = new wchar_t[wcslen(path) + wcslen(wcsTexFileName) + 1];
                    wcscpy(wcsTexFilePath, path);
                    wcscat(wcsTexFilePath, wcsTexFileName);
                   

                    material->setNormalMap(loadTexture(wcsTexFilePath));

                    delete [] wcsTexFilePath;
                    delete [] wcsTexFileName;
                    delete [] path;
                }
                else {
                    material->setNormalMap(loadTexture(L"textures/default.dds"));
                }


                drawable->setMaterial(material);
                geode->addDrawable(drawable);
            }

            for (unsigned int i = 0; i < ainode->mNumChildren; ++i) {
                ainodes.push_back(ainode->mChildren[i]);
                Geode* newNode = new Geode();
                geode->addChild(newNode);
                geodes.push_back(newNode);
            }
        }

        
    }
    else {
        node = (Geode*)iNode->second;

        _resourceManager->_nodes.unlock(); 
    }

    return node;
}



void ResourceManager::LoaderThread::operator()()
{
    beginBackgroundMode();

    bool empty = true;
 
    while (canRun()) {
        empty = true;

        if (isPaused()) {
            Sleep(100);
        }

        else {
            TextureLoadTask textureLoadTask;
            NodeLoadTask nodeLoadTask;

            bool hasTexture = false;
            bool hasNode = false;

            _resourceManager->_textureLoadQueue.lock();
            
            if (!_resourceManager->_textureLoadQueue.empty()) {
                empty = false;
                hasTexture = true;
                textureLoadTask = _resourceManager->_textureLoadQueue.front();
                _resourceManager->_textureLoadQueue.pop_front();
            }

            _resourceManager->_textureLoadQueue.unlock();

            if (hasTexture)
                *(textureLoadTask.texture) = loadTexture(textureLoadTask.fileName.c_str());



            _resourceManager->_nodeLoadQueue.lock();
            
            if (!_resourceManager->_nodeLoadQueue.empty()) {
                empty = false;
                hasNode = true;
                nodeLoadTask = _resourceManager->_nodeLoadQueue.front();
                _resourceManager->_nodeLoadQueue.pop_front();
            }

            _resourceManager->_nodeLoadQueue.unlock();

            if (hasNode)
                *(nodeLoadTask.node) = loadNode(nodeLoadTask.fileName.c_str());


            
            if (empty) {
                _resourceManager->_activityMask[_threadIndex] = false;
                suspend();
            }
        }
    }

    endBackgroundMode();
}



ResourceManager::ResourceManager()
{
    try {
        MAX_THREAD_NUM = AppConfig::instance().get<int>(L"Core", L"MaxLoadThreads");

        if (MAX_THREAD_NUM <= 0) {
            MAX_THREAD_NUM = 1;
            AppConfig::instance().set<int>(L"Core", L"MaxLoadThreads", MAX_THREAD_NUM);
        }
    }
    catch(AppConfig::InvalidEntryException&) {
        AppConfig::instance().set<int>(L"Core", L"MaxLoadThreads", MAX_THREAD_NUM);
    }

    _loaderThread = new LoaderThread*[MAX_THREAD_NUM];
    _activityMask = new bool[MAX_THREAD_NUM];

    for (int i = 0; i < MAX_THREAD_NUM; ++i) {
        _loaderThread[i] = new LoaderThread(this, i);
        _loaderThread[i]->start();
    }
}

ResourceManager::~ResourceManager()
{
    for (int i = 0; i < MAX_THREAD_NUM; ++i) {
        _loaderThread[i]->stop();
        _loaderThread[i]->resume();
        _loaderThread[i]->join();

        delete _loaderThread[i];
        _loaderThread[i] = 0;
    }

    delete [] _loaderThread;
    delete [] _activityMask;
}

ResourceManager& ResourceManager::instance()
{
    static ResourceManager resourceManager;
    return resourceManager;
}

void ResourceManager::setRenderDevice(RenderDevice* renderDevice)
{
    _renderDevice = renderDevice;
}

Texture* ResourceManager::loadTexture(uint32 width, uint32 height, float* data)
{
    return _loaderThread[0]->loadTexture(width, height, data);
}

Texture* ResourceManager::loadTexture(const wchar_t* fileName)
{
    return _loaderThread[0]->loadTexture(fileName);
}

Node* ResourceManager::loadNode(const wchar_t* fileName)
{
    return _loaderThread[0]->loadNode(fileName);
}

void ResourceManager::loadTextureNonBlocking(const wchar_t* fileName, Texture** texture)
{
    addToTextureLoadQueue(fileName, texture);

    for (int i = 0; i < MAX_THREAD_NUM; ++i) {
        if (!_activityMask[i]) {
            _activityMask[i] = true;
            _loaderThread[i]->resume();
        }
    }
}

void ResourceManager::loadNodeNonBlocking(const wchar_t* fileName, Node** node)
{
    addToNodeLoadQueue(fileName, node);

    for (int i = 0; i < MAX_THREAD_NUM; ++i) {
        if (!_activityMask[i]) {
            _activityMask[i] = true;
            _loaderThread[i]->resume();
        }
    }
}


Shader* ResourceManager::createShader(const wchar_t* fileName)
{
    Shader* shader = _renderDevice->createShader(fileName);
    return shader;
}

RenderTarget* ResourceManager::createRenderTarget(uint32 width, uint32 height, uint32 sampleCount)
{
    RenderTarget* renderTarget = _renderDevice->createRenderTarget(width, height, sampleCount);
    return renderTarget;
}

DepthStencilBuffer* ResourceManager::createDepthStencilBuffer(uint32 width, uint32 height, uint32 sampleCount)
{
    DepthStencilBuffer* depthStencilBuffer = _renderDevice->createDepthStencilBuffer(width, height, sampleCount);
    return depthStencilBuffer;
}

void ResourceManager::addToTextureLoadQueue(const wchar_t* fileName, Texture** texture)
{
    TextureLoadTask loadTask;
    loadTask.fileName = fileName;
    loadTask.texture = texture;

    _textureLoadQueue.lock();
    _textureLoadQueue.push_back(loadTask);
    _textureLoadQueue.unlock();
}

void ResourceManager::addToNodeLoadQueue(const wchar_t* fileName, Node** node)
{
    NodeLoadTask loadTask;
    loadTask.fileName = fileName;
    loadTask.node = node;

    _nodeLoadQueue.lock();
    _nodeLoadQueue.push_back(loadTask);
    _nodeLoadQueue.unlock();
}