/* -*- mia-c++  -*-
 *
 * This file is part of MIA - a toolbox for medical image analysis
 * Copyright (c) Leipzig, Madrid 1999-2017 Gert Wollny
 *
 * MIA is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with MIA; if not, see <http://www.gnu.org/licenses/>.
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <iostream>
#include <fstream>
#include <algorithm>
#include <vector>
#include <cstring>


#include <mia/core/file.hh>
#include <mia/core/filter.hh>
#include <mia/core/msgstream.hh>
#include <mia/mesh/triangularMesh.hh>
#include <mia/mesh/triangulate.hh>

// documentation of the format:
// http://www.geomview.org/docs/html/OFF.html

NS_BEGIN(off_mesh_io)

NS_MIA_USE;
using namespace std;

static char const *const format = "off";

#define USE_FILEIO



class COffMeshIO: public CMeshIOPlugin
{
public:
       COffMeshIO();
private:
       typedef TPolyTriangulator<CTriangleMesh::CVertexfield, vector<unsigned int>>  CPolyTriangulator;
       virtual PTriangleMesh do_load(string const&   filename)const;
       virtual bool do_save(string const&   filename, const CTriangleMesh& data)const;
       virtual const string  do_get_descr()const;

       PTriangleMesh do_load_it(CInputFile& inp)const;

       bool load_vertices(CInputFile& inp,
                          CTriangleMesh::PVertexfield vertices,
                          CTriangleMesh::PNormalfield normals,
                          CTriangleMesh::PColorfield colors,
                          bool has_texture_coordinates,
                          unsigned int nvertices)const;

       bool load_triangles(CInputFile& inp, vector<CTriangleMesh::triangle_type>& tri,
                           unsigned int nfaces, unsigned int nvertice,
                           const CPolyTriangulator& triangulator)const;
       bool read_polygon(istream& inp, vector<CTriangleMesh::triangle_type>& tri,
                         unsigned int nvertices, const CPolyTriangulator& triangulator)const;


       void skip_to_newline(istream& inp)const;
};


extern "C" EXPORT CPluginBase *get_plugin_interface()
{
       return new COffMeshIO();
}

COffMeshIO::COffMeshIO(): CMeshIOPlugin(format)
{
       add_suffix(".off");
       add_suffix(".OFF");
}


const string  COffMeshIO::do_get_descr()const
{
       return string("plugin to load/store some Geomview OFF files. "
                     "Supported is only the ASCII format 3D meshes with normales and per-vertex RGB colors. "
                     "Alpha values and texture coordinates, as well as per face properties are ignored. "
                     "Polygons are read and triangulated. "
                    );
}

PTriangleMesh COffMeshIO::do_load(string const&   filename)const
{
       CInputFile f(filename);

       if (!f)
              return PTriangleMesh();

       return do_load_it(f);
}

void COffMeshIO::skip_to_newline(istream& inp)const
{
       char c;

       do  {
              inp >> c;
       } while (c != '\n' && inp.good());
}

static bool read_line(char *buf, size_t size, FILE *f)
{
       char *success;

       do {
              success = fgets( buf, size - 1, f);

              while (success && (*success == ' ' || *success == '\t'))  ++success;
       } while (success && (*success == '#' || *success == '\n' || *success == '\r'));

       buf[size - 1] = 0;
       return success != NULL;
}

bool COffMeshIO::load_vertices(CInputFile& inf,
                               CTriangleMesh::PVertexfield vertices,
                               CTriangleMesh::PNormalfield normals,
                               CTriangleMesh::PColorfield colors,
                               bool has_texture_coordinates,
                               unsigned int nvertices)const
{
       unsigned int i = 0;
       CTriangleMesh::vertex_iterator v = vertices->begin();
       CTriangleMesh::normal_iterator n;
       CTriangleMesh::color_iterator  c;
       int expect_count = has_texture_coordinates ? 5 : 2;
       int c_offest = 0;

       if (normals) {
              n = normals->begin();
              c_offest += 3;
              expect_count += 3;
       }

       if (colors) {
              c = colors->begin();
              expect_count += 3;
       }

       char buf[2048];
       bool success = read_line(buf, 2048, inf);

       if (!success)
              return false;

       int first_count = 0;

       do {
              float x[9];
              float *xc = &x[c_offest];
              int count = sscanf(buf, "%f %f %f %f %f %f %f %f %f %f %f %f",
                                 &v->x, &v->y, &v->z,
                                 &x[0], &x[1], &x[2],
                                 &x[3], &x[4], &x[5], &x[6],
                                 &x[7], &x[8]);

              if (count < expect_count) {
                     throw create_exception<runtime_error>("OFF: unsupported file type. "
                                                           "Most likely the file type uses color map indices.");
              }

              if (first_count > 0) {
                     if (first_count != count) {
                            throw create_exception<runtime_error>("OFF: inconsistent vertex definitions");
                     }
              } else {
                     first_count = count;
              }

              if (normals) {
                     n->x = x[0];
                     n->y = x[1];
                     n->z = x[2];
                     ++n;
                     count -= 6;
              }

              if (colors) {
                     if (count < 3)
                            throw create_exception<invalid_argument>("OFF: color indices not supported");

                     c->x = xc[0] < 1.0 ? xc[0] : xc[0] / 255.0f;
                     c->y = xc[1] < 1.0 ? xc[1] : xc[1] / 255.0f;
                     c->z = xc[2] < 1.0 ? xc[2] : xc[2] / 255.0f;
                     ++c;
              }

              ++v;
              ++i;
       } while (i < nvertices && read_line(buf, 2048, inf));

       return i == nvertices;
}

bool COffMeshIO::read_polygon(istream& inp, vector<CTriangleMesh::triangle_type>& tri,
                              unsigned int nvertices,
                              const COffMeshIO::CPolyTriangulator& triangulator)const
{
       int nvert;
       inp >> ws >> nvert;

       if (nvert == 3) {
              CTriangleMesh::triangle_type triangle;
              inp >> ws >> triangle.x >> ws >>  triangle.y >> ws >> triangle.z;
              tri.push_back(triangle);

              if (triangle.x >=  nvertices ||  triangle.y >=  nvertices ||  triangle.z >=  nvertices) {
                     cverr() << "COffMeshIO::load_triangles: index out of range\n";
                     return false;
              }
       } else {
              vector<unsigned int> poly(nvert);

              for (unsigned  k = 0; k < poly.size(); ++k) {
                     inp >> ws >> poly[k];

                     if (poly[k] >= nvertices) {
                            cverr() << "COffMeshIO::load_triangles: index out of range\n";
                            return false;
                     }
              }

              triangulator.triangulate(tri, poly);
       }

       return true;
}

bool COffMeshIO::load_triangles(CInputFile& inp, vector<CTriangleMesh::triangle_type>& tri,
                                unsigned int nfaces, unsigned int nvertices,
                                const CPolyTriangulator& triangulator)const
{
       int show_thresh_start = nfaces / 50;
       int show_thresh = show_thresh_start;
       char line[2048];
       bool success = read_line(line, 2048, inp);

       while (nfaces-- && success) {
              // it is ensured in read_line that the string in "line"  is null-terminated
              // coverity [TAINTED_SCALAR]
              istringstream in_line(line);

              if (!read_polygon(in_line, tri, nvertices, triangulator))
                     return false;

              show_thresh--;

              if (!show_thresh) {
                     show_thresh = show_thresh_start;
              }

              success = read_line(line, 2048, inp);
       }

       return true;
}


class CTriangleChecker
{
public:

       CTriangleChecker();

       void operator ()(const CTriangleMesh::triangle_type& tri);

       void print();
private:


       void evaluate();

       map<int, map< int, int >> m_count;

       int m_edges;
       int m_non_manifold;
};


CTriangleChecker::CTriangleChecker():
       m_edges(0),
       m_non_manifold(0)
{
}

void CTriangleChecker::operator ()(const CTriangleMesh::triangle_type& tri)
{
       if (tri.x > tri.y) m_count[tri.x][tri.y] += 1;
       else  m_count[tri.y][tri.x] += 1;

       if (tri.x > tri.z) m_count[tri.x][tri.z] += 1;
       else  m_count[tri.z][tri.x] += 1;

       if (tri.z > tri.y) m_count[tri.z][tri.y] += 1;
       else  m_count[tri.y][tri.z] += 1;
}

void CTriangleChecker::print()
{
       if (cverb.show_debug()) {
              evaluate();
              cvdebug() << "Mesh has \n";
              cvdebug() << "  nonmanifold edges: " << m_non_manifold << "\n";
              cvdebug() << "           of edges: " << m_edges << "\n";
       }
}

void CTriangleChecker::evaluate()
{
       map< int, map< int, int >>::const_iterator io = m_count.begin();
       map< int, map< int, int >>::const_iterator eo = m_count.end();

       while (io != eo) {
              m_edges += io->second.size();
              map< int, int >::const_iterator i1 = io->second.begin();
              map< int, int >::const_iterator e1 = io->second.end();

              while (i1 != e1) {
                     if (i1->second != 2) {
                            ++m_non_manifold;

                            if (m_non_manifold < 10) {
                                   cvdebug() << io->first << ":"
                                             << i1->first << " "
                                             << i1->second << "\n";
                            }
                     }

                     ++i1;
              }

              ++io;
       }
}


enum EVertexFlags {vt_texture = 1,
                   vt_color   = 2,
                   vt_normal  = 4,
                   vt_size4   = 8,
                   vt_sizeN   = 16,
                   vt_is_off  = 32
                  };

int get_vertex_flags(const char *header)
{
       int flags = 0;

       if (!strncmp(header, "ST", 2)) {
              flags |= vt_texture;
              header += 2;
       }

       if (*header == 'C') {
              flags |= vt_color;
              ++header;
       }

       if (*header == 'N') {
              flags |= vt_normal;
              ++header;
       }

       if (*header == '4') {
              flags |= vt_size4;
              ++header;
       } else if (*header == 'n') {
              flags |= vt_sizeN;
              ++header;
       }

       if (!strncmp(header, "OFF", 3))
              return flags | vt_is_off;

       return 0;
}

PTriangleMesh COffMeshIO::do_load_it(CInputFile& inp)const
{
       int c;
       const int c_max_header = 10;
       int max_header = c_max_header;
       char header[c_max_header];
       char *p_header =  header;

       // read header
       while ( (c = fgetc(inp)) != EOF && strchr("STCNn4OF", c) && --max_header)
              * p_header++ = c;

       int flags = get_vertex_flags(header);

       if (!(flags & vt_is_off)) {
              cvdebug() << "no OFF mesh\n";
              return PTriangleMesh();
       }

       int vertex_size = 3;

       if (flags & vt_sizeN) {
              if (fscanf(inp, "%d\n", & vertex_size) != 1)
                     throw runtime_error("OFF: Unable to read from file.");
       } else if (flags & vt_size4) {
              vertex_size = 4;
       }

       if (vertex_size != 3) {
              throw create_exception<invalid_argument>("OFF: currently only 3D vertexes are supported, ",
                            "file contains ", vertex_size, "D vertices");
       }

       int n_vertices = 0;
       int n_faces = 0;
       int n_edges = 0;
       char buffer[2048];

       if (!read_line(buffer, 2048, inp))
              throw create_exception<runtime_error>("OFF: Unable to read from input file.");

       if (sscanf(buffer, "%d %d %d", &n_vertices, &n_faces, &n_edges) != 3) {
              throw create_exception<runtime_error>("OFF: parse error reading from '", buffer, "'");
       }

       cvdebug() << "found file with " << n_vertices << " vertices, and " <<  n_faces << " faces\n";
       CTriangleMesh::PVertexfield vertices(new CTriangleMesh::CVertexfield(n_vertices));
       CTriangleMesh::PNormalfield normals;
       CTriangleMesh::PColorfield  colors;

       if (flags & vt_color) {
              colors.reset(new CTriangleMesh::CColorfield(n_vertices));
       }

       if (flags & vt_normal) {
              normals.reset(new CTriangleMesh::CNormalfield(n_vertices));
       }

       if ( !load_vertices(inp, vertices, normals, colors, flags & vt_texture, n_vertices)) {
              throw create_exception<runtime_error>("OFF: Error reading vertices");
       }

       vector<CTriangleMesh::triangle_type> tri;
       CPolyTriangulator triangulator(*vertices);

       if (!load_triangles(inp, tri, n_faces, n_vertices, triangulator)) {
              throw create_exception<runtime_error>("OFF: Error reading triangles");
       }

       CTriangleChecker tricheck;

       for (vector<CTriangleMesh::triangle_type>::const_iterator i = tri.begin();
            i != tri.end(); ++i)
              tricheck(*i);

       cvdebug() << "tricheck.print();\n";
       tricheck.print();
       CTriangleMesh::PTrianglefield triangles(new CTriangleMesh::CTrianglefield(tri.size()));
       copy(tri.begin(), tri.end(), triangles->begin());
       return PTriangleMesh(new CTriangleMesh(triangles, vertices, normals, colors, nullptr));
}

bool COffMeshIO::do_save(string const&   filename, const CTriangleMesh& data)const
{
       COutputFile f(filename);

       if (!f)
              return false;

       int flags = data.get_available_data();
       stringstream h_stream;

       // write header
       if ( (flags & CTriangleMesh::ed_scale)  || (flags & CTriangleMesh::ed_color))
              h_stream << 'C';

       if (  flags & CTriangleMesh::ed_normal )
              h_stream << 'N';

       h_stream << "OFF\n";
       fprintf(f, "%s\n", h_stream.str().c_str());
       fprintf(f, "%d %d 0\n", data.vertices_size(), data.triangle_size());
       int written = 1;

       if (flags & CTriangleMesh::ed_scale) {
              cvwarn() << "OFF-save: Mesh has scale values, but they are "
                       << "not supported by the OFF file format and will be lost\n";
       }

       for (size_t i = 0; i < data.vertices_size() && written > 0; ++i) {
              const CTriangleMesh::vertex_type& v = data.vertex_at(i);
              fprintf(f, "%f %f %f", v.x, v.y, v.z);

              if (flags & CTriangleMesh::ed_normal) {
                     const CTriangleMesh::normal_type& n = data.normal_at(i);
                     written += fprintf(f, " %f %f %f", n.x, n.y, n.z);
              }

              if (flags & CTriangleMesh::ed_color) {
                     const CTriangleMesh::color_type& c = data.color_at(i);
                     written += fprintf(f, " %f %f %f", c.x, c.y, c.z);
              }

              written += fprintf(f, "\n");
       }

       CTriangleMesh::const_triangle_iterator tb = data.triangles_begin();
       CTriangleMesh::const_triangle_iterator te = data.triangles_end();

       while (tb != te && written > 0) {
              written = fprintf(f, "3 %d %d %d\n", tb->x, tb->y, tb->z);
              ++tb;
       }

       return written > 0;
}

}

