//  BlockyFroggy
//  Copyright © 2017 John Ryland.
//  All rights reserved.
#include "ObjModel.h"
#include "Log.h"
#include <string>
#include <sstream>
#include <cstdio>
#include <cstdlib>
#include <functional>
#include "ResourceLoader.h"
#include "Utilities.h"


DECLARE_LOG_CONTEXT(OBJ)


/*
 To test, compile with:
    g++ -std=c++11 ObjModel.cpp -o test -D TEST
*/


/*

  Two concepts that could be taken
        - OOC - out of core - processing the data progressively - useful in case of large models and in a pipeline or converter
        - in core - slurp the whole file in to memory, and then just work from RAM - more efficient - limited to what fits in RAM - not a streaming solution

  Code is/was using files and doing line based reading and parsing so would be suited for OOC, but I think for what is actually needed
  Now abstracted this, DataSource is the base class, 2 implementations, one directed towards OOC, other using resource loader.
  ResourceLoader one is async

*/


std::string removeCRLF(std::string line)
{
  if (line[line.size()-1] == '\n')
    return line.substr(0, line.size()-1);
  return line;
}

class DataSource
{
public:
    DataSource() {}
    virtual ~DataSource() {}
    virtual void open(const char* fileName, std::function<bool(bool okay, DataSource& fd)> callback) = 0;
    virtual bool getNextLine(std::string& str) = 0;
};

class FileDataSource : public DataSource
{
public:
    FileDataSource() : fd(nullptr) {}
    ~FileDataSource() override {
      if (fd)
        fclose(fd);
    }
    void open(const char* fileName, std::function<bool(bool okay, DataSource& fd)> callback) override {
      std::string f = "../../Data/";
      f += fileName;
      if (!(fd = fopen(f.c_str(), "rb")))
        Log(LL_Error, "failed to open file");
      callback(!!fd, *this);
    }
    bool getNextLine(std::string& str) override {
      char buf[512];
      bool res = fd && fgets(buf, 511, fd);
      if (res)
        str = removeCRLF(buf);
      return res;
    }
    FILE* fd;
};

class ResourceLoaderDataSource : public DataSource
{
public:
    ResourceLoaderDataSource() {}
    ~ResourceLoaderDataSource() override {
    }
    void open(const char* fileName, std::function<bool(bool okay, DataSource& fd)> callback) override {
      m_offset = 0;
      m_res = loadFileAsync(fileName, true, [callback](Resource* res) {
          ResourceLoaderDataSource tmp;
          tmp.m_offset = 0;
          tmp.m_res = res;
          callback(!!res, tmp);
      });
    }
    bool getNextLine(std::string& str) override {
      str = "";
      if (m_res && m_res->isLoaded() && m_res->data.size()) {
        ByteArray& buffer = m_res->data;
        if (m_offset >= buffer.size()) return false;
        std::string ret;
        while (m_offset < buffer.size() && buffer[m_offset] != '\n') {
          ret += buffer[m_offset];
          m_offset++;
        }
        m_offset++;
        str = removeCRLF(ret);
        return true;
      }
      return false;
    }
    size_t m_offset;
    Resource* m_res;
    FILE* fd;
};


//using Loader = FileDataSource;
using Loader = ResourceLoaderDataSource;


bool readMaterial(Material& mat, std::string& line, bool& more, DataSource& fd)
{
  std::vector<std::string> tokens = Utilities::split(line, ' ');
  mat.materialName = tokens[1];
  while (fd.getNextLine(line)) {
    std::vector<std::string> tokens = Utilities::split(line, ' ');
    if (tokens.size() >= 2) {
      if (tokens[0] == "Ns") {
        mat.Ns = atof(tokens[1].c_str());
      } else if (tokens[0] == "Ni") {
        mat.Ni = atof(tokens[1].c_str());
      } else if (tokens[0] == "Ka") {
        if (tokens.size() != 4)
          return false;
        for (size_t i = 0; i < 3; i++)
          mat.Ka[i] = atof(tokens[i+1].c_str());
      } else if (tokens[0] == "Kd") {
        if (tokens.size() != 4)
          return false;
        for (size_t i = 0; i < 3; i++)
          mat.Kd[i] = atof(tokens[i+1].c_str());
        //mat.Kd = { atof(tokens[1].c_str()), atof(tokens[2].c_str()), atof(tokens[3].c_str()) };
      } else if (tokens[0] == "map_Kd") {
        if (tokens.size() != 2)
          return false;
        mat.map_Kd = tokens[1];
      } else if (tokens[0] == "Ks") {
        if (tokens.size() != 4)
          return false;
        for (size_t i = 0; i < 3; i++)
          mat.Ks[i] = atof(tokens[i+1].c_str());
        //mat.Ks = { atof(tokens[1].c_str()), atof(tokens[2].c_str()), atof(tokens[3].c_str()) };
      } else if (tokens[0] == "d") {
        mat.d = atof(tokens[1].c_str());
      } else if (tokens[0] == "illum") {
        mat.illum = atoi(tokens[1].c_str());
      } else if (tokens[0][0] == '#') {
        // Just a comment, skip
      } else {
        // Not our token, return from here with more set to true
        more = true;
        return true;
      }
    }
  }
  return true;
}


void readMaterialLibrary(ObjScene& outScene, const char* fileName)
{
  Loader fd;
  fd.open(fileName, [&outScene](bool okay, DataSource& fd)->bool
  {
    if (!okay)
      return false;
    std::string line;
    while (fd.getNextLine(line)) {
      bool more = true;
      while (more) {
        more = false;
        std::vector<std::string> tokens = Utilities::split(line, ' ');
        if (tokens.size()) {
          if (tokens[0] == "newmtl") {
            Material newMat;
            Log(LL_Error, "reading material %s", tokens[1].c_str());
            if (!readMaterial(newMat, line, more, fd)) {
              return false;
            }
            outScene.materialLibrary[tokens[1]] = newMat;
          } else if (tokens[0][0] == '#') {
            // Just a comment
          } else {
            Log(LL_Error, "Unexpected token");
          }
        }
      }
    }
    return true;
  });
}


bool readObject(Object& obj, std::string& line, bool& more, DataSource& fd)
{
  std::vector<std::string> tokens = Utilities::split(line, ' ');
  obj.objectName = tokens[1];
  while (fd.getNextLine(line)) {
    std::vector<std::string> tokens = Utilities::split(line, ' ');
    if (tokens.size()) {
      if (tokens[0] == "usemtl") {
        obj.materialName = tokens[1];
      } else if (tokens[0] == "v") {
        //Log(LL_Error, "doing vert");
        if (tokens.size() != 4) {
          Log(LL_Error, "bad vert size: %li", tokens.size());
          return false;
        }
        obj.vertices.emplace_back(atof(tokens[1].c_str()), atof(tokens[2].c_str()), atof(tokens[3].c_str()));
      } else if (tokens[0] == "vn") {
        //Log(LL_Error, "doing vn");
        if (tokens.size() != 4) {
          Log(LL_Error, "bad normal size: %li", tokens.size());
          return false;
        }
        obj.normals.emplace_back(atof(tokens[1].c_str()), atof(tokens[2].c_str()), atof(tokens[3].c_str()));
      } else if (tokens[0] == "vt") {
        //Log(LL_Error, "doing vt");
        if (tokens.size() != 3) {
          Log(LL_Error, "bad uv size: %li", tokens.size());
          return false;
        }
        obj.uvs.emplace_back(atof(tokens[1].c_str()), atof(tokens[2].c_str()));
      } else if (tokens[0] == "f") {
        obj.faces.emplace_back();
        Face &face = obj.faces.back();
        for (size_t i = 1; i < tokens.size(); i++) {
          std::vector<std::string> faceVert = Utilities::split(tokens[i], '/');
          face.indices.emplace_back();
          Face::FaceDetail& d = face.indices.back();
          for (size_t i = 0; i < 3; i++)
            if (faceVert.size() >= (i+1))
              d.asArray[i] = atoi(faceVert[i].c_str());
        }
      } else if (tokens[0] == "s") {
        if (tokens.size() != 2) {
          Log(LL_Error, "bad smoothing size");
          return false;
        }
        if (tokens[1] == "off" || tokens[1] == "0")
          obj.smoothing = false;
        else if (tokens[1] == "on" || tokens[1] == "1")
          obj.smoothing = true;
        else {
          Log(LL_Error, "bad smoothing value");
          return false;
        }
      } else if (tokens[0][0] == '#') {
        // Just a comment, skip
      } else {
        Log(LL_Error, "other token: %s", tokens[0].c_str());
        // Not our token, return from here with more set to true
        more = true;
        return true;
      }
    }
  }
  return true;
}


std::pair<std::string,std::string> splitFilename(const std::string& filePath)
{
  std::size_t found = filePath.find_last_of("/\\");
  return std::make_pair(filePath.substr(0,found), filePath.substr(found+1));
}


bool readObjFile(ObjScene& outScene, const char* fileName, std::function<void(bool okay)> callback)
{
  std::string path = splitFilename(fileName).first;

  Loader fd;
  fd.open(fileName, [path, &outScene, callback](bool okay, DataSource& fd)->bool
  {
    if (!okay) {
      callback(false);
      return false;
    }
    std::string line;
    while (fd.getNextLine(line)) {
      bool more = true;
      while (more) {
        more = false;
        std::vector<std::string> tokens = Utilities::split(line, ' ');
        if (tokens.size()) {
          if (tokens[0] == "mtllib") {
            // std::string mtlFile = path + "/" + tokens[1];
            std::string mtlFile = tokens[1];
            Log(LL_Error, "reading material library %s", mtlFile.c_str());
            readMaterialLibrary(outScene, mtlFile.c_str());
            //Log(LL_Error, "OBJ", "error reading material library");
          } else if (tokens[0] == "o") {
            Log(LL_Error, "reading object %s", tokens[1].c_str());
            outScene.objects.emplace_back();
            if (!readObject(outScene.objects.back(), line, more, fd)) {
              Log(LL_Error, "error while reading object");
              callback(false);
              return false;
            }
            // The memory could move, so pointer to vector item in map is bad idea
            //outScene.objectMap[tokens[1]] = &outScene.objects.back();
          } else if (tokens[0][0] == '#') {
            // Just a comment
          } else {
            Log(LL_Error, "Unexpected token");
          }
        }
      }
    }
    callback(true);
    return true;
  });
  return true;
}



#ifdef TEST


int main(int argc, char* argv[])
{
  if ( argc >= 2) {
    Log(LL_Debug, "using argv[1] == %s as input file", argv[1]);
    ObjScene scene;
    if (readObjFile(scene, argv[1]))
      Log(LL_Debug, "success");
    else
      Log(LL_Debug, "failed");
  }
  return 0;
}


#endif


