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

#include <mi/neuraylib.h>

#include <string>
#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 importer state, which is used in the importer function to carry additional data around for
// possible recursive invocations of importers. It contains a URI for the imported resource, a line
// number, and a pointer to a parent state supporting recursive imports.
class Vanilla_import_state
  : public mi::base::Interface_implement< mi::neuraylib::IImpexp_state>
{
public:
    // Constructor.
    Vanilla_import_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 importer. It defines all meta information, for example, author, version numbers, which
// formats it supports etc. The longer format detection functions and the import_elements() function
// are implemented outside of the class body.
class Vanilla_importer
  : public mi::base::Interface_implement< mi::neuraylib::IImporter>
{
public:
    // Constructor.
    Vanilla_importer( mi::neuraylib::IPlugin_api* plugin_api)
        : m_plugin_api( plugin_api, mi::base::DUP_INTERFACE) { }

    // 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_import_state( uri, parent_state);
    }

    // This importer 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) importer"; }

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

    mi::base::Uuid get_uuid() const
    {
        mi::base::Uuid uuid;
        uuid.m_id1 = 0x338eca60;
        uuid.m_id2 = 0x31004802;
        uuid.m_id3 = 0xaab9046b;
        uuid.m_id4 = 0x9e0b1d9b;
        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::IReader* reader) const;

    mi::neuraylib::IImport_result* import_elements(
        mi::neuraylib::ITransaction* transaction,
        const char* extension,
        mi::neuraylib::IReader* reader,
        const mi::IMap* options,
        mi::neuraylib::IImpexp_state* import_state) const;

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

    // Format message with context and append it to the messages in the result.
    static mi::neuraylib::IImport_result_ext* report_message(
        mi::neuraylib::IImport_result_ext* result,
        mi::Uint32 message_number,
        mi::base::Message_severity message_severity,
        std::string message,
        const mi::neuraylib::IImpexp_state* import_state)
    {
        std::ostringstream s;
        s << import_state->get_uri()
          << ":" << import_state->get_line_number() << ": "
          << "Vanilla importer message " << message_number << ", "
          << "severity " << enum_to_str( message_severity) << ": "
          << message;
        // Report context of all parent import states from recursive invocations of
        // import_elements() in their own lines with indentation.
        mi::base::Handle<const mi::neuraylib::IImpexp_state> parent_state(
            import_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;
    }

    mi::base::Handle<mi::neuraylib::IPlugin_api> m_plugin_api;
};

bool Vanilla_importer::test_file_type( const char* extension) const
{
    // This importer 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_importer::test_file_type(
    const char* extension, const mi::neuraylib::IReader* reader) const
{
    // Use magic header check if lookahead is available
    if( reader->supports_lookahead()) {
        // File has to start with "VANILLA" and linebreak, which can be \n or \r depending on the
        // line ending convention in the file.
        const char** buffer = 0;
        mi::Sint64 n = reader->lookahead( 8, buffer);
        return ( n >= 8) && (0 == std::strncmp( *buffer, "VANILLA", 7))
                         && ((*buffer[7] == '\n') || (*buffer[7] == '\r'));
    }
    // This importer 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")));
}

mi::neuraylib::IImport_result* Vanilla_importer::import_elements(
    mi::neuraylib::ITransaction* transaction,
    const char* extension,
    mi::neuraylib::IReader* reader,
    const mi::IMap* importer_options,
    mi::neuraylib::IImpexp_state* import_state) const
{
    // Create the importer result instance for the return value. If that fails something is really
    // wrong and we return 0.
    mi::neuraylib::IImport_result_ext* result
        = transaction->create<mi::neuraylib::IImport_result_ext>( "Import_result_ext");
    if( !result)
        return 0;

    // Get the 'prefix' option.
    MISTD::string prefix;
    if( importer_options && importer_options->has_key( "prefix")) {
        mi::base::Handle<const mi::IString> option(
            importer_options->get_value<mi::IString>( "prefix"));
        prefix = option->get_c_str();
    }

    // Get the 'list_elements' option.
    bool list_elements = false;
    if( importer_options && importer_options->has_key( "list_elements")) {
        mi::base::Handle<const mi::IBoolean> option(
            importer_options->get_value<mi::IBoolean>( "list_elements"));
        list_elements = option->get_value<bool>();
    }

    // Before we start parsing the file, we create a group that collects all top-level elements.
    // This will be our rootgroup.
    std::string root_group_name = prefix + "Vanilla::root_group";
    mi::base::Handle<mi::neuraylib::IGroup> rootgroup(
        transaction->create<mi::neuraylib::IGroup>( "Group"));
    mi::Sint32 error_code = transaction->store( rootgroup.get(), root_group_name.c_str());
    if( error_code != 0)
        return report_message( result, 4010, mi::base::MESSAGE_SEVERITY_ERROR,
            "failed to create the root group", import_state);
    rootgroup = transaction->edit<mi::neuraylib::IGroup>( root_group_name.c_str());

    // Register the rootgroup with the importer result.
    result->set_rootgroup( root_group_name.c_str());

    // If the element list flag is set, record the rootgroup element also in the elements array of
    // the result.
    if( list_elements)
        result->element_push_back( root_group_name.c_str());

    // Assume it is a line based text format and read it line by line. Assume lines are no longer
    // than 256 chars, otherwise read it in pieces
    char buffer[257];
    while( reader->readline( buffer, 257) && buffer[0] != '\0') {

        // Periodically check whether the transaction is still open. If not, stop importing.
        if( !transaction->is_open())
            break;

        // Here you can process the buffer content of size len. It corresponds to the line
        // import_state->get_line_number() of the input file.
        mi::Size len = std::strlen( buffer);

        // We illustrate some actions triggered by fixed line numbers: Line 3 of a ".vnl" file
        // triggers the recursive inclusion of the file "test2.van" file with a prefix.
        mi::Size ext_len = std::strlen( extension);
        if( 3 == import_state->get_line_number()
             && (ext_len > 3)
             && 0 == strcmp( extension + ext_len - 4, ".vnl")) {
            // Get a IImport_api handle to call its import_elements() function
            mi::base::Handle<mi::neuraylib::IImport_api> import_api(
                m_plugin_api->get_api_component<mi::neuraylib::IImport_api>());
            if( !import_api.is_valid_interface())
                // Error numbers from 4000 to 5999 are reserved for custom importer messages like
                // this one
                return report_message( result, 4001, mi::base::MESSAGE_SEVERITY_ERROR,
                   "did not get a valid IImport_api object, import failed", import_state);
            // Call the importer recursively to illustrate the handling of include files and similar
            // things. We trigger this import only on a ".vnl" file and include the fixed file
            // "test2.van". We give the included file an extra prefix "Prefix_".
            mi::base::Handle<mi::neuraylib::IFactory> factory(
                m_plugin_api->get_api_component<mi::neuraylib::IFactory>());
            mi::base::Handle<mi::IString> child_prefix( factory->create<mi::IString>( "String"));
            child_prefix->set_c_str( "Prefix_");
            mi::base::Handle<mi::IMap> child_importer_options( factory->clone<mi::IMap>(
                importer_options));
            child_importer_options->erase( "prefix");
            child_importer_options->insert( "prefix", child_prefix.get());
            mi::base::Handle<const mi::neuraylib::IImport_result> include_result(
                import_api->import_elements( transaction, "file:test2.van",
                    child_importer_options.get(), import_state));
            // Safety check, if this fails, the import is not continued.
            if( !include_result.is_valid_interface())
                return report_message( result, 4002, mi::base::MESSAGE_SEVERITY_ERROR,
                   "import was not able to create result object, import failed", import_state);
            // Process the result. Even in the case of an error, we need to process the elements
            // array.
            if( list_elements)
                result->append_elements( include_result.get());
            // Append messages of include to this result
            result->append_messages( include_result.get());
            // React on errors during processing of the include
            if( include_result->get_error_number() > 0) {
                // Report the failure of the include as a separate message too
                report_message( result, 4003, mi::base::MESSAGE_SEVERITY_ERROR,
                    "including file 'test2.van' failed.", import_state);
            } else {
                // Recursive import was successful. The rootgroup of the import is now appended to
                // this rootgroup
                if( 0 == include_result->get_rootgroup())
                    report_message( result, 4004, mi::base::MESSAGE_SEVERITY_ERROR,
                        "include file 'test2.van' did not contain a rootgroup", import_state);
                else
                    rootgroup->attach( include_result->get_rootgroup());
            }
        }

        // Line 4 triggers several messages and adds an empty group to the rootgroup.
        if( 4 == import_state->get_line_number()) {

            // Several messages, file parsing continues
            report_message( result, 4005, mi::base::MESSAGE_SEVERITY_FATAL,
                "test message in line 4", import_state);
            report_message( result, 4006, mi::base::MESSAGE_SEVERITY_ERROR,
                "test message in line 4", import_state);
            report_message( result, 4007, mi::base::MESSAGE_SEVERITY_WARNING,
                "test message in line 4", import_state);
            report_message( result, 4008, mi::base::MESSAGE_SEVERITY_INFO,
                "test message in line 4", import_state);
            report_message( result, 4009, mi::base::MESSAGE_SEVERITY_VERBOSE,
                 "test message in line 4", import_state);

            // Create a group "Vanilla::Group1"
            std::string group_name = prefix + "Vanilla::Group1";
            mi::base::Handle<mi::neuraylib::IGroup> group(
                transaction->create<mi::neuraylib::IGroup>( "Group"));
            mi::Sint32 error_code = transaction->store( group.get(), group_name.c_str());
            if( error_code != 0)
                report_message( result, 4011, mi::base::MESSAGE_SEVERITY_ERROR,
                    "unexpected error in line 4", import_state);
            else {
                // Add this group to the rootgroup
                rootgroup->attach( group_name.c_str());
                // If get_list_elements_flag is set, record the new element in the elements array of
                // the result.
                if( list_elements)
                    result->element_push_back( group_name.c_str());
            }
        }

        // Handle line numbers, buffer might end in '\n' or not if line was too long.
        if( (len > 0) && ('\n' == buffer[len-1]))
            import_state->incr_line_number();
    }

    if( reader->eof()) {
        // Normal end
        return result;
    }

    // Report error condition for a failed reader call
    return report_message(
        result,
        static_cast<mi::Uint32>( reader->get_error_number()),
        mi::base::MESSAGE_SEVERITY_ERROR,
        reader->get_error_message() ? reader->get_error_message() : "",
        import_state);
}