/**
 * MOAB, a Mesh-Oriented datABase, is a software component for creating,
 * storing and accessing finite element mesh data.
 *
 * Copyright 2004 Sandia Corporation.  Under the terms of Contract
 * DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government
 * retains certain rights in this software.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 */

#include "ReadOBJ.hpp"
#include <iostream>
#include <sstream>
#include <fstream>
#include <vector>
#include <cstdlib>
#include <map>
#include <cassert>
#include <cmath>

#include "moab/Core.hpp"
#include "moab/Interface.hpp"
#include "moab/ReadUtilIface.hpp"
#include "Internals.hpp"
#include "moab/Range.hpp"
#include "moab/CartVect.hpp"
#include "moab/FileOptions.hpp"
#include "FileTokenizer.hpp"
#include "MBTagConventions.hpp"
#include "moab/CN.hpp"
#include "moab/GeomTopoTool.hpp"

namespace moab
{

ReaderIface* ReadOBJ::factory( Interface* iface )
{
    return new ReadOBJ( iface );
}

// Subset of starting tokens currently supported
const char* ReadOBJ::delimiters = " ";
const char* object_start_token  = "o";
const char* group_start_token   = "g";
const char* vertex_start_token  = "v";
const char* face_start_token    = "f";

#define OBJ_AMBIGUOUS "AMBIGUOUS"
#define OBJ_UNDEFINED "UNDEFINED"

// Name of geometric entities
const char* const geom_name[] = { "Vertex\0", "Curve\0", "Surface\0", "Volume\0" };

// Geometric Categories
const char geom_category[][CATEGORY_TAG_SIZE] = { "Vertex\0", "Curve\0", "Surface\0", "Volume\0", "Group\0" };

// Constructor
ReadOBJ::ReadOBJ( Interface* impl )
    : MBI( impl ), geom_tag( 0 ), id_tag( 0 ), name_tag( 0 ), category_tag( 0 ), faceting_tol_tag( 0 ),
      geometry_resabs_tag( 0 ), obj_name_tag( 0 )
{
    assert( NULL != impl );
    MBI->query_interface( readMeshIface );
    myGeomTool = new GeomTopoTool( impl );
    assert( NULL != readMeshIface );

    // Get all handles
    int negone = -1;
    ErrorCode rval;
    rval = MBI->tag_get_handle( GEOM_DIMENSION_TAG_NAME, 1, MB_TYPE_INTEGER, geom_tag, MB_TAG_SPARSE | MB_TAG_CREAT,
                                &negone );MB_CHK_ERR_RET( rval );

    id_tag = MBI->globalId_tag();

    rval = MBI->tag_get_handle( NAME_TAG_NAME, NAME_TAG_SIZE, MB_TYPE_OPAQUE, name_tag, MB_TAG_SPARSE | MB_TAG_CREAT );MB_CHK_ERR_RET( rval );

    rval = MBI->tag_get_handle( CATEGORY_TAG_NAME, CATEGORY_TAG_SIZE, MB_TYPE_OPAQUE, category_tag,
                                MB_TAG_SPARSE | MB_TAG_CREAT );MB_CHK_ERR_RET( rval );

    rval = MBI->tag_get_handle( "OBJECT_NAME", 32, MB_TYPE_OPAQUE, obj_name_tag, MB_TAG_SPARSE | MB_TAG_CREAT );MB_CHK_ERR_RET( rval );

    rval = MBI->tag_get_handle( "FACETING_TOL", 1, MB_TYPE_DOUBLE, faceting_tol_tag, MB_TAG_SPARSE | MB_TAG_CREAT );MB_CHK_ERR_RET( rval );

    rval =
        MBI->tag_get_handle( "GEOMETRY_RESABS", 1, MB_TYPE_DOUBLE, geometry_resabs_tag, MB_TAG_SPARSE | MB_TAG_CREAT );MB_CHK_ERR_RET( rval );
}

// Destructor
ReadOBJ::~ReadOBJ()
{
    if( readMeshIface )
    {
        MBI->release_interface( readMeshIface );
        readMeshIface = 0;
    }

    delete myGeomTool;
}

ErrorCode ReadOBJ::read_tag_values( const char* /*file_name*/, const char* /*tag_name*/, const FileOptions& /*opts*/,
                                    std::vector< int >& /*tag_values_out*/, const SubsetList* /*subset_list*/ )
{
    return MB_NOT_IMPLEMENTED;
}

// Load the file as called by the Interface function
ErrorCode ReadOBJ::load_file( const char* filename, const EntityHandle*, const FileOptions&,
                              const ReaderIface::SubsetList* subset_list, const Tag* /*file_id_tag*/ )
{
    ErrorCode rval;
    int ignored = 0;   // Number of lines not beginning with o, v, or f
    std::string line;  // The current line being read
    EntityHandle vert_meshset;
    EntityHandle curr_meshset;  // Current object meshset
    std::string object_name;
    std::vector< EntityHandle > vertex_list;
    int object_id = 0, group_id = 0;  // ID number for each volume/surface
    int num_groups;

    // At this time, there is no support for reading a subset of the file
    if( subset_list ) { MB_SET_ERR( MB_UNSUPPORTED_OPERATION, "Reading subset of files not supported for OBJ." ); }

    std::ifstream input_file( filename );  // Filestream for OBJ file

    // Check that the file can be read
    if( !input_file.good() )
    {
        std::cout << "Problems reading file = " << filename << std::endl;
        return MB_FILE_DOES_NOT_EXIST;
    }

    // If the file can be read
    if( input_file.is_open() )
    {

        // create meshset for global vertices
        rval = MBI->create_meshset( MESHSET_SET, vert_meshset );MB_CHK_SET_ERR( rval, "Failed to create global vert meshset." );

        while( std::getline( input_file, line ) )
        {
            // Skip blank lines in file
            if( line.length() == 0 ) continue;

            // Tokenize the line
            std::vector< std::string > tokens;
            tokenize( line, tokens, delimiters );

            // Each group and object line must have a name, token size is at least 2
            // Each vertex and face line should have token size of at least 4
            if( tokens.size() < 2 ) continue;

            switch( get_keyword( tokens ) )
            {
                // Object line
                case object_start: {
                    object_id++;
                    object_name = tokens[1];  // Get name of object

                    // Create new meshset for object
                    rval = create_new_object( object_name, object_id, curr_meshset );MB_CHK_ERR( rval );
                    break;
                }

                // Group line
                case group_start: {
                    group_id++;
                    num_groups             = tokens.size() - 1;
                    std::string group_name = "Group";
                    for( int i = 0; i < num_groups; i++ )
                    {
                        group_name = group_name + '_' + tokens[i + 1];
                    }

                    // Create new meshset for group
                    rval = create_new_group( group_name, group_id, curr_meshset );MB_CHK_ERR( rval );
                    break;
                }

                // Vertex line
                case vertex_start: {
                    // Read vertex and return EH
                    EntityHandle new_vertex_eh;
                    rval = create_new_vertex( tokens, new_vertex_eh );MB_CHK_ERR( rval );

                    // Add new vertex EH to list
                    vertex_list.push_back( new_vertex_eh );

                    // Add new vertex EH to the meshset
                    MBI->add_entities( vert_meshset, &new_vertex_eh, 1 );MB_CHK_SET_ERR( rval, "Failed to add vertex to global meshset." );
                    break;
                }

                // Face line
                case face_start: {
                    // Faces in .obj file can have 2, 3, or 4 vertices. If the face has
                    // 3 vertices, the EH will be immediately added to the meshset.
                    // If 4, face is split into triangles.  Anything else is ignored.
                    EntityHandle new_face_eh;

                    if( tokens.size() == 4 )
                    {
                        rval = create_new_face( tokens, vertex_list, new_face_eh );MB_CHK_ERR( rval );

                        if( rval == MB_SUCCESS )
                        {
                            // Add new face EH to the meshset
                            MBI->add_entities( curr_meshset, &new_face_eh, 1 );
                        }
                    }

                    else if( tokens.size() == 5 )
                    {
                        // Split_quad fxn will create 2 new triangles from 1 quad
                        Range new_faces_eh;
                        rval = split_quad( tokens, vertex_list, new_faces_eh );MB_CHK_ERR( rval );

                        // Add new faces created by split quad to meshset
                        if( rval == MB_SUCCESS ) { MBI->add_entities( curr_meshset, new_faces_eh ); }
                    }

                    else
                    {
                        std::cout << "Neither tri nor a quad: " << line << std::endl;
                    }

                    break;
                }

                case valid_unsupported: {
                    // First token is not recognized as a supported character
                    ++ignored;
                    break;
                }

                default: {
                    MB_SET_ERR( MB_FAILURE, "Invalid/unrecognized line" );
                }
            }
        }
    }

    // If no object lines are read (those beginning w/ 'o'), file is not obj type
    if( object_id == 0 && group_id == 0 ) { MB_SET_ERR( MB_FAILURE, "This is not an obj file. " ); }

    std::cout << "There were " << ignored << " ignored lines in this file." << std::endl;

    input_file.close();

    return MB_SUCCESS;
}

/* The tokenize function will split an input line
 * into a vector of strings based upon the delimiter
 */
void ReadOBJ::tokenize( const std::string& str, std::vector< std::string >& tokens, const char* delimiters2 )
{
    tokens.clear();

    std::string::size_type next_token_end, next_token_start = str.find_first_not_of( delimiters2, 0 );

    while( std::string::npos != next_token_start )
    {
        next_token_end = str.find_first_of( delimiters2, next_token_start );
        if( std::string::npos == next_token_end )
        {
            tokens.push_back( str.substr( next_token_start ) );
            next_token_start = std::string::npos;
        }
        else
        {
            tokens.push_back( str.substr( next_token_start, next_token_end - next_token_start ) );
            next_token_start = str.find_first_not_of( delimiters2, next_token_end );
        }
    }
}

keyword_type ReadOBJ::get_keyword( std::vector< std::string > tokens )
{
    std::map< std::string, keyword_type > keywords;

    // currently supported
    keywords["o"] = object_start;
    keywords["g"] = group_start;
    keywords["f"] = face_start;
    keywords["v"] = vertex_start;

    // not currently supported, will be ignored
    keywords["vn"]         = valid_unsupported;
    keywords["vt"]         = valid_unsupported;
    keywords["vp"]         = valid_unsupported;
    keywords["s"]          = valid_unsupported;
    keywords["mtllib"]     = valid_unsupported;
    keywords["usemtl"]     = valid_unsupported;
    keywords["#"]          = valid_unsupported;
    keywords["cstype"]     = valid_unsupported;
    keywords["deg"]        = valid_unsupported;
    keywords["bmat"]       = valid_unsupported;
    keywords["step"]       = valid_unsupported;
    keywords["p"]          = valid_unsupported;
    keywords["l"]          = valid_unsupported;
    keywords["curv"]       = valid_unsupported;
    keywords["curv2"]      = valid_unsupported;
    keywords["surf"]       = valid_unsupported;
    keywords["parm"]       = valid_unsupported;
    keywords["trim"]       = valid_unsupported;
    keywords["hole"]       = valid_unsupported;
    keywords["scrv"]       = valid_unsupported;
    keywords["sp"]         = valid_unsupported;
    keywords["end"]        = valid_unsupported;
    keywords["mg"]         = valid_unsupported;
    keywords["bevel"]      = valid_unsupported;
    keywords["c_interp"]   = valid_unsupported;
    keywords["d_interp"]   = valid_unsupported;
    keywords["lod"]        = valid_unsupported;
    keywords["shadow_obj"] = valid_unsupported;
    keywords["trace_obj"]  = valid_unsupported;
    keywords["ctech"]      = valid_unsupported;
    keywords["stech"]      = valid_unsupported;

    return keywords[match( tokens[0], keywords )];
}

template < typename T >
std::string ReadOBJ::match( const std::string& token, std::map< std::string, T >& tokenList )
{
    // Initialize with no match and obj_undefined as return string
    std::string best_match = OBJ_UNDEFINED;

    // Search the map
    for( typename std::map< std::string, T >::iterator thisToken = tokenList.begin(); thisToken != tokenList.end();
         ++thisToken )
    {
        // If a perfect match break the loop (assume keyword list is unambiguous)
        if( token == ( *thisToken ).first )
        {
            best_match = token;
            break;
        }
    }

    // Possible return values: OBJ_UNDEFINED, keyword from list
    return best_match;
}

/*
 * The create_new_object function starts a new meshset for each object
 * that will contain all faces that make up the object.
 */
ErrorCode ReadOBJ::create_new_object( std::string object_name, int curr_object, EntityHandle& object_meshset )
{
    ErrorCode rval;

    // Create meshset to store object
    // This is also referred to as the surface meshset
    rval = MBI->create_meshset( MESHSET_SET, object_meshset );MB_CHK_SET_ERR( rval, "Failed to generate object mesh set." );

    // Set surface meshset tags
    rval = MBI->tag_set_data( name_tag, &object_meshset, 1, object_name.c_str() );MB_CHK_SET_ERR( rval, "Failed to set mesh set name tag." );

    rval = MBI->tag_set_data( id_tag, &object_meshset, 1, &( curr_object ) );MB_CHK_SET_ERR( rval, "Failed to set mesh set ID tag." );

    int dim = 2;
    rval    = MBI->tag_set_data( geom_tag, &object_meshset, 1, &( dim ) );MB_CHK_SET_ERR( rval, "Failed to set mesh set dim tag." );

    rval = MBI->tag_set_data( category_tag, &object_meshset, 1, geom_category[2] );MB_CHK_SET_ERR( rval, "Failed to set mesh set category tag." );

    /* Create volume entity set corresponding to surface
       The volume meshset will have one child--
       the meshset of the surface that bounds the object.
     */
    EntityHandle vol_meshset;
    rval = MBI->create_meshset( MESHSET_SET, vol_meshset );MB_CHK_SET_ERR( rval, "Failed to create volume mesh set." );

    rval = MBI->add_parent_child( vol_meshset, object_meshset );MB_CHK_SET_ERR( rval, "Failed to add object mesh set as child of volume mesh set." );

    /* Set volume meshset tags
       The volume meshset is tagged with the same name as the surface meshset
       for each object because of the direct relation between these entities.
     */
    rval = MBI->tag_set_data( obj_name_tag, &vol_meshset, 1, object_name.c_str() );MB_CHK_SET_ERR( rval, "Failed to set mesh set name tag." );

    rval = MBI->tag_set_data( id_tag, &vol_meshset, 1, &( curr_object ) );MB_CHK_SET_ERR( rval, "Failed to set mesh set ID tag." );

    dim  = 3;
    rval = MBI->tag_set_data( geom_tag, &vol_meshset, 1, &( dim ) );MB_CHK_SET_ERR( rval, "Failed to set mesh set dim tag." );

    rval = MBI->tag_set_data( name_tag, &vol_meshset, 1, geom_name[3] );MB_CHK_SET_ERR( rval, "Failed to set mesh set name tag." );

    rval = MBI->tag_set_data( category_tag, &vol_meshset, 1, geom_category[3] );MB_CHK_SET_ERR( rval, "Failed to set mesh set category tag." );

    rval = myGeomTool->set_sense( object_meshset, vol_meshset, SENSE_FORWARD );MB_CHK_SET_ERR( rval, "Failed to set surface sense." );

    return rval;
}

/*
 * The create_new_group function starts a new meshset for each group
 * that will contain all faces that make up the group
 */
ErrorCode ReadOBJ::create_new_group( std::string group_name, int curr_group, EntityHandle& group_meshset )
{
    ErrorCode rval;

    // Create meshset to store group
    rval = MBI->create_meshset( MESHSET_SET, group_meshset );MB_CHK_SET_ERR( rval, "Failed to generate group mesh set." );

    // Set meshset tags
    rval = MBI->tag_set_data( name_tag, &group_meshset, 1, group_name.c_str() );MB_CHK_SET_ERR( rval, "Failed to set mesh set name tag." );

    rval = MBI->tag_set_data( id_tag, &group_meshset, 1, &( curr_group ) );MB_CHK_SET_ERR( rval, "Failed to set mesh set ID tag." );

    return rval;
}

/* The create_new_vertex function converts a vector
   of tokens (v x y z) to
   the vertex format; a structure that has the three
   coordinates as members.
 */
ErrorCode ReadOBJ::create_new_vertex( std::vector< std::string > v_tokens, EntityHandle& vertex_eh )
{
    ErrorCode rval;
    vertex next_vertex;

    for( int i = 1; i < 4; i++ )
        next_vertex.coord[i - 1] = atof( v_tokens[i].c_str() );

    rval = MBI->create_vertex( next_vertex.coord, vertex_eh );MB_CHK_SET_ERR( rval, "Unbale to create vertex." );

    return rval;
}

/* The create_new_face function converts a vector
   of tokens ( f v1 v2 v3) ) to the face format;
   a structure that has the three
   connectivity points as members.
 */
ErrorCode ReadOBJ::create_new_face( std::vector< std::string > f_tokens, const std::vector< EntityHandle >& vertex_list,
                                    EntityHandle& face_eh )
{
    face next_face;
    ErrorCode rval;

    for( int i = 1; i < 4; i++ )
    {
        int vertex_id = atoi( f_tokens[i].c_str() );

        // Some faces contain format 'vertex/texture'
        // Remove the '/texture' and add the vertex to the list
        std::size_t slash = f_tokens[i].find( '/' );
        if( slash != std::string::npos )
        {
            std::string face = f_tokens[i].substr( 0, slash );
            vertex_id        = atoi( face.c_str() );
        }

        next_face.conn[i - 1] = vertex_list[vertex_id - 1];
    }

    rval = MBI->create_element( MBTRI, next_face.conn, 3, face_eh );MB_CHK_SET_ERR( rval, "Unable to create new face." );

    return rval;
}

// The split_quad function divides a quad face into 4 tri faces.
ErrorCode ReadOBJ::split_quad( std::vector< std::string > f_tokens, std::vector< EntityHandle >& vertex_list,
                               Range& face_eh )
{
    ErrorCode rval;
    std::vector< EntityHandle > quad_vert_eh;

    // Loop over quad connectivity getting vertex EHs
    for( int i = 1; i < 5; i++ )
    {
        int vertex_id     = atoi( f_tokens[i].c_str() );
        std::size_t slash = f_tokens[i].find( '/' );
        if( slash != std::string::npos )
        {
            std::string face = f_tokens[i].substr( 0, slash );
            vertex_id        = atoi( face.c_str() );
        }

        quad_vert_eh.push_back( vertex_list[vertex_id - 1] );
    }

    // Create new tri faces
    rval = create_tri_faces( quad_vert_eh, face_eh );MB_CHK_SET_ERR( rval, "Failed to create triangles when splitting quad." );

    return rval;
}

ErrorCode ReadOBJ::create_tri_faces( std::vector< EntityHandle > quad_vert_eh,
                                     //				       EntityHandle center_vertex_eh,
                                     Range& face_eh )
{
    ErrorCode rval;
    EntityHandle connectivity[3];
    EntityHandle new_face;

    connectivity[0] = quad_vert_eh[0];
    connectivity[1] = quad_vert_eh[1];
    connectivity[2] = quad_vert_eh[2];
    rval            = MBI->create_element( MBTRI, connectivity, 3, new_face );
    face_eh.insert( new_face );

    connectivity[0] = quad_vert_eh[2];
    connectivity[1] = quad_vert_eh[3];
    connectivity[2] = quad_vert_eh[0];
    rval            = MBI->create_element( MBTRI, connectivity, 3, new_face );
    face_eh.insert( new_face );

    return rval;
}

}  // namespace moab
