//--------------------------------------------------------------------------------------
// File: WaveFrontReader.h
//
// Code for loading basic mesh data from a WaveFront OBJ file
//
// http://en.wikipedia.org/wiki/Wavefront_.obj_file
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//
// http://go.microsoft.com/fwlink/?LinkID=324981
//--------------------------------------------------------------------------------------

#pragma once

#ifdef _WIN32
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4005)
#endif
#define WIN32_LEAN_AND_MEAN
#ifndef NOMINMAX
#define NOMINMAX 1
#endif
#define NODRAWTEXT
#define NOGDI
#define NOMCX
#define NOSERVICE
#define NOHELP
#ifdef _MSC_VER
#pragma warning(pop)
#endif

#include <Windows.h>
#ifdef __MINGW32__
#include <unknwn.h>
#endif
#else // !WIN32
#include <wsl/winadapter.h>
#include <wsl/wrladapter.h>

#ifndef MAX_PATH
#define MAX_PATH 4096
#endif
#endif

#include <algorithm>
#include <cstdint>
#include <cwctype>
#include <fstream>
#include <locale>
#include <string>
#include <vector>
#include <unordered_map>

#ifndef _WIN32
#include <filesystem>
#endif

#include <DirectXMath.h>
#include <DirectXCollision.h>

namespace DX
{
    template<class index_t>
    class WaveFrontReader
    {
    public:
        struct Vertex
        {
            DirectX::XMFLOAT3 position;
            DirectX::XMFLOAT3 normal;
            DirectX::XMFLOAT2 textureCoordinate;
        };

        WaveFrontReader() noexcept : hasNormals(false), hasTexcoords(false) {}

        HRESULT Load(_In_z_ const wchar_t* szFileName, bool ccw = true, bool loadmtl = true)
        {
            Clear();

            if (!szFileName)
                return E_INVALIDARG;

            constexpr size_t MAX_POLY = 64;

            using namespace DirectX;

            std::wifstream InFile(szFileName);
            if (!InFile)
                return /* HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) */ static_cast<HRESULT>(0x80070002L);

            InFile.imbue(std::locale::classic());

        #ifdef _WIN32
            wchar_t fname[_MAX_FNAME] = {};
            _wsplitpath_s(szFileName, nullptr, 0, nullptr, 0, fname, _MAX_FNAME, nullptr, 0);
            name = fname;
        #else
            auto path = std::filesystem::path(szFileName);
            name = path.filename().c_str();
        #endif

            std::vector<XMFLOAT3>   positions;
            std::vector<XMFLOAT3>   normals;
            std::vector<XMFLOAT2>   texCoords;

            VertexCache  vertexCache;

            Material defmat;

            wcscpy_s(defmat.strName, L"default");
            materials.emplace_back(defmat);

            uint32_t curSubset = 0;

            wchar_t strMaterialFilename[MAX_PATH] = {};
            for (;; )
            {
                std::wstring strCommand;
                InFile.width(MAX_PATH);
                InFile >> strCommand;
                if (!InFile)
                    break;

                if (strCommand.empty() || *strCommand.c_str() == 0)
                {
                    continue;
                }
                else if (*strCommand.c_str() == L'#')
                {
                    // Comment
                }
                else if (0 == wcscmp(strCommand.c_str(), L"o"))
                {
                    // Object name ignored
                }
                else if (0 == wcscmp(strCommand.c_str(), L"g"))
                {
                    // Group name ignored
                }
                else if (0 == wcscmp(strCommand.c_str(), L"s"))
                {
                    // Smoothing group ignored
                }
                else if (0 == wcscmp(strCommand.c_str(), L"v"))
                {
                    // Vertex Position
                    float x, y, z;
                    InFile >> x >> y >> z;
                    positions.emplace_back(XMFLOAT3(x, y, z));
                }
                else if (0 == wcscmp(strCommand.c_str(), L"vt"))
                {
                    // Vertex TexCoord
                    float u, v;
                    InFile >> u >> v;
                    texCoords.emplace_back(XMFLOAT2(u, v));

                    hasTexcoords = true;
                }
                else if (0 == wcscmp(strCommand.c_str(), L"vn"))
                {
                    // Vertex Normal
                    float x, y, z;
                    InFile >> x >> y >> z;
                    normals.emplace_back(XMFLOAT3(x, y, z));

                    hasNormals = true;
                }
                else if (0 == wcscmp(strCommand.c_str(), L"f"))
                {
                    // Face
                    INT iPosition, iTexCoord, iNormal;
                    Vertex vertex;

                    uint32_t faceIndex[MAX_POLY];
                    size_t iFace = 0;
                    for (;;)
                    {
                        if (iFace >= MAX_POLY)
                        {
                            // Too many polygon verts for the reader
                            return E_FAIL;
                        }

                        memset(&vertex, 0, sizeof(vertex));

                        InFile >> iPosition;

                        uint32_t vertexIndex = 0;
                        if (!iPosition)
                        {
                            // 0 is not allowed for index
                            return E_UNEXPECTED;
                        }
                        else if (iPosition < 0)
                        {
                            // Negative values are relative indices
                            vertexIndex = uint32_t(ptrdiff_t(positions.size()) + iPosition);
                        }
                        else
                        {
                            // OBJ format uses 1-based arrays
                            vertexIndex = uint32_t(iPosition - 1);
                        }

                        if (vertexIndex >= positions.size())
                            return E_FAIL;

                        vertex.position = positions[vertexIndex];

                        if ('/' == InFile.peek())
                        {
                            InFile.ignore();

                            if ('/' != InFile.peek())
                            {
                                // Optional texture coordinate
                                InFile >> iTexCoord;

                                uint32_t coordIndex = 0;
                                if (!iTexCoord)
                                {
                                    // 0 is not allowed for index
                                    return E_UNEXPECTED;
                                }
                                else if (iTexCoord < 0)
                                {
                                    // Negative values are relative indices
                                    coordIndex = uint32_t(ptrdiff_t(texCoords.size()) + iTexCoord);
                                }
                                else
                                {
                                    // OBJ format uses 1-based arrays
                                    coordIndex = uint32_t(iTexCoord - 1);
                                }

                                if (coordIndex >= texCoords.size())
                                    return E_FAIL;

                                vertex.textureCoordinate = texCoords[coordIndex];
                            }

                            if ('/' == InFile.peek())
                            {
                                InFile.ignore();

                                // Optional vertex normal
                                InFile >> iNormal;

                                uint32_t normIndex = 0;
                                if (!iNormal)
                                {
                                    // 0 is not allowed for index
                                    return E_UNEXPECTED;
                                }
                                else if (iNormal < 0)
                                {
                                    // Negative values are relative indices
                                    normIndex = uint32_t(ptrdiff_t(normals.size()) + iNormal);
                                }
                                else
                                {
                                    // OBJ format uses 1-based arrays
                                    normIndex = uint32_t(iNormal - 1);
                                }

                                if (normIndex >= normals.size())
                                    return E_FAIL;

                                vertex.normal = normals[normIndex];
                            }
                        }

                        // If a duplicate vertex doesn't exist, add this vertex to the Vertices
                        // list. Store the index in the Indices array. The Vertices and Indices
                        // lists will eventually become the Vertex Buffer and Index Buffer for
                        // the mesh.
                        const uint32_t index = AddVertex(vertexIndex, &vertex, vertexCache);
                        if (index == uint32_t(-1))
                            return E_OUTOFMEMORY;

                        constexpr uint32_t maxIndex = (sizeof(index_t) == 2) ? UINT16_MAX : UINT32_MAX;
                        if (index >= maxIndex)
                        {
                            // Too many indices for IB!
                            return E_FAIL;
                        }

                        faceIndex[iFace] = index;
                        ++iFace;

                        // Check for more face data or end of the face statement
                        bool faceEnd = false;
                        for (;;)
                        {
                            const wchar_t p = InFile.peek();

                            if ('\n' == p || !InFile)
                            {
                                faceEnd = true;
                                break;
                            }
                            else if (isdigit(p) || p == '-' || p == '+')
                                break;

                            InFile.ignore();
                        }

                        if (faceEnd)
                            break;
                    }

                    if (iFace < 3)
                    {
                        // Need at least 3 points to form a triangle
                        return E_FAIL;
                    }

                    // Convert polygons to triangles
                    const uint32_t i0 = faceIndex[0];
                    uint32_t i1 = faceIndex[1];

                    for (size_t j = 2; j < iFace; ++j)
                    {
                        const uint32_t index = faceIndex[j];
                        indices.emplace_back(static_cast<index_t>(i0));
                        if (ccw)
                        {
                            indices.emplace_back(static_cast<index_t>(i1));
                            indices.emplace_back(static_cast<index_t>(index));
                        }
                        else
                        {
                            indices.emplace_back(static_cast<index_t>(index));
                            indices.emplace_back(static_cast<index_t>(i1));
                        }

                        attributes.emplace_back(curSubset);

                        i1 = index;
                    }

                    assert(attributes.size() * 3 == indices.size());
                }
                else if (0 == wcscmp(strCommand.c_str(), L"mtllib"))
                {
                    // Material library
                    InFile.width(MAX_PATH);
                    InFile >> strMaterialFilename;
                }
                else if (0 == wcscmp(strCommand.c_str(), L"usemtl"))
                {
                    // Material
                    wchar_t strName[MAX_PATH] = {};
                    InFile.width(MAX_PATH);
                    InFile >> strName;

                    bool bFound = false;
                    uint32_t count = 0;
                    for (auto it = materials.cbegin(); it != materials.cend(); ++it, ++count)
                    {
                        if (0 == wcscmp(it->strName, strName))
                        {
                            bFound = true;
                            curSubset = count;
                            break;
                        }
                    }

                    if (!bFound)
                    {
                        Material mat;
                        curSubset = static_cast<uint32_t>(materials.size());
                        wcscpy_s(mat.strName, MAX_PATH - 1, strName);
                        materials.emplace_back(mat);
                    }
                }
                else if (!std::iswprint(*strCommand.c_str()))
                {
                    // non-printable characters outside of comments mean this is not a text file
                    return E_FAIL;
                }
                else
                {
                #ifdef _DEBUG
                    // Unimplemented or unrecognized command
                    OutputDebugStringW(strCommand.c_str());
                #endif
                }

                InFile.ignore(1000, L'\n');
            }

            if (positions.empty())
                return E_FAIL;

            // Cleanup
            InFile.close();

            BoundingBox::CreateFromPoints(bounds, positions.size(), positions.data(), sizeof(XMFLOAT3));

            // If an associated material file was found, read that in as well.
            if (*strMaterialFilename && loadmtl)
            {
            #ifdef _WIN32
                wchar_t ext[_MAX_EXT] = {};
                _wsplitpath_s(strMaterialFilename, nullptr, 0, nullptr, 0, fname, _MAX_FNAME, ext, _MAX_EXT);

                wchar_t drive[_MAX_DRIVE] = {};
                wchar_t dir[_MAX_DIR] = {};
                _wsplitpath_s(szFileName, drive, _MAX_DRIVE, dir, _MAX_DIR, nullptr, 0, nullptr, 0);

                wchar_t szPath[MAX_PATH] = {};
                _wmakepath_s(szPath, MAX_PATH, drive, dir, fname, ext);
                HRESULT hr = LoadMTL(szPath);
                if (FAILED(hr))
                    return hr;
            #else
                auto path = std::filesystem::path(szFileName);
                auto mtlpath = std::filesystem::path(strMaterialFilename);
                path.replace_filename(mtlpath.filename());
                path.replace_extension(mtlpath.extension());

                HRESULT hr = LoadMTL(path.c_str());
                if (FAILED(hr))
                    return hr;
            #endif
            }

            return S_OK;
        }

        HRESULT LoadMTL(_In_z_ const wchar_t* szFileName)
        {
            if (!szFileName)
                return E_INVALIDARG;

            using namespace DirectX;

            // Assumes MTL is in CWD along with OBJ
            std::wifstream InFile(szFileName);
            if (!InFile)
                return /* HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) */ static_cast<HRESULT>(0x80070002L);

            InFile.imbue(std::locale::classic());

            auto curMaterial = materials.end();
            bool foundmat = false;

            for (;; )
            {
                std::wstring strCommand;
                InFile >> strCommand;
                if (!InFile)
                    break;

                if (strCommand.empty() || *strCommand.c_str() == 0)
                    continue;

                if (*strCommand.c_str() == L'#')
                {
                    // Comment
                    InFile.ignore(1000, L'\n');
                    continue;
                }
                else if (0 == wcscmp(strCommand.c_str(), L"newmtl"))
                {
                    foundmat = true;

                    // Switching active materials
                    wchar_t strName[MAX_PATH] = {};
                    InFile.width(MAX_PATH);
                    InFile >> strName;

                    curMaterial = materials.end();
                    for (auto it = materials.begin(); it != materials.end(); ++it)
                    {
                        if (0 == wcscmp(it->strName, strName))
                        {
                            curMaterial = it;
                            break;
                        }
                    }
                }
                else if (!std::iswprint(*strCommand.c_str()))
                {
                    // non-printable characters outside of comments mean this is not a text file
                    return E_FAIL;
                }

                // The rest of the commands rely on an active material
                if (curMaterial == materials.end())
                    continue;

                if (0 == wcscmp(strCommand.c_str(), L"Ka"))
                {
                    // Ambient color
                    // TODO: check for 'spectral' or 'xyz'
                    float r, g, b;
                    InFile >> r >> g >> b;
                    curMaterial->vAmbient = XMFLOAT3(r, g, b);
                }
                else if (0 == wcscmp(strCommand.c_str(), L"Kd"))
                {
                    // Diffuse color
                    // TODO: check for 'spectral' or 'xyz'
                    float r, g, b;
                    InFile >> r >> g >> b;
                    curMaterial->vDiffuse = XMFLOAT3(r, g, b);
                }
                else if (0 == wcscmp(strCommand.c_str(), L"Ks"))
                {
                    // Specular color
                    // TODO: check for 'spectral' or 'xyz'
                    float r, g, b;
                    InFile >> r >> g >> b;
                    curMaterial->vSpecular = XMFLOAT3(r, g, b);
                }
                else if (0 == wcscmp(strCommand.c_str(), L"Ke"))
                {
                    // Emissive color
                    // TODO: check for 'spectral' or 'xyz'
                    float r, g, b;
                    InFile >> r >> g >> b;
                    curMaterial->vEmissive = XMFLOAT3(r, g, b);
                    if (r > 0.f || g > 0.f || b > 0.f)
                    {
                        curMaterial->bEmissive = true;
                    }
                }
                else if (0 == wcscmp(strCommand.c_str(), L"d"))
                {
                    // Alpha
                    // TODO: check for '-halo'
                    float alpha;
                    InFile >> alpha;
                    curMaterial->fAlpha = std::min(1.f, std::max(0.f, alpha));
                }
                else if (0 == wcscmp(strCommand.c_str(), L"Tr"))
                {
                    // Transparency (inverse of alpha)
                    float invAlpha;
                    InFile >> invAlpha;
                    curMaterial->fAlpha = std::min(1.f, std::max(0.f, 1.f - invAlpha));
                }
                else if (0 == wcscmp(strCommand.c_str(), L"Ns"))
                {
                    // Shininess
                    int nShininess;
                    InFile >> nShininess;
                    curMaterial->nShininess = uint32_t(nShininess);
                }
                else if (0 == wcscmp(strCommand.c_str(), L"illum"))
                {
                    // Specular on/off
                    int illumination;
                    InFile >> illumination; // Illumination model value should be between 0 and 10
                    curMaterial->bSpecular = (illumination == 2);
                }
                else if (0 == wcscmp(strCommand.c_str(), L"map_Kd"))
                {
                    // Diffuse texture
                    // TODO: check for '-options args'
                    LoadTexturePath(InFile, curMaterial->strTexture, MAX_PATH);
                }
                else if (0 == wcscmp(strCommand.c_str(), L"map_Ks"))
                {
                    // Specular texture
                    // TODO: check for '-options args'
                    LoadTexturePath(InFile, curMaterial->strSpecularTexture, MAX_PATH);
                }
                else if (0 == wcscmp(strCommand.c_str(), L"map_Kn")
                    || 0 == wcscmp(strCommand.c_str(), L"norm"))
                {
                    // Normal texture
                    // TODO: check for '-options args'
                    LoadTexturePath(InFile, curMaterial->strNormalTexture, MAX_PATH);
                }
                else if (0 == wcscmp(strCommand.c_str(), L"map_Ke")
                    || 0 == wcscmp(strCommand.c_str(), L"map_emissive"))
                {
                    // Emissive texture
                    // TODO: check for '-options args'
                    LoadTexturePath(InFile, curMaterial->strEmissiveTexture, MAX_PATH);
                    curMaterial->bEmissive = true;
                }
                else if (0 == wcscmp(strCommand.c_str(), L"map_RMA")
                    || 0 == wcscmp(strCommand.c_str(), L"map_ORM"))
                {
                    // RMA texture
                    // TODO: check for '-options args'
                    LoadTexturePath(InFile, curMaterial->strRMATexture, MAX_PATH);
                }
                else
                {
                    // Unimplemented or unrecognized command

                    // bump, decal, disp, map_d, map_Ka, map_Ns
                    // map_aat
                    // Ni, sharpness, Tf
                    // refl
                }

                InFile.ignore(1000, L'\n');
            }

            InFile.close();

            return (foundmat) ? S_OK : E_FAIL;
        }

        void Clear()
        {
            vertices.clear();
            indices.clear();
            attributes.clear();
            materials.clear();
            name.clear();
            hasNormals = false;
            hasTexcoords = false;

            bounds.Center.x = bounds.Center.y = bounds.Center.z = 0.f;
            bounds.Extents.x = bounds.Extents.y = bounds.Extents.z = 0.f;
        }

        HRESULT LoadVBO(_In_z_ const wchar_t* szFileName)
        {
            Clear();

            if (!szFileName)
                return E_INVALIDARG;

            using namespace DirectX;

        #ifdef _WIN32
            wchar_t fname[_MAX_FNAME] = {};
            _wsplitpath_s(szFileName, nullptr, 0, nullptr, 0, fname, _MAX_FNAME, nullptr, 0);
            name = fname;
        #else
            auto path = std::filesystem::path(szFileName);
            name = path.filename().c_str();
        #endif

            Material defmat;
            wcscpy_s(defmat.strName, L"default");
            materials.emplace_back(defmat);

            std::ifstream vboFile(szFileName, std::ifstream::in | std::ifstream::binary);
            if (!vboFile.is_open())
                return /* HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) */ static_cast<HRESULT>(0x80070002L);

            hasNormals = hasTexcoords = true;

            uint32_t numVertices = 0;
            uint32_t numIndices = 0;

            vboFile.read(reinterpret_cast<char*>(&numVertices), sizeof(uint32_t));
            if (!numVertices)
                return E_FAIL;

            vboFile.read(reinterpret_cast<char*>(&numIndices), sizeof(uint32_t));
            if (!numIndices)
                return E_FAIL;

            vertices.resize(numVertices);
            vboFile.read(reinterpret_cast<char*>(vertices.data()), sizeof(Vertex) * numVertices);

        #if (__cplusplus >= 201703L)
            if constexpr (sizeof(index_t) == 2)
            #else
        #pragma warning( suppress : 4127 )
            if (sizeof(index_t) == 2)
            #endif
            {
                indices.resize(numIndices);
                vboFile.read(reinterpret_cast<char*>(indices.data()), sizeof(uint16_t) * numIndices);
            }
            else
            {
                std::vector<uint16_t> tmp;
                tmp.resize(numIndices);
                vboFile.read(reinterpret_cast<char*>(tmp.data()), sizeof(uint16_t) * numIndices);

                indices.reserve(numIndices);
                for (const auto it : tmp)
                {
                    indices.emplace_back(it);
                }
            }

            BoundingBox::CreateFromPoints(bounds, vertices.size(), reinterpret_cast<const XMFLOAT3*>(vertices.data()), sizeof(Vertex));

            vboFile.close();

            return S_OK;
        }

        struct Material
        {
            DirectX::XMFLOAT3 vAmbient;
            DirectX::XMFLOAT3 vDiffuse;
            DirectX::XMFLOAT3 vSpecular;
            DirectX::XMFLOAT3 vEmissive;
            uint32_t nShininess;
            float fAlpha;

            bool bSpecular;
            bool bEmissive;

            wchar_t strName[MAX_PATH];
            wchar_t strTexture[MAX_PATH];
            wchar_t strNormalTexture[MAX_PATH];
            wchar_t strSpecularTexture[MAX_PATH];
            wchar_t strEmissiveTexture[MAX_PATH];
            wchar_t strRMATexture[MAX_PATH];

            Material() noexcept :
                vAmbient(0.2f, 0.2f, 0.2f),
                vDiffuse(0.8f, 0.8f, 0.8f),
                vSpecular(1.0f, 1.0f, 1.0f),
                vEmissive(0.f, 0.f, 0.f),
                nShininess(0),
                fAlpha(1.f),
                bSpecular(false),
                bEmissive(false),
                strName{},
                strTexture{},
                strNormalTexture{},
                strSpecularTexture{},
                strEmissiveTexture{},
                strRMATexture{}
            {}
        };

        std::vector<Vertex>     vertices;
        std::vector<index_t>    indices;
        std::vector<uint32_t>   attributes;
        std::vector<Material>   materials;

        std::wstring            name;
        bool                    hasNormals;
        bool                    hasTexcoords;

        DirectX::BoundingBox    bounds;

    private:
        using VertexCache = std::unordered_multimap<uint32_t, uint32_t>;

        uint32_t AddVertex(uint32_t hash, const Vertex* pVertex, VertexCache& cache)
        {
            auto f = cache.equal_range(hash);

            for (auto it = f.first; it != f.second; ++it)
            {
                auto& tv = vertices[it->second];

                if (0 == memcmp(pVertex, &tv, sizeof(Vertex)))
                {
                    return it->second;
                }
            }

            auto index = static_cast<uint32_t>(vertices.size());
            vertices.emplace_back(*pVertex);

            VertexCache::value_type entry(hash, index);
            cache.insert(entry);
            return index;
        }

        void LoadTexturePath(std::wifstream& InFile, _Out_writes_(maxChar) wchar_t* texture, size_t maxChar)
        {
            wchar_t buff[1024] = {};
            InFile.getline(buff, 1024, L'\n');
            InFile.putback(L'\n');

            std::wstring path = buff;

            // Ignore any end-of-line comment
            size_t pos = path.find_first_of(L'#');
            if (pos != std::wstring::npos)
            {
                path = path.substr(0, pos);
            }

            // Trim any trailing whitespace
            pos = path.find_last_not_of(L" \t");
            if (pos != std::wstring::npos)
            {
                path = path.substr(0, pos + 1);
            }

            // Texture path should be last element in line
            pos = path.find_last_of(' ');
            if (pos != std::wstring::npos)
            {
                path = path.substr(pos + 1);
            }

            if (!path.empty())
            {
            #ifdef _WIN32
                wcscpy_s(texture, maxChar, path.c_str());
            #else
                wcscpy(texture, path.c_str());
            #endif
            }
        }
    };
}
