#include "Model.h"

Model::Model(std::string path) { loadModel(path); }
Model::Model(Mesh mesh) { this->meshes.push_back(mesh); }
Model::Model(std::vector<Mesh> meshes) { this->meshes = meshes; }

void Model::draw(ShaderLoader &shader) {
  for (unsigned int i = 0; i < this->meshes.size(); i++) {
    shader.setMat4("Model", this->model);
    this->meshes[i].draw(shader);
  }
}

void Model::translate(glm::vec3 translation) {
  // set worldspace postition
  glm::mat4 trans = glm::mat4(1.0f);
  trans = glm::translate(trans, translation);
  glm::vec3 position = trans * this->position;

  // set model transform
  this->model = trans * this->model;

  // set position based on the current model
  this->position = model * glm::vec4(0.0f, 0.0f, 0.0f, 1.0f);
}
void Model::resize(glm::vec3 scale) {
  // Store objects position.
  glm::vec3 pos = this->position;
  // Reset models position so scale scales the object not it's transformation
  this->setPositionToOrigin();
  // Scale the model
  this->model = glm::scale(this->model, scale);
  // Return model to position.
  this->setPosition(pos);

  this->scale = this->scale * scale;
}
void Model::rotate(float angle, glm::vec3 axis) {
  this->model = glm::rotate(this->model, angle, axis);
}

void Model::loadModel(std::string path) {
  // Attempt to import model data using assimp
  Assimp::Importer import;
  const aiScene *scene =
      import.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);

  // Check the model got imported correctly
  if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE ||
      !scene->mRootNode) {
    error.crash("Assimp failed to load model data", import.GetErrorString());
    return;
  }
  directory = path.substr(0, path.find_last_of('/')) + '/';
  processNode(scene->mRootNode, scene);
}
void Model::processNode(aiNode *node, const aiScene *scene) {
  // if the node has meshes process them
  for (unsigned int i = 0; i < node->mNumMeshes; i++) {
    aiMesh *mesh = scene->mMeshes[node->mMeshes[i]];
    meshes.push_back(processMesh(mesh, scene));
  }
  // Now process any nodes that this node contains
  for (unsigned int i = 0; i < node->mNumChildren; i++) {
    processNode(node->mChildren[i], scene);
  }
}
// transform assimps mesh format to our own custom one
Mesh Model::processMesh(aiMesh *mesh, const aiScene *scene) {
  std::vector<Vertex> vertices;
  std::vector<unsigned int> indecies;
  std::vector<Texture> textures;

  for (unsigned int i = 0; i < mesh->mNumVertices; i++) {
    Vertex vertex;
    // process vertex postions and add to our mesh
    vertex.Position = glm::vec3(mesh->mVertices[i].x, mesh->mVertices[i].y,
                                mesh->mVertices[i].z);
    vertex.Normal = glm::vec3(mesh->mNormals[i].x, mesh->mNormals[i].y,
                              mesh->mNormals[i].z);
    // Handle texture coords
    if (mesh->mTextureCoords[0]) {
      vertex.TexCoords =
          glm::vec2(mesh->mTextureCoords[0][i].x, mesh->mTextureCoords[0][i].y);
    } else
      vertex.TexCoords = glm::vec2(0.0f, 0.0f);

    vertices.push_back(vertex);
  }
  // Handle indeces
  // Loop through the meshes faces to get the correct order
  for (unsigned int i = 0; i < mesh->mNumFaces; i++) {
    aiFace face = mesh->mFaces[i];
    // loop through and add each face's indecies
    for (unsigned int faceIndicie = 0; faceIndicie < face.mNumIndices;
         faceIndicie++) {
      indecies.push_back(face.mIndices[faceIndicie]);
    }
  }
  // Handle Assimps material format
  if (mesh->mMaterialIndex >= 0) {
    aiMaterial *material = scene->mMaterials[mesh->mMaterialIndex];
    std::vector<Texture> diffuseMaps = loadMaterialTextures(
        material, aiTextureType_DIFFUSE, "texture_diffuse");
    textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
    std::vector<Texture> normalMaps =
        loadMaterialTextures(material, aiTextureType_NORMALS, "texture_normal");
    textures.insert(textures.end(), normalMaps.begin(), normalMaps.end());

    // WARNING: As assimp updates to keep up with obj's mtl format
    // aiTextureType_sheen may become incorrect. Using sheen because assimp maps
    // map_Ps to both RMA  and sheen, and doesn't seem to use mtl's map_RMA
    // anywhere and I can't find an RMA texture type in this version of the
    // library.
    // https://github.com/assimp/assimp/commit/19371af6e65608ffc968df646bf20278e6d99414
    std::vector<Texture> rmaMaps =
        loadMaterialTextures(material, aiTextureType_SHEEN, "texture_rma");
    textures.insert(textures.end(), rmaMaps.begin(), rmaMaps.end());
  }
  return Mesh(vertices, indecies, textures);
}

std::vector<Texture> Model::loadMaterialTextures(aiMaterial *material,
                                                 aiTextureType type,
                                                 std::string typeName) {
  std::vector<Texture> textures;
  for (unsigned int i = 0; i < material->GetTextureCount(type); i++) {
    aiString str;
    material->GetTexture(type, i, &str);
    bool skip = false;

    // check we're not loading in a texture we already have
    for (unsigned int loadedtex = 0; loadedtex < textures_loaded.size();
         loadedtex++) {

      if (std::strcmp(textures_loaded[loadedtex].path.data(), str.C_Str()) ==
          0) {
        textures.push_back(textures_loaded[loadedtex]);
        skip = true;
        break;
      }
    }
    // If the texture isn't already loaded load it here
    if (!skip) {
      Texture texture;
      texture.id = loadTextureFromFile(str.C_Str(), directory);
      texture.type = typeName;
      texture.path = str.C_Str();
      textures.push_back(texture);
      // Store the texture in the models loaded texture bank so we can check if
      // we've already loaded it
      textures_loaded.push_back(texture);
    }
  }
  return textures;
}

unsigned int Model::loadTextureFromFile(std::string file,
                                        std::string directory) {
  // Use sdl2_image to load the texture.
  unsigned int texture;
  SDL_Surface *image = IMG_Load((directory + file).c_str());
  if (image == nullptr) {
    error.crash("SDL2_image was unable to load a texture", IMG_GetError());
  }

  // Generate the texture and put its reference id in the texture variable
  glGenTextures(1, &texture);
  glBindTexture(GL_TEXTURE_2D, texture);

  // set some textue defaults
  float borderColor[] = {1.0f, 1.0f, 0.0f, 1.0f};
  glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
                  GL_LINEAR_MIPMAP_LINEAR);

  // Handle different SDL Surface data types
  int mode = GL_RGB;
  if (image->format->BytesPerPixel == 4) {
    mode = GL_RGBA;
  }

  // Put image data into texture we've just generated
  glTexImage2D(GL_TEXTURE_2D, 0, mode, image->w, image->h, 0, mode,
               GL_UNSIGNED_BYTE, image->pixels);
  // Whilst we're here we might as well generate mipmaps
  glGenerateMipmap(GL_TEXTURE_2D);
  // remove image surface now it's no longer needed to create texture
  SDL_FreeSurface(image);
  image = nullptr;

  return texture;
}

void Model::unloadTextures() {
  for (int i = 0; i < textures_loaded.size(); i++) {
    glDeleteTextures(1, &textures_loaded[i].id);
  }
}

void Model::setPositionToOrigin() {
  this->translate(this->position * -1.0f);
  this->position = glm::vec4(0.0f, 0.0f, 0.0f, 1.0f);
}

void Model::setPosition(glm::vec3 position) {
  this->setPositionToOrigin();
  this->translate(position);
}
Model::~Model() { this->unloadTextures(); }
