#pragma once

#include <fstream>
#include <vector>
#include "Math.h"
#include "Mesh.h"
#include "tiny_obj_loader.h"

namespace dae {
    namespace Utils {
        //Just parses vertices and indices
#pragma warning(push)
#pragma warning(disable : 4505) //Warning unreferenced local function

        static bool ParseOBJ(const std::string &filename, std::vector<VertexIn> &vertices, std::vector<uint32_t> &indices,
                             bool flipAxisAndWinding = true) {
            std::ifstream file(filename);
            if (!file)
                return false;

            std::vector<Vector3> positions{};
            std::vector<Vector3> normals{};
            std::vector<Vector2> UVs{};

            vertices.clear();
            indices.clear();

            std::string sCommand;
            // start a while iteration ending when the end of file is reached (ios::eof)
            while (!file.eof()) {
                //read the first word of the string, use the >> operator (istream::operator>>)
                file >> sCommand;
                //use conditional statements to process the different commands
                if (sCommand == "#") {
                    // Ignore Comment
                } else if (sCommand == "v") {
                    //Vertex
                    float x, y, z;
                    file >> x >> y >> z;

                    positions.emplace_back(x, y, z);
                } else if (sCommand == "vt") {
                    // Vertex TexCoord
                    float u, v;
                    file >> u >> v;
                    UVs.emplace_back(u, 1 - v);
                } else if (sCommand == "vn") {
                    // Vertex Normal
                    float x, y, z;
                    file >> x >> y >> z;

                    normals.emplace_back(x, y, z);
                } else if (sCommand == "f") {
                    //if a face is read:
                    //construct the 3 vertices, add them to the vertex array
                    //add three indices to the index array
                    //add the material index as attibute to the attribute array
                    //
                    // Faces or triangles
                    VertexIn vertex{};
                    size_t iPosition, iTexCoord, iNormal;

                    uint32_t tempIndices[3];
                    for (size_t iFace = 0; iFace < 3; iFace++) {
                        // OBJ format uses 1-based arrays
                        file >> iPosition;
                        vertex.position = positions[iPosition - 1].ToVector4();

                        if ('/' == file.peek())//is next in buffer ==  '/' ?
                        {
                            file.ignore();//read and ignore one element ('/')

                            if ('/' != file.peek()) {
                                // Optional texture coordinate
                                file >> iTexCoord;
                                vertex.uv = UVs[iTexCoord - 1];
                            }

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

                                // Optional vertex normal
                                file >> iNormal;
//                                vertex.normal = normals[iNormal - 1];
                            }
                        }

                        vertices.push_back(vertex);
                        tempIndices[iFace] = uint32_t(vertices.size()) - 1;
//                        indices.push_back(uint32_t(vertices.size()) - 1);
                    }

                    indices.push_back(tempIndices[0]);
                    if (flipAxisAndWinding) {
                        indices.push_back(tempIndices[2]);
                        indices.push_back(tempIndices[1]);
                    } else {
                        indices.push_back(tempIndices[1]);
                        indices.push_back(tempIndices[2]);
                    }
                }
                //read till end of line and ignore all remaining chars
                file.ignore(1000, '\n');
            }

            //Cheap Tangent Calculations
            for (uint32_t i = 0; i < indices.size(); i += 3) {
                uint32_t index0 = indices[i];
                uint32_t index1 = indices[size_t(i) + 1];
                uint32_t index2 = indices[size_t(i) + 2];

                const Vector3 &p0 = vertices[index0].position;
                const Vector3 &p1 = vertices[index1].position;
                const Vector3 &p2 = vertices[index2].position;
                const Vector2 &uv0 = vertices[index0].uv;
                const Vector2 &uv1 = vertices[index1].uv;
                const Vector2 &uv2 = vertices[index2].uv;

                const Vector3 edge0 = p1 - p0;
                const Vector3 edge1 = p2 - p0;
                const Vector2 diffX = Vector2(uv1.x - uv0.x, uv2.x - uv0.x);
                const Vector2 diffY = Vector2(uv1.y - uv0.y, uv2.y - uv0.y);
                float r = 1.f / Vector2::Cross(diffX, diffY);

                Vector3 tangent = (edge0 * diffY.y - edge1 * diffY.x) * r;
                vertices[index0].tangent += tangent;
                vertices[index1].tangent += tangent;
                vertices[index2].tangent += tangent;
            }

            //Fix the tangents per vertex now because we accumulated
            for (auto &v: vertices) {
                v.tangent = Vector3::Reject(v.tangent, v.normal).Normalized();

                if (flipAxisAndWinding) {
                    v.position.z *= -1.f;
                    v.normal.z *= -1.f;
                    v.tangent.z *= -1.f;
                }

            }

            return true;
        }

#pragma warning(pop)

        static bool ParseOBJNew(const std::string &fileName, std::vector<VertexIn> &vertices, std::vector<uint32_t> &indices,
                                bool flipAxisAndWinding = true) {
            tinyobj::attrib_t attrib;
            std::vector<tinyobj::shape_t> shapes;
            std::vector<tinyobj::material_t> materials;
            std::string warn, err;

            if (!tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, fileName.c_str())) {
                if (!warn.empty()) {
                    std::cerr << "Warning: " << warn << std::endl;
                }
                if (!err.empty()) {
                    std::cerr << "Error: " << err << std::endl;
                }
                return false;
            }

            vertices.clear();
            indices.clear();

            for (const auto &shape: shapes) {
                size_t index_offset = 0;

                for (size_t f = 0; f < shape.mesh.num_face_vertices.size(); f++) {
                    auto fv = size_t(shape.mesh.num_face_vertices[f]);

                    if (fv != 3) {
                        std::cerr << "Error: Only triangular faces are supported." << std::endl;
                        return false;
                    }

                    uint32_t tempIndices[3];
                    VertexIn tempVertices[3];

                    for (size_t v = 0; v < fv; v++) {
                        tinyobj::index_t idx = shape.mesh.indices[index_offset + v];

                        VertexIn vertex;

                        if (idx.vertex_index >= 0) {
                            vertex.position = Vector3(
                                    attrib.vertices[3 * idx.vertex_index + 0],
                                    attrib.vertices[3 * idx.vertex_index + 1],
                                    attrib.vertices[3 * idx.vertex_index + 2]
                            );
                            if (flipAxisAndWinding) {
                                vertex.position.z *= -1.0f;
                            }
                        }

                        if (idx.texcoord_index >= 0) {
                            vertex.uv = Vector2(
                                    attrib.texcoords[2 * idx.texcoord_index + 0],
                                    1.0f - attrib.texcoords[2 * idx.texcoord_index + 1] // Flip V
                            );
                        }

                        if (idx.normal_index >= 0) {
                            vertex.normal = Vector3(
                                    attrib.normals[3 * idx.normal_index + 0],
                                    attrib.normals[3 * idx.normal_index + 1],
                                    attrib.normals[3 * idx.normal_index + 2]
                            );
                            if (flipAxisAndWinding) {
                                vertex.normal.z *= -1.0f;
                            }
                        }

                        tempVertices[v] = vertex;
                        vertices.push_back(vertex);
                        tempIndices[v] = static_cast<uint32_t>(vertices.size() - 1);
                    }

                    indices.push_back(tempIndices[0]);
                    if (flipAxisAndWinding) {
                        indices.push_back(tempIndices[2]);
                        indices.push_back(tempIndices[1]);
                    } else {
                        indices.push_back(tempIndices[1]);
                        indices.push_back(tempIndices[2]);
                    }

                    const Vector3 &pos0 = tempVertices[0].position;
                    const Vector3 &pos1 = tempVertices[1].position;
                    const Vector3 &pos2 = tempVertices[2].position;

                    const Vector2 &uv0 = tempVertices[0].uv;
                    const Vector2 &uv1 = tempVertices[1].uv;
                    const Vector2 &uv2 = tempVertices[2].uv;

                    Vector3 edge1 = pos1 - pos0;
                    Vector3 edge2 = pos2 - pos0;

                    Vector2 deltaUV1 = uv1 - uv0;
                    Vector2 deltaUV2 = uv2 - uv0;

                    float temp = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV1.y * deltaUV2.x);

                    Vector3 tangent = temp * (deltaUV2.y * edge1 - deltaUV1.y * edge2);
                    tangent = tangent.Normalized();

                    for (size_t v = 0; v < fv; v++) {
                        vertices[tempIndices[v]].tangent = tangent;
                    }

                    index_offset += fv;
                }
            }

            return true;
        }
    }
}