// OBJ_Loader.h - A Single Header OBJ Model Loader

#pragma once

// Iostream - STD I/O Library
#include <iostream>

// Vector - STD Vector/Array Library
#include <vector>

// String - STD String Library
#include <string>

// fStream - STD File I/O Library
#include <fstream>

// Math.h - STD math Library
#include <math.h>

// Print progress to console while loading (large models)
#define OBJL_CONSOLE_OUTPUT

// Namespace: OBJL
//
// Description: The namespace that holds eveyrthing that
//	is needed and used for the OBJ Model Loader
namespace objl {
// Structure: Vector2
//
// Description: A 2D Vector that Holds Positional Data
struct Vector2 {
  // Default Constructor
  Vector2() {
    X = 0.0f;
    Y = 0.0f;
  }
  // Variable Set Constructor
  Vector2(float X_, float Y_) {
    X = X_;
    Y = Y_;
  }
  // Bool Equals Operator Overload
  bool operator==(const Vector2 &other) const {
    return (this->X == other.X && this->Y == other.Y);
  }
  // Bool Not Equals Operator Overload
  bool operator!=(const Vector2 &other) const {
    return !(this->X == other.X && this->Y == other.Y);
  }
  // Addition Operator Overload
  Vector2 operator+(const Vector2 &right) const {
    return Vector2(this->X + right.X, this->Y + right.Y);
  }
  // Subtraction Operator Overload
  Vector2 operator-(const Vector2 &right) const {
    return Vector2(this->X - right.X, this->Y - right.Y);
  }
  // Float Multiplication Operator Overload
  Vector2 operator*(const float &other) const {
    return Vector2(this->X * other, this->Y * other);
  }

  // Positional Variables
  float X;
  float Y;
};

// Structure: Vector3
//
// Description: A 3D Vector that Holds Positional Data
struct Vector3 {
  // Default Constructor
  Vector3() {
    X = 0.0f;
    Y = 0.0f;
    Z = 0.0f;
  }
  // Variable Set Constructor
  Vector3(float X_, float Y_, float Z_) {
    X = X_;
    Y = Y_;
    Z = Z_;
  }
  // Bool Equals Operator Overload
  bool operator==(const Vector3 &other) const {
    return (this->X == other.X && this->Y == other.Y && this->Z == other.Z);
  }
  // Bool Not Equals Operator Overload
  bool operator!=(const Vector3 &other) const {
    return !(this->X == other.X && this->Y == other.Y && this->Z == other.Z);
  }
  // Addition Operator Overload
  Vector3 operator+(const Vector3 &right) const {
    return Vector3(this->X + right.X, this->Y + right.Y, this->Z + right.Z);
  }
  // Subtraction Operator Overload
  Vector3 operator-(const Vector3 &right) const {
    return Vector3(this->X - right.X, this->Y - right.Y, this->Z - right.Z);
  }
  // Float Multiplication Operator Overload
  Vector3 operator*(const float &other) const {
    return Vector3(this->X * other, this->Y * other, this->Z * other);
  }
  // Float Division Operator Overload
  Vector3 operator/(const float &other) const {
    return Vector3(this->X / other, this->Y / other, this->Z / other);
  }

  // Positional Variables
  float X;
  float Y;
  float Z;
};

// Structure: Vertex
//
// Description: Model Vertex object that holds
//	a Position, Normal, and Texture Coordinate
struct Vertex {
  // Position Vector
  Vector3 Position;

  // Normal Vector
  Vector3 Normal;

  // Texture Coordinate Vector
  Vector2 TextureCoordinate;
};

struct Material {
  Material() {
    Ns = 0.0f;
    Ni = 0.0f;
    d = 0.0f;
    illum = 0;
  }

  // Material Name
  std::string name;
  // Ambient Color
  Vector3 Ka;
  // Diffuse Color
  Vector3 Kd;
  // Specular Color
  Vector3 Ks;
  // Specular Exponent
  float Ns;
  // Optical Density
  float Ni;
  // Dissolve
  float d;
  // Illumination
  int illum;
  // Ambient Texture Map
  std::string map_Ka;
  // Diffuse Texture Map
  std::string map_Kd;
  // Specular Texture Map
  std::string map_Ks;
  // Specular Hightlight Map
  std::string map_Ns;
  // Alpha Texture Map
  std::string map_d;
  // Bump Map
  std::string map_bump;
};

// Structure: Mesh
//
// Description: A Simple Mesh Object that holds
//	a name, a vertex list, and an index list
struct Mesh {
  // Default Constructor
  Mesh() {}
  // Variable Set Constructor
  Mesh(std::vector<Vertex> &_Vertices, std::vector<unsigned int> &_Indices) {
    Vertices = _Vertices;
    Indices = _Indices;
  }
  // Mesh Name
  std::string MeshName;
  // Vertex List
  std::vector<Vertex> Vertices;
  // Index List
  std::vector<unsigned int> Indices;

  // Material
  Material MeshMaterial;
};

// Namespace: Math
//
// Description: The namespace that holds all of the math
//	functions need for OBJL
namespace math {
// Vector3 Cross Product
Vector3 CrossV3(const Vector3 a, const Vector3 b) {
  return Vector3(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z,
                 a.X * b.Y - a.Y * b.X);
}

// Vector3 Magnitude Calculation
float MagnitudeV3(const Vector3 in) {
  return (sqrtf(powf(in.X, 2) + powf(in.Y, 2) + powf(in.Z, 2)));
}

// Vector3 DotProduct
float DotV3(const Vector3 a, const Vector3 b) {
  return (a.X * b.X) + (a.Y * b.Y) + (a.Z * b.Z);
}

// Angle between 2 Vector3 Objects
float AngleBetweenV3(const Vector3 a, const Vector3 b) {
  float angle = DotV3(a, b);
  angle /= (MagnitudeV3(a) * MagnitudeV3(b));
  return angle = acosf(angle);
}

// Projection Calculation of a onto b
Vector3 ProjV3(const Vector3 a, const Vector3 b) {
  Vector3 bn = b / MagnitudeV3(b);
  return bn * DotV3(a, bn);
}
} // namespace math

// Namespace: Algorithm
//
// Description: The namespace that holds all of the
// Algorithms needed for OBJL
namespace algorithm {
// Vector3 Multiplication Opertor Overload
Vector3 operator*(const float &left, const Vector3 &right) {
  return Vector3(right.X * left, right.Y * left, right.Z * left);
}

// A test to see if P1 is on the same side as P2 of a line segment ab
bool SameSide(Vector3 p1, Vector3 p2, Vector3 a, Vector3 b) {
  Vector3 cp1 = math::CrossV3(b - a, p1 - a);
  Vector3 cp2 = math::CrossV3(b - a, p2 - a);

  if (math::DotV3(cp1, cp2) >= 0)
    return true;
  else
    return false;
}

// Generate a cross produect normal for a triangle
Vector3 GenTriNormal(Vector3 t1, Vector3 t2, Vector3 t3) {
  Vector3 u = t2 - t1;
  Vector3 v = t3 - t1;

  Vector3 normal = math::CrossV3(u, v);

  return normal;
}

// Check to see if a Vector3 Point is within a 3 Vector3 Triangle
bool inTriangle(Vector3 point, Vector3 tri1, Vector3 tri2, Vector3 tri3) {
  // Test to see if it is within an infinite prism that the triangle outlines.
  bool within_tri_prisim = SameSide(point, tri1, tri2, tri3) &&
                           SameSide(point, tri2, tri1, tri3) &&
                           SameSide(point, tri3, tri1, tri2);

  // If it isn't it will never be on the triangle
  if (!within_tri_prisim)
    return false;

  // Calulate Triangle's Normal
  Vector3 n = GenTriNormal(tri1, tri2, tri3);

  // Project the point onto this normal
  Vector3 proj = math::ProjV3(point, n);

  // If the distance from the triangle to the point is 0
  //	it lies on the triangle
  if (math::MagnitudeV3(proj) == 0)
    return true;
  else
    return false;
}

// Split a String into a string array at a given token
inline void split(const std::string &in, std::vector<std::string> &out,
                  std::string token) {
  out.clear();

  std::string temp;

  for (int i = 0; i < int(in.size()); i++) {
    std::string test = in.substr(i, token.size());

    if (test == token) {
      if (!temp.empty()) {
        out.push_back(temp);
        temp.clear();
        i += (int)token.size() - 1;
      } else {
        out.push_back("");
      }
    } else if (i + token.size() >= in.size()) {
      temp += in.substr(i, token.size());
      out.push_back(temp);
      break;
    } else {
      temp += in[i];
    }
  }
}

// Get tail of string after first token and possibly following spaces
inline std::string tail(const std::string &in) {
  size_t token_start = in.find_first_not_of(" \t");
  size_t space_start = in.find_first_of(" \t", token_start);
  size_t tail_start = in.find_first_not_of(" \t", space_start);
  size_t tail_end = in.find_last_not_of(" \t");
  if (tail_start != std::string::npos && tail_end != std::string::npos) {
    return in.substr(tail_start, tail_end - tail_start + 1);
  } else if (tail_start != std::string::npos) {
    return in.substr(tail_start);
  }
  return "";
}

// Get first token of string
inline std::string firstToken(const std::string &in) {
  if (!in.empty()) {
    size_t token_start = in.find_first_not_of(" \t");
    size_t token_end = in.find_first_of(" \t", token_start);
    if (token_start != std::string::npos && token_end != std::string::npos) {
      return in.substr(token_start, token_end - token_start);
    } else if (token_start != std::string::npos) {
      return in.substr(token_start);
    }
  }
  return "";
}

// Get element at given index position
template <class T>
inline const T &getElement(const std::vector<T> &elements, std::string &index) {
  int idx = std::stoi(index);
  if (idx < 0)
    idx = int(elements.size()) + idx;
  else
    idx--;
  return elements[idx];
}
} // namespace algorithm

// Class: Loader
//
// Description: The OBJ Model Loader
class Loader {
public:
  // Default Constructor
  Loader() {}
  ~Loader() { LoadedMeshes.clear(); }

  // Load a file into the loader
  //
  // If file is loaded return true
  //
  // If the file is unable to be found
  // or unable to be loaded return false
  bool LoadFile(std::string Path) {
    // If the file is not an .obj file return false
    if (Path.substr(Path.size() - 4, 4) != ".obj")
      return false;

    std::ifstream file(Path);

    if (!file.is_open())
      return false;

    LoadedMeshes.clear();
    LoadedVertices.clear();
    LoadedIndices.clear();

    std::vector<Vector3> Positions;
    std::vector<Vector2> TCoords;
    std::vector<Vector3> Normals;

    std::vector<Vertex> Vertices;
    std::vector<unsigned int> Indices;

    std::vector<std::string> MeshMatNames;

    bool listening = false;
    std::string meshname;

    Mesh tempMesh;

#ifdef OBJL_CONSOLE_OUTPUT
    const unsigned int outputEveryNth = 1000;
    unsigned int outputIndicator = outputEveryNth;
#endif

    std::string curline;
    while (std::getline(file, curline)) {
#ifdef OBJL_CONSOLE_OUTPUT
      if ((outputIndicator = ((outputIndicator + 1) % outputEveryNth)) == 1) {
        if (!meshname.empty()) {
          std::cout << "\r- " << meshname << "\t| vertices > "
                    << Positions.size() << "\t| texcoords > " << TCoords.size()
                    << "\t| normals > " << Normals.size() << "\t| triangles > "
                    << (Vertices.size() / 3)
                    << (!MeshMatNames.empty()
                            ? "\t| material: " + MeshMatNames.back()
                            : "");
        }
      }
#endif

      // Generate a Mesh Object or Prepare for an object to be created
      if (algorithm::firstToken(curline) == "o" ||
          algorithm::firstToken(curline) == "g" || curline[0] == 'g') {
        if (!listening) {
          listening = true;

          if (algorithm::firstToken(curline) == "o" ||
              algorithm::firstToken(curline) == "g") {
            meshname = algorithm::tail(curline);
          } else {
            meshname = "unnamed";
          }
        } else {
          // Generate the mesh to put into the array

          if (!Indices.empty() && !Vertices.empty()) {
            // Create Mesh
            tempMesh = Mesh(Vertices, Indices);
            tempMesh.MeshName = meshname;

            // Insert Mesh
            LoadedMeshes.push_back(tempMesh);

            // Cleanup
            Vertices.clear();
            Indices.clear();
            meshname.clear();

            meshname = algorithm::tail(curline);
          } else {
            if (algorithm::firstToken(curline) == "o" ||
                algorithm::firstToken(curline) == "g") {
              meshname = algorithm::tail(curline);
            } else {
              meshname = "unnamed";
            }
          }
        }
#ifdef OBJL_CONSOLE_OUTPUT
        std::cout << std::endl;
        outputIndicator = 0;
#endif
      }
      // Generate a Vertex Position
      if (algorithm::firstToken(curline) == "v") {
        std::vector<std::string> spos;
        Vector3 vpos;
        algorithm::split(algorithm::tail(curline), spos, " ");

        vpos.X = std::stof(spos[0]);
        vpos.Y = std::stof(spos[1]);
        vpos.Z = std::stof(spos[2]);

        Positions.push_back(vpos);
      }
      // Generate a Vertex Texture Coordinate
      if (algorithm::firstToken(curline) == "vt") {
        std::vector<std::string> stex;
        Vector2 vtex;
        algorithm::split(algorithm::tail(curline), stex, " ");

        vtex.X = std::stof(stex[0]);
        vtex.Y = std::stof(stex[1]);

        TCoords.push_back(vtex);
      }
      // Generate a Vertex Normal;
      if (algorithm::firstToken(curline) == "vn") {
        std::vector<std::string> snor;
        Vector3 vnor;
        algorithm::split(algorithm::tail(curline), snor, " ");

        vnor.X = std::stof(snor[0]);
        vnor.Y = std::stof(snor[1]);
        vnor.Z = std::stof(snor[2]);

        Normals.push_back(vnor);
      }
      // Generate a Face (vertices & indices)
      if (algorithm::firstToken(curline) == "f") {
        // Generate the vertices
        std::vector<Vertex> vVerts;
        GenVerticesFromRawOBJ(vVerts, Positions, TCoords, Normals, curline);

        // Add Vertices
        for (int i = 0; i < int(vVerts.size()); i++) {
          Vertices.push_back(vVerts[i]);

          LoadedVertices.push_back(vVerts[i]);
        }

        std::vector<unsigned int> iIndices;

        VertexTriangluation(iIndices, vVerts);

        // Add Indices
        for (int i = 0; i < int(iIndices.size()); i++) {
          unsigned int indnum =
              (unsigned int)((Vertices.size()) - vVerts.size()) + iIndices[i];
          Indices.push_back(indnum);

          indnum = (unsigned int)((LoadedVertices.size()) - vVerts.size()) +
                   iIndices[i];
          LoadedIndices.push_back(indnum);
        }
      }
      // Get Mesh Material Name
      if (algorithm::firstToken(curline) == "usemtl") {
        MeshMatNames.push_back(algorithm::tail(curline));

        // Create new Mesh, if Material changes within a group
        if (!Indices.empty() && !Vertices.empty()) {
          // Create Mesh
          tempMesh = Mesh(Vertices, Indices);
          tempMesh.MeshName = meshname;
          int i = 2;
          while (1) {
            tempMesh.MeshName = meshname + "_" + std::to_string(i);

            for (auto &m : LoadedMeshes)
              if (m.MeshName == tempMesh.MeshName)
                continue;
            break;
          }

          // Insert Mesh
          LoadedMeshes.push_back(tempMesh);

          // Cleanup
          Vertices.clear();
          Indices.clear();
        }

#ifdef OBJL_CONSOLE_OUTPUT
        outputIndicator = 0;
#endif
      }
      // Load Materials
      if (algorithm::firstToken(curline) == "mtllib") {
        // Generate LoadedMaterial

        // Generate a path to the material file
        std::vector<std::string> temp;
        algorithm::split(Path, temp, "/");

        std::string pathtomat = "";

        if (temp.size() != 1) {
          for (int i = 0; i < temp.size() - 1; i++) {
            pathtomat += temp[i] + "/";
          }
        }

        pathtomat += algorithm::tail(curline);

#ifdef OBJL_CONSOLE_OUTPUT
        std::cout << std::endl
                  << "- find materials in: " << pathtomat << std::endl;
#endif

        // Load Materials
        LoadMaterials(pathtomat);
      }
    }

#ifdef OBJL_CONSOLE_OUTPUT
    std::cout << std::endl;
#endif

    // Deal with last mesh

    if (!Indices.empty() && !Vertices.empty()) {
      // Create Mesh
      tempMesh = Mesh(Vertices, Indices);
      tempMesh.MeshName = meshname;

      // Insert Mesh
      LoadedMeshes.push_back(tempMesh);
    }

    file.close();

    // Set Materials for each Mesh
    for (int i = 0; i < MeshMatNames.size(); i++) {
      std::string matname = MeshMatNames[i];

      // Find corresponding material name in loaded materials
      // when found copy material variables into mesh material
      for (int j = 0; j < LoadedMaterials.size(); j++) {
        if (LoadedMaterials[j].name == matname) {
          LoadedMeshes[i].MeshMaterial = LoadedMaterials[j];
          break;
        }
      }
    }

    if (LoadedMeshes.empty() && LoadedVertices.empty() &&
        LoadedIndices.empty()) {
      return false;
    } else {
      return true;
    }
  }

  // Loaded Mesh Objects
  std::vector<Mesh> LoadedMeshes;
  // Loaded Vertex Objects
  std::vector<Vertex> LoadedVertices;
  // Loaded Index Positions
  std::vector<unsigned int> LoadedIndices;
  // Loaded Material Objects
  std::vector<Material> LoadedMaterials;

private:
  // Generate vertices from a list of positions,
  //	tcoords, normals and a face line
  void GenVerticesFromRawOBJ(std::vector<Vertex> &oVerts,
                             const std::vector<Vector3> &iPositions,
                             const std::vector<Vector2> &iTCoords,
                             const std::vector<Vector3> &iNormals,
                             std::string icurline) {
    std::vector<std::string> sface, svert;
    Vertex vVert;
    algorithm::split(algorithm::tail(icurline), sface, " ");

    bool noNormal = false;

    // For every given vertex do this
    for (int i = 0; i < int(sface.size()); i++) {
      // See What type the vertex is.
      int vtype;

      algorithm::split(sface[i], svert, "/");

      // Check for just position - v1
      if (svert.size() == 1) {
        // Only position
        vtype = 1;
      }

      // Check for position & texture - v1/vt1
      if (svert.size() == 2) {
        // Position & Texture
        vtype = 2;
      }

      // Check for Position, Texture and Normal - v1/vt1/vn1
      // or if Position and Normal - v1//vn1
      if (svert.size() == 3) {
        if (svert[1] != "") {
          // Position, Texture, and Normal
          vtype = 4;
        } else {
          // Position & Normal
          vtype = 3;
        }
      }

      // Calculate and store the vertex
      switch (vtype) {
      case 1: // P
      {
        vVert.Position = algorithm::getElement(iPositions, svert[0]);
        vVert.TextureCoordinate = Vector2(0, 0);
        noNormal = true;
        oVerts.push_back(vVert);
        break;
      }
      case 2: // P/T
      {
        vVert.Position = algorithm::getElement(iPositions, svert[0]);
        vVert.TextureCoordinate = algorithm::getElement(iTCoords, svert[1]);
        noNormal = true;
        oVerts.push_back(vVert);
        break;
      }
      case 3: // P//N
      {
        vVert.Position = algorithm::getElement(iPositions, svert[0]);
        vVert.TextureCoordinate = Vector2(0, 0);
        vVert.Normal = algorithm::getElement(iNormals, svert[2]);
        oVerts.push_back(vVert);
        break;
      }
      case 4: // P/T/N
      {
        vVert.Position = algorithm::getElement(iPositions, svert[0]);
        vVert.TextureCoordinate = algorithm::getElement(iTCoords, svert[1]);
        vVert.Normal = algorithm::getElement(iNormals, svert[2]);
        oVerts.push_back(vVert);
        break;
      }
      default: {
        break;
      }
      }
    }

    // take care of missing normals
    // these may not be truly acurate but it is the
    // best they get for not compiling a mesh with normals
    if (noNormal) {
      Vector3 A = oVerts[0].Position - oVerts[1].Position;
      Vector3 B = oVerts[2].Position - oVerts[1].Position;

      Vector3 normal = math::CrossV3(A, B);

      for (int i = 0; i < int(oVerts.size()); i++) {
        oVerts[i].Normal = normal;
      }
    }
  }

  // Triangulate a list of vertices into a face by printing
  //	inducies corresponding with triangles within it
  void VertexTriangluation(std::vector<unsigned int> &oIndices,
                           const std::vector<Vertex> &iVerts) {
    // If there are 2 or less verts,
    // no triangle can be created,
    // so exit
    if (iVerts.size() < 3) {
      return;
    }
    // If it is a triangle no need to calculate it
    if (iVerts.size() == 3) {
      oIndices.push_back(0);
      oIndices.push_back(1);
      oIndices.push_back(2);
      return;
    }

    // Create a list of vertices
    std::vector<Vertex> tVerts = iVerts;

    while (true) {
      // For every vertex
      for (int i = 0; i < int(tVerts.size()); i++) {
        // pPrev = the previous vertex in the list
        Vertex pPrev;
        if (i == 0) {
          pPrev = tVerts[tVerts.size() - 1];
        } else {
          pPrev = tVerts[i - 1];
        }

        // pCur = the current vertex;
        Vertex pCur = tVerts[i];

        // pNext = the next vertex in the list
        Vertex pNext;
        if (i == tVerts.size() - 1) {
          pNext = tVerts[0];
        } else {
          pNext = tVerts[i + 1];
        }

        // Check to see if there are only 3 verts left
        // if so this is the last triangle
        if (tVerts.size() == 3) {
          // Create a triangle from pCur, pPrev, pNext
          for (int j = 0; j < int(tVerts.size()); j++) {
            if (iVerts[j].Position == pCur.Position)
              oIndices.push_back(j);
            if (iVerts[j].Position == pPrev.Position)
              oIndices.push_back(j);
            if (iVerts[j].Position == pNext.Position)
              oIndices.push_back(j);
          }

          tVerts.clear();
          break;
        }
        if (tVerts.size() == 4) {
          // Create a triangle from pCur, pPrev, pNext
          for (int j = 0; j < int(iVerts.size()); j++) {
            if (iVerts[j].Position == pCur.Position)
              oIndices.push_back(j);
            if (iVerts[j].Position == pPrev.Position)
              oIndices.push_back(j);
            if (iVerts[j].Position == pNext.Position)
              oIndices.push_back(j);
          }

          Vector3 tempVec;
          for (int j = 0; j < int(tVerts.size()); j++) {
            if (tVerts[j].Position != pCur.Position &&
                tVerts[j].Position != pPrev.Position &&
                tVerts[j].Position != pNext.Position) {
              tempVec = tVerts[j].Position;
              break;
            }
          }

          // Create a triangle from pCur, pPrev, pNext
          for (int j = 0; j < int(iVerts.size()); j++) {
            if (iVerts[j].Position == pPrev.Position)
              oIndices.push_back(j);
            if (iVerts[j].Position == pNext.Position)
              oIndices.push_back(j);
            if (iVerts[j].Position == tempVec)
              oIndices.push_back(j);
          }

          tVerts.clear();
          break;
        }

        // If Vertex is not an interior vertex
        float angle = math::AngleBetweenV3(pPrev.Position - pCur.Position,
                                           pNext.Position - pCur.Position) *
                      (180 / 3.14159265359);
        if (angle <= 0 && angle >= 180)
          continue;

        // If any vertices are within this triangle
        bool inTri = false;
        for (int j = 0; j < int(iVerts.size()); j++) {
          if (algorithm::inTriangle(iVerts[j].Position, pPrev.Position,
                                    pCur.Position, pNext.Position) &&
              iVerts[j].Position != pPrev.Position &&
              iVerts[j].Position != pCur.Position &&
              iVerts[j].Position != pNext.Position) {
            inTri = true;
            break;
          }
        }
        if (inTri)
          continue;

        // Create a triangle from pCur, pPrev, pNext
        for (int j = 0; j < int(iVerts.size()); j++) {
          if (iVerts[j].Position == pCur.Position)
            oIndices.push_back(j);
          if (iVerts[j].Position == pPrev.Position)
            oIndices.push_back(j);
          if (iVerts[j].Position == pNext.Position)
            oIndices.push_back(j);
        }

        // Delete pCur from the list
        for (int j = 0; j < int(tVerts.size()); j++) {
          if (tVerts[j].Position == pCur.Position) {
            tVerts.erase(tVerts.begin() + j);
            break;
          }
        }

        // reset i to the start
        // -1 since loop will add 1 to it
        i = -1;
      }

      // if no triangles were created
      if (oIndices.size() == 0)
        break;

      // if no more vertices
      if (tVerts.size() == 0)
        break;
    }
  }

  // Load Materials from .mtl file
  bool LoadMaterials(std::string path) {
    // If the file is not a material file return false
    if (path.substr(path.size() - 4, path.size()) != ".mtl")
      return false;

    std::ifstream file(path);

    // If the file is not found return false
    if (!file.is_open())
      return false;

    Material tempMaterial;

    bool listening = false;

    // Go through each line looking for material variables
    std::string curline;
    while (std::getline(file, curline)) {
      // new material and material name
      if (algorithm::firstToken(curline) == "newmtl") {
        if (!listening) {
          listening = true;

          if (curline.size() > 7) {
            tempMaterial.name = algorithm::tail(curline);
          } else {
            tempMaterial.name = "none";
          }
        } else {
          // Generate the material

          // Push Back loaded Material
          LoadedMaterials.push_back(tempMaterial);

          // Clear Loaded Material
          tempMaterial = Material();

          if (curline.size() > 7) {
            tempMaterial.name = algorithm::tail(curline);
          } else {
            tempMaterial.name = "none";
          }
        }
      }
      // Ambient Color
      if (algorithm::firstToken(curline) == "Ka") {
        std::vector<std::string> temp;
        algorithm::split(algorithm::tail(curline), temp, " ");

        if (temp.size() != 3)
          continue;

        tempMaterial.Ka.X = std::stof(temp[0]);
        tempMaterial.Ka.Y = std::stof(temp[1]);
        tempMaterial.Ka.Z = std::stof(temp[2]);
      }
      // Diffuse Color
      if (algorithm::firstToken(curline) == "Kd") {
        std::vector<std::string> temp;
        algorithm::split(algorithm::tail(curline), temp, " ");

        if (temp.size() != 3)
          continue;

        tempMaterial.Kd.X = std::stof(temp[0]);
        tempMaterial.Kd.Y = std::stof(temp[1]);
        tempMaterial.Kd.Z = std::stof(temp[2]);
      }
      // Specular Color
      if (algorithm::firstToken(curline) == "Ks") {
        std::vector<std::string> temp;
        algorithm::split(algorithm::tail(curline), temp, " ");

        if (temp.size() != 3)
          continue;

        tempMaterial.Ks.X = std::stof(temp[0]);
        tempMaterial.Ks.Y = std::stof(temp[1]);
        tempMaterial.Ks.Z = std::stof(temp[2]);
      }
      // Specular Exponent
      if (algorithm::firstToken(curline) == "Ns") {
        tempMaterial.Ns = std::stof(algorithm::tail(curline));
      }
      // Optical Density
      if (algorithm::firstToken(curline) == "Ni") {
        tempMaterial.Ni = std::stof(algorithm::tail(curline));
      }
      // Dissolve
      if (algorithm::firstToken(curline) == "d") {
        tempMaterial.d = std::stof(algorithm::tail(curline));
      }
      // Illumination
      if (algorithm::firstToken(curline) == "illum") {
        tempMaterial.illum = std::stoi(algorithm::tail(curline));
      }
      // Ambient Texture Map
      if (algorithm::firstToken(curline) == "map_Ka") {
        tempMaterial.map_Ka = algorithm::tail(curline);
      }
      // Diffuse Texture Map
      if (algorithm::firstToken(curline) == "map_Kd") {
        tempMaterial.map_Kd = algorithm::tail(curline);
      }
      // Specular Texture Map
      if (algorithm::firstToken(curline) == "map_Ks") {
        tempMaterial.map_Ks = algorithm::tail(curline);
      }
      // Specular Hightlight Map
      if (algorithm::firstToken(curline) == "map_Ns") {
        tempMaterial.map_Ns = algorithm::tail(curline);
      }
      // Alpha Texture Map
      if (algorithm::firstToken(curline) == "map_d") {
        tempMaterial.map_d = algorithm::tail(curline);
      }
      // Bump Map
      if (algorithm::firstToken(curline) == "map_Bump" ||
          algorithm::firstToken(curline) == "map_bump" ||
          algorithm::firstToken(curline) == "bump") {
        tempMaterial.map_bump = algorithm::tail(curline);
      }
    }

    // Deal with last material

    // Push Back loaded Material
    LoadedMaterials.push_back(tempMaterial);

    // Test to see if anything was loaded
    // If not return false
    if (LoadedMaterials.empty())
      return false;
    // If so return true
    else
      return true;
  }
};
} // namespace objl