//
// Copyright (c) 2017 The Khronos Group Inc.
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#include "os_helpers.h"
#include "errorHelpers.h"

// =================================================================================================
// C++ interface.
// =================================================================================================

#include <cerrno>     // errno, error constants
#include <climits>    // PATH_MAX
#include <cstdlib>    // abort, _splitpath, _makepath
#include <cstring>    // strdup, strerror_r
#include <sstream>

#include <vector>

#define CHECK_PTR( ptr )    \
    if ( (ptr) == NULL ) {  \
        abort();            \
    }

typedef std::vector< char > buffer_t;

#if ! defined( PATH_MAX )
    #define PATH_MAX 1000
#endif

int const _size  = PATH_MAX + 1;    // Initial buffer size for path.
int const _count = 8;               // How many times we will try to double buffer size.

// -------------------------------------------------------------------------------------------------
// MacOS X
// -------------------------------------------------------------------------------------------------

#if defined( __APPLE__ )


    #include <mach-o/dyld.h>    // _NSGetExecutablePath
    #include <libgen.h>         // dirname


    static
    std::string
    _err_msg(
        int err,     // Error number (e. g. errno).
        int level    // Nesting level, for avoiding infinite recursion.
    ) {

        /*
            There are 3 incompatible versions of strerror_r:

                char * strerror_r( int, char *, size_t );  // GNU version
                int    strerror_r( int, char *, size_t );  // BSD version
                int    strerror_r( int, char *, size_t );  // XSI version

            BSD version returns error code, while XSI version returns 0 or -1 and sets errno.

        */

        // BSD version of strerror_r.
        buffer_t buffer( 100 );
        int      count = _count;
        for ( ; ; ) {
            int rc = strerror_r( err, & buffer.front(), buffer.size() );
            if ( rc == EINVAL ) {
                // Error code is not recognized, but anyway we got the message.
                return & buffer.front();
            } else if ( rc == ERANGE ) {
                // Buffer is not enough.
                if ( count > 0 ) {
                    // Enlarge the buffer.
                    -- count;
                    buffer.resize( buffer.size() * 2 );
                } else {
                    std::stringstream ostr;
                    ostr
                        << "Error " << err << " "
                        << "(Getting error message failed: "
                        << "Buffer of " << buffer.size() << " bytes is still too small"
                        << ")";
                    return ostr.str();
                }; // if
            } else if ( rc == 0 ) {
                // We got the message.
                return & buffer.front();
            } else {
                std::stringstream ostr;
                ostr
                    << "Error " << err << " "
                    << "(Getting error message failed: "
                    << ( level < 2 ? _err_msg( rc, level + 1 ) : "Oops" )
                    << ")";
                return ostr.str();
            }; // if
        }; // forever

    } // _err_msg


    std::string
    dir_sep(
    ) {
        return "/";
    } // dir_sep


    std::string
    exe_path(
    ) {
        buffer_t path( _size );
        int      count = _count;
        for ( ; ; ) {
            uint32_t size = path.size();
            int rc = _NSGetExecutablePath( & path.front(), & size );
            if ( rc == 0 ) {
                break;
            }; // if
            if ( count > 0 ) {
                -- count;
                path.resize( size );
            } else {
                log_error(
                    "ERROR: Getting executable path failed: "
                    "_NSGetExecutablePath failed: Buffer of %lu bytes is still too small\n",
                    (unsigned long) path.size()
                );
                exit( 2 );
            }; // if
        }; // forever
        return & path.front();
    } // exe_path


    std::string
    exe_dir(
    ) {
        std::string path = exe_path();
        // We cannot pass path.c_str() to `dirname' bacause `dirname' modifies its argument.
        buffer_t buffer( path.c_str(), path.c_str() + path.size() + 1 ); // Copy with trailing zero.
        return dirname( & buffer.front() );
    } // exe_dir


#endif // __APPLE__

// -------------------------------------------------------------------------------------------------
// Linux
// -------------------------------------------------------------------------------------------------

#if defined( __linux__ )


    #include <cerrno>      // errno
    #include <libgen.h>    // dirname
    #include <unistd.h>    // readlink


    static
    std::string
    _err_msg(
        int err,
        int level
    ) {

        /*
            There are 3 incompatible versions of strerror_r:

                char * strerror_r( int, char *, size_t );  // GNU version
                int    strerror_r( int, char *, size_t );  // BSD version
                int    strerror_r( int, char *, size_t );  // XSI version

            BSD version returns error code, while XSI version returns 0 or -1 and sets errno.

        */

        #if defined(__ANDROID__) || ( ( _POSIX_C_SOURCE >= 200112L || _XOPEN_SOURCE >= 600 ) && ! _GNU_SOURCE )

            // XSI version of strerror_r.
            #warning Not tested!
            buffer_t buffer( 200 );
            int      count = _count;
            for ( ; ; ) {
                int rc = strerror_r( err, & buffer.front(), buffer.size() );
                if ( rc == -1 ) {
                    int _err = errno;
                    if ( _err == ERANGE ) {
                        if ( count > 0 ) {
                            // Enlarge the buffer.
                            -- count;
                            buffer.resize( buffer.size() * 2 );
                        } else {
                            std::stringstream ostr;
                            ostr
                                << "Error " << err << " "
                                << "(Getting error message failed: "
                                << "Buffer of " << buffer.size() << " bytes is still too small"
                                << ")";
                            return ostr.str();
                        }; // if
                    } else {
                        std::stringstream ostr;
                        ostr
                            << "Error " << err << " "
                            << "(Getting error message failed: "
                            << ( level < 2 ? _err_msg( _err, level + 1 ) : "Oops" )
                            << ")";
                        return ostr.str();
                    }; // if
                } else {
                    // We got the message.
                    return & buffer.front();
                }; // if
            }; // forever

        #else

            // GNU version of strerror_r.
            char buffer[ 2000 ];
            return strerror_r( err, buffer, sizeof( buffer ) );

        #endif

    } // _err_msg


    std::string
    dir_sep(
    ) {
        return "/";
    } // dir_sep


    std::string
    exe_path(
    ) {

        static std::string const exe = "/proc/self/exe";

        buffer_t    path( _size );
        int         count = _count;  // Max number of iterations.

        for ( ; ; ) {

            ssize_t len = readlink( exe.c_str(), & path.front(), path.size() );

            if ( len < 0 ) {
                // Oops.
                int err = errno;
                log_error(
                    "ERROR: Getting executable path failed: "
                    "Reading symlink `%s' failed: %s\n",
                    exe.c_str(), err_msg( err ).c_str()
                );
                exit( 2 );
            }; // if

            if ( len < path.size() ) {
                // We got the path.
                path.resize( len );
                break;
            }; // if

            // Oops, buffer is too small.
            if ( count > 0 ) {
                -- count;
                // Enlarge the buffer.
                path.resize( path.size() * 2 );
            } else {
                log_error(
                    "ERROR: Getting executable path failed: "
                    "Reading symlink `%s' failed: Buffer of %lu bytes is still too small\n",
                    exe.c_str(),
                    (unsigned long) path.size()
                );
                exit( 2 );
            }; // if

        }; // forever

        return std::string( & path.front(), path.size() );

    } // exe_path


    std::string
    exe_dir(
    ) {
        std::string path = exe_path();
        // We cannot pass path.c_str() to `dirname' bacause `dirname' modifies its argument.
        buffer_t buffer( path.c_str(), path.c_str() + path.size() + 1 ); // Copy with trailing zero.
        return dirname( & buffer.front() );
    } // exe_dir

#endif // __linux__

// -------------------------------------------------------------------------------------------------
// MS Windows
// -------------------------------------------------------------------------------------------------

#if defined( _WIN32 )


    #include <windows.h>
    #if defined( max )
        #undef max
    #endif

    #include <cctype>
    #include <algorithm>


    static
    std::string
    _err_msg(
        int err,
        int level
    ) {

        std::string msg;

        LPSTR  buffer = NULL;
        DWORD  flags  =
            FORMAT_MESSAGE_ALLOCATE_BUFFER |
            FORMAT_MESSAGE_FROM_SYSTEM |
            FORMAT_MESSAGE_IGNORE_INSERTS;

        DWORD len =
            FormatMessageA(
                flags,
                NULL,
                err,
                LANG_USER_DEFAULT,
                reinterpret_cast< LPSTR >( & buffer ),
                0,
                NULL
            );

        if ( buffer == NULL || len == 0 ) {

            int _err = GetLastError();
            char str[1024] = { 0 };
            snprintf(str, sizeof(str), "Error 0x%08x (Getting error message failed: %s )", err, ( level < 2 ? _err_msg( _err, level + 1 ).c_str() : "Oops" ));
            msg = std::string(str);

        } else {

            // Trim trailing whitespace (including `\r' and `\n').
            while ( len > 0 && isspace( buffer[ len - 1 ] ) ) {
                -- len;
            }; // while

            // Drop trailing full stop.
            if ( len > 0 && buffer[ len - 1 ] == '.' ) {
                -- len;
            }; // if

            msg.assign( buffer, len );

        }; //if

        if ( buffer != NULL ) {
            LocalFree( buffer );
        }; // if

        return msg;

    } // _get_err_msg


    std::string
    dir_sep(
    ) {
        return "\\";
    } // dir_sep


    std::string
    exe_path(
    ) {

        buffer_t path( _size );
        int      count = _count;

        for ( ; ; ) {

            DWORD len = GetModuleFileNameA( NULL, & path.front(), path.size() );

            if ( len == 0 ) {
                int err = GetLastError();
                log_error( "ERROR: Getting executable path failed: %s\n", err_msg( err ).c_str() );
                exit( 2 );
            }; // if

            if ( len < path.size() ) {
                path.resize( len );
                break;
            }; // if

            // Buffer too small.
            if ( count > 0 ) {
                -- count;
                path.resize( path.size() * 2 );
            } else {
                log_error(
                    "ERROR: Getting executable path failed: "
                    "Buffer of %lu bytes is still too small\n",
                    (unsigned long) path.size()
                );
                exit( 2 );
            }; // if

        }; // forever

        return std::string( & path.front(), path.size() );

    } // exe_path


    std::string
    exe_dir(
    ) {

        std::string exe = exe_path();
        int count = 0;

        // Splitting path into components.
        buffer_t drv( _MAX_DRIVE );
        buffer_t dir( _MAX_DIR   );
        count = _count;
#if defined(_MSC_VER)
        for ( ; ; ) {
            int rc =
                _splitpath_s(
                    exe.c_str(),
                    & drv.front(), drv.size(),
                    & dir.front(), dir.size(),
                    NULL, 0,   // We need neither name
                    NULL, 0    // nor extension
                );
            if ( rc == 0 ) {
                break;
            } else if ( rc == ERANGE ) {
                if ( count > 0 ) {
                    -- count;
                    // Buffer is too small, but it is not clear which one.
                    // So we have to enlarge all.
                    drv.resize( drv.size() * 2 );
                    dir.resize( dir.size() * 2 );
                } else {
                    log_error(
                        "ERROR: Getting executable path failed: "
                        "Splitting path `%s' to components failed: "
                        "Buffers of %lu and %lu bytes are still too small\n",
                        exe.c_str(),
                        (unsigned long) drv.size(),
                        (unsigned long) dir.size()
                    );
                    exit( 2 );
                }; // if
            } else {
                log_error(
                    "ERROR: Getting executable path failed: "
                    "Splitting path `%s' to components failed: %s\n",
                    exe.c_str(),
                    err_msg( rc ).c_str()
                );
                exit( 2 );
            }; // if
        }; // forever

#else // __MINGW32__

        // MinGW does not have the "secure" _splitpath_s, use the insecure version instead.
        _splitpath(
            exe.c_str(),
            & drv.front(),
            & dir.front(),
            NULL,   // We need neither name
            NULL    // nor extension
        );
#endif // __MINGW32__

        // Combining components back to path.
        // I failed with "secure" `_makepath_s'. If buffer is too small, instead of returning
        // ERANGE, `_makepath_s' pops up dialog box and offers to debug the program. D'oh!
        // So let us try to guess the size of result and go with insecure `_makepath'.
        buffer_t path( std::max( drv.size() + dir.size(), size_t( _MAX_PATH ) ) + 10 );
        _makepath( & path.front(), & drv.front(), & dir.front(), NULL, NULL );

        return & path.front();

    } // exe_dir


#endif // _WIN32


std::string
err_msg(
    int err
) {

    return _err_msg( err, 0 );

} // err_msg


// =================================================================================================
// C interface.
// =================================================================================================


char *
get_err_msg(
    int err
) {
    char * msg = strdup( err_msg( err ).c_str() );
    CHECK_PTR( msg );
    return msg;
} // get_err_msg


char *
get_dir_sep(
) {
    char * sep = strdup( dir_sep().c_str() );
    CHECK_PTR( sep );
    return sep;
} // get_dir_sep


char *
get_exe_path(
) {
    char * path = strdup( exe_path().c_str() );
    CHECK_PTR( path );
    return path;
} // get_exe_path


char *
get_exe_dir(
) {
    char * dir = strdup( exe_dir().c_str() );
    CHECK_PTR( dir );
    return dir;
} // get_exe_dir


// end of file //
