//=============================================================================
// Copyright (C) 2001-2005 by Computer Graphics Group, RWTH Aachen
// Copyright (C) 2011 by Graphics & Geometry Group, Bielefeld University
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Library General Public License
// as published by the Free Software Foundation, version 2.
//
// This library is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Library General Public License for more details.
//
// You should have received a copy of the GNU Library General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
//=============================================================================


//== INCLUDES =================================================================

#include "IO.h"
#include <stdio.h>

#include <sstream>
#include <fstream>


//== NAMESPACES ===============================================================


namespace graphene {
namespace surface_mesh {


//== IMPLEMENTATION ===========================================================

//twaltema: helper function to read a specific material from a mtllib file
bool read_material(Surface_mesh& mesh, const std::string& filename, const std::string& mtllib_fname, const std::string& mtl_name)
{
    size_t pos = filename.rfind('/');
    if (pos == std::string::npos)
    {
        pos = filename.rfind('\\');
    }
    std::string filepath = filename.substr(0, pos+1);

    std::string full_filename = filepath + mtllib_fname;

    std::stringstream ss;
    std::string line,str;

    std::ifstream ifs(full_filename.c_str());

    if (!ifs)
        return false;

    //just find the right material entry
    while (ifs.good())
    {
        std::getline(ifs, line);
        if (line.find("newmtl") == 0)
            if (line.find(mtl_name.c_str()) != std::string::npos)
                break;
    }

    //next lines contain values for desired material
    while (ifs.good())
    {
        std::getline(ifs,line);

        // only read selected material; break at next material
        if (line.find("newmtl") != std::string::npos)
            break;

        ss.clear();
        ss.str(line);
        str.clear();
        ss >> str;
        if (str == "map_Kd")
        {
            ss >> str;
            Surface_mesh::Mesh_property<std::string> tex_name = mesh.mesh_property<std::string>("m:texturename");
            tex_name[0] = filepath + str;
        }
        else if (str == "map_Ks")
        {
            ss >> str;
            Surface_mesh::Mesh_property<std::string> tex_name = mesh.mesh_property<std::string>("m:texturename_spec");
            tex_name[0] = filepath + str;
        }
        else if (str == "map_bump" || str == "bump")
        {
            ss >> str;
            Surface_mesh::Mesh_property<std::string> tex_name = mesh.mesh_property<std::string>("m:texturename_nm");
            tex_name[0] = filepath + str;
        }
    }


    ifs.close();

    return true;
}

//-----------------------------------------------------------------------------

bool read_obj(Surface_mesh& mesh, const std::string& filename)
{
    char   s[200],tmp[32];
    float  x, y, z;
    std::vector<Surface_mesh::Vertex>  vertices;
    std::vector<Texture_coordinate> all_tex_coords;   //individual texture coordinates
    std::vector<int> halfedge_tex_idx; //texture coordinates sorted for halfedges
    std::string mtllib_name; // name of the mtllib
    Surface_mesh::Halfedge_property <Texture_coordinate> tex_coords;// = mesh.halfedge_property<Texture_coordinate>("h:texcoord");
    bool with_tex_coord=false;

    // clear mesh
    mesh.clear();


    // open file (in ASCII mode)
    FILE* in = fopen(filename.c_str(), "r");
    if (!in) return false;


    // clear line once
    memset(&s, 0, 200);


    // parse line by line (currently only supports vertex positions & faces
    while(in && !feof(in) && fgets(s, 200, in))
    {
        // comment
        if (s[0] == '#' || isspace(s[0])) continue;

        // vertex
        else if (strncmp(s, "v ", 2) == 0)
        {
            if (sscanf(s, "v %f %f %f", &x, &y, &z))
            {
                mesh.add_vertex(Point(x,y,z));
            }
        }
        // normal
        else if (strncmp(s, "vn ", 3) == 0)
        {
          if (sscanf(s, "vn %f %f %f", &x, &y, &z))
          {
            // problematic as it can be either a vertex property when interpolated
            // or a halfedge property for hard edges
          }
        }

        // texture coordinate
        else if (strncmp(s, "vt ", 3) == 0)
        {
          if (sscanf(s, "vt %f %f", &x, &y))
          {
            z=1;
            all_tex_coords.push_back(Texture_coordinate(x,y,z));
          }
        }

        else if (strncmp(s, "mtllib", 6) == 0)
        {
            if (sscanf(s,"mtllib %s", tmp))
            {
                //twaltema: save mtllib filename for later use
                mtllib_name = tmp;
            }
        }

        else if (strncmp(s, "usemtl", 6) == 0)
        {
            if (sscanf(s, "usemtl %s", tmp))
            {
                //twaltema: read material with name tmp from mtllib file
                read_material(mesh,filename, mtllib_name, tmp);
            }
        }


        // face
        else if (strncmp(s, "f ", 2) == 0)
        {
          int component(0), nV(0);
          bool endOfVertex(false);
          char *p0, *p1(s+1);

          vertices.clear();
          halfedge_tex_idx.clear();

          // skip white-spaces
          while (*p1==' ') ++p1;

          while (p1)
          {
            p0 = p1;

            // overwrite next separator

            // skip '/', '\n', ' ', '\0', '\r' <-- don't forget Windows
            while (*p1!='/' && *p1!='\r' && *p1!='\n' && *p1!=' ' && *p1!='\0') ++p1;

            // detect end of vertex
            if (*p1 != '/')
            {
              endOfVertex = true;
            }

            // replace separator by '\0'
            if (*p1 != '\0')
            {
              *p1 = '\0';
              p1++; // point to next token
            }

            // detect end of line and break
            if (*p1 == '\0' || *p1 == '\n')
            {
              p1 = 0;
            }

            // read next vertex component
            if (*p0 != '\0')
            {
              switch (component)
              {
                case 0: // vertex
                {
                  vertices.push_back( Surface_mesh::Vertex(atoi(p0) - 1) );
                  break;
                }
                case 1: // texture coord
                {
                  int idx = atoi(p0)-1;
                  halfedge_tex_idx.push_back(idx);
                  with_tex_coord=true;
                  break;
                }
                case 2: // normal
                  break;
              }
            }

            ++component;

            if (endOfVertex)
            {
              component = 0;
              nV++;
              endOfVertex = false;
            }
          }

          Surface_mesh::Face f=mesh.add_face(vertices);


          // add texture coordinates
          if(with_tex_coord)
          {
              if (!tex_coords)
              {
                  tex_coords = mesh.halfedge_property<Texture_coordinate>("h:texcoord", Texture_coordinate(0.0f));
              }

              Surface_mesh::Halfedge_around_face_circulator h_fit = mesh.halfedges(f);
              Surface_mesh::Halfedge_around_face_circulator h_end = h_fit;
              unsigned v_idx =0;
              do
              {
                  tex_coords[*h_fit]=all_tex_coords.at(halfedge_tex_idx.at(v_idx));
                  ++v_idx;
                  ++h_fit;
              }
              while(h_fit!=h_end);
          }
        }
        // clear line
        memset(&s, 0, 200);
    }

    fclose(in);
    return true;
}

//-----------------------------------------------------------------------------

bool write_obj(const Surface_mesh& mesh, const std::string& filename)
{
    FILE* out = fopen(filename.c_str(), "w");
    if (!out)
    {
        printf("[ERROR] Cannot open %s for writing\n", filename.c_str());
        return false;
    }

    // comment
    fprintf(out, "# OBJ export from graphene\n");

    //vertices
    Surface_mesh::Vertex_property<Point> points = mesh.get_vertex_property<Point>("v:point");
    for (Surface_mesh::Vertex_iterator vit=mesh.vertices_begin(); vit!=mesh.vertices_end(); ++vit)
    {
        const Point& p = points[*vit];
        fprintf(out, "v %.10f %.10f %.10f\n", p[0], p[1], p[2]);
    }
    //normals
    Surface_mesh::Vertex_property<Point> normals = mesh.get_vertex_property<Point>("v:normal");
    for (Surface_mesh::Vertex_iterator vit=mesh.vertices_begin(); vit!=mesh.vertices_end(); ++vit)
    {
        const Point& p = normals[*vit];
        fprintf(out, "vn %.10f %.10f %.10f\n", p[0], p[1], p[2]);
    }

    //optionally texture coordinates
    // do we have them?
    std::vector<std::string> h_props= mesh.halfedge_properties();
    bool with_tex_coord = false;
    std::vector<std::string>::iterator h_prop_end = h_props.end();
    std::vector<std::string>::iterator h_prop_start= h_props.begin();
    while(h_prop_start!=h_prop_end)
    {
        if(0==(*h_prop_start).compare("h:texcoord"))
        {
            with_tex_coord=true;
        }
        ++h_prop_start;
    }
    //if so then add
    if(with_tex_coord)
    {
        Surface_mesh::Halfedge_property<Texture_coordinate> tex_coord = mesh.get_halfedge_property<Texture_coordinate>("h:texcoord");
        for (Surface_mesh::Halfedge_iterator hit=mesh.halfedges_begin(); hit!=mesh.halfedges_end(); ++hit)
        {
            const Texture_coordinate& pt = tex_coord[*hit];
            fprintf(out, "vt %.10f %.10f %.10f\n", pt[0], pt[1], pt[2]);
        }
    }

    //faces
    for (Surface_mesh::Face_iterator fit=mesh.faces_begin(); fit!=mesh.faces_end(); ++fit)
    {
        fprintf(out, "f");
        Surface_mesh::Vertex_around_face_circulator fvit=mesh.vertices(*fit), fvend=fvit;
        Surface_mesh::Halfedge_around_face_circulator fhit=mesh.halfedges(*fit);
        do
        {
            if(with_tex_coord)
            {
                // write vertex index, tex_coord index and normal index
                fprintf(out, " %d/%d/%d", (*fvit).idx()+1, (*fhit).idx()+1, (*fvit).idx()+1);
                ++fhit;
            }
            else
            {
                // write vertex index and normal index
                fprintf(out, " %d//%d", (*fvit).idx()+1, (*fvit).idx()+1);
            }
        }
        while (++fvit != fvend);
        fprintf(out, "\n");
    }

    fclose(out);
    return true;
}

//-----------------------------------------------------------------------------

bool write_obj(const std::vector<Surface_mesh*>& meshes, const std::string& filename)
{
    if (meshes.size() == 0)
    {
        printf("[ERROR] %s: No meshes given.\n", __FUNCTION__);
        return false;
    }

    FILE* out = fopen(filename.c_str(), "w");
    if (!out)
    {
        printf("[ERROR] Cannot open %s for writing\n", filename.c_str());
        return false;
    }

    // comment
    fprintf(out, "# OBJ export from graphene\n");

    struct Index_offsets
    {
        int vertex_offset;
        int texcoord_offset;
    };

    std::vector<Index_offsets> index_offsets;

    index_offsets.push_back({0, 0});
    for (size_t i = 1; i < meshes.size(); ++i)
    {
        auto* last_mesh = meshes[i - 1];
        size_t n_vertices  = last_mesh->n_vertices();
        size_t n_halfedges = last_mesh->n_halfedges();

        auto& last_offsets = index_offsets[i - 1];
        int vertex_offset   = last_offsets.vertex_offset + n_vertices;
        int texcoord_offset = last_offsets.texcoord_offset + n_halfedges;

        index_offsets.push_back({vertex_offset, texcoord_offset});
    }

    bool all_with_tex_coord = true;

    for (auto* mesh : meshes)
    {
        //vertices
        auto points = mesh->get_vertex_property<Point>("v:point");
        for (auto v : mesh->vertices())
        {
            const Point& p = points[v];
            fprintf(out, "v %.10f %.10f %.10f\n", p[0], p[1], p[2]);
        }
        //normals
        auto normals = mesh->get_vertex_property<Point>("v:normal");
        for (auto v : mesh->vertices())
        {
            const Point& p = normals[v];
            fprintf(out, "vn %.10f %.10f %.10f\n", p[0], p[1], p[2]);
        }

        //optionally texture coordinates
        // do we have them?
        std::vector<std::string> h_props= mesh->halfedge_properties();
        bool with_tex_coord = false;
        auto h_prop_end   = h_props.end();
        auto h_prop_start = h_props.begin();
        while(h_prop_start != h_prop_end)
        {
            if( 0 == (*h_prop_start).compare("h:texcoord"))
            {
                with_tex_coord = true;
            }
            ++h_prop_start;
        }
        //if so then add
        if(with_tex_coord)
        {
            auto tex_coord = mesh->get_halfedge_property<Texture_coordinate>("h:texcoord");
            for (auto he : mesh->halfedges())
            {
                const Texture_coordinate& pt = tex_coord[he];
                fprintf(out, "vt %.10f %.10f %.10f\n", pt[0], pt[1], pt[2]);
            }
        }
        else
        {
            all_with_tex_coord = false;
        }
    }


    for (size_t i = 0; i < meshes.size(); ++i)
    {
        auto* mesh = meshes[i];
        int v_offset = index_offsets[i].vertex_offset;
        int t_offset = index_offsets[i].texcoord_offset;

        //faces
        for (auto f : mesh->faces())
        {
            fprintf(out, "f");
            auto fvit  = mesh->vertices(f);
            auto fvend = fvit;
            auto fhit  = mesh->halfedges(f);
            do
            {
                if(all_with_tex_coord)
                {
                    // write vertex index, tex_coord index and normal index
                    fprintf(out, " %d/%d/%d",
                            (*fvit).idx() + v_offset + 1,
                            (*fhit).idx() + t_offset + 1,
                            (*fvit).idx() + v_offset + 1);
                    ++fhit;
                }
                else
                {
                    // write vertex index and normal index
                    fprintf(out, " %d//%d",
                            (*fvit).idx() + v_offset + 1,
                            (*fvit).idx() + v_offset + 1);
                }
            }
            while (++fvit != fvend);
            fprintf(out, "\n");
        }

    }

    fclose(out);
    return true;

}

//=============================================================================
} // namespace surface_mesh
} // namespace graphene
//=============================================================================
