/******************************************************************************
 * Copyright 1986, 2016 NVIDIA Corporation. All rights reserved.
 *****************************************************************************/

#include <mi/neuraylib.h>

#include <string>
#include <list>
#include <set>
#include <map>
#include <utility>      // for std::pair
#include <sstream>

// Support function to handle mi::base::Message_severity to std::string conversion
static std::string enum_to_str( mi::base::Message_severity severity)
{
    switch( severity) {
        case mi::base::MESSAGE_SEVERITY_FATAL:   return "fatal";
        case mi::base::MESSAGE_SEVERITY_ERROR:   return "error";
        case mi::base::MESSAGE_SEVERITY_WARNING: return "warning";
        case mi::base::MESSAGE_SEVERITY_INFO:    return "information";
        case mi::base::MESSAGE_SEVERITY_VERBOSE: return "verbose";
        case mi::base::MESSAGE_SEVERITY_DEBUG:   return "debug";
        default:                                 return "unknown" ;
    }
}

// Define exporter state, which is used in the exporter function to carry additional data around for
// possible recursive invocations of exporters. It contains a URI for the exported resource, a name
// space, a line number, and a pointer to a parent state supporting recursive exports.
class Vanilla_export_state
  : public mi::base::Interface_implement< mi::neuraylib::IImpexp_state>
{
public:
    // Constructor.
    Vanilla_export_state( const char* uri, const mi::neuraylib::IImpexp_state* parent_state)
      : m_line_number( 1),
        m_uri( uri ? uri : ""),
        m_parent_state( parent_state, mi::base::DUP_INTERFACE) { } 

    // Definition of all interface functions.

    const char* get_uri() const { return m_uri.empty() ? 0 : m_uri.c_str(); }

    mi::Uint32  get_line_number() const { return m_line_number; }

    void set_line_number( mi::Uint32 number) { m_line_number = number; }

    void incr_line_number() { ++m_line_number; }

    const mi::neuraylib::IImpexp_state* get_parent_state() const
    {
        if( m_parent_state)
            m_parent_state->retain();
        return m_parent_state.get();
    }

private:
    mi::Uint32 m_line_number;
    std::string m_uri;
    mi::base::Handle<const mi::neuraylib::IImpexp_state> m_parent_state;
};

// Define exporter. It defines all meta information, for example, author, version numbers, which
// formats it supports etc. The longer format detection functions and export_elements() function are
// implemented outside of the class body.
class Vanilla_exporter
  : public mi::base::Interface_implement< mi::neuraylib::IExporter>
{
public:
    // Definition of all interface functions.

    mi::neuraylib::IImpexp_state* create_impexp_state(
        const char* uri, const mi::neuraylib::IImpexp_state* parent_state) const
    {
        return new Vanilla_export_state( uri, parent_state);
    }

    // This exporter supports the file name extensions ".vnl" and ".van".
    const char* get_supported_extensions( mi::Uint32 i) const
    {
        switch( i) {
            case 0:  return ".vnl";
            case 1:  return ".van";
            default: return 0;
        }
    }

    mi::neuraylib::Impexp_priority get_priority() const
    {
        return mi::neuraylib::IMPEXP_PRIORITY_WELL_DEFINED;
    }

    const char* get_name() const { return "NVIDIA example vanilla (v1) exporter"; }

    const char* get_author() const { return "NVIDIA ARC GmbH, Berlin, Germany"; }

    mi::base::Uuid get_uuid() const
    {
        mi::base::Uuid uuid;
        uuid.m_id1 = 0x7b0a3b59;
        uuid.m_id2 = 0xbed44329;
        uuid.m_id3 = 0xad1a2353;
        uuid.m_id4 = 0xb0ab89a8;
        return uuid;
    }

    mi::Uint32 get_major_version() const { return 1; }

    mi::Uint32 get_minor_version() const { return 0; }

    bool test_file_type( const char* extension) const;

    bool test_file_type( const char* extension, const mi::neuraylib::IWriter* writer) const;

    mi::neuraylib::IExport_result* export_scene(
        mi::neuraylib::ITransaction* transaction,
        const char* extension,
        mi::neuraylib::IWriter* writer,
        const char* rootgroup,
        const char* caminst,
        const char* options,
        const mi::IMap* exporter_options,
        mi::neuraylib::IImpexp_state* export_state) const;

    mi::neuraylib::IExport_result* export_elements(
        mi::neuraylib::ITransaction* transaction,
        const char* extension,
        mi::neuraylib::IWriter* writer,
        const mi::IArray* elements,
        const mi::IMap* exporter_options,
        mi::neuraylib::IImpexp_state* export_state) const;

private:
    // Definition of support functions. They are not part of the interface.

    // Formats message with context and appends it to the messages in the result.
    static mi::neuraylib::IExport_result_ext* report_message(
        mi::neuraylib::IExport_result_ext* result,
        mi::Uint32 message_number,
        mi::base::Message_severity message_severity,
        std::string message,
        const mi::neuraylib::IImpexp_state* export_state)
    {
        std::ostringstream s;
        const char* uri = export_state->get_uri();
        s << (uri ? uri : "(no URI)")
          << ":" << export_state->get_line_number() << ": "
          << "Vanilla exporter message " << message_number << ", "
          << "severity " << enum_to_str( message_severity) << ": "
          << message;
        // Report context of all parent export states from recursive invocations of
        // export_elements() in their own lines with indentation.
        mi::base::Handle<const mi::neuraylib::IImpexp_state> parent_state(
            export_state->get_parent_state());
        while( parent_state.is_valid_interface()) {
            s << "\n    included from: " << parent_state->get_uri()
              << ":" << parent_state->get_line_number();
            parent_state = parent_state->get_parent_state();
        }
        result->message_push_back( message_number, message_severity, s.str().c_str());
        return result;
    }

    // Writes name to writer, writes the name without the leading prefix if it is equal to the
    // prefix parameter.
    static void write_name(
        const std::string& name, const std::string& prefix, mi::neuraylib::IWriter* writer)
    {
        if( prefix.size() > 0 && 0 == name.compare( 0, prefix.size(), prefix))
            writer->writeline( name.c_str() + prefix.size());
        else
            writer->writeline( name.c_str());
    }

    // Returns the element type of an element.
    static std::string get_element_type( const mi::base::IInterface* interface)
    {
        mi::base::Handle<const mi::neuraylib::IGroup> group(
            interface->get_interface<mi::neuraylib::IGroup>());
        if( group.is_valid_interface())
            return "Group";
        mi::base::Handle<const mi::neuraylib::IInstance> instance(
            interface->get_interface<mi::neuraylib::IInstance>());
        if( instance.is_valid_interface())
            return "Instance";
        return "Unknown";
    }
};

bool Vanilla_exporter::test_file_type( const char* extension) const
{
    // This exporter supports the file name extensions ".vnl" and ".van".
    mi::Size len = std::strlen( extension);
    return (len > 3)
        &&  (( 0 == strcmp( extension + len - 4, ".vnl"))
          || ( 0 == strcmp( extension + len - 4, ".van")));
}

bool Vanilla_exporter::test_file_type(
    const char* extension, const mi::neuraylib::IWriter*) const
{
    // The writer capabilities do not matter for this simple format. More involved formats might
    // require random access from the writer.
    return test_file_type( extension);
}

mi::neuraylib::IExport_result* Vanilla_exporter::export_scene(
    mi::neuraylib::ITransaction* transaction,
    const char* /*extension*/,
    mi::neuraylib::IWriter* writer,
    const char* rootgroup,
    const char* caminst,
    const char* options,
    const mi::IMap* exporter_options,
    mi::neuraylib::IImpexp_state* export_state) const
{
    // Create the exporter result instance for the return value. If that fails something is really
    // wrong and we return 0.
    mi::neuraylib::IExport_result_ext* result
        = transaction->create<mi::neuraylib::IExport_result_ext>( "Export_result_ext");
    if( !result)
        return 0;

    // Get the 'strip_prefix' option.
    std::string strip_prefix;
    if( exporter_options && exporter_options->has_key( "strip_prefix")) {
        mi::base::Handle<const mi::IString> option(
            exporter_options->get_value<mi::IString>( "strip_prefix"));
        if( !option.is_valid_interface())
            return report_message(
                result, 6, mi::base::MESSAGE_SEVERITY_ERROR,
                "The option 'strip_prefix' has an invalid type.", export_state);
        strip_prefix = option->get_c_str();
    }

    // Two data structures maintain the information during export. The elements list keeps the names
    // of all elements that we want to export and a bit if they have been expanded already. The
    // order of elements follows the .mi requirements; elements that are referenced are first in the
    // list and exported before the elements that reference them. The elements_exported map
    // maintains a list of all exported elements. That allows us to handle objects that are
    // referenced multiple times and export them only once.
    std::list< std::pair< std::string, bool> > elements;
    std::set< std::string>                     elements_exported;

    // Initialize the elements list with the three input parameters.
    if( options && options[0] != '\0')
        elements.push_back( std::make_pair( std::string( options), false));
    if( caminst && caminst[0] != '\0')
        elements.push_back( std::make_pair( std::string( caminst), false));
    elements.push_back( std::make_pair( std::string( rootgroup), false));

    // Start file with magic header and use Windows line-ending convention with CR LF pairs.
    writer->writeline( "VANILLA\r\n");

    // Main loop through all elements
    // This is a simplified recursive directed acyclic graph traversal on the scene graph that
    // performs a depth first search. The traversal is implemented as an iterative loop and a stack
    // on the elements list data structure. The boolean value of entries in the elements list
    // encodes whether we are descending or are ascending in the graph. This flag determines whether
    // we need to expand into the element and put all its children on the stack, or whether we are
    // done with the element and can write it out to file. Other exporters might need to manage more
    // data during the traversal, such as a transformation stack.
    while( (0 == writer->get_error_number()) && (!elements.empty())) {
        if( elements.front().second) {

            // Traversal is ascending in the scene graph
            // Keep element name and remove it from list
            std::string name = elements.front().first;
            elements.pop_front();
            // Check if element has not been written yet
            if( elements_exported.find( name) == elements_exported.end()) {
                // Element can be written to file, mark it as written
                elements_exported.insert( name);
                // Access the element in the DB
                mi::base::Handle<const mi::base::IInterface> element(
                    transaction->access( name.c_str()));
                if( !element.is_valid_interface()) {
                    // The element is not in the DB. Export fails with customized message.
                    std::string message( "Element '");
                    message += name + "' does not exist in database, export failed.";
                    // Error numbers from 6000 to 7999 are reserved for custom
                    // exporter messages like this one
                    return report_message( result, 6001, mi::base::MESSAGE_SEVERITY_ERROR,
                        message, export_state);
                }
                writer->writeline( get_element_type( element.get()).c_str());
                writer->writeline( " \"");
                write_name( name, strip_prefix, writer);
                writer->writeline( "\"\r\n");
            }

        } else {

            // Traversal is descending in the scene graph, mark element as expanded.
            elements.front().second = true;
            // Expand front element, but keep it in the list.
            std::string name = elements.front().first;
            // Access the element in the DB.
            mi::base::Handle<const mi::base::IInterface> element(
                transaction->access( name.c_str()));
            if( !element.is_valid_interface()) {
                // The element is not in the DB. Export fails with customized message.
                std::string message( "Element '");
                message += name + "' does not exist in database, export failed.";
                return report_message( result, 6002, mi::base::MESSAGE_SEVERITY_ERROR,
                    message, export_state);
            }
            // Dispatch on the type name of the element.
            std::string element_type = get_element_type( element.get());
            if( element_type == "Group") {

                mi::base::Handle<const mi::neuraylib::IGroup> group(
                    element->get_interface<mi::neuraylib::IGroup>());
                // Enumerate all elements in the group and push them in reverse order on the
                // elements list front.
                mi::Uint32 group_size = group->get_length();
                for( mi::Uint32 i = 0; i != group_size; ++i) {
                    const char* element_name = group->get_element( group_size - i -1);
                    // Optimization: put name only in the elements list if it has not been exported
                    // yet.
                    if( elements_exported.find( element_name) == elements_exported.end())
                        elements.push_front( std::make_pair( std::string( element_name), false));
                }

            } else if( element_type == "Instance") {

                mi::base::Handle<const mi::neuraylib::IInstance> instance(
                    element->get_interface<mi::neuraylib::IInstance>());
                // Get element in the instance and push it on the elements list front.
                const char* element_name = instance->get_item();
                // Optimization: put name only in the elements list if it has not been exported yet.
                if( elements_exported.find( element_name) == elements_exported.end())
                    elements.push_front( std::make_pair( std::string( element_name), false));

            }
        }
    }

    // Report message condition for a possibly failed writer call
    if( writer->get_error_number() != 0)
        return report_message(
            result,
            static_cast<mi::Uint32>( writer->get_error_number()),
            mi::base::MESSAGE_SEVERITY_ERROR,
            writer->get_error_message() ? writer->get_error_message() : "",
            export_state);

    return result;
}

// Exports all scene elements mentioned in the elements array through the writer.
mi::neuraylib::IExport_result* Vanilla_exporter::export_elements(
    mi::neuraylib::ITransaction* transaction,
    const char* /*extension*/,
    mi::neuraylib::IWriter* writer,
    const mi::IArray* elements,
    const mi::IMap* exporter_options,
    mi::neuraylib::IImpexp_state* export_state) const
{
    // Create the exporter result instance for the return value.
    // If that fails something is really wrong and we return 0.
    mi::neuraylib::IExport_result_ext* result
        = transaction->create<mi::neuraylib::IExport_result_ext>( "Export_result_ext");
    if( !result)
        return 0;

    // Get the 'strip_prefix' option.
    std::string strip_prefix;
    if( exporter_options && exporter_options->has_key( "strip_prefix")) {
        mi::base::Handle<const mi::IString> option(
            exporter_options->get_value<mi::IString>( "strip_prefix"));
        if( !option.is_valid_interface())
            return report_message(
                 result, 6, mi::base::MESSAGE_SEVERITY_ERROR,
                "The option 'strip_prefix' has an invalid type.", export_state);
        strip_prefix = option->get_c_str();
    }

    // Start file with magic header and use Windows line-ending convention with CR LF pairs.
    writer->writeline( "VANILLA\x0D\x0A");

    // Iterate through the string array of element names
    mi::Size size = elements->get_length();
    for( mi::Size i = 0; (0 == writer->get_error_number()) && i < size; ++i) {

        // Get string for element i from the array
        mi::base::Handle<const mi::IString> name( elements->get_element<mi::IString>( i));
        if( !name.is_valid_interface()) {
            return report_message( result, 6007, mi::base::MESSAGE_SEVERITY_ERROR,
               "element array contains an invalid object", export_state);
        }
        const char* element_name = name->get_c_str();

        // Access the element in the DB
        mi::base::Handle<const mi::base::IInterface> element( transaction->access( element_name));
        if( !element.is_valid_interface()) {
            std::string message( "Element '");
            message += element_name;
            message += "' does not exist in database, export failed.";
            return report_message( result, 6008, mi::base::MESSAGE_SEVERITY_ERROR,
                message, export_state);
        }

        // Write element to file
        writer->writeline( get_element_type( element.get()).c_str());
        writer->writeline( " \"");
        write_name( element_name, strip_prefix, writer);
        writer->writeline( "\"\x0D\x0A");
    }

    return result;
}