//
// LDASTools AL - A library collection to provide an abstraction layer
//
// Copyright (C) 2018 California Institute of Technology
//
// LDASTools AL is free software; you may redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 (GPLv2) of the
// License or at your discretion, any later version.
//
// LDASTools AL is distributed in the hope that it will be useful, but
// without any warranty or even the implied warranty of merchantability
// or fitness for a particular purpose. See the GNU General Public
// License (GPLv2) for more details.
//
// Neither the names of the California Institute of Technology (Caltech),
// The Massachusetts Institute of Technology (M.I.T), The Laser
// Interferometer Gravitational-Wave Observatory (LIGO), nor the names
// of its contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//
// You should have received a copy of the licensing terms for this
// software included in the file LICENSE located in the top-level
// directory of this package. If you did not, you can view a copy at
// http://dcc.ligo.org/M1500244/LICENSE
//

#include "ldastoolsal_config.h"

#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <list>
#include <map>

#if HAVE_VALGRIND_VALGRIND_H
#include "valgrind/valgrind.h"
#endif /* HAVE_VALGRIND_VALGRIND_H */

#include "ldastoolsal/MemChecker.hh"
#include "ldastoolsal/mutexlock.hh"
#include "ldastoolsal/ReadWriteLock.hh"

#include "MallocAllocator.hh"

#define EXITING_LOCK 1

namespace
{
    using LDASTools::AL::MemChecker;
    using LDASTools::AL::MutexLock;
    using LDASTools::AL::ReadWriteLock;

    template < class T >
    inline bool
    operator==( const malloc_allocator< T >&, const malloc_allocator< T >& )
    {
        return true;
    }

    template < class T >
    inline bool
    operator!=( const malloc_allocator< T >&, const malloc_allocator< T >& )
    {
        return false;
    }

    //---------------------------------------------------------------------
    //
    //---------------------------------------------------------------------
    class GCQueueNode
    {
    public:
        ~GCQueueNode( )
        {
            while ( data_container.size( ) > 0 )
            {
                data_container.back( ).purge( );
                data_container.pop_back( );
            }
        }

        void
        Append( MemChecker::CleanupFunction Function,
                const char*                 Name,
                bool                        Always )
        {
#if defined( __VALGRIND_MAJOR__ )
            if ( RUNNING_ON_VALGRIND || Always )
            {
                data_container.push_back( data_type( Function, Name ) );
            }
#endif /* defined( __VALGRIND_MAJOR__ ) */
        }

        void
        Info( const char* Leader ) const
        {
            for ( data_container_type::const_iterator
                      cur = data_container.begin( ),
                      last = data_container.end( );
                  cur != last;
                  ++cur )
            {
                std::cerr << Leader << cur->name << std::endl << std::flush;
            }
        }

    private:
        class func_node
        {
        public:
            typedef MemChecker::CleanupFunction function_type;

            function_type function;
            const char*   name;

            func_node( function_type Function, const char* Name )
                : function( Function ), name( ::strdup( Name ) )
            {
            }

            void
            purge( )
            {
                if ( function )
                {
                    if ( MemChecker::Trigger::IsRegistered( ) )
                    {
                        //-----------------------------------------------------------
                        // Cleanup is being called within main() so it is safe.
                        // If not called from within main(), then shared
                        // objects may be unloaded before reaching here.
                        //-----------------------------------------------------------
                        ( *function )( );
                    }
                    function = (function_type)NULL;
                }
                if ( name )
                {
                    ::free( const_cast< char* >( name ) );
                    name = (const char*)NULL;
                }
            }
        };

        typedef func_node data_type;
        typedef std::list< data_type, malloc_allocator< data_type > >
            data_container_type;

        data_container_type data_container;
    };

    class GCQueue
    {
    public:
        GCQueue( )
            :
#if EXITING_LOCK
              is_exiting_baton( /*__FILE__, __LINE__,*/ false ),
#endif /* EXITING_LOCK */
              gc_queue_baton( __FILE__, __LINE__, false )
        {
#if EXITING_LOCK
#if VERBOSE_DEBEBUGGING
            std::cerr << "DEBUG: is_exitint_baton: "
                      << is_exiting_baton.Handle( ) << std::endl;
#endif /* VERBOSE_DEBEBUGGING */
#endif /* EXITING_LOCK */
        }

        inline void
        Append( MemChecker::CleanupFunction Function,
                const char*                 Name,
                int                         Ring,
                bool                        Always )
        {
            MutexLock l( gc_queue_baton, __FILE__, __LINE__ );

            if ( IsExiting( ) )
            {
                ( *Function )( );
                return;
            }
            gc_queue[ Ring ].Append( Function, Name, Always );
        }

        inline void
        Cleanup( )
        {
            MutexLock l( gc_queue_baton, __FILE__, __LINE__ );

            bool state;

            state = IsExiting( true );

            while ( gc_queue.size( ) > 0 )
            {
                gc_queue_type::iterator cur = gc_queue.end( );
                --cur;
                gc_queue.erase( cur );
            }

            IsExiting( state );
        }

        inline void
        Info( ) const
        {
            //-----------------------------------------------------------------
            // Print the queue in the order that it will be deallocated
            //-----------------------------------------------------------------
            std::cerr << "Exiting Status: " << is_exiting << std::endl
                      << std::flush;
            for ( gc_queue_type::const_reverse_iterator
                      cur = gc_queue.rbegin( ),
                      last = gc_queue.rend( );
                  cur != last;
                  ++cur )
            {
                std::cerr << "Exit Queue Level: " << cur->first << std::endl
                          << std::flush;
                cur->second.Info( "\t" );
            }
        }

        inline bool
        IsExiting( ) const
        {
            bool retval = is_exiting;
            if ( retval == false )
            {
#if EXITING_LOCK
                ReadWriteLock l(
                    is_exiting_baton, ReadWriteLock::READ, __FILE__, __LINE__ );
#endif /* EXIING_LOCK */

                return retval;
            }
            return retval;
        }

        inline bool
        IsExiting( bool Value )
        {
#if EXITING_LOCK
            ReadWriteLock l(
                is_exiting_baton, ReadWriteLock::WRITE, __FILE__, __LINE__ );
#endif /* EXITING_LOCK */

            bool retval = is_exiting;

            is_exiting = Value;

            return retval;
        }

    private:
        typedef std::map<
            int,
            GCQueueNode,
            std::less< int >,
            malloc_allocator< std::pair< const int, GCQueueNode > > >
            gc_queue_type;

        bool is_exiting;
#if EXITING_LOCK
        ReadWriteLock::baton_type is_exiting_baton;
#endif /* EXITING_LOCK */

        gc_queue_type         gc_queue;
        MutexLock::baton_type gc_queue_baton;
    };

    GCQueue&
    gc_queue( )
    {
        static GCQueue eq;

        return eq;
    }
} // namespace

namespace LDASTools
{
    namespace AL
    {
        bool MemChecker::Trigger::registered = false;

        MemChecker::Trigger::Trigger( bool Final ) : final( Final )
        {
            registered = true;
        }

        MemChecker::Trigger::~Trigger( )
        {
            DoGarbageCollection( );
        }

        void
        MemChecker::Trigger::DoGarbageCollection( ) const
        {
            if ( registered )
            {
                if ( final )
                {
                    MemChecker::is_exiting( true );
                }
                MemChecker::cleanup( );
                registered = false;
            }
        }

        //-------------------------------------------------------------------
        /// \note
        ///     On systems that support cleaning of the memory prior to exit,
        ///     setting the environment variable 'MEMCHECK_GARBAGE_COLLECTION'
        ///     prevents the system from registering cleanup helpers.
        //-------------------------------------------------------------------
        void
        MemChecker::Append( MemChecker::CleanupFunction Function,
                            const std::string&          Name,
                            int                         Ring,
                            bool                        Always )
        {
            static bool init = false;
            static bool do_memcheck = true;

            if ( !init )
            {
                const char* val = ::getenv( "MEMCHECK_GARBAGE_COLLECTION" );
                if ( val )
                {
                    switch ( *val )
                    {
                    case '0':
                    case 'f':
                    case 'F':
                        do_memcheck = false;
                        break;
                    default:
#if defined( __VALGRIND_MAJOR__ )
                        do_memcheck = ( RUNNING_ON_VALGRIND );
#else /* defined( __VALGRIND_MAJOR__ ) */
                        do_memcheck = false;
#endif /* defined( __VALGRIND_MAJOR__ ) */
                    }
                }
                init = true;
            }
            if ( do_memcheck )
            {
                gc_queue( ).Append( Function, Name.c_str( ), Ring, Always );
            }
        }

        bool
        MemChecker::IsExiting( )
        {
            return gc_queue( ).IsExiting( );
        }

        void
        MemChecker::Info( )
        {
            gc_queue( ).Info( );
        }

        void
        MemChecker::cleanup( )
        {
            gc_queue( ).Cleanup( );
        }

        void
        MemChecker::is_exiting( bool Value )
        {
            gc_queue( ).IsExiting( Value );
        }

    } // namespace AL
} // namespace LDASTools
